├── WrathPatches ├── IgnoreHarmonyVersion.txt ├── IsExternalInit.cs ├── WrathPatchAttribute.cs ├── Patches │ ├── SaveSlotGroupVM.HandleNewSave.cs │ ├── MakeBlueprintBeingReadThreadStatic.cs │ ├── ShaderWrapper.cs │ ├── HambeardLeakFixes.cs │ ├── AbilityExecutionContextSpellDescriptor.cs │ ├── LogsHotkey.cs │ ├── Element_AssetGuidShort_Fix.cs │ ├── ProgressionRoot_HumanRace.cs │ ├── Experimental │ │ └── BlueprintsCacheInitAddBlueprint.cs │ ├── ActingInSurpriseRoundFix.cs │ ├── GameStatistic.Tick.PlayerNullFix.cs │ ├── MissingSettingsUISoundsFix.cs │ ├── CurrentSelectedCharacterNREFix.cs │ ├── EventSubscriptionLeakFixes.cs │ ├── BloodyFaceControllerUnitEntityView.cs │ ├── LoadAssembliesFix.cs │ ├── OwlcatButtonClickDelayFix.cs │ ├── KeyboardAccess.Bind.cs │ ├── SetMagusFeatureActiveFix.cs │ ├── ModReloadClearBlueprintReferenceCache.cs │ ├── ActuallyFixSharedStrings.cs │ ├── UnitMarkManager.LateUpdate.cs │ ├── AnimationClipWrapperStateMachineBehaviour.cs │ ├── WeaponSetGripVMFix.cs │ ├── KingmakerInputModule.CheckEventSystem.cs │ ├── BetterModLogger.cs │ ├── LogExceptions.cs │ ├── OwlcatModification.LoadAssemblies_Patch.cs │ ├── AddFactsIgnoreDeadReferences.cs │ ├── SpellSlotComparisonFixes.cs │ ├── OwnerBlueprintWarning.cs │ ├── OwlModDirectReferenceBundleDependenciesFix.cs │ ├── OMMExtensions.cs │ ├── TheEnhance.cs │ ├── EntityFactComponent.Activate.cs │ └── OnDemandBlueprints.cs ├── CheckHarmonyVersion.cs ├── TranspilerUtil.cs ├── HarmonyPatchCategory.cs ├── Main.cs ├── LinqExtensions.cs └── WrathPatches.csproj ├── ShaderWrapper ├── scriptableshaderwrapper ├── Main.cs ├── ShaderWrapper.csproj └── ScriptableShaderWrapper.cs ├── common.props ├── Repository.json ├── LICENSE.txt ├── WrathPatches.sln ├── README.md ├── .gitattributes └── .gitignore /WrathPatches/IgnoreHarmonyVersion.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ShaderWrapper/scriptableshaderwrapper: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftenator2022/WrathPatches/HEAD/ShaderWrapper/scriptableshaderwrapper -------------------------------------------------------------------------------- /WrathPatches/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | 4 | namespace System.Runtime.CompilerServices 5 | { 6 | [DebuggerNonUserCode] 7 | internal static class IsExternalInit { } 8 | } -------------------------------------------------------------------------------- /common.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.19.1 4 | 5 | 6 | debug 7 | 8 | -------------------------------------------------------------------------------- /Repository.json: -------------------------------------------------------------------------------- 1 | { 2 | "Releases": [ 3 | { 4 | "Id": "WrathPatches", 5 | "Version": "1.19.1", 6 | "DownloadUrl": "https://github.com/microsoftenator2022/WrathPatches/releases/download/v1.19.1/WrathPatches-1.19.1.zip" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /WrathPatches/WrathPatchAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace WrathPatches 8 | { 9 | [AttributeUsage(AttributeTargets.Class)] 10 | internal sealed class WrathPatchAttribute(string name) : Attribute 11 | { 12 | public readonly string Name = name; 13 | public string Description = ""; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /WrathPatches/Patches/SaveSlotGroupVM.HandleNewSave.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | using HarmonyLib; 8 | 9 | using Kingmaker.UI.MVVM._VM.SaveLoad; 10 | 11 | namespace WrathPatches 12 | { 13 | [WrathPatch("Fix save slot VM double bind")] 14 | [HarmonyPatch(typeof(SaveSlotGroupVM), nameof(SaveSlotGroupVM.HandleNewSave))] 15 | internal static class SaveSlotGroupVM_HandleNewSave 16 | { 17 | static void Prefix(SaveSlotGroupVM __instance, SaveSlotVM slot) 18 | { 19 | if (slot.Owner == null) return; 20 | 21 | slot.Owner.RemoveDisposable(slot); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ShaderWrapper/Main.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Kingmaker.Modding; 7 | using HarmonyLib; 8 | using System.Reflection; 9 | 10 | namespace ShaderWrapper 11 | { 12 | internal class Main 13 | { 14 | static Harmony harmony; 15 | 16 | //[OwlcatModificationEnterPoint] 17 | //public static void EntryPoint(OwlcatModification owlcatModification) 18 | //{ 19 | // if (harmony == null) 20 | // { 21 | // harmony = new Harmony(owlcatModification.Manifest.UniqueName); 22 | // harmony.PatchAll(Assembly.GetExecutingAssembly()); 23 | // } 24 | //} 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /WrathPatches/Patches/MakeBlueprintBeingReadThreadStatic.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using HarmonyLib; 4 | 5 | using Kingmaker.Blueprints.JsonSystem; 6 | 7 | namespace WrathPatches.Patches; 8 | 9 | [WrathPatch("Make BlueprintBeingRead ThreadStatic")] 10 | [HarmonyPatch(typeof(Json), nameof(Json.BlueprintBeingRead))] 11 | internal static class MakeBlueprintBeingReadThreadStatic 12 | { 13 | [ThreadStatic] 14 | static BlueprintJsonWrapper? blueprintBeingRead; 15 | 16 | [HarmonyPatch(MethodType.Setter)] 17 | [HarmonyPostfix] 18 | static void SetterPatch(BlueprintJsonWrapper value) => 19 | blueprintBeingRead = value; 20 | 21 | [HarmonyPatch(MethodType.Getter)] 22 | [HarmonyPostfix] 23 | static void GetterPatch(ref BlueprintJsonWrapper? __result) => 24 | __result = blueprintBeingRead; 25 | } 26 | -------------------------------------------------------------------------------- /WrathPatches/Patches/ShaderWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | using HarmonyLib; 10 | 11 | using Kingmaker.Modding; 12 | 13 | namespace WrathPatches.Patches; 14 | 15 | [WrathPatch("@Kurufinve ShaderWrapper fix for owlmod material shaders")] 16 | [HarmonyPatch(typeof(OwlcatModificationsManager), nameof(OwlcatModificationsManager.Start))] 17 | static class ShaderWrapper 18 | { 19 | static bool Applied = false; 20 | static void Prefix() 21 | { 22 | if (Applied) return; 23 | 24 | var shaderWrapperAssembly = 25 | Assembly.LoadFrom(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "ShaderWrapper.dll")); 26 | 27 | Main.HarmonyInstance.PatchAll(shaderWrapperAssembly); 28 | 29 | Applied = true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /WrathPatches/Patches/HambeardLeakFixes.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | using HarmonyLib; 4 | 5 | using Kingmaker.UI._ConsoleUI.TurnBasedMode; 6 | using Kingmaker.UI.MVVM._PCView.ActionBar; 7 | using Kingmaker.UI.MVVM._VM.ActionBar; 8 | 9 | namespace WrathPatches.Patches; 10 | 11 | [WrathPatch("Event subscription leak fixes")] 12 | [HarmonyPatch] 13 | static partial class EventSubscriptionLeakFixes 14 | { 15 | [HarmonyPatch(typeof(ActionBarSpellGroupPCView), nameof(ActionBarSpellGroupPCView.AddEmptySlots))] 16 | [HarmonyPostfix] 17 | static void PatchAddEmptySlots(ActionBarSpellGroupPCView __instance, List slotVms) => 18 | slotVms.ForEach(__instance.AddDisposable); 19 | 20 | [HarmonyPatch(typeof(InitiativeTrackerUnitVM), nameof(InitiativeTrackerUnitVM.UpdateBuffs))] 21 | [HarmonyPrefix] 22 | static void PatchUpdateBuffs(InitiativeTrackerUnitVM __instance) => 23 | __instance.UnitBuffs.ForEach(x => x.Dispose()); 24 | } -------------------------------------------------------------------------------- /WrathPatches/Patches/AbilityExecutionContextSpellDescriptor.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | 3 | using Kingmaker.Blueprints.Classes.Spells; 4 | using Kingmaker.EntitySystem.Entities; 5 | using Kingmaker.RuleSystem; 6 | using Kingmaker.UnitLogic.Abilities; 7 | using Kingmaker.UnitLogic.Mechanics; 8 | using Kingmaker.Utility; 9 | 10 | namespace WrathPatches.Patches 11 | { 12 | [WrathPatch("Add AbilityData spell descriptor to AbilityExecutionContext")] 13 | [HarmonyPatch(typeof(AbilityExecutionContext))] 14 | static class AbilityExecutionContextSpellDescriptor 15 | { 16 | [HarmonyPatch(MethodType.Constructor, 17 | [ 18 | typeof(AbilityData), 19 | typeof(AbilityParams), 20 | typeof(TargetWrapper), 21 | typeof(RulebookEventContext), 22 | typeof(UnitEntityData) 23 | ])] 24 | [HarmonyPostfix] 25 | static void Postfix(AbilityExecutionContext __instance) => 26 | __instance.SpellDescriptor |= __instance.Ability.SpellDescriptor; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /WrathPatches/Patches/LogsHotkey.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | using HarmonyLib; 9 | 10 | using Kingmaker; 11 | using Kingmaker.Cheats; 12 | 13 | using UnityEngine; 14 | 15 | namespace WrathPatches.Patches; 16 | 17 | [WrathPatch("OpenGameLogFull opens Player.log too")] 18 | [HarmonyPatch(typeof(CheatsDebug), nameof(CheatsDebug.OpenGameLogFull), [typeof(string)])] 19 | internal static class LogsHotkey 20 | { 21 | public static void EnableHotKey() 22 | { 23 | 24 | Main.Logger.Log("Enabling GameLogFull keybind"); 25 | 26 | Game.Instance.Keyboard.Bind("OpenGameLogFull", CheatsDebug.OpenGameLogFull); 27 | } 28 | 29 | [HarmonyPostfix] 30 | [HarmonyPatch] 31 | static void OpenGameLogFull_Postfix() 32 | { 33 | try 34 | { 35 | Application.OpenURL(Path.Combine(Application.persistentDataPath, "Player.log")); 36 | } 37 | catch 38 | { 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aaron Maslen (microsoftenator) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /WrathPatches/Patches/Element_AssetGuidShort_Fix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | using HarmonyLib; 8 | 9 | using Kingmaker; 10 | using Kingmaker.ElementsSystem; 11 | 12 | namespace WrathPatches 13 | { 14 | [WrathPatch("Element AssetGuid null fix")] 15 | [HarmonyPatch(typeof(Element))] 16 | static class Element_AssetGuidShort_Fix 17 | { 18 | static bool handleNullName(Element __instance, ref string __result) 19 | { 20 | __result = "anonymous"; 21 | 22 | return !string.IsNullOrEmpty(__instance.name); 23 | } 24 | 25 | [HarmonyPatch(nameof(Element.AssetGuidShort), MethodType.Getter)] 26 | [HarmonyPrefix] 27 | static bool AssetGuidShort_Prefix(Element __instance, ref string __result) => 28 | handleNullName(__instance, ref __result); 29 | 30 | [HarmonyPatch(nameof(Element.AssetGuid), MethodType.Getter)] 31 | [HarmonyPrefix] 32 | static bool AssetGuid_Prefix(Element __instance, ref string __result) => 33 | handleNullName(__instance, ref __result); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /WrathPatches/Patches/ProgressionRoot_HumanRace.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection.Emit; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | using HarmonyLib; 9 | 10 | using Kingmaker.Blueprints; 11 | using Kingmaker.Blueprints.Classes; 12 | using Kingmaker.Blueprints.JsonSystem; 13 | using Kingmaker.Blueprints.Root; 14 | 15 | namespace WrathPatches.Patches; 16 | 17 | [WrathPatch("Make ProgressionRoot.HumanRace not break for new races")] 18 | [HarmonyPatch(typeof(ProgressionRoot), nameof(ProgressionRoot.HumanRace), MethodType.Getter)] 19 | internal static class ProgressionRoot_HumanRace 20 | { 21 | static IEnumerable Transpiler(IEnumerable _) 22 | { 23 | yield return new CodeInstruction(OpCodes.Ldnull); 24 | yield return new CodeInstruction(OpCodes.Ret); 25 | } 26 | 27 | static BlueprintRace Postfix(BlueprintRace _, ProgressionRoot __instance) 28 | { 29 | __instance.m_HumanRaceCached ??= ResourcesLibrary.TryGetBlueprint("0a5d473ead98b0646b94495af250fdc4"); 30 | 31 | return __instance.m_HumanRaceCached; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /WrathPatches/CheckHarmonyVersion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace WrathPatches 8 | { 9 | static partial class Main 10 | { 11 | static Assembly GetHarmonyAss() => AppDomain.CurrentDomain.GetAssemblies() 12 | .FirstOrDefault(ass => ass.GetName().Name == "0Harmony"); 13 | 14 | static Version HarmonyVersion => GetHarmonyAss().GetName().Version; 15 | 16 | static Assembly GetUmmAss() => AppDomain.CurrentDomain.GetAssemblies() 17 | .FirstOrDefault(ass => ass.GetName().Name == "UnityModManager"); 18 | 19 | static string UmmHarmonyPath => Path.Combine(Path.GetDirectoryName(GetUmmAss().Location), "0Harmony.dll"); 20 | 21 | static Version UmmHarmonyVersion => Version.Parse(FileVersionInfo.GetVersionInfo(UmmHarmonyPath).FileVersion); 22 | 23 | static void ReplaceHarmony(string harmonyPath, string ummHarmonyPath) 24 | { 25 | Logger.Warning($"Copying {ummHarmonyPath} -> {harmonyPath}. Please restart the game."); 26 | File.Copy(harmonyPath, Path.Combine(Path.GetDirectoryName(harmonyPath), $"0Harmony.{HarmonyVersion}.old")); 27 | File.Copy(ummHarmonyPath, harmonyPath, true); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /WrathPatches/Patches/Experimental/BlueprintsCacheInitAddBlueprint.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | using HarmonyLib; 8 | 9 | using Kingmaker.Blueprints; 10 | using Kingmaker.Blueprints.JsonSystem; 11 | 12 | namespace WrathPatches.Experimental 13 | { 14 | #if DEBUG 15 | //[HarmonyPatchCategory("Experimental")] 16 | //[WrathPatch("Allow add existing in BlueprintsCache.Init")] 17 | //[HarmonyPatch(typeof(BlueprintsCache))] 18 | static class BlueprintsCacheInitAddBlueprint 19 | { 20 | //[HarmonyPatch(nameof(BlueprintsCache.Init))] 21 | //[HarmonyTranspiler] 22 | static IEnumerable Init_Transpiler(IEnumerable instructions) 23 | { 24 | Main.Logger.Log($"{nameof(BlueprintsCacheInitAddBlueprint)}.{nameof(Init_Transpiler)}"); 25 | 26 | var (_, instruction) = instructions.Indexed() 27 | .First(i => i.item.Calls( 28 | AccessTools.Method(typeof(Dictionary), 29 | nameof(Dictionary.Add)))); 30 | 31 | instruction.operand = AccessTools.PropertySetter(typeof(Dictionary), "Item"); 32 | 33 | return instructions; 34 | } 35 | } 36 | #endif 37 | } 38 | -------------------------------------------------------------------------------- /ShaderWrapper/ShaderWrapper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net472 5 | latest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | PreserveNewest 19 | 20 | 21 | 22 | 23 | 24 | 25 | all 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /WrathPatches/Patches/ActingInSurpriseRoundFix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Reflection.Emit; 6 | 7 | using HarmonyLib; 8 | 9 | using TurnBased.Controllers; 10 | 11 | namespace WrathPatches.Patches; 12 | 13 | [WrathPatch("Fix surprise round turns")] 14 | [HarmonyPatch] 15 | internal class ActingInSurpriseRoundFix 16 | { 17 | static IEnumerable TargetMethods() 18 | { 19 | yield return AccessTools.Method(typeof(CombatController), nameof(CombatController.HandleCombatStart)); 20 | foreach (var m in typeof(CombatController).GetNestedTypes(AccessTools.all) 21 | .SelectMany(AccessTools.GetDeclaredMethods) 22 | .Where(m => m.Name.Contains(nameof(CombatController.HandleCombatStart)))) 23 | yield return m; 24 | } 25 | 26 | static IEnumerable Transpiler(IEnumerable instructions, MethodBase __originalMethod) 27 | { 28 | var Timespan_get_Seconds = AccessTools.PropertyGetter(typeof(TimeSpan), nameof(TimeSpan.Seconds)); 29 | 30 | foreach (var i in instructions) 31 | { 32 | if (i.opcode == OpCodes.Call && (MethodInfo)i.operand == Timespan_get_Seconds) 33 | { 34 | i.operand = AccessTools.PropertyGetter(typeof(TimeSpan), nameof(TimeSpan.TotalSeconds)); 35 | #if DEBUG 36 | Main.Logger.Log($"[{nameof(ActingInSurpriseRoundFix)}] patched method {__originalMethod.FullDescription()}"); 37 | #endif 38 | } 39 | 40 | 41 | yield return i; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /WrathPatches/Patches/GameStatistic.Tick.PlayerNullFix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection.Emit; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | using HarmonyLib; 9 | 10 | using Kingmaker; 11 | 12 | namespace WrathPatches 13 | { 14 | [WrathPatch("GameStatistic.Tick null Player fix")] 15 | [HarmonyPatch(typeof(GameStatistic), nameof(GameStatistic.Tick))] 16 | internal static class GameStatistic_Tick_PlayerNullFix 17 | { 18 | static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) 19 | { 20 | var insertAfter = instructions.Indexed().FirstOrDefault(i => i.item.opcode == OpCodes.Nop); 21 | 22 | if (insertAfter == default) 23 | { 24 | return instructions; 25 | } 26 | 27 | var insertAtIndex = insertAfter.index + 1; 28 | 29 | var iList = instructions.ToList(); 30 | 31 | var targetNop = new CodeInstruction(OpCodes.Nop); 32 | var jumpLabel = generator.DefineLabel(); 33 | targetNop.labels.Add(jumpLabel); 34 | 35 | iList.InsertRange(insertAtIndex, new[] 36 | { 37 | new CodeInstruction(OpCodes.Ldarg_1), 38 | new CodeInstruction(OpCodes.Callvirt, AccessTools.PropertyGetter(typeof(Game), nameof(Game.Player))), 39 | new CodeInstruction(OpCodes.Brtrue_S, jumpLabel), 40 | new CodeInstruction(OpCodes.Ret), 41 | targetNop 42 | }); 43 | 44 | return iList; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /WrathPatches/Patches/MissingSettingsUISoundsFix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | using HarmonyLib; 9 | 10 | using Kingmaker.UI; 11 | using Kingmaker.UI.MVVM._PCView.Settings; 12 | 13 | namespace WrathPatches.Patches 14 | { 15 | [WrathPatch("Don't try to play missing settings UI sound types")] 16 | [HarmonyPatch] 17 | class MissingSettingsUISoundsFix 18 | { 19 | [HarmonyTargetMethods] 20 | static IEnumerable TargetMethods() => 21 | [ 22 | AccessTools.Method(typeof(SettingsPCView), nameof(SettingsPCView.Show)), 23 | AccessTools.Method(typeof(SettingsPCView), nameof(SettingsPCView.Hide)) 24 | ]; 25 | 26 | [HarmonyTranspiler] 27 | static IEnumerable Transpiler(IEnumerable instructions) 28 | { 29 | var iList = instructions.ToArray(); 30 | 31 | var UISoundController_Play = 32 | AccessTools.Method(typeof(UISoundController), nameof(UISoundController.Play), [typeof(UISoundType)]); 33 | 34 | for (var i = 0; i < iList.Length; i++) 35 | { 36 | if (iList[i].Calls(UISoundController_Play)) 37 | { 38 | #if DEBUG 39 | var value = (UISoundType)(sbyte)(iList[i - 1].operand); 40 | Main.Logger.Log($"{value} -> {UISoundType.None}"); 41 | #endif 42 | iList[i - 1].operand = (int)UISoundType.None; 43 | } 44 | } 45 | 46 | return iList; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /WrathPatches/Patches/CurrentSelectedCharacterNREFix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection.Emit; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | using HarmonyLib; 9 | 10 | using Kingmaker.Armies.TacticalCombat.Data; 11 | using Kingmaker.Controllers; 12 | using Kingmaker.EntitySystem.Entities; 13 | 14 | namespace WrathPatches.Patches; 15 | 16 | [WrathPatch("Fix NRE in SelectionCharacterController.CurrentSelectedCharacter")] 17 | [HarmonyPatch( 18 | typeof(SelectionCharacterController), 19 | nameof(SelectionCharacterController.CurrentSelectedCharacter), 20 | MethodType.Getter)] 21 | static class CurrentSelectedCharacterNREFix 22 | { 23 | static IEnumerable Transpiler(IEnumerable instructions, ILGenerator ilGen) 24 | { 25 | var matcher = new CodeMatcher(instructions, ilGen) 26 | .MatchStartForward( 27 | CodeMatch.Calls( 28 | AccessTools.PropertyGetter( 29 | typeof(TurnState), nameof(TurnState.Unit))), 30 | CodeMatch.StoresLocal(), 31 | CodeMatch.LoadsLocal(), 32 | CodeMatch.Calls( 33 | AccessTools.PropertyGetter( 34 | typeof(UnitEntityData), nameof(UnitEntityData.IsDirectlyControllable)))) 35 | .Advance(1) 36 | .InsertAndAdvance([new(OpCodes.Dup)]); 37 | 38 | return matcher 39 | .InsertBranchAndAdvance(OpCodes.Brtrue_S, matcher.Pos) 40 | .InsertAndAdvance([new(OpCodes.Pop), new(OpCodes.Ldnull), new(OpCodes.Ret)]) 41 | .InstructionEnumeration(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /WrathPatches/Patches/EventSubscriptionLeakFixes.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.Items; 3 | using Kingmaker.PubSubSystem; 4 | using Kingmaker.UI._ConsoleUI.TurnBasedMode; 5 | using Kingmaker.UI.MVVM._PCView.ActionBar; 6 | using Kingmaker.UI.MVVM._VM.ActionBar; 7 | using Kingmaker.UnitLogic.Abilities; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Reflection.Emit; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | using WrathPatches.TranspilerUtil; 15 | 16 | namespace WrathPatches.Patches 17 | { 18 | [HarmonyPatch] 19 | partial class EventSubscriptionLeakFixes 20 | { 21 | [HarmonyPatch(typeof(ActionBarVM), MethodType.Constructor)] 22 | [HarmonyTranspiler] 23 | static IEnumerable DoubleSubscribe_Transpiler(IEnumerable instructions) 24 | { 25 | var toMatch = new Func[] 26 | { 27 | ci => ci.opcode == OpCodes.Ldarg_0, 28 | ci => ci.Calls(AccessTools.Method(typeof(EventBus), nameof(EventBus.Subscribe), [typeof(object)])), 29 | ci => ci.opcode == OpCodes.Pop 30 | }; 31 | 32 | var match = instructions.FindInstructionsIndexed(toMatch).ToArray(); 33 | 34 | if (match.Length != toMatch.Length) 35 | throw new Exception("Could not find target instructions"); 36 | 37 | var iList = instructions.ToList(); 38 | 39 | foreach (var (index, _) in match) 40 | { 41 | iList[index].opcode = OpCodes.Nop; 42 | iList[index].operand = null; 43 | } 44 | 45 | return iList; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /WrathPatches/Patches/BloodyFaceControllerUnitEntityView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection.Emit; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | using HarmonyLib; 9 | 10 | using Kingmaker.View; 11 | using Kingmaker.Visual.MaterialEffects; 12 | 13 | using Owlcat.Runtime.Core; 14 | 15 | namespace WrathPatches.Patches 16 | { 17 | [WrathPatch("No BloodyFaceController for non-UnitEntityView")] 18 | [HarmonyPatch(typeof(StandardMaterialController), nameof(StandardMaterialController.Awake))] 19 | internal class BloodyFaceControllerUnitEntityView 20 | { 21 | static bool HasUnitEntityView(StandardMaterialController smc) => smc.gameObject.GetComponent() is not null; 22 | 23 | [HarmonyTranspiler] 24 | static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) 25 | { 26 | var Application_isPlaying = 27 | AccessTools.PropertyGetter(typeof(UnityEngine.Application), nameof(UnityEngine.Application.isPlaying)); 28 | 29 | var index = instructions.FindIndex(ci => ci.Calls(Application_isPlaying)); 30 | 31 | if (index < 0) 32 | throw new Exception("Could not find target instruction"); 33 | 34 | var iList = instructions.ToList(); 35 | 36 | var ifFalse = iList[index + 1]; 37 | 38 | iList.InsertRange(index - 2, 39 | [ 40 | new CodeInstruction(OpCodes.Ldarg_0), 41 | CodeInstruction.Call((StandardMaterialController x) => HasUnitEntityView(x)), 42 | ifFalse 43 | ]); 44 | 45 | return iList; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /WrathPatches/Patches/LoadAssembliesFix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection.Emit; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | using HarmonyLib; 10 | 11 | using Kingmaker.Modding; 12 | 13 | namespace WrathPatches.Patches 14 | { 15 | [WrathPatch("Only load .dll files from owlmod Assemblies directories")] 16 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.LoadAssemblies))] 17 | static class LoadAssembliesFix 18 | { 19 | static IEnumerable GetAssemblies(IEnumerable files) 20 | { 21 | //return files.Where(f => Path.GetExtension(f) == ".dll"); 22 | 23 | foreach (var f in files) 24 | { 25 | if (Path.GetExtension(f) == ".dll") 26 | { 27 | #if DEBUG 28 | Main.Logger.Log($"Loading assembly: {f}"); 29 | #endif 30 | 31 | yield return f; 32 | } 33 | #if DEBUG 34 | else 35 | { 36 | Main.Logger.Log($"Skipping non-dll file: {f}"); 37 | } 38 | #endif 39 | } 40 | } 41 | static IEnumerable Transpiler(IEnumerable instructions) 42 | { 43 | foreach (var i in instructions) 44 | { 45 | yield return i; 46 | 47 | if (i.Calls(AccessTools.Method(typeof(OwlcatModification), nameof(OwlcatModification.GetFilesFromDirectory)))) 48 | { 49 | yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(LoadAssembliesFix), nameof(GetAssemblies))); 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /WrathPatches/Patches/OwlcatButtonClickDelayFix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Reflection.Emit; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | using HarmonyLib; 10 | 11 | using Owlcat.Runtime.UI.Controls.Button; 12 | 13 | using WrathPatches.TranspilerUtil; 14 | 15 | namespace WrathPatches.Patches; 16 | 17 | [WrathPatch("Reduce button click delay from 300ms to 1 frame")] 18 | [HarmonyPatch] 19 | internal static class OwlcatButtonClickDelayFix 20 | { 21 | internal static IEnumerable TargetMethods() => new Type[] 22 | { 23 | typeof(OwlcatButton), 24 | typeof(OwlcatMultiButton) 25 | }.SelectMany(t => t 26 | .GetNestedTypes(AccessTools.all) 27 | .Where(t => t.Name.Contains("LeftClickTime"))) 28 | .Select(t => AccessTools.Method(t, "MoveNext")); 29 | 30 | [HarmonyTranspiler] 31 | internal static IEnumerable Transpiler(IEnumerable instructions, MethodBase originalMethod) 32 | { 33 | var match = instructions.FindInstructionsIndexed( 34 | [ 35 | ci => ci.opcode == OpCodes.Ldc_R4 && (float)ci.operand == 0.3f, 36 | ci => ci.opcode == OpCodes.Blt_S 37 | ]).ToArray(); 38 | 39 | if (match.Length != 2) 40 | { 41 | Main.Logger.Warning($"Could not find patch target in {originalMethod}"); 42 | 43 | #if DEBUG 44 | Main.Logger.Log(string.Join("\n", instructions)); 45 | #endif 46 | 47 | return instructions; 48 | } 49 | 50 | var iList = instructions.ToList(); 51 | 52 | iList[match[0].index].operand = 0.0f; 53 | iList[match[1].index].opcode = OpCodes.Bgt_S; 54 | 55 | return iList; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /WrathPatches/Patches/KeyboardAccess.Bind.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Reflection.Emit; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | using HarmonyLib; 10 | 11 | using Kingmaker; 12 | using Kingmaker.UI; 13 | 14 | using WrathPatches.TranspilerUtil; 15 | 16 | namespace WrathPatches 17 | { 18 | [WrathPatch("Silence 'no binding' warnings")] 19 | [HarmonyPatch] 20 | internal static class SilenceNoBindingLog 21 | { 22 | [HarmonyTargetMethods] 23 | static IEnumerable Methods() => 24 | [ 25 | AccessTools.Method(typeof(KeyboardAccess), nameof(KeyboardAccess.Bind)), 26 | AccessTools.Method(typeof(KeyboardAccess), nameof(KeyboardAccess.DoUnbind)) 27 | ]; 28 | 29 | [HarmonyTranspiler] 30 | static IEnumerable Patch_Transpiler(IEnumerable instructions) 31 | { 32 | var PFLog_get_Default = AccessTools.PropertyGetter(typeof(PFLog), nameof(PFLog.Default)); 33 | 34 | var match = instructions.FindInstructionsIndexed( 35 | [ 36 | ci => ci.opcode == OpCodes.Brfalse_S, 37 | ci => ci.Calls(PFLog_get_Default), 38 | ci => ci.opcode == OpCodes.Ldstr && ci.operand as string == "Bind: no binding named {0}" 39 | ]); 40 | 41 | if (match.Count() != 3) 42 | throw new KeyNotFoundException("Unable to find patch location"); 43 | 44 | var (index, i) = match.First(); 45 | 46 | var iList = instructions.ToList(); 47 | 48 | iList[index] = new CodeInstruction(OpCodes.Br, i.operand); 49 | iList.Insert(index, new CodeInstruction(OpCodes.Pop)); 50 | 51 | return iList; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /WrathPatches/Patches/SetMagusFeatureActiveFix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection.Emit; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | using HarmonyLib; 9 | 10 | using Kingmaker.Items; 11 | using Kingmaker.UnitLogic.FactLogic; 12 | using WrathPatches.TranspilerUtil; 13 | 14 | namespace WrathPatches.Patches 15 | { 16 | [WrathPatch("Remove unnecessary code in SetMagusFeatureActive.OnTurnOff")] 17 | [HarmonyPatch(typeof(SetMagusFeatureActive), nameof(SetMagusFeatureActive.OnTurnOff))] 18 | internal static class SetMagusFeatureActiveFix 19 | { 20 | [HarmonyTranspiler] 21 | static IEnumerable Transpiler(IEnumerable instructions) 22 | { 23 | var HandsEquipmentSet_get_GripType = AccessTools.PropertyGetter(typeof(HandsEquipmentSet), nameof(HandsEquipmentSet.GripType)); 24 | 25 | var toMatch = new Func[] 26 | { 27 | ci => ci.opcode == OpCodes.Ldarg_0, 28 | ci => ci.opcode == OpCodes.Call, 29 | ci => ci.opcode == OpCodes.Callvirt, 30 | ci => ci.opcode == OpCodes.Callvirt, 31 | ci => ci.Calls(HandsEquipmentSet_get_GripType), 32 | ci => ci.opcode == OpCodes.Pop 33 | }; 34 | 35 | var match = instructions.FindInstructionsIndexed(toMatch).ToArray(); 36 | 37 | if (match.Length != toMatch.Length) 38 | throw new Exception("Could not find target instructions"); 39 | 40 | var iList = instructions.ToList(); 41 | 42 | foreach (var (index, _) in match) 43 | { 44 | iList[index].opcode = OpCodes.Nop; 45 | iList[index].operand = null; 46 | } 47 | 48 | return iList; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /WrathPatches/Patches/ModReloadClearBlueprintReferenceCache.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using HarmonyLib; 4 | 5 | using Kingmaker.Blueprints; 6 | using Kingmaker.Modding; 7 | 8 | namespace WrathPatches.Patches; 9 | 10 | [WrathPatch("Clear BlueprintReference caches when reloading OMM mods")] 11 | [HarmonyPatch] 12 | public static class ModReloadClearBlueprintReferenceCache 13 | { 14 | static readonly HashSet references = []; 15 | 16 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.Reload))] 17 | [HarmonyPrefix] 18 | public static void ClearCaches(OwlcatModification __instance) 19 | { 20 | var rs = references 21 | .Where(r => __instance.Blueprints.Contains(r.guid)) 22 | .ToArray(); 23 | 24 | #if DEBUG 25 | Main.Logger.Log($"Clearing {rs.Length} blueprint reference caches for {__instance.Manifest.UniqueName}"); 26 | #endif 27 | 28 | foreach (var r in rs) 29 | { 30 | r.Cached = null; 31 | _ = references.Remove(r); 32 | } 33 | } 34 | 35 | [HarmonyPatch( 36 | typeof(BlueprintReferenceBase), 37 | nameof(BlueprintReferenceBase.Cached), 38 | MethodType.Setter), HarmonyPrefix] 39 | static void SetCached_Prefix(BlueprintReferenceBase __instance, SimpleBlueprint value) 40 | { 41 | if (value is null) 42 | return; 43 | 44 | var assetId = __instance.Guid.ToString(); 45 | 46 | foreach (var mod in OwlcatModificationsManager.Instance.AppliedModifications) 47 | if (mod.Blueprints.Contains(assetId)) 48 | { 49 | #if DEBUG 50 | Main.Logger.Log($"Tracking reference {__instance} to mod {mod.Manifest.UniqueName} blueprint {value}"); 51 | #endif 52 | 53 | _ = references.Add(__instance); 54 | break; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /WrathPatches.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.7.33711.374 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WrathPatches", "WrathPatches\WrathPatches.csproj", "{2D651098-A88E-4BC8-8008-2D4414497A77}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4DE6A293-0C63-4588-A521-A577923C51A3}" 9 | ProjectSection(SolutionItems) = preProject 10 | common.props = common.props 11 | Repository.json = Repository.json 12 | EndProjectSection 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShaderWrapper", "ShaderWrapper\ShaderWrapper.csproj", "{CCD98E38-C932-49F6-8DEE-CDFE87E7DA82}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {2D651098-A88E-4BC8-8008-2D4414497A77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {2D651098-A88E-4BC8-8008-2D4414497A77}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {2D651098-A88E-4BC8-8008-2D4414497A77}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {2D651098-A88E-4BC8-8008-2D4414497A77}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {CCD98E38-C932-49F6-8DEE-CDFE87E7DA82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {CCD98E38-C932-49F6-8DEE-CDFE87E7DA82}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {CCD98E38-C932-49F6-8DEE-CDFE87E7DA82}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {CCD98E38-C932-49F6-8DEE-CDFE87E7DA82}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {ED66E901-F169-4704-980D-5A24E4C767D6} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /WrathPatches/Patches/ActuallyFixSharedStrings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection.Emit; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | using UnityEngine; 9 | 10 | using Kingmaker.Blueprints.JsonSystem.Converters; 11 | using Kingmaker.Localization; 12 | 13 | using HarmonyLib; 14 | 15 | using System.Reflection; 16 | 17 | namespace WrathPatches 18 | { 19 | [WrathPatch("Fix Shared Strings")] 20 | [HarmonyPatch] 21 | public static class ActuallyFixSharedStrings 22 | { 23 | static MethodInfo? CreateSharedStringInstance => typeof(ScriptableObject) 24 | .GetMembers(BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy) 25 | .OfType() 26 | .Where(m => m.Name == nameof(ScriptableObject.CreateInstance) && m.IsGenericMethodDefinition) 27 | .FirstOrDefault() 28 | ?.MakeGenericMethod(typeof(SharedStringAsset)); 29 | 30 | [HarmonyPatch(typeof(SharedStringConverter), nameof(SharedStringConverter.ReadJson))] 31 | [HarmonyTranspiler] 32 | static IEnumerable SharedStringConverter_ReadJson_Transpiler(IEnumerable instructions) 33 | { 34 | //Main.Logger.Log($"{nameof(SharedStringConverter_ReadJson_Transpiler)}"); 35 | 36 | var ssCons = typeof(SharedStringAsset).GetConstructor([]); 37 | 38 | var createSs = CreateSharedStringInstance ?? throw new Exception($"Unable to find or create constructor"); 39 | 40 | var count = 0; 41 | 42 | foreach (var i in instructions) 43 | { 44 | if (i.Is(OpCodes.Newobj, ssCons)) 45 | yield return new CodeInstruction(OpCodes.Call, createSs); 46 | 47 | else yield return i; 48 | 49 | count++; 50 | } 51 | 52 | if (count == 0) 53 | throw new Exception("Could not find instructions to patch"); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wrath Patches 2 | 3 | A collection of small patches for Pathfinder: Wrath of the Righteous. Mostly aimed at cleaning up the 4 | game's log files, but fixed some minor bugs along the way. 5 | 6 | Any patch can be disabled by removing the corresponding dll from the mod directory. 7 | 8 | Thanks to Kurufinve on the Owlcat discord for the assistance and inspiration. Also Kurufinve and Hambeard for their contributions. 9 | 10 | ## Current fixes 11 | 12 | This list is infrequently updated and likely out of date 13 | 14 | - Add AbilityData spell descriptor to AbilityExecutionContext 15 | - Fix Shared Strings 16 | - AddFacts.UpdateFacts NRE fix 17 | - Silence missing animator state errors 18 | - Add per-OwlMod log sinks 19 | - Use OwlMod logger in place of PFLog.Mods 20 | - Catch and log Blueprint patch exceptions 21 | - No BloodyFaceController for non-UnitEntityView 22 | - Fix NRE in SelectionCharacterController.CurrentSelectedCharacter 23 | - Element AssetGuid null fix 24 | - Better EntityFactComponent error messages 25 | - Make BlueprintBeingRead ThreadStatic 26 | - GameStatistic.Tick null Player fix 27 | - Event subscription leak fixes 28 | - Silence 'no binding' warnings 29 | - Silence 'Await event system' messages 30 | - Only load .dll files from owlmod Assemblies directories 31 | - Less verbose short log exceptions 32 | - OpenGameLogFull opens Player.log too 33 | - Don't try to play missing settings UI sound types 34 | - OwlcatModificationManager: UMM mods as dependencies for OMM mods 35 | - OwlcatModificationManager: Compare OMM mod versions like UMM 36 | - Reduce button click delay from 300ms to 1 frame 37 | - Add components from mod assemblies to binder cache 38 | - Load OwlMod BlueprintDirectReferences dependencies 39 | - Non-matching BlueprintComponent.OwnerBlueprint warning 40 | - Make ProgressionRoot.HumanRace not break for new races 41 | - Hambeard: Fix save slot VM double bind 42 | - Remove unnecessary code in SetMagusFeatureActive.OnTurnOff 43 | - Kurufinve: ShaderWrapper fix for owlmod material shaders 44 | - Fix Spell Slot comparison 45 | - Kurufinve: Owlmod Enhancer 46 | - Fix UnitMarkManager.LateUpdate iterator mutation 47 | -------------------------------------------------------------------------------- /WrathPatches/Patches/UnitMarkManager.LateUpdate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection.Emit; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | using HarmonyLib; 9 | 10 | using Kingmaker.UI.Selection; 11 | 12 | using WrathPatches.TranspilerUtil; 13 | 14 | namespace WrathPatches 15 | { 16 | [WrathPatch("Fix UnitMarkManager.LateUpdate iterator mutation")] 17 | [HarmonyPatch(typeof(UnitMarkManager), nameof(UnitMarkManager.LateUpdate))] 18 | internal static class UnitMarkManager_LateUpdate 19 | { 20 | [HarmonyTranspiler] 21 | static IEnumerable Transpiler(IEnumerable instructions) 22 | { 23 | var UnitMarkManager_m_Marks = typeof(UnitMarkManager).GetField(nameof(UnitMarkManager.m_Marks), AccessTools.all); 24 | var Dictionary_string_UIDecalBase_Values = 25 | AccessTools.PropertyGetter(typeof(Dictionary), nameof(Dictionary.Values)); 26 | var Dictionary_string_UIDecalBase_ValueCollection = 27 | typeof(Dictionary.ValueCollection) 28 | .GetMethod(nameof(Dictionary.ValueCollection.GetEnumerator)); 29 | 30 | var match = new Func[] 31 | { 32 | ci => ci.opcode == OpCodes.Ldarg_0, 33 | ci => ci.LoadsField(UnitMarkManager_m_Marks), 34 | ci => ci.Calls(Dictionary_string_UIDecalBase_Values), 35 | ci => ci.Calls(Dictionary_string_UIDecalBase_ValueCollection) 36 | }; 37 | 38 | var iList = instructions.ToList(); 39 | 40 | var iMatch = instructions.FindInstructionsIndexed(match); 41 | 42 | if (!iMatch.Any()) return instructions; 43 | 44 | var index = iMatch.First().index + 2; 45 | 46 | iList.Insert(index, new CodeInstruction(OpCodes.Newobj, 47 | AccessTools.Constructor(typeof(Dictionary), new[] { typeof(Dictionary) } ))); 48 | 49 | return iList; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /WrathPatches/Patches/AnimationClipWrapperStateMachineBehaviour.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Reflection.Emit; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | using HarmonyLib; 10 | 11 | using Kingmaker.Visual.Animation.Events; 12 | 13 | namespace WrathPatches 14 | { 15 | [WrathPatch("Silence missing animator state errors")] 16 | [HarmonyPatch(typeof(AnimationClipWrapperStateMachineBehaviour))] 17 | internal static class AnimationClipWrapperStateMachineBehaviour_Patch 18 | { 19 | [HarmonyPatch(nameof(AnimationClipWrapperStateMachineBehaviour.OnStateEnter))] 20 | [HarmonyTranspiler] 21 | static IEnumerable OnStateEnter_Transpiler(IEnumerable instructions) 22 | { 23 | var AnimationClipWrapperStateMachineBehaviour_AnimationParameterIdleOffsetIsMissing = 24 | AccessTools.Field(typeof(AnimationClipWrapperStateMachineBehaviour), 25 | nameof(AnimationClipWrapperStateMachineBehaviour.AnimationParameterIdleOffsetIsMissing)); 26 | 27 | var Nullable_bool_Value = AccessTools.PropertyGetter(typeof(bool?), nameof(Nullable.Value)); 28 | 29 | var matched = instructions.FindSequence( 30 | [ 31 | ci => ci.opcode == OpCodes.Ldflda && 32 | ((FieldInfo)ci.operand) == AnimationClipWrapperStateMachineBehaviour_AnimationParameterIdleOffsetIsMissing, 33 | ci => 34 | ci.opcode == OpCodes.Call && ((MethodInfo)ci.operand) == Nullable_bool_Value, 35 | ci => ci.opcode == OpCodes.Brfalse_S 36 | ]).ToArray(); 37 | 38 | if (matched.Length == 3) 39 | { 40 | matched[1].opcode = OpCodes.Pop; 41 | matched[1].operand = null; 42 | 43 | matched[2].opcode = OpCodes.Br; 44 | } 45 | else 46 | { 47 | Main.Logger.Error($"{nameof(AnimationClipWrapperStateMachineBehaviour_Patch)}.{nameof(OnStateEnter_Transpiler)} failed to find instructions to patch"); 48 | } 49 | 50 | return instructions; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /WrathPatches/Patches/WeaponSetGripVMFix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Reflection.Emit; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | using HarmonyLib; 10 | 11 | using Kingmaker.PubSubSystem; 12 | using Kingmaker.UI.MVVM._VM.Slots; 13 | using Kingmaker.UnitLogic.FactLogic; 14 | using Kingmaker.Utility; 15 | 16 | namespace WrathPatches.Patches 17 | { 18 | #if false 19 | //[HarmonyPatchCategory("Experimental")] 20 | [WrathPatch("Add null check to WeaponSetGripVM.HandleUnitChangedGripAutoMode")] 21 | [HarmonyPatch] 22 | internal static class WeaponSetGripVMFix 23 | { 24 | [HarmonyTargetMethod] 25 | static MethodInfo TargetMethod() 26 | { 27 | var interfaceMap = typeof(WeaponSetGripVM).GetInterfaceMap(typeof(IUnityChangedGripAutoModeHandler)); 28 | 29 | if (interfaceMap.InterfaceMethods 30 | .Zip(interfaceMap.TargetMethods) 31 | .TryFind((pair) => 32 | pair.Item1.Name == nameof(IUnityChangedGripAutoModeHandler.HandleUnitChangedGripAutoMode), 33 | out var pair)) 34 | { 35 | return pair.Item2; 36 | } 37 | 38 | throw new Exception("Missing interface method"); 39 | } 40 | 41 | [HarmonyTranspiler] 42 | static IEnumerable Transpiler(IEnumerable instructions, ILGenerator ilGen) 43 | { 44 | var index = instructions.FindIndex(ci => 45 | ci.LoadsField(AccessTools.Field(typeof(WeaponSetGripVM), nameof(WeaponSetGripVM.m_HandsEquipmentSet)))); 46 | 47 | if (index < 0) 48 | { 49 | throw new Exception("Could not find target instruction"); 50 | } 51 | 52 | var iList = instructions.ToList(); 53 | 54 | var label = ilGen.DefineLabel(); 55 | 56 | iList.InsertRange(index + 1, 57 | [ 58 | new(OpCodes.Dup), 59 | new(OpCodes.Brtrue_S, label), 60 | new(OpCodes.Pop), 61 | new(OpCodes.Pop), 62 | new(OpCodes.Ret), 63 | new(OpCodes.Nop) { labels = [label] } 64 | ]); 65 | 66 | return iList; 67 | } 68 | } 69 | #endif 70 | } 71 | -------------------------------------------------------------------------------- /WrathPatches/Patches/KingmakerInputModule.CheckEventSystem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Reflection.Emit; 6 | using System.Runtime.CompilerServices; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | using HarmonyLib; 11 | 12 | using Kingmaker.UI.Selection; 13 | 14 | using Owlcat.Runtime.Core.Logging; 15 | 16 | using WrathPatches.TranspilerUtil; 17 | 18 | namespace WrathPatches 19 | { 20 | [WrathPatch("Silence 'Await event system' messages")] 21 | [HarmonyPatch] 22 | internal static class KingmakerInputModule_CheckEventSystem 23 | { 24 | static MethodBase? TargetMethod() => 25 | typeof(KingmakerInputModule) 26 | .GetNestedTypes(AccessTools.all) 27 | .Where(t => t.GetCustomAttributes().Any()) 28 | .Select(t => t.GetMethod("MoveNext", AccessTools.all)) 29 | .FirstOrDefault(); 30 | 31 | [HarmonyTranspiler] 32 | static IEnumerable Transpiler(IEnumerable instructions) 33 | { 34 | Main.Logger.Log($"{nameof(KingmakerInputModule_CheckEventSystem)}.{nameof(Transpiler)}"); 35 | 36 | var LogChannel_System = typeof(LogChannel).GetField(nameof(LogChannel.System), AccessTools.all); 37 | 38 | var match = new Func[] 39 | { 40 | ci => ci.LoadsField(LogChannel_System), 41 | ci => ci.opcode == OpCodes.Ldstr && "Await event system".Equals(ci.operand), 42 | ci => ci.opcode == OpCodes.Call, 43 | ci => ci.opcode == OpCodes.Callvirt 44 | }; 45 | 46 | var iMatch = instructions.FindInstructionsIndexed(match); 47 | 48 | if (!iMatch.Any()) 49 | { 50 | Main.Logger.Log("No match found"); 51 | return instructions; 52 | } 53 | 54 | var iList = instructions.ToList(); 55 | 56 | foreach ((var index, var _) in iMatch) 57 | { 58 | var i = new CodeInstruction(OpCodes.Nop) 59 | { 60 | labels = iList[index].labels, 61 | blocks = iList[index].blocks 62 | }; 63 | iList[index] = i; 64 | } 65 | 66 | return iList; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /WrathPatches/TranspilerUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection.Emit; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | using HarmonyLib; 9 | 10 | namespace WrathPatches.TranspilerUtil 11 | { 12 | public static class TranspilerUtil 13 | { 14 | public static IEnumerable GetNestedTypesAndSelf(this Type t) 15 | { 16 | yield return t; 17 | 18 | foreach (var n in t.GetNestedTypes().SelectMany(n => n.GetNestedTypesAndSelf())) 19 | { 20 | yield return n; 21 | } 22 | } 23 | 24 | public static IEnumerable<(int index, CodeInstruction instruction)> FindInstructionsIndexed( 25 | this IEnumerable instructions, 26 | IEnumerable> matchFuncs) 27 | { 28 | var matched = instructions 29 | .Indexed() 30 | .FindSequence(matchFuncs 31 | .Select, Func<(int index, CodeInstruction item), bool>>(f => 32 | i => f(i.item))); 33 | 34 | if (!matched.Any()) return Enumerable.Empty<(int, CodeInstruction)>(); 35 | 36 | return matched; 37 | } 38 | 39 | public static IEnumerable ReplaceInstructions( 40 | IEnumerable source, 41 | IEnumerable match, 42 | IEnumerable replaceWith) 43 | { 44 | var matchIndexed = match.Select>(m => 45 | ((int, CodeInstruction instruction) ici) => 46 | m.opcode == ici.instruction.opcode && 47 | (m.operand is null || m.operand == ici.instruction.operand)); 48 | 49 | (int index, CodeInstruction i)[] matchedInstructions = source.Indexed().FindSequence(matchIndexed).ToArray(); 50 | 51 | if (!matchedInstructions.Any()) 52 | { 53 | return Enumerable.Empty(); 54 | } 55 | 56 | var index = matchedInstructions.First().index; 57 | 58 | var iList = source.ToList(); 59 | 60 | iList.RemoveRange(index, matchedInstructions.Length); 61 | iList.InsertRange(index, replaceWith); 62 | 63 | 64 | return iList; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /WrathPatches/Patches/BetterModLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Reflection.Emit; 7 | using System.Runtime.CompilerServices; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | using HarmonyLib; 12 | 13 | using Kingmaker; 14 | using Kingmaker.Blueprints; 15 | using Kingmaker.Blueprints.JsonSystem; 16 | using Kingmaker.Localization.Shared; 17 | using Kingmaker.Modding; 18 | using Kingmaker.UnitLogic.FactLogic; 19 | 20 | using Newtonsoft.Json; 21 | 22 | using Owlcat.Runtime.Core.Logging; 23 | 24 | namespace WrathPatches.Patches; 25 | 26 | [WrathPatch("Add per-OwlMod log sinks")] 27 | [HarmonyPatch] 28 | static class BetterModLogger 29 | { 30 | static readonly Dictionary ModLoggers = []; 31 | 32 | [HarmonyPatch(typeof(OwlcatModification), MethodType.Constructor, [typeof(string), typeof(string), typeof(OwlcatModificationManifest), typeof(Exception)])] 33 | [HarmonyPostfix] 34 | internal static void AddModLogSink(OwlcatModification __instance, string dataFolderPath) 35 | { 36 | if (__instance is null || __instance.Manifest is null) 37 | return; 38 | 39 | Main.Logger.Log($"{__instance.Manifest.UniqueName}.Logger.Name = {__instance.Logger?.Name}"); 40 | 41 | if (__instance.Logger is null || __instance.Logger.Name != __instance.Manifest.UniqueName) 42 | return; 43 | 44 | var path = Path.GetDirectoryName(__instance.DataFilePath); 45 | 46 | var fileName = $"{OwlcatModification.InvalidPathCharsRegex.Replace(__instance.Manifest.UniqueName, "")}_Log.txt"; 47 | 48 | Main.Logger.Log($"Log file path: {Path.Combine(path, fileName)}"); 49 | 50 | if (ModLoggers.ContainsKey(fileName)) 51 | { 52 | PFLog.Mods.Error($"Logger for {__instance.Manifest.UniqueName} has already been created"); 53 | return; 54 | } 55 | 56 | var sink = new UberLoggerFilter(new UberLoggerFile(fileName, path), LogSeverity.Disabled, [__instance.Manifest.UniqueName]); 57 | 58 | ModLoggers.Add(fileName, sink); 59 | 60 | Logger.Instance.AddLogger(sink, false); 61 | } 62 | } 63 | 64 | [WrathPatch("Use OwlMod logger in place of PFLog.Mods")] 65 | [HarmonyPatch] 66 | static class PatchModLogInvocations 67 | { 68 | static IEnumerable TargetMethods() => 69 | AccessTools.GetDeclaredMethods(typeof(OwlcatModification)).Where(m => !m.IsStatic && !m.IsGenericMethod); 70 | 71 | static IEnumerable Transpiler(IEnumerable instructions) 72 | { 73 | foreach (var i in instructions) 74 | { 75 | if (i.Calls(AccessTools.PropertyGetter(typeof(PFLog), nameof(PFLog.Mods)))) 76 | { 77 | yield return new(OpCodes.Ldarg_0); 78 | 79 | i.opcode = OpCodes.Ldfld; 80 | i.operand = AccessTools.Field(typeof(OwlcatModification), nameof(OwlcatModification.Logger)); 81 | } 82 | 83 | yield return i; 84 | } 85 | } 86 | } 87 | 88 | [WrathPatch("Catch and log Blueprint patch exceptions")] 89 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.TryPatchBlueprint))] 90 | static class CatchBlueprintPatchExceptions 91 | { 92 | static Exception? Finalizer(Exception __exception, OwlcatModification __instance) 93 | { 94 | if (__exception is not null) 95 | __instance.Logger.Exception(__exception); 96 | 97 | return null; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /WrathPatches/HarmonyPatchCategory.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using System; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | using HarmonyLib; 9 | 10 | namespace WrathPatches 11 | { 12 | internal static class HarmonyAttributeExtensions 13 | { 14 | static readonly FieldInfo containerType = 15 | typeof(PatchClassProcessor).GetField(nameof(containerType), BindingFlags.Instance | BindingFlags.NonPublic); 16 | 17 | static readonly FieldInfo containerAttributes = 18 | typeof(PatchClassProcessor).GetField(nameof(containerAttributes), BindingFlags.Instance | BindingFlags.NonPublic); 19 | 20 | public static bool HasPatchAttribute(this PatchClassProcessor patchClass) => containerAttributes.GetValue(patchClass) is not null; 21 | 22 | public static string? GetCategory(this PatchClassProcessor patchClass) => 23 | (containerType.GetValue(patchClass) as Type)?.GetCustomAttribute()?.Category; 24 | 25 | /// Searches an assembly for Harmony-annotated classes without category annotations and uses them to create patches 26 | /// The harmony instance 27 | /// 28 | public static void PatchAllUncategorized(this Harmony harmony) 29 | { 30 | var method = new StackTrace().GetFrame(1).GetMethod(); 31 | var assembly = method.ReflectedType.Assembly; 32 | harmony.PatchAllUncategorized(assembly); 33 | } 34 | 35 | /// Searches an assembly for Harmony-annotated classes without category annotations and uses them to create patches 36 | /// The harmony instance 37 | /// The assembly 38 | /// 39 | public static void PatchAllUncategorized(this Harmony harmony, Assembly assembly) 40 | { 41 | var patchClasses = AccessTools.GetTypesFromAssembly(assembly).Select(harmony.CreateClassProcessor).ToArray(); 42 | patchClasses.DoIf((patchClass => string.IsNullOrEmpty(patchClass.GetCategory())), (patchClass => patchClass.Patch())); 43 | } 44 | 45 | /// Searches an assembly for Harmony annotations with a specific category and uses them to create patches 46 | /// The harmony instance 47 | /// Name of patch category 48 | /// 49 | public static void PatchCategory(this Harmony harmony, string category) 50 | { 51 | var method = new StackTrace().GetFrame(1).GetMethod(); 52 | var assembly = method.ReflectedType.Assembly; 53 | harmony.PatchCategory(assembly, category); 54 | } 55 | 56 | /// Searches an assembly for Harmony annotations with a specific category and uses them to create patches 57 | /// The harmony instance 58 | /// The assembly 59 | /// Name of patch category 60 | /// 61 | public static void PatchCategory(this Harmony harmony, Assembly assembly, string category) 62 | { 63 | var patchClasses = AccessTools.GetTypesFromAssembly(assembly).Select(harmony.CreateClassProcessor).ToArray(); 64 | patchClasses.DoIf((patchClass => patchClass.GetCategory() == category), (patchClass => patchClass.Patch())); 65 | } 66 | } 67 | 68 | /// Annotation to define a category for use with PatchCategory 69 | /// 70 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] 71 | internal class HarmonyPatchCategory : Attribute 72 | { 73 | public readonly string Category; 74 | 75 | /// Annotation specifying the category 76 | /// Name of patch category 77 | /// 78 | public HarmonyPatchCategory(string category) 79 | { 80 | this.Category = category; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /WrathPatches/Patches/LogExceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection.Emit; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | using HarmonyLib; 10 | 11 | using Kingmaker; 12 | using Kingmaker.Logging.Configuration.Platforms; 13 | 14 | using Owlcat.Runtime.Core.Logging; 15 | 16 | namespace WrathPatches 17 | { 18 | [WrathPatch("Less verbose short log exceptions")] 19 | [HarmonyPatch] 20 | internal static class Log_Exceptions_Patch 21 | { 22 | class ShortLogWithoutCallstacks : UberLoggerFile, IDisposableLogSink, ILogSink 23 | { 24 | public ShortLogWithoutCallstacks(string filename, string path = null!, bool includeCallStacks = true, bool extendedLog = false) 25 | : base(filename, path, includeCallStacks, extendedLog) 26 | { 27 | #if DEBUG 28 | Main.Logger.Log(string.Join(" ", "new", typeof(ShortLogWithoutCallstacks))); 29 | 30 | if (this.LogFileWriter.BaseStream is not FileStream fs) 31 | Main.Logger.Error($"{nameof(LogFileWriter)} is not a FileStream"); 32 | else 33 | Main.Logger.Log($"File: {fs.Name}"); 34 | #endif 35 | } 36 | 37 | void ILogSink.Log(LogInfo logInfo) 38 | { 39 | #if DEBUG 40 | Main.Logger.Log(string.Join(".", nameof(ShortLogWithoutCallstacks), nameof(Log))); 41 | Main.Logger.Log($"IsException? {logInfo.IsException}"); 42 | Main.Logger.Log(logInfo.Message); 43 | #endif 44 | 45 | if (!logInfo.IsException) 46 | { 47 | logInfo = new LogInfo( 48 | logInfo.Source, 49 | logInfo.Channel, 50 | logInfo.TimeStamp, 51 | logInfo.Severity, 52 | null, 53 | logInfo.IsException, 54 | logInfo.Message); 55 | } 56 | 57 | base.Log(logInfo); 58 | } 59 | } 60 | 61 | [HarmonyPatch(typeof(LogSinkFactory), nameof(LogSinkFactory.CreateShort))] 62 | [HarmonyTranspiler] 63 | static IEnumerable LogSinkFactory_CreateShort_Transpiler(IEnumerable instructions) 64 | { 65 | Main.Logger.Log(nameof(LogSinkFactory_CreateShort_Transpiler)); 66 | 67 | foreach (var i in instructions) 68 | { 69 | if (i.opcode == OpCodes.Newobj && 70 | typeof(UberLoggerFile).GetConstructors(AccessTools.all) 71 | .Any(i.OperandIs)) 72 | i.operand = typeof(ShortLogWithoutCallstacks).GetConstructors().First(); 73 | 74 | yield return i; 75 | } 76 | } 77 | 78 | [HarmonyPatch(typeof(Logger), nameof(Logger.Log))] 79 | [HarmonyTranspiler] 80 | static IEnumerable Logger_Log_Transpiler(IEnumerable instructions, ILGenerator ilGen) 81 | { 82 | var exParamIndex = 4; 83 | 84 | var lb = ilGen.DeclareLocal(typeof(Exception)); 85 | lb.SetLocalSymInfo("ex2"); 86 | var localIndex = lb.LocalIndex; 87 | 88 | var iList = instructions.ToList(); 89 | 90 | iList.InsertRange(0, new[] 91 | { 92 | new CodeInstruction(OpCodes.Ldarg_S, exParamIndex), // Exception ex 93 | new CodeInstruction(OpCodes.Stloc_S, localIndex), 94 | }); 95 | 96 | var logInfoCtorIndex = iList 97 | .Indexed() 98 | .Where(i => i.item.opcode == OpCodes.Newobj && 99 | typeof(LogInfo).GetConstructors() 100 | .Any(i.item.OperandIs)) 101 | .Select(i => i.index) 102 | .First(); 103 | 104 | var getEx = iList[logInfoCtorIndex - 5]; 105 | 106 | if (!getEx.IsLdarg(exParamIndex)) 107 | throw new InvalidOperationException($"{getEx} should be ldarg"); 108 | 109 | getEx.opcode = OpCodes.Ldloc_S; 110 | getEx.operand = localIndex; 111 | 112 | return iList; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /WrathPatches/Patches/OwlcatModification.LoadAssemblies_Patch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | using HarmonyLib; 8 | 9 | using Kingmaker; 10 | using Kingmaker.Blueprints.JsonSystem; 11 | using Kingmaker.Modding; 12 | 13 | using UnityModManagerNet; 14 | 15 | namespace WrathPatches; 16 | 17 | [WrathPatch("Add components from mod assemblies to binder cache")] 18 | [HarmonyPatch(typeof(OwlcatModification), "LoadAssemblies")] 19 | internal static class OwlcatModification_LoadAssemblies_Patch 20 | { 21 | [HarmonyPostfix] 22 | static void Postfix(OwlcatModification __instance) 23 | { 24 | var binder = (GuidClassBinder)Json.Serializer.Binder; 25 | 26 | foreach (var assembly in __instance.LoadedAssemblies) 27 | { 28 | foreach (var (type, guid) in assembly.GetTypes() 29 | .Select(type => (type, type.GetCustomAttribute()?.GuidString)) 30 | .Where(t => t.GuidString is not null)) 31 | { 32 | 33 | var logMessage = $"Adding {type} with TypeId {guid} to binder cache"; 34 | WrathPatches.Main.Logger.Log(logMessage); 35 | __instance.Logger.Log(logMessage); 36 | 37 | binder.AddToCache(type, guid); 38 | } 39 | } 40 | } 41 | } 42 | 43 | [HarmonyPatch(typeof(OwlcatModificationsManager), nameof(OwlcatModificationsManager.Start))] 44 | static class UmmModsToGuidClassBinder 45 | { 46 | 47 | static readonly Dictionary> DuplicateTypeIds = []; 48 | 49 | static void Prefix() 50 | { 51 | var binder = (GuidClassBinder)Json.Serializer.Binder; 52 | 53 | foreach (var mod in UnityModManager.modEntries.Where(me => me.Started)) 54 | { 55 | Main.Logger.Log($"Adding types to binder from {mod.Info.DisplayName}"); 56 | 57 | foreach (var f in Directory 58 | .EnumerateFiles(mod.Path, "*.dll", SearchOption.AllDirectories) 59 | //.Where(path => AppDomain.CurrentDomain.GetAssemblies() 60 | // .Select(ass => Path.GetFullPath((ass.Location))) 61 | // .Contains(Path.GetFullPath(path))) 62 | ) 63 | { 64 | Type[] types = []; 65 | 66 | try 67 | { 68 | types = Assembly.LoadFrom(f).GetTypes(); 69 | } 70 | catch (ReflectionTypeLoadException rtle) 71 | { 72 | types = rtle.Types; 73 | } 74 | catch (Exception e) 75 | { 76 | Main.Logger.LogException(e); 77 | continue; 78 | } 79 | try 80 | { 81 | foreach (var (type, guid) in types 82 | .Select(type => (type, type.GetCustomAttribute()?.GuidString)) 83 | .Where(t => t.GuidString is not null)) 84 | { 85 | if (guid is null) 86 | continue; 87 | 88 | Main.Logger.Log($"Adding {type} with TypeId {guid} to binder cache"); 89 | 90 | if (binder.m_GuidToTypeCache.ContainsKey(guid)) 91 | { 92 | //PFLog.Mods.Error("I told kuru this would happen"); 93 | 94 | if (!DuplicateTypeIds.TryGetValue(guid, out var ts)) 95 | ts = DuplicateTypeIds[guid] = []; 96 | 97 | ts.Add(type); 98 | 99 | PFLog.Mods.Log($"Duplicate typeid {guid}\n" + string.Join("\n", ts.Select(t => $"{t.Assembly.Location}: {t.FullName}"))); 100 | 101 | continue; 102 | } 103 | 104 | binder.m_GuidToTypeCache.Add(guid, type); 105 | binder.m_TypeToGuidCache.Add(type, guid); 106 | } 107 | } 108 | catch (Exception ex) 109 | { 110 | PFLog.Mods.Exception(ex); 111 | continue; 112 | } 113 | } 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /WrathPatches/Main.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | using HarmonyLib; 7 | 8 | using UnityEngine; 9 | 10 | using UnityModManagerNet; 11 | 12 | using WrathPatches.Patches; 13 | 14 | namespace WrathPatches; 15 | 16 | static partial class Main 17 | { 18 | static void OnGUI(UnityModManager.ModEntry _) 19 | { 20 | GUILayout.BeginHorizontal(); 21 | { 22 | GUILayout.BeginVertical(); 23 | { 24 | GUILayout.Label("Patches Status"); 25 | 26 | var font = GUI.skin.label.font; 27 | 28 | foreach (var (t, pc) in PatchClasses.Value) 29 | { 30 | var name = t.Name; 31 | 32 | AppliedPatches.TryGetValue(t, out var applied); 33 | 34 | if (t.GetCustomAttribute() is { } attr) 35 | name = attr.Name; 36 | 37 | if (IsExperimental(pc)) 38 | name = $"(Experimental) {name}"; 39 | 40 | GUILayout.Toggle(applied is true, name); 41 | } 42 | } 43 | GUILayout.EndVertical(); 44 | 45 | GUILayout.BeginVertical(); 46 | { 47 | static bool IsVersionMisMatch() => HarmonyVersion < UmmHarmonyVersion; 48 | 49 | GUILayout.BeginHorizontal(); 50 | { 51 | GUILayout.Label($"Harmony version: {HarmonyVersion}"); 52 | 53 | if (IsVersionMisMatch() && ModEntry!.Active) 54 | { 55 | if (GUILayout.Button("Update")) 56 | { 57 | ModEntry!.OnToggle = (_, value) => !value; 58 | ModEntry!.Active = false; 59 | ModEntry!.Info.DisplayName = $"{ModEntry!.Info.DisplayName} - RESTART REQUIRED"; 60 | 61 | ReplaceHarmony(GetHarmonyAss().Location, UmmHarmonyPath); 62 | } 63 | } 64 | } 65 | GUILayout.EndHorizontal(); 66 | 67 | GUILayout.Label($"UMM Harmony Version: {UmmHarmonyVersion}"); 68 | } 69 | GUILayout.EndVertical(); 70 | } 71 | GUILayout.EndHorizontal(); 72 | } 73 | 74 | private static UnityModManager.ModEntry? ModEntry; 75 | 76 | public static UnityModManager.ModEntry.ModLogger Logger => ModEntry!.Logger; 77 | 78 | internal static Harmony HarmonyInstance = null!; 79 | 80 | static readonly Lazy<(Type t, PatchClassProcessor pc)[]> PatchClasses = new(() => 81 | AccessTools.GetTypesFromAssembly(Assembly.GetExecutingAssembly()) 82 | .Select(t => (t, pc: HarmonyInstance!.CreateClassProcessor(t))) 83 | .Where(tuple => tuple.pc.HasPatchAttribute()) 84 | .ToArray()); 85 | 86 | static readonly Dictionary AppliedPatches = new(); 87 | 88 | static bool IsExperimental(PatchClassProcessor pc) => pc.GetCategory() == "Experimental"; 89 | 90 | static void RunPatches(IEnumerable<(Type, PatchClassProcessor)> typesAndPatches) 91 | { 92 | foreach (var (t, pc) in typesAndPatches) 93 | { 94 | try 95 | { 96 | Logger.Log($"Running patch class {t.Name}"); 97 | pc.Patch(); 98 | 99 | AppliedPatches[t] = true; 100 | } 101 | catch (Exception ex) 102 | { 103 | Logger.Error($"Exception in patch class {t.Name}"); 104 | Logger.LogException(ex); 105 | 106 | AppliedPatches[t] = false; 107 | } 108 | } 109 | } 110 | 111 | internal static bool Load(UnityModManager.ModEntry modEntry) 112 | { 113 | ModEntry = modEntry; 114 | 115 | ModEntry.OnGUI = OnGUI; 116 | 117 | ModEntry.OnToggle = (_, value) => value; 118 | 119 | HarmonyInstance = new Harmony(modEntry.Info.Id); 120 | 121 | RunPatches(PatchClasses.Value.Where(tuple => !IsExperimental(tuple.pc))); 122 | 123 | #if DEBUG 124 | var harmonyDebug = Harmony.DEBUG; 125 | Harmony.DEBUG = true; 126 | 127 | Logger.Log("Running experimental patches"); 128 | try 129 | { 130 | RunPatches(PatchClasses.Value.Where(tuple => IsExperimental(tuple.pc))); 131 | } 132 | catch(Exception ex) 133 | { 134 | Logger.LogException(ex); 135 | } 136 | 137 | Harmony.DEBUG = harmonyDebug; 138 | #endif 139 | 140 | LogsHotkey.EnableHotKey(); 141 | 142 | return true; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /WrathPatches/Patches/AddFactsIgnoreDeadReferences.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Reflection.Emit; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | using HarmonyLib; 10 | 11 | using Kingmaker.Blueprints; 12 | using Kingmaker.Blueprints.Facts; 13 | using Kingmaker.UnitLogic; 14 | using Kingmaker.UnitLogic.FactLogic; 15 | 16 | using WrathPatches.TranspilerUtil; 17 | 18 | namespace WrathPatches 19 | { 20 | internal static class AddFactsIgnoreDeadReferences 21 | { 22 | //[HarmonyPatch(typeof(AddFacts), nameof(AddFacts.Facts), MethodType.Getter)] 23 | //static class AddFacts_Facts_Patch 24 | //{ 25 | // static ReferenceArrayProxy 26 | // Postfix(ReferenceArrayProxy arr) 27 | // { 28 | // return arr.m_Array.Where(bpRef => ResourcesLibrary.BlueprintsCache.m_LoadedBlueprints.ContainsKey(bpRef.deserializedGuid)).ToArray(); 29 | // } 30 | //} 31 | 32 | static readonly MethodInfo HasItemMethod = 33 | typeof(Kingmaker.Utility.LinqExtensions).GetMethods() 34 | .First(mi => 35 | { 36 | if (mi.Name != nameof(Kingmaker.Utility.LinqExtensions.HasItem)) 37 | return false; 38 | 39 | var ps = mi.GetParameters(); 40 | 41 | return ps.Length == 2 && 42 | ps[1].ParameterType.IsGenericType && 43 | ps[1].ParameterType.GetGenericTypeDefinition() == 44 | typeof(Func).GetGenericTypeDefinition(); 45 | }).MakeGenericMethod(typeof(UnitFact)); 46 | 47 | [WrathPatch("AddFacts.UpdateFacts NRE fix")] 48 | [HarmonyPatch(typeof(AddFacts), nameof(AddFacts.UpdateFacts))] 49 | static class AddFacts_UpdateFacts_Patch 50 | { 51 | [HarmonyTranspiler] 52 | static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) 53 | { 54 | Func[] toMatch = 55 | [ 56 | ci => ci.opcode == OpCodes.Ldloc_S, // V_6 - closure object 57 | ci => ci.opcode == OpCodes.Ldloca_S, // V_5 - ReferenceArrayProxy.Enumerator struct 58 | ci => ci.opcode == OpCodes.Call, // ReferenceArrayProxy.Enumerator.Current (getter) :: ReferenceArrayProxy.Enumerator -> BlueprintUnitFact 59 | ci => ci.opcode == OpCodes.Stfld, // store to state machine "local" :: (V_6 * BlueprintUnitFact) -> () 60 | ci => ci.opcode == OpCodes.Ldarg_0, // this 61 | ci => ci.opcode == OpCodes.Call, // this.Data (getter) :: AddFacts -> AddFactsData 62 | ci => ci.opcode == OpCodes.Ldfld, // this.Data.AppliedFacts :: AddFactsData -> List 63 | ci => ci.opcode == OpCodes.Ldloc_S, // V_6 - closure object 64 | ci => ci.opcode == OpCodes.Ldftn, // lambda body pointer? 65 | ci => ci.opcode == OpCodes.Newobj, // Func constructor :: (object * nativeint) -> Func 66 | ci => ci.Calls(HasItemMethod), // Kingmaker.Utility.LinqExtensions.HasItem :: (IEnumerable * Func) -> bool 67 | ci => ci.opcode == OpCodes.Brtrue_S, // :: bool -> () 68 | ci => ci.opcode == OpCodes.Ldloc_1, // TempList (List ci.opcode == OpCodes.Ldloc_S, // V_6 - closure object 70 | ci => ci.opcode == OpCodes.Ldfld, // load state machine local :: V_6 -> BlueprintUnitFact 71 | ci => ci.opcode == OpCodes.Callvirt, // List.Add :: (List * BlueprintUnitFact) -> () 72 | ci => ci.opcode == OpCodes.Ldloca_S, // V_5 - ReferenceArrayProxy.Enumerator struct 73 | // ci => ci.opcode == OpCodes.Call, // ReferenceArrayProxy.Enumerator.MoveNext() :: ReferenceArrayProxy.Enumerator -> bool 74 | ]; 75 | 76 | var match = instructions.FindInstructionsIndexed(toMatch).ToArray(); 77 | 78 | if (match.Length != toMatch.Length) 79 | { 80 | Main.Logger.Error($"Failed to match in Transpiler for {nameof(AddFacts_UpdateFacts_Patch)}"); 81 | return instructions; 82 | } 83 | 84 | var ifNull = generator.DefineLabel(); 85 | match.Last().instruction.labels.Add(ifNull); 86 | 87 | CodeInstruction[] toInsert = 88 | [ 89 | new(match[1].instruction), 90 | new(match[2].instruction), 91 | new(OpCodes.Brfalse_S, ifNull) 92 | ]; 93 | 94 | var iList = instructions.ToList(); 95 | 96 | iList.InsertRange(match[0].index, toInsert); 97 | 98 | return iList; 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /WrathPatches/LinqExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace WrathPatches 8 | { 9 | public static class LinqExtensions 10 | { 11 | public static IEnumerable<(int index, T item)> Indexed(this IEnumerable source) 12 | { 13 | var index = 0; 14 | foreach (var item in source) 15 | yield return (index++, item); 16 | } 17 | 18 | public static IEnumerable FindSequence(this IEnumerable source, int length, IEnumerable> predicateSequence) 19 | { 20 | var i = 0; 21 | foreach (var result in predicateSequence.Zip(source, (f, x) => f(x))) 22 | { 23 | if (!result) return source.Skip(1).FindSequence(length, predicateSequence); 24 | 25 | i++; 26 | 27 | if (i >= length) return source.Take(i); 28 | } 29 | 30 | return Enumerable.Empty(); 31 | } 32 | 33 | public static IEnumerable FindSequence(this IEnumerable source, IEnumerable> predicateSequence) => 34 | source.FindSequence(predicateSequence.Count(), predicateSequence); 35 | 36 | public static IEnumerable FindSequence(this IEnumerable source, int length, Func, bool> predicate) 37 | { 38 | var subSeq = source.Take(length); 39 | if (subSeq.Count() < length) return Enumerable.Empty(); 40 | 41 | if (predicate(subSeq)) return subSeq; 42 | 43 | return source.Skip(1).FindSequence(length, predicate); 44 | } 45 | 46 | public static IEnumerable> Windowed(this IEnumerable source, int windowSize) 47 | { 48 | var buffer = new T[windowSize * 2]; 49 | 50 | var e = source.GetEnumerator(); 51 | 52 | var currentIndex = 0; 53 | 54 | IEnumerable yieldWindow(int startIndex) 55 | { 56 | return buffer.Skip(startIndex).Take(windowSize); 57 | } 58 | 59 | while (e.MoveNext()) 60 | { 61 | buffer[currentIndex] = e.Current; 62 | 63 | if (currentIndex >= windowSize) 64 | { 65 | yield return yieldWindow(currentIndex - windowSize); 66 | } 67 | 68 | currentIndex++; 69 | 70 | if (currentIndex >= buffer.Length) 71 | { 72 | var newBuffer = new T[windowSize * 2]; 73 | Array.Copy(buffer, windowSize, newBuffer, 0, windowSize); 74 | buffer = newBuffer; 75 | currentIndex = windowSize; 76 | } 77 | } 78 | 79 | if (currentIndex >= windowSize) 80 | { 81 | yield return yieldWindow(currentIndex - windowSize); 82 | } 83 | else 84 | { 85 | yield return buffer.Take(currentIndex); 86 | } 87 | } 88 | 89 | public static IEnumerable<(T, T)> Pairwise(this IEnumerable source) 90 | { 91 | foreach (var window in source.Windowed(2)) 92 | { 93 | T x, y; 94 | 95 | var e = window.GetEnumerator(); 96 | if (e.MoveNext()) 97 | { 98 | x = e.Current; 99 | 100 | if (e.MoveNext()) 101 | { 102 | y = e.Current; 103 | 104 | yield return (x, y); 105 | } 106 | } 107 | } 108 | } 109 | 110 | public static IEnumerable<(A, B)> Zip(this IEnumerable first, IEnumerable second) 111 | { 112 | return first.Zip(second, (a, b) => (a, b)); 113 | //var enumeratorA = first.GetEnumerator(); 114 | //var enumeratorB = second.GetEnumerator(); 115 | 116 | //while (enumeratorA.MoveNext() && enumeratorB.MoveNext()) 117 | // yield return (enumeratorA.Current, enumeratorB.Current); 118 | } 119 | 120 | public static IEnumerable EmptyIfNull(this T? item) where T : class 121 | { 122 | if (item == null) 123 | yield break; 124 | 125 | yield return item; 126 | } 127 | 128 | public static IEnumerable SkipIfNull(this IEnumerable source) where T : class => 129 | source.SelectMany(EmptyIfNull); 130 | 131 | public static IEnumerable NotNull(this IEnumerable source) where T : class => 132 | source.SelectMany(x => x.EmptyIfNull()); 133 | 134 | public static IEnumerable Push(this IEnumerable source, T value) 135 | { 136 | yield return value; 137 | 138 | foreach (var item in source) 139 | { 140 | yield return item; 141 | } 142 | } 143 | 144 | public static TValue GetOrAdd(this Dictionary source, TKey key, Func valueFactory) 145 | { 146 | if (!source.TryGetValue(key, out var value)) 147 | source[key] = value = valueFactory(); 148 | 149 | return value; 150 | } 151 | 152 | public static TValue GetOrAdd(this Dictionary source, TKey key) where TValue : new() 153 | => source.GetOrAdd(key, () => new()); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /WrathPatches/WrathPatches.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | net481 7 | latest 8 | enable 9 | True 10 | $(MSBuildProjectName.Replace(" ", "_")) 11 | $(SolutionDir)\bin\$(Configuration)\$(TargetFramework)\$(SolutionName) 12 | 13 | https://api.nuget.org/v3/index.json; 14 | https://nuget.bepinex.dev/v3/index.json; 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | all 52 | runtime; build; native; contentfiles; analyzers; buildtransitive 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Never 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /WrathPatches/Patches/SpellSlotComparisonFixes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection.Emit; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | using HarmonyLib; 9 | 10 | using Kingmaker.UI.MVVM._VM.ActionBar; 11 | using Kingmaker.UI.MVVM._VM.Tooltip.Bricks; 12 | using Kingmaker.UI.MVVM._VM.Tooltip.Templates; 13 | using Kingmaker.UI.UnitSettings; 14 | using Kingmaker.UnitLogic; 15 | using Kingmaker.UnitLogic.Abilities; 16 | using Kingmaker.Utility; 17 | 18 | using Owlcat.Runtime.UI.Tooltips; 19 | 20 | namespace WrathPatches.Patches; 21 | 22 | [WrathPatch("Fix Spell Slot comparison")] 23 | [HarmonyPatch] 24 | internal class SpellSlotComparisonFixes 25 | { 26 | static int MetaComparator(AbilityData a1, AbilityData a2) 27 | { 28 | #if DEBUG 29 | Main.Logger.Log($"Compare {a1.Name} to {a2.Name}"); 30 | Main.Logger.Log($"{a1} metamagic mask: {a1.MetamagicData?.MetamagicMask}"); 31 | Main.Logger.Log($"{a2} metamagic mask: {a2.MetamagicData?.MetamagicMask}"); 32 | #endif 33 | 34 | var result = (a2.MetamagicData != null ? (int)a2.MetamagicData.MetamagicMask : 0) - (a1.MetamagicData != null ? (int)a1.MetamagicData.MetamagicMask : 0); 35 | 36 | #if DEBUG 37 | Main.Logger.Log($"Result: {result}"); 38 | #endif 39 | 40 | return result; 41 | } 42 | 43 | static int CompareSpellbooks(Spellbook sb1, Spellbook sb2) 44 | { 45 | #if DEBUG 46 | Main.Logger.Log($"Compare spellbooks: sb1 = {sb1} sb2 = {sb2}"); 47 | #endif 48 | 49 | var sb2Index = sb2.Owner.Spellbooks.IndexOf(sb2); 50 | var sb1Index = sb1.Owner.Spellbooks.IndexOf(sb1); 51 | 52 | var result = sb2Index - sb1Index; 53 | 54 | #if DEBUG 55 | Main.Logger.Log($"Result: {sb2Index} - {sb1Index} = {result}"); 56 | #endif 57 | 58 | return result; 59 | } 60 | 61 | static int Compare(AbilityData a1, AbilityData a2) 62 | { 63 | #if DEBUG 64 | Main.Logger.Log($"{nameof(SpellSlotComparisonFixes)}.{nameof(Compare)}"); 65 | #endif 66 | 67 | if (a1.Spellbook is { } sb1 && a2.Spellbook is { } sb2) 68 | { 69 | var compareSpellbooks = CompareSpellbooks(sb1, sb2); 70 | 71 | if (compareSpellbooks != 0) return compareSpellbooks; 72 | } 73 | 74 | return MetaComparator(a1, a2); 75 | } 76 | 77 | [HarmonyPatch(typeof(ActionBarSpellbookHelper), nameof(ActionBarSpellbookHelper.Comparator))] 78 | [HarmonyPostfix] 79 | private static int Comparator_Postfix(int result, MechanicActionBarSlotSpell s1, MechanicActionBarSlotSpell s2) 80 | { 81 | if (result != 0) return result; 82 | 83 | var compare = Compare(s1.Spell, s2.Spell); 84 | 85 | #if DEBUG 86 | Main.Logger.Log($"{nameof(ActionBarSpellbookHelper)}.{nameof(Comparator_Postfix)}: {compare}"); 87 | #endif 88 | 89 | return compare; 90 | } 91 | 92 | [HarmonyPatch(typeof(ActionBarSpellbookHelper), nameof(ActionBarSpellbookHelper.IsEquals), [typeof(SpellSlot), typeof(SpellSlot)])] 93 | [HarmonyPostfix] 94 | static bool ActionBarSpellbookHelper_IsEquals_SpellSlot_Postfix(bool result, SpellSlot s1, SpellSlot s2) 95 | { 96 | var compare = Compare(s1.Spell, s2.Spell); 97 | 98 | #if DEBUG 99 | Main.Logger.Log($"{nameof(ActionBarSpellbookHelper_IsEquals_SpellSlot_Postfix)}: {compare}"); 100 | #endif 101 | 102 | return result && compare == 0; 103 | } 104 | 105 | [HarmonyPatch(typeof(ActionBarSpellbookHelper), nameof(ActionBarSpellbookHelper.IsEquals), [typeof(AbilityData), typeof(AbilityData)])] 106 | [HarmonyPostfix] 107 | static bool ActionBarSpellbookHelper_IsEquals_AbilityData_Postfix(bool result, AbilityData a1, AbilityData a2) 108 | { 109 | var compare = Compare(a1, a2); 110 | 111 | #if DEBUG 112 | Main.Logger.Log($"{nameof(ActionBarSpellbookHelper_IsEquals_AbilityData_Postfix)}: {compare}"); 113 | #endif 114 | 115 | return result && compare == 0; 116 | } 117 | 118 | [HarmonyPatch(typeof(Spellbook), nameof(Spellbook.GetAvailableForCastSpellCount))] 119 | [HarmonyTranspiler] 120 | static IEnumerable ActionBarSpellbookHelper_TryAddSpell_Transpiler(IEnumerable instructions) 121 | { 122 | var applied = false; 123 | 124 | var SpellSlot_SpellShell = AccessTools.PropertyGetter(typeof(SpellSlot), nameof(SpellSlot.SpellShell)); 125 | 126 | foreach (var i in instructions) 127 | { 128 | yield return i; 129 | 130 | if (i.opcode == OpCodes.Bne_Un_S) 131 | { 132 | var targetLabel = i.operand; 133 | 134 | Main.Logger.Log(nameof(ActionBarSpellbookHelper_TryAddSpell_Transpiler)); 135 | 136 | yield return new(OpCodes.Ldloc_S, 4); 137 | yield return new(OpCodes.Callvirt, SpellSlot_SpellShell); 138 | yield return new(OpCodes.Ldarg_1); 139 | yield return CodeInstruction.Call((AbilityData a1, AbilityData a2) => Compare(a1, a2)); 140 | yield return new(OpCodes.Brtrue_S, targetLabel); 141 | 142 | applied = true; 143 | } 144 | } 145 | 146 | if (!applied) 147 | throw new Exception("Failed to find target instruction"); 148 | } 149 | 150 | [HarmonyPatch(typeof(TooltipTemplateAbility), nameof(TooltipTemplateAbility.GetHeader), [typeof(TooltipTemplateType)])] 151 | static IEnumerable Postfix(IEnumerable __result, TooltipTemplateAbility __instance) 152 | { 153 | if (!__result.Any() || 154 | __instance.m_AbilityData is null || 155 | !__instance.m_AbilityData.Blueprint.IsSpell || 156 | __instance.m_AbilityData.Spellbook?.Blueprint?.DisplayName is not { } spellbookName) 157 | return __result; 158 | 159 | return __result 160 | .Skip(1) 161 | .Push(new TooltipBrickEntityHeader(__instance.m_Name, __instance.m_Icon, $"{__instance.m_Type} - {spellbookName}", __instance.m_School, __instance.m_Level, isItem: false)); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /WrathPatches/Patches/OwnerBlueprintWarning.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | using HarmonyLib; 9 | 10 | using Kingmaker.AreaLogic.Capital; 11 | using Kingmaker.AreaLogic.Etudes; 12 | using Kingmaker.Armies.Components; 13 | using Kingmaker.Armies.TacticalCombat.LeaderSkills; 14 | using Kingmaker.Blueprints; 15 | using Kingmaker.Blueprints.Classes.Experience; 16 | using Kingmaker.Blueprints.Classes.Prerequisites; 17 | using Kingmaker.Blueprints.JsonSystem; 18 | using Kingmaker.Craft; 19 | using Kingmaker.Designers.EventConditionActionSystem.Events; 20 | using Kingmaker.Designers.Mechanics.Buffs; 21 | using Kingmaker.Dungeon.FactLogic; 22 | using Kingmaker.EntitySystem.Persistence.JsonUtility; 23 | using Kingmaker.Kingdom.Settlements.BuildingComponents; 24 | using Kingmaker.Tutorial; 25 | using Kingmaker.UnitLogic.Abilities.Components; 26 | using Kingmaker.UnitLogic.Abilities.Components.Base; 27 | using Kingmaker.UnitLogic.Abilities.Components.TargetCheckers; 28 | using Kingmaker.UnitLogic.ActivatableAbilities; 29 | using Kingmaker.UnitLogic.FactLogic; 30 | using Kingmaker.UnitLogic.Mechanics.Components; 31 | using Kingmaker.UnitLogic.Mechanics.Properties; 32 | using Kingmaker.Utility; 33 | 34 | using Newtonsoft.Json; 35 | 36 | namespace WrathPatches.Patches; 37 | 38 | [WrathPatch("Non-matching BlueprintComponent.OwnerBlueprint warning")] 39 | [HarmonyPatch] 40 | public class OwnerBlueprintWarning 41 | { 42 | /// 43 | /// List of BlueprintComponent types that use OwnerBlueprint 44 | /// 45 | public static readonly IEnumerable ErrorComponentTypes = 46 | [ 47 | typeof(CapitalCompanionLogic), 48 | typeof(EtudeBracketMusic), 49 | typeof(EtudeBracketSetCompanionPosition), 50 | typeof(ArmyUnitComponent), 51 | typeof(LeaderPercentAttributeBonus), 52 | typeof(MaxArmySquadsBonusLeaderComponent), 53 | typeof(SquadsActionOnTacticalCombatStart), 54 | typeof(Experience), 55 | typeof(PrerequisiteArchetypeLevel), 56 | typeof(PrerequisiteClassLevel), 57 | typeof(PrerequisiteFeature), 58 | typeof(CraftInfoComponent), 59 | typeof(EvaluatedUnitCombatTrigger), 60 | typeof(ControlledProjectileHolder), 61 | typeof(DungeonAddLootToVendor), 62 | typeof(BuildingUpgradeBonus), 63 | typeof(TutorialPage), 64 | typeof(AbilityCustomDimensionDoor), 65 | typeof(AbilityDeliverProjectileOnGrid), 66 | typeof(AbilityIsBomb), 67 | typeof(AbilityDeliverEffect), 68 | typeof(MarkUsableWhileCan), 69 | typeof(ActivatableAbilitySet), 70 | typeof(ActivatableAbilitySetItem), 71 | typeof(AddAbilityUseTrigger), 72 | typeof(AddFeaturesFromSelectionToDescription), 73 | typeof(AddTriggerOnActivationChanged), 74 | typeof(AddVendorItems), 75 | typeof(NenioSpecialPolymorphWhileEtudePlaying), 76 | typeof(ChangeSpellElementalDamage), 77 | typeof(ContextCalculateAbilityParams), 78 | typeof(ContextRankConfig), 79 | typeof(ContextSetAbilityParams), 80 | typeof(UnitPropertyComponent) 81 | ]; 82 | 83 | #if DEBUG 84 | public static IEnumerable GenericComponentTypes = 85 | [ 86 | typeof(EtudeBracketTrigger) 87 | ]; 88 | #endif 89 | 90 | static readonly HashSet<(BlueprintGuid, string)> AlreadyWarned = []; 91 | 92 | [HarmonyPatch(typeof(BlueprintsCache), nameof(BlueprintsCache.Load))] 93 | [HarmonyPostfix] 94 | static void Postfix(SimpleBlueprint __result, BlueprintGuid guid) 95 | { 96 | if (__result is not BlueprintScriptableObject blueprint) 97 | return; 98 | #if DEBUG 99 | foreach (var (i, c) in blueprint.ComponentsArray.Indexed()) 100 | #else 101 | foreach (var c in blueprint.ComponentsArray) 102 | #endif 103 | { 104 | if (c.OwnerBlueprint != blueprint) 105 | { 106 | #if !DEBUG 107 | if (AlreadyWarned.Contains((blueprint.AssetGuid, c.name))) 108 | continue; 109 | #endif 110 | 111 | if (ErrorComponentTypes.Any(t => t.IsAssignableFrom(c.GetType())) 112 | #if DEBUG 113 | || GenericComponentTypes.Any(t => c.GetType().GetAllBaseTypes(true).Where(t => t.IsGenericType).Select(t => t.GetGenericTypeDefinition()).Contains(t)) 114 | #endif 115 | ) 116 | { 117 | Main.Logger.Error($"In blueprint {guid} \"{blueprint.name}\": " + 118 | $"Non-matching OwnerBlueprint {c.OwnerBlueprint?.ToString() ?? "NULL"} on {c.GetType()} \"{c.name}\". " + 119 | $"THIS COMPONENT MAY NOT WORK AS EXPECTED!"); 120 | #if DEBUG 121 | Main.Logger.Log("Trying fix"); 122 | 123 | Json.BlueprintBeingRead = new BlueprintJsonWrapper(blueprint); 124 | 125 | var ms = new MemoryStream(); 126 | 127 | var writer = new StreamWriter(ms); 128 | var jsonWriter = new JsonTextWriter(writer); 129 | Json.Serializer.Serialize(jsonWriter, c); 130 | 131 | jsonWriter.Flush(); 132 | 133 | ms.Position = 0; 134 | 135 | var reader = new StreamReader(ms); 136 | var jsonReader = new JsonTextReader(reader); 137 | blueprint.ComponentsArray[i] = (BlueprintComponent)Json.Serializer.Deserialize(jsonReader, c.GetType()); 138 | 139 | blueprint.ComponentsArray[i].OwnerBlueprint = blueprint; 140 | 141 | Json.BlueprintBeingRead = null; 142 | #endif 143 | } 144 | #if DEBUG 145 | else 146 | { 147 | Main.Logger.Warning($"In blueprint {guid} \"{blueprint.name}\": " + 148 | $"Non-matching OwnerBlueprint {c.OwnerBlueprint?.ToString() ?? "NULL"} on {c.GetType()} \"{c.name}\""); 149 | } 150 | #endif 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /ShaderWrapper/ScriptableShaderWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using HarmonyLib; 4 | using Kingmaker.BundlesLoading; 5 | using System.Linq; 6 | using Kingmaker.Modding; 7 | using System.Collections.Generic; 8 | using Kingmaker; 9 | using System.Reflection; 10 | using System.IO; 11 | using System.Reflection.Emit; 12 | 13 | namespace ShaderWrapper 14 | { 15 | public class ScriptableShaderWrapper : ScriptableObject 16 | { 17 | const string AssetName = "scriptableshaderwrapperinstance"; 18 | const string BundleName = "scriptableshaderwrapper"; 19 | 20 | static AssetBundle Bundle; 21 | 22 | static ScriptableShaderWrapper m_Instance; 23 | 24 | public static ScriptableShaderWrapper Instance 25 | { 26 | get 27 | { 28 | if (m_Instance == null) 29 | { 30 | if (BundlesLoadService.Instance is null) 31 | { 32 | throw new Exception("Attempt to get ScriptableShaderWrapper Instance before BundlesLoadService Instance is set."); 33 | } 34 | try 35 | { 36 | //var Bundle = BundlesLoadService.Instance.RequestBundleForAsset(AssetName); 37 | 38 | if (Bundle == null) 39 | Bundle = AssetBundle.LoadFromFile( 40 | Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), BundleName)); 41 | 42 | if (Bundle == null) 43 | { 44 | throw new Exception("Acquired null bundle when trying to set ScriptableShaderWrapper Instance"); 45 | } 46 | 47 | m_Instance = Bundle.LoadAsset(AssetName); 48 | } 49 | catch(Exception Ex) 50 | { 51 | Kingmaker.PFLog.Bundles.Exception(Ex); 52 | return null; 53 | } 54 | if (m_Instance == null) 55 | { 56 | Kingmaker.PFLog.Bundles.Warning("ScriptableShaderWrapper null Instance after trying to get it!"); 57 | } 58 | } 59 | return m_Instance; 60 | } 61 | } 62 | 63 | public Shader[] shaders; 64 | } 65 | 66 | //[HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.PatchMaterialShaders))] 67 | [HarmonyPatch] 68 | static public class PatchForShaderFind 69 | { 70 | static bool Applied = false; 71 | 72 | 73 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.PatchMaterialShaders))] 74 | static bool Prefix(IEnumerable materials) 75 | { 76 | PFLog.Mods.Warning("This should not be called."); 77 | 78 | PatchMaterialShaders(materials, null!); 79 | return false; 80 | } 81 | 82 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.LoadBundle))] 83 | [HarmonyTranspiler] 84 | static IEnumerable Transpiler(IEnumerable instructions) 85 | { 86 | if (Applied) 87 | PFLog.Mods.Warning("Reapplying transpiler"); 88 | 89 | foreach (var i in instructions) 90 | { 91 | if (i.Calls(AccessTools.Method(typeof(OwlcatModification), nameof(OwlcatModification.PatchMaterialShaders)))) 92 | { 93 | yield return new(OpCodes.Ldarg_0); 94 | //i.operand = AccessTools.Method(typeof(PatchForShaderFind), nameof(PatchForShaderFind.PatchMaterialShaders)); 95 | yield return CodeInstruction.Call((IEnumerable materials, OwlcatModification mod) => PatchMaterialShaders(materials, mod)); 96 | 97 | Applied = true; 98 | } 99 | else 100 | yield return i; 101 | } 102 | 103 | if (!Applied) 104 | throw new Exception("Failed to find target instruction"); 105 | } 106 | 107 | //internal static bool Prefix(IEnumerable materials) 108 | public static void PatchMaterialShaders(IEnumerable materials, OwlcatModification mod = null) 109 | { 110 | var logger = 111 | mod?.Logger ?? 112 | PFLog.Mods; 113 | 114 | logger.Log("PatchForShaderFind enter"); 115 | foreach(var material in materials) 116 | { 117 | string shaderName = material?.shader?.name; 118 | logger.Log($"PatchForShaderFind material is {material?.name ?? "NULL"}, shader is {shaderName ?? "Null"}"); 119 | 120 | if (shaderName == null) 121 | { 122 | logger.Log($"PatchForShaderFind continue"); 123 | continue; 124 | } 125 | var shaderNew = Shader.Find(shaderName); 126 | logger.Log($"PatchForShaderFind shader after Find is null? {shaderNew == null}."); 127 | 128 | if (shaderNew == null) 129 | { 130 | if (BundlesLoadService.Instance == null) 131 | { 132 | logger.Log($"PatchForShaderFind BundlesLoadService is null. Continue"); 133 | continue; 134 | } 135 | else if (ScriptableShaderWrapper.Instance is ScriptableShaderWrapper shaderWrapper) 136 | { 137 | foreach(var shader in shaderWrapper.shaders) 138 | { 139 | bool equals = shader?.name == shaderName; 140 | logger.Log($"PatchForShaderFind current shader is {shader?.name ?? "null"}. Equals? {equals}"); 141 | if (equals) 142 | { 143 | shaderNew = shader; 144 | break; 145 | } 146 | } 147 | //shaderNew = shaderWrapper.shaders.FirstOrDefault(x => x != null && x.name == shaderName); 148 | logger.Log($"PatchForShaderFind shaderNew after wrapper is null? {shaderNew == null}"); 149 | } 150 | } 151 | if (shaderNew != null) 152 | material.shader= shaderNew; 153 | } 154 | //return false; 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /WrathPatches/Patches/OwlModDirectReferenceBundleDependenciesFix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection.Emit; 6 | 7 | using HarmonyLib; 8 | 9 | using Kingmaker; 10 | using Kingmaker.Blueprints.JsonSystem.Converters; 11 | using Kingmaker.BundlesLoading; 12 | using Kingmaker.Modding; 13 | using Kingmaker.SharedTypes; 14 | 15 | using UnityEngine; 16 | 17 | using WrathPatches.TranspilerUtil; 18 | 19 | namespace WrathPatches.Patches; 20 | 21 | [WrathPatch("Load OwlMod BlueprintDirectReferences dependencies")] 22 | [HarmonyPatch] 23 | public static class OwlModDirectReferenceBundleDependenciesFix 24 | { 25 | public static OwlcatModification? ModificationBeingApplied; 26 | 27 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.ApplyInternal))] 28 | [HarmonyPrefix] 29 | static void OwlcatModification_ApplyInternal_Prefix(OwlcatModification __instance) => ModificationBeingApplied = __instance; 30 | 31 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.ApplyInternal))] 32 | [HarmonyFinalizer] 33 | static void OwlcatModification_ApplyInternal_Finalizer() => ModificationBeingApplied = null; 34 | 35 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.TryLoadBundle))] 36 | [HarmonyPostfix] 37 | static AssetBundle? OwlcatModification_TryLoadBundle_Postfix(AssetBundle? __result, OwlcatModification __instance, string bundleName) 38 | { 39 | if (__result != null) 40 | return __result; 41 | 42 | if (__instance.Bundles.Contains(bundleName)) 43 | { 44 | __instance.Logger.Log($"Loading {bundleName} from {__instance.Manifest.UniqueName}/Bundles"); 45 | return __instance.LoadBundle(bundleName); 46 | } 47 | 48 | return null; 49 | } 50 | 51 | //#if DEBUG 52 | [HarmonyPatch(typeof(OwlcatModificationsManager), nameof(OwlcatModificationsManager.TryLoadBundle))] 53 | [HarmonyPrefix] 54 | static bool OwlcatModificationsManager_TryLoadBundle_Prefix(string bundleName, ref AssetBundle? __result) 55 | { 56 | __result = ModificationBeingApplied?.TryLoadBundle(bundleName); 57 | 58 | if (__result != null) 59 | ModificationBeingApplied!.Logger.Log($"Loaded {bundleName} while applying modification"); 60 | 61 | return __result == null; 62 | } 63 | 64 | [HarmonyPatch(typeof(OwlcatModificationsManager), nameof(OwlcatModificationsManager.GetDependenciesForBundle))] 65 | [HarmonyPrefix] 66 | static bool OwlcatModificationsManager_GetDependenciesForBundle_Prefix(string bundleName, ref DependencyData? __result) 67 | { 68 | __result = ModificationBeingApplied?.GetDependenciesForBundle(bundleName); 69 | 70 | if (__result != null) 71 | ModificationBeingApplied!.Logger.Log($"Got dependencies [{string.Join(", ", __result.BundleToDependencies[bundleName])}] for {bundleName} while applying modification"); 72 | 73 | return __result == null; 74 | } 75 | 76 | [HarmonyPatch(typeof(OwlcatModificationsManager), nameof(OwlcatModificationsManager.GetBundleNameForAsset))] 77 | [HarmonyPrefix] 78 | static bool OwlcatModificationsManager_GetBundleNameForAsset_Prefix(string guid, ref string? __result) 79 | { 80 | __result = ModificationBeingApplied?.GetBundleNameForAsset(guid); 81 | 82 | if (__result != null) 83 | ModificationBeingApplied!.Logger.Log($"Got bundle name {__result} for asset {guid} while applying modification"); 84 | 85 | return __result == null; 86 | } 87 | 88 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.LoadBundle))] 89 | [HarmonyPostfix] 90 | static void OwlcatModification_LoadBundle_Postfix(string bundleName, OwlcatModification __instance, AssetBundle __result) 91 | { 92 | __instance.Logger.Log($"Load bundle {bundleName}. Is null? {__result == null}"); 93 | } 94 | //#endif 95 | 96 | [HarmonyPatch(typeof(OwlcatModificationsManager), nameof(OwlcatModificationsManager.AppliedModifications), MethodType.Getter)] 97 | [HarmonyPostfix] 98 | static OwlcatModification[] OwlcatModificationsManager_AppliedModifications_Postfix(OwlcatModification[] __result) => 99 | ModificationBeingApplied is null ? __result : ([ModificationBeingApplied, .. __result]); 100 | 101 | const string DirectReferenceBundleName = "BlueprintDirectReferences"; 102 | 103 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.LoadBundles))] 104 | [HarmonyTranspiler] 105 | static IEnumerable OwlcatModification_LoadBundles_Transpiler(IEnumerable instructions) 106 | { 107 | var iList = instructions.ToList(); 108 | 109 | var skipSectionStart = instructions.FindInstructionsIndexed( 110 | [ 111 | ci => ci.opcode == OpCodes.Ldloc_2, 112 | ci => ci.opcode == OpCodes.Ldstr && ci.operand is DirectReferenceBundleName 113 | ]).ToArray(); 114 | 115 | var IEnumerator_MoveNext = AccessTools.Method(typeof(IEnumerator), nameof(IEnumerator.MoveNext)); 116 | 117 | var skipSectionEnd = instructions.FindInstructionsIndexed( 118 | [ 119 | ci => ci.opcode == OpCodes.Ldloc_0, 120 | ci => ci.Calls(IEnumerator_MoveNext), 121 | ci => ci.opcode == OpCodes.Brtrue 122 | ]).ToArray(); 123 | 124 | if (skipSectionStart.Length != 2 || skipSectionEnd.Length != 3) 125 | throw new Exception("Could not find target instructions"); 126 | 127 | var startIndex = skipSectionStart[0].index; 128 | var endIndex = skipSectionEnd[0].index; 129 | 130 | iList.RemoveRange(startIndex, endIndex - startIndex); 131 | 132 | //Main.Logger.Log(string.Join("\n", iList)); 133 | 134 | return iList; 135 | } 136 | 137 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.LoadBundles))] 138 | [HarmonyPostfix] 139 | static void OwlcatModification_LoadBundles_Postfix(OwlcatModification __instance) 140 | { 141 | var bundleName = __instance.Bundles.SingleOrDefault(b => b.EndsWith(DirectReferenceBundleName)); 142 | 143 | if (bundleName is null) 144 | return; 145 | 146 | #if DEBUG 147 | __instance.Logger.Log($"Try load {DirectReferenceBundleName} ({bundleName})"); 148 | #endif 149 | 150 | BundlesLoadService.Instance.LoadDependencies(bundleName); 151 | __instance.m_ReferencedAssetsBundle = BundlesLoadService.Instance.RequestBundle(bundleName); 152 | 153 | #if DEBUG 154 | __instance.Logger.Log($"Bundle {bundleName} is not null? {__instance.m_ReferencedAssetsBundle != null}"); 155 | #endif 156 | 157 | if (__instance.m_ReferencedAssetsBundle != null) 158 | { 159 | #if DEBUG 160 | __instance.Logger.Log("Load BlueprintReferencedAssets"); 161 | #endif 162 | 163 | __instance.m_ReferencedAssets = __instance.m_ReferencedAssetsBundle.LoadAllAssets().Single(); 164 | 165 | if (__instance.m_ReferencedAssets != null) 166 | { 167 | #if DEBUG 168 | __instance.Logger.Log($"{__instance.m_ReferencedAssets.m_Entries.Count} entries"); 169 | foreach (var e in __instance.m_ReferencedAssets.m_Entries) 170 | { 171 | __instance.Logger.Log($" ({e.AssetId}, {e.FileId}) {e.Asset?.GetType().ToString() ?? "NULL"}"); 172 | } 173 | #endif 174 | 175 | UnityObjectConverter.ModificationAssetLists.Add(__instance.m_ReferencedAssets); 176 | 177 | } 178 | 179 | BundlesLoadService.Instance.UnloadBundle(bundleName); 180 | } 181 | 182 | BundlesLoadService.Instance.UnloadDependencies(bundleName); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /WrathPatches/Patches/OMMExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Reflection.Emit; 7 | using System.Runtime.Serialization; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | using HarmonyLib; 12 | 13 | using Kingmaker.Modding; 14 | 15 | using Newtonsoft.Json; 16 | 17 | using UnityModManagerNet; 18 | 19 | using WrathPatches.TranspilerUtil; 20 | 21 | namespace WrathPatches.Patches; 22 | 23 | //[HarmonyPatchCategory("Experimental")] 24 | //[WrathPatch("OwlcatModificationManager Assemblies load patch")] 25 | //[HarmonyPatch] 26 | internal static class LoadAssembliesFromList 27 | { 28 | //[HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.GetFilesFromDirectory))] 29 | //[HarmonyPostfix] 30 | static IEnumerable GetFilesFromDirectory_Postfix(IEnumerable result, string directory) 31 | { 32 | var dirName = new DirectoryInfo(directory).Name; 33 | 34 | var isAssemblies = dirName.Equals("Assemblies", StringComparison.InvariantCultureIgnoreCase); 35 | 36 | var assembliesListJson = Directory.EnumerateFiles(directory, "AssembliesList.json").FirstOrDefault(); 37 | 38 | var assembliesList = assembliesListJson is not null ? JsonConvert.DeserializeObject(File.ReadAllText(assembliesListJson)) : null; 39 | 40 | foreach (var item in result) 41 | { 42 | if (!isAssemblies) 43 | { 44 | yield return item; 45 | continue; 46 | } 47 | 48 | if (!Path.GetExtension(item).Equals("dll", StringComparison.InvariantCultureIgnoreCase)) 49 | continue; 50 | 51 | if (assembliesList is null) 52 | { 53 | yield return item; 54 | continue; 55 | } 56 | 57 | if (assembliesList.AssemblyNames.Any(assName => Path.GetFileNameWithoutExtension(item).Equals(assName, StringComparison.InvariantCultureIgnoreCase))) 58 | yield return item; 59 | } 60 | } 61 | 62 | class AssembliesList 63 | { 64 | public List AssemblyNames = []; 65 | } 66 | } 67 | 68 | [WrathPatch("OwlcatModificationManager: UMM mods as dependencies for OMM mods")] 69 | [HarmonyPatch(typeof(OwlcatModificationsManager))] 70 | internal static class UMMDependency 71 | { 72 | static IEnumerable UMMModManifests 73 | { 74 | get 75 | { 76 | foreach (var modInfo in UnityModManager.modEntries.Select(me => me.Info)) 77 | { 78 | var manifest = new OwlcatModificationManifest() 79 | { 80 | UniqueName = modInfo.Id ?? "", 81 | DisplayName = modInfo.DisplayName ?? "", 82 | Version = modInfo.Version ?? "", 83 | Description = "", 84 | Author = modInfo.Author ?? "", 85 | Repository = modInfo.Repository ?? "", 86 | HomePage = modInfo.HomePage ?? "", 87 | Dependencies = [] 88 | }; 89 | 90 | yield return manifest; 91 | } 92 | } 93 | } 94 | 95 | static IEnumerable FakeOwlmods 96 | { 97 | get 98 | { 99 | foreach (var manifest in UMMModManifests) 100 | { 101 | var fakeMod = FormatterServices.GetUninitializedObject(typeof(OwlcatModification)); 102 | 103 | typeof(OwlcatModification).GetField(nameof(OwlcatModification.Manifest)).SetValue(fakeMod, manifest); 104 | 105 | yield return (OwlcatModification)fakeMod; 106 | } 107 | } 108 | } 109 | 110 | [HarmonyPatch(nameof(OwlcatModificationsManager.CheckDependencies))] 111 | [HarmonyPrefix] 112 | static void InjectUMMMods(ref List appliedModifications) 113 | { 114 | appliedModifications = appliedModifications.Concat(FakeOwlmods).ToList(); 115 | } 116 | 117 | //static OwlcatModificationManifest? TryGetUMMMod(OwlcatModificationManifest.Dependency dependency) => 118 | // UMMModManifests.FirstOrDefault(m => m.UniqueName == dependency.Name); 119 | 120 | //static FieldInfo DependencyLocalValue => 121 | // typeof(OwlcatModificationsManager).GetNestedTypes(AccessTools.all) 122 | // .Select(t => t.GetField("dependency", AccessTools.all)) 123 | // .SkipIfNull() 124 | // .Single(fi => fi.FieldType == typeof(OwlcatModificationManifest.Dependency)); 125 | 126 | //[HarmonyPatch(nameof(OwlcatModificationsManager.CheckDependencies))] 127 | //[HarmonyTranspiler] 128 | //static IEnumerable CheckDependencies_Transpiler(IEnumerable instructions) 129 | //{ 130 | // var match = instructions.FindInstructionsIndexed( 131 | // [ 132 | // ci => ci.opcode == OpCodes.Stloc_S && ci.operand is LocalBuilder { LocalIndex: 5 }, 133 | // ci => ci.opcode == OpCodes.Ldloc_S && ci.operand is LocalBuilder { LocalIndex: 5 }, 134 | // ci => ci.opcode == OpCodes.Brtrue_S 135 | // ]).ToArray(); 136 | 137 | // if (match.Length != 3) 138 | // throw new Exception($"Could not find instructions to patch"); 139 | 140 | // var dependencyFound = match[2].instruction.operand; 141 | 142 | // var iList = instructions.ToList(); 143 | 144 | // var getUMMDependency = new CodeInstruction[] 145 | // { 146 | // new(OpCodes.Ldloc_S, 4), 147 | // new(OpCodes.Ldfld, DependencyLocalValue), 148 | // CodeInstruction.Call((OwlcatModificationManifest.Dependency dependency) => TryGetUMMMod(dependency)), 149 | // new(OpCodes.Dup), 150 | // new(OpCodes.Stloc_S, 5), 151 | // new(OpCodes.Brtrue_S, dependencyFound) 152 | // }; 153 | 154 | // iList.InsertRange(match[2].index + 1, getUMMDependency); 155 | 156 | // return iList; 157 | //} 158 | } 159 | 160 | [WrathPatch("OwlcatModificationManager: Compare OMM mod versions like UMM")] 161 | [HarmonyPatch(typeof(OwlcatModificationsManager))] 162 | static class OMMVersionCheck 163 | { 164 | // Return true if the version is too low 165 | static bool VersionCheck(string? thisVersionString, string? otherVersionString) 166 | { 167 | if (thisVersionString is null || otherVersionString is null) return true; 168 | 169 | var thisVersion = UnityModManager.ParseVersion(thisVersionString); 170 | var otherVersion = UnityModManager.ParseVersion(otherVersionString); 171 | 172 | return otherVersion < thisVersion; 173 | } 174 | 175 | [HarmonyPatch(nameof(OwlcatModificationsManager.CheckDependencies))] 176 | [HarmonyTranspiler] 177 | static IEnumerable CheckDependencies_Transpiler(IEnumerable instructions) 178 | { 179 | #if DEBUG 180 | var patched = false; 181 | #endif 182 | var string_op_Inequality = AccessTools.Method(typeof(string), "op_Inequality", [typeof(string), typeof(string)]); 183 | 184 | foreach (var i in instructions) 185 | { 186 | if (i.Calls(string_op_Inequality)) 187 | { 188 | #if DEBUG 189 | patched = true; 190 | #endif 191 | 192 | //yield return CodeInstruction.Call((string a, string b) => VersionCheck(a, b)).WithLabels(i.labels); 193 | //continue; 194 | 195 | i.operand = AccessTools.Method(typeof(OMMVersionCheck), nameof(VersionCheck)); 196 | } 197 | 198 | yield return i; 199 | } 200 | 201 | #if DEBUG 202 | if (!patched) 203 | throw new Exception("Could not find instructions to patch"); 204 | #endif 205 | } 206 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | lib/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET Core 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.vspscc 97 | *.vssscc 98 | .builds 99 | *.pidb 100 | *.svclog 101 | *.scc 102 | 103 | # Chutzpah Test files 104 | _Chutzpah* 105 | 106 | # Visual C++ cache files 107 | ipch/ 108 | *.aps 109 | *.ncb 110 | *.opendb 111 | *.opensdf 112 | *.sdf 113 | *.cachefile 114 | *.VC.db 115 | *.VC.VC.opendb 116 | 117 | # Visual Studio profiler 118 | *.psess 119 | *.vsp 120 | *.vspx 121 | *.sap 122 | 123 | # Visual Studio Trace Files 124 | *.e2e 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Coverlet is a free, cross platform Code Coverage Tool 148 | coverage*.json 149 | coverage*.xml 150 | coverage*.info 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio LightSwitch build output 300 | **/*.HTMLClient/GeneratedArtifacts 301 | **/*.DesktopClient/GeneratedArtifacts 302 | **/*.DesktopClient/ModelManifest.xml 303 | **/*.Server/GeneratedArtifacts 304 | **/*.Server/ModelManifest.xml 305 | _Pvt_Extensions 306 | 307 | # Paket dependency manager 308 | .paket/paket.exe 309 | paket-files/ 310 | 311 | # FAKE - F# Make 312 | .fake/ 313 | 314 | # CodeRush personal settings 315 | .cr/personal 316 | 317 | # Python Tools for Visual Studio (PTVS) 318 | __pycache__/ 319 | *.pyc 320 | 321 | # Cake - Uncomment if you are using it 322 | # tools/** 323 | # !tools/packages.config 324 | 325 | # Tabs Studio 326 | *.tss 327 | 328 | # Telerik's JustMock configuration file 329 | *.jmconfig 330 | 331 | # BizTalk build output 332 | *.btp.cs 333 | *.btm.cs 334 | *.odx.cs 335 | *.xsd.cs 336 | 337 | # OpenCover UI analysis results 338 | OpenCover/ 339 | 340 | # Azure Stream Analytics local run output 341 | ASALocalRun/ 342 | 343 | # MSBuild Binary and Structured Log 344 | *.binlog 345 | 346 | # NVidia Nsight GPU debugger configuration file 347 | *.nvuser 348 | 349 | # MFractors (Xamarin productivity tool) working folder 350 | .mfractor/ 351 | 352 | # Local History for Visual Studio 353 | .localhistory/ 354 | 355 | # BeatPulse healthcheck temp database 356 | healthchecksdb 357 | 358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 359 | MigrationBackup/ 360 | 361 | # Ionide (cross platform F# VS Code tools) working folder 362 | .ionide/ 363 | 364 | # Fody - auto-generated XML schema 365 | FodyWeavers.xsd -------------------------------------------------------------------------------- /WrathPatches/Patches/TheEnhance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Xml.Linq; 7 | using HarmonyLib; 8 | using Kingmaker; 9 | using Kingmaker.Blueprints; 10 | using Kingmaker.Blueprints.JsonSystem; 11 | using Kingmaker.EntitySystem.Persistence.JsonUtility; 12 | using Kingmaker.Modding; 13 | using Kingmaker.UnitLogic.Mechanics.Actions; 14 | using Kingmaker.Utility; 15 | using Newtonsoft.Json; 16 | using Newtonsoft.Json.Linq; 17 | 18 | using Owlcat.Runtime.Core.Logging; 19 | 20 | using WrathPatches; 21 | 22 | namespace OwlModPatcherEnhancer; 23 | 24 | [WrathPatch("Owlmod Enhancer")] 25 | [HarmonyPatch] 26 | public static class TheEnhance 27 | { 28 | //static JsonSerializerSettings settings = new JsonSerializerSettings() 29 | //{ 30 | 31 | // TypeNameHandling = TypeNameHandling.Auto, 32 | // NullValueHandling = NullValueHandling.Ignore, 33 | // DefaultValueHandling = DefaultValueHandling.Ignore, 34 | // PreserveReferencesHandling = PreserveReferencesHandling.Objects, 35 | // ReferenceLoopHandling = ReferenceLoopHandling.Error, 36 | // Formatting = Formatting.Indented, 37 | // Binder = new GuidClassBinder(), 38 | // Converters = DefaultJsonSettings.CommonConverters.ToList(), 39 | // ContractResolver = new OptInContractResolver(), 40 | 41 | //}; 42 | 43 | static readonly JsonSerializerSettings settings = new() 44 | { 45 | ContractResolver = Json.Serializer.ContractResolver, 46 | TypeNameHandling = TypeNameHandling.Auto, 47 | PreserveReferencesHandling = PreserveReferencesHandling.None, 48 | DefaultValueHandling = DefaultValueHandling.Include, 49 | ReferenceLoopHandling = ReferenceLoopHandling.Error, 50 | Formatting = Formatting.Indented, 51 | Binder = Json.Serializer.Binder, 52 | Converters = [.. Json.GetConverters()] 53 | }; 54 | 55 | static class ThreadStaticData 56 | { 57 | [ThreadStatic] 58 | public static BlueprintJsonWrapper? WrapperInstance; 59 | 60 | [ThreadStatic] 61 | public static OwlcatModification? CurrentMod; 62 | } 63 | 64 | static BlueprintJsonWrapper WrapperInstance => ThreadStaticData.WrapperInstance ??= new(); 65 | 66 | static JsonMergeSettings MergeSettings = default!; 67 | 68 | static LogChannel ModLogger => ThreadStaticData.CurrentMod?.Logger ?? PFLog.Mods; 69 | 70 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.TryPatchBlueprint))] 71 | [HarmonyPrefix] 72 | static void TryPatchBlueprint_Prefix(OwlcatModification __instance) 73 | { 74 | ThreadStaticData.CurrentMod = __instance; 75 | } 76 | 77 | [HarmonyPrepare] 78 | static bool Prepare() 79 | { 80 | MergeSettings ??= (JsonMergeSettings)AccessTools.DeclaredField(typeof(OwlcatModificationBlueprintPatcher), "MergeSettings").GetValue(null); 81 | #if DEBUG 82 | PFLog.Mods.Log($"OwlModPatcherEnhancer Prepare - MergeSettings is null? {MergeSettings == null}"); 83 | #endif 84 | return MergeSettings != null; 85 | } 86 | 87 | [HarmonyPatch(typeof(OwlcatModificationBlueprintPatcher), "ApplyPatchEntry", new Type[] { typeof(JObject), typeof(JObject) })] 88 | [HarmonyPostfix] 89 | public static void PostfixComponent(JObject jsonBlueprint, JObject patchEntry) 90 | { 91 | if (jsonBlueprint["Components"] is not JArray components) 92 | return; 93 | 94 | if (patchEntry["ComponentPatches"] is not JArray componentPatches) 95 | return; 96 | 97 | using IEnumerator enumerator = componentPatches.GetEnumerator(); 98 | 99 | while (enumerator.MoveNext()) 100 | { 101 | if (enumerator.Current is not JObject currentComponentPatch) 102 | continue; 103 | var componentName = currentComponentPatch["Name"].ToString(); 104 | 105 | try 106 | { 107 | if (componentName.IsNullOrEmpty()) 108 | { 109 | ModLogger.Warning($"OwlModPatcherEnhance - a component patch with empty Name"); 110 | continue; 111 | } 112 | JObject? componentToPatch = null; 113 | 114 | using (var comps = components.GetEnumerator()) 115 | { 116 | while (comps.MoveNext()) 117 | { 118 | var name = (comps.Current as JObject)?["name"]?.ToString(); 119 | { 120 | if (name.IsNullOrEmpty()) 121 | ModLogger.Warning("OwlModPatcherEnhance Found a component with null or empty name: \n" + (comps.Current as JObject)); 122 | } 123 | if (name != componentName) 124 | continue; 125 | componentToPatch = comps.Current as JObject; 126 | } 127 | } 128 | 129 | if (componentToPatch == null) 130 | { 131 | ModLogger.Warning($"Failed to find component with the name '{componentName}' when patching blueprint {jsonBlueprint["name"]} (guid {jsonBlueprint["AssetGuid"]}"); 132 | } 133 | var patchString = currentComponentPatch["Data"]; 134 | componentToPatch!.Merge(patchString, MergeSettings); 135 | } 136 | catch (Exception ex) 137 | { 138 | ModLogger.Exception(ex, $"Error on patching blueprint {jsonBlueprint["name"]} (guid {jsonBlueprint["AssetGuid"]}) with component {componentName}"); 139 | } 140 | } 141 | } 142 | 143 | [HarmonyPatch(typeof(OwlcatModificationBlueprintPatcher), nameof(OwlcatModificationBlueprintPatcher.ApplyPatch), new Type[] { typeof(SimpleBlueprint), typeof(JObject)})] 144 | [HarmonyPostfix] 145 | public static void PostfixElement(SimpleBlueprint __result, JObject patch) 146 | { 147 | if (__result is not BlueprintScriptableObject blueprint) 148 | { 149 | return; 150 | } 151 | 152 | if (patch["ElementsPatches"] is not JArray elementPatches) 153 | { 154 | return; 155 | } 156 | 157 | using IEnumerator enumerator = elementPatches.GetEnumerator(); 158 | 159 | while (enumerator.MoveNext()) 160 | { 161 | var currentPatch = (JObject)enumerator.Current; 162 | var elementName = currentPatch["Name"].ToString(); 163 | if (elementName.IsNullOrEmpty()) 164 | { 165 | ModLogger.Warning($"OwlModPatcherEnhancer found a patch with empty element name when patching blueprint {blueprint.name} (guid {blueprint.AssetGuid})"); 166 | continue; 167 | } 168 | var element = blueprint.ElementsArray.FirstOrDefault(el => el.name == elementName); 169 | if (element == null) 170 | { 171 | ModLogger.Warning($"OwlModPatcherEnhancer failed to find an element with name {elementName} when patching blueprint {blueprint.name} (guid {blueprint.AssetGuid})"); 172 | continue; 173 | } 174 | try 175 | { 176 | var elementPatch = currentPatch["Data"].ToString(); 177 | WrapperInstance.Data = blueprint; 178 | Json.BlueprintBeingRead = WrapperInstance; 179 | JsonConvert.PopulateObject(elementPatch, element, settings); 180 | Json.BlueprintBeingRead = null; 181 | blueprint.RemoveFromElementsList(element); 182 | } 183 | catch (Exception ex) 184 | { 185 | ModLogger.Exception(ex, $"Exception when using OwlModPatcherEnhancer to patch element {elementName} on blueprint {blueprint.name} (guid {blueprint.AssetGuid}"); 186 | } 187 | } 188 | } 189 | 190 | ////Disable old PatchEnhancer 191 | //[HarmonyPatch(typeof(OwlcatModificationsManager), MethodType.Constructor, [])] 192 | //[HarmonyPostfix] 193 | //static void OMM_Constructor_Postfix(OwlcatModificationsManager __instance) 194 | //{ 195 | // if (__instance.m_Settings is null) 196 | // return; 197 | 198 | // var enabledMods = __instance.m_Settings.EnabledModifications.Where(m => m != "PatchEnhancer").ToArray(); 199 | 200 | // if (enabledMods.Length < __instance.m_Settings.EnabledModifications.Length) 201 | // { 202 | // PFLog.Mods.Warning("Disabling old PatchEnhancer"); 203 | 204 | // AccessTools.Field(typeof(OwlcatModificationsManager.SettingsData), nameof(OwlcatModificationsManager.SettingsData.EnabledModifications)) 205 | // .SetValue(__instance.m_Settings, enabledMods); 206 | // } 207 | //} 208 | } 209 | -------------------------------------------------------------------------------- /WrathPatches/Patches/EntityFactComponent.Activate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | using System.Reflection.Emit; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | using HarmonyLib; 11 | 12 | using Kingmaker.EntitySystem; 13 | using Kingmaker.EntitySystem.Entities; 14 | 15 | using Owlcat.Runtime.Core.Logging; 16 | 17 | using WrathPatches.TranspilerUtil; 18 | 19 | namespace WrathPatches 20 | { 21 | [WrathPatch("Better EntityFactComponent error messages")] 22 | [HarmonyPatch] 23 | public static class EntityFactComponent_ExceptionMessages 24 | { 25 | static string PrintBlueprintAndOwner(EntityFact? fact) 26 | { 27 | var blueprint = fact?.Blueprint; 28 | var maybeOwner = fact?.Owner; 29 | 30 | var sb = new StringBuilder(); 31 | 32 | sb.Append(" Blueprint: "); 33 | if (blueprint is { }) 34 | { 35 | sb.Append($"{blueprint.AssetGuid}"); 36 | if (blueprint.name is not null) 37 | sb.Append($" ({blueprint.name})"); 38 | } 39 | else 40 | sb.Append(""); 41 | sb.AppendLine(); 42 | 43 | sb.Append(" Owner: "); 44 | if (maybeOwner is UnitEntityData owner) 45 | sb.Append($"{owner?.CharacterName}"); 46 | else 47 | sb.Append(""); 48 | sb.AppendLine(); 49 | 50 | return sb.ToString(); 51 | } 52 | 53 | public static string ExceptionMessage(object? entityFactComponent) 54 | { 55 | var sb = new StringBuilder(); 56 | sb.AppendLine($"Exception occured in {entityFactComponent?.GetType()}.{nameof(EntityFactComponent.Activate)} ({entityFactComponent})"); 57 | sb.AppendLine(PrintBlueprintAndOwner((entityFactComponent as EntityFactComponent)?.Fact)); 58 | return sb.ToString(); 59 | } 60 | 61 | static IEnumerable PatchExceptionLog(IEnumerable instructions, CodeInstruction callExceptionMessage) 62 | { 63 | var iList = instructions.ToList(); 64 | 65 | //var Kingmaker_PFLog_EntityFact = typeof(Kingmaker.PFLog).GetField("EntityFact", AccessTools.all); 66 | var Owlcat_Runtime_Core_Logging_LogChannel_Exception = typeof(LogChannel).GetMethod( 67 | nameof(LogChannel.Exception), 68 | new[] { typeof(Exception), typeof(string), typeof(object[]) }); 69 | 70 | var matchLogChannelException = new Func[] 71 | { 72 | ci => ci.IsStloc(), 73 | ci => ci.opcode == OpCodes.Ldsfld && ci.operand is FieldInfo fi && fi.FieldType == typeof(LogChannel), 74 | ci => ci.IsLdloc(), 75 | ci => ci.opcode == OpCodes.Ldnull, 76 | ci => ci.opcode == OpCodes.Call && ci.OperandIs(CodeInstruction.Call(() => Array.Empty()).operand), 77 | ci => ci.Calls(Owlcat_Runtime_Core_Logging_LogChannel_Exception) 78 | }; 79 | 80 | var toInsert = new[] 81 | { 82 | new CodeInstruction(OpCodes.Call, typeof(Exception).GetProperty(nameof(Exception.InnerException)).GetGetMethod()), 83 | new CodeInstruction(OpCodes.Ldarg_0), 84 | callExceptionMessage 85 | }; 86 | 87 | var matched = instructions.FindInstructionsIndexed(matchLogChannelException); 88 | 89 | if (matched.Count() != matchLogChannelException.Count()) 90 | { 91 | #if DEBUG 92 | Main.Logger.Log($"Could not find match"); 93 | #endif 94 | 95 | return instructions; 96 | } 97 | 98 | var ldnullOffset = matched.First(ci => ci.instruction.opcode == OpCodes.Ldnull).index; 99 | 100 | var exceptionBlocks = iList[ldnullOffset].blocks; 101 | 102 | iList.RemoveAt(ldnullOffset); 103 | //iList.InsertRange(ldnullOffset, toInsert.Select(ci => { ci.blocks = exceptionBlocks; return ci; })); 104 | iList.InsertRange(ldnullOffset, toInsert); 105 | 106 | return iList; 107 | } 108 | 109 | [HarmonyPatch(typeof(EntityFactComponent), nameof(EntityFactComponent.Activate))] 110 | [HarmonyTranspiler] 111 | static IEnumerable EntityFactComponent_Activate_Transpiler(IEnumerable instructions) 112 | { 113 | #if DEBUG 114 | Main.Logger.Log($"{nameof(EntityFactComponent_Activate_Transpiler)}"); 115 | #endif 116 | 117 | return PatchExceptionLog(instructions, CodeInstruction.Call(obj => ExceptionMessage(obj))); 118 | } 119 | 120 | static string DelegateExceptionMessage(object? componentRuntime) 121 | { 122 | if (componentRuntime?.GetType() is not { } type) return null!; 123 | 124 | try 125 | { 126 | var maybeFact = type.GetProperty("Fact", AccessTools.all)?.GetValue(componentRuntime); 127 | 128 | //var blueprint = (maybeFact as EntityFact)?.Blueprint; 129 | 130 | var sb = new StringBuilder(); 131 | 132 | sb.AppendLine($"Exception occured in {type}.{nameof(EntityFactComponentDelegate.ComponentRuntime.OnActivate)} ({componentRuntime})"); 133 | 134 | var delegateProperty = type.GetProperty("Delegate", AccessTools.all); 135 | var delegateObj = delegateProperty?.GetValue(componentRuntime); 136 | var delegateType = delegateObj?.GetType(); 137 | 138 | sb.AppendLine($" Delegate type: {delegateType?.ToString() ?? delegateProperty?.GetType()?.ToString()}"); 139 | 140 | sb.Append(PrintBlueprintAndOwner(maybeFact as EntityFact)); 141 | 142 | return sb.ToString(); 143 | } 144 | catch (Exception ex) 145 | { 146 | Main.Logger.Error("Exception occured generating exception message"); 147 | Main.Logger.LogException(ex); 148 | 149 | return null!; 150 | } 151 | } 152 | 153 | static readonly Dictionary lambdaDict = []; 154 | 155 | static void ComponentRuntime_Delegate_OnActivate(object instance) 156 | { 157 | //Main.Logger.Log(nameof(ComponentRuntime_Delegate_OnActivate)); 158 | 159 | var t = instance.GetType(); 160 | 161 | if (!lambdaDict.TryGetValue(t, out var @delegate)) 162 | { 163 | var delegateProperty = t.GetProperty("Delegate", AccessTools.all); 164 | var delegateType = delegateProperty?.PropertyType; 165 | var delegateOnActivate = delegateType?.GetMethod("OnActivate", AccessTools.all); 166 | 167 | var objectExpr = Expression.Parameter(t, "instance"); 168 | var getDelegateExpr = Expression.PropertyOrField(objectExpr, "Delegate"); 169 | var callOnActivate = Expression.Call(getDelegateExpr, delegateOnActivate); 170 | 171 | var lambda = Expression.Lambda(callOnActivate, objectExpr); 172 | 173 | @delegate = lambda.Compile(); 174 | 175 | lambdaDict[t] = @delegate; 176 | } 177 | 178 | _ = @delegate.DynamicInvoke(instance); 179 | } 180 | 181 | [HarmonyPatch(typeof(EntityFactComponentDelegate.ComponentRuntime), 182 | nameof(EntityFactComponentDelegate.ComponentRuntime.OnActivate))] 183 | [HarmonyTranspiler] 184 | static IEnumerable EntityFactComponentDelegate_ComponentRuntime_OnActivate_Transpiler(IEnumerable instructions) 185 | { 186 | #if DEBUG 187 | Main.Logger.Log($"{nameof(EntityFactComponentDelegate_ComponentRuntime_OnActivate_Transpiler)}"); 188 | #endif 189 | var matchDelegateOnActivate = new Func[] 190 | { 191 | ci => ci.opcode == OpCodes.Ldarg_0, 192 | ci => ci.opcode == OpCodes.Call, // instance method of generic type, throws InvalidCastException 193 | ci => ci.opcode == OpCodes.Callvirt 194 | }; 195 | 196 | var matched = instructions.FindInstructionsIndexed(matchDelegateOnActivate).Select(i => i.instruction).ToArray(); 197 | 198 | if (!matched.Any()) return instructions; 199 | 200 | matched[1].operand = CodeInstruction.Call((object instance) => ComponentRuntime_Delegate_OnActivate(instance)).operand; 201 | matched[2].opcode = OpCodes.Nop; 202 | matched[2].operand = null; 203 | 204 | return PatchExceptionLog(instructions, 205 | CodeInstruction.Call(obj => DelegateExceptionMessage(obj))); 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /WrathPatches/Patches/OnDemandBlueprints.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Reflection.Emit; 8 | 9 | using HarmonyLib; 10 | 11 | using Kingmaker.Blueprints; 12 | using Kingmaker.Blueprints.JsonSystem; 13 | using Kingmaker.Modding; 14 | 15 | using Newtonsoft.Json; 16 | 17 | using UnityModManagerNet; 18 | 19 | namespace WrathPatches.Patches; 20 | 21 | [WrathPatch("Load Blueprints on demand")] 22 | [HarmonyPatch] 23 | public static class OnDemandBlueprints 24 | { 25 | public delegate object? ResourceLoadEvent(string guid, object? resource); 26 | 27 | public static event ResourceLoadEvent? BeforeResourceLoad; 28 | public static event ResourceLoadEvent? AfterResourceLoad; 29 | 30 | static bool Prepare() => !UnityModManager.modEntries.Where(me => me.Enabled).Select(me => me.Info.Id).Contains("0ToyBox0"); 31 | 32 | static object? OnResourceLoad(ResourceLoadEvent? e, string guid, object? resource) 33 | { 34 | var subscribers = e?.GetInvocationList().OfType(); 35 | 36 | if (subscribers is null) 37 | return null; 38 | 39 | var original = resource; 40 | 41 | foreach (var f in subscribers) 42 | resource = f(guid, resource) ?? resource; 43 | 44 | return resource != original ? resource : null; 45 | } 46 | 47 | [HarmonyPatch(typeof(OwlcatModificationsManager), nameof(OwlcatModificationsManager.OnResourceLoaded))] 48 | [HarmonyPrefix] 49 | static void OwlcatModificationsManager_OnResourceLoaded_Prefix(ref object? resource, string guid, out object? __state) 50 | { 51 | __state = OnResourceLoad(BeforeResourceLoad, guid, resource); 52 | 53 | if (__state != null) 54 | resource = __state; 55 | } 56 | 57 | // replacement will be null unless an owlmod did something 58 | [HarmonyPatch(typeof(OwlcatModificationsManager), nameof(OwlcatModificationsManager.OnResourceLoaded))] 59 | [HarmonyPostfix] 60 | static void OwlcatModificationsManager_OnResourceLoaded_Postfix(object? resource, string guid, ref object? replacement, object? __state) 61 | { 62 | var result = OnResourceLoad(AfterResourceLoad, guid, replacement ?? __state ?? resource); 63 | 64 | if (result != null) 65 | replacement = result; 66 | else if (replacement == null) 67 | replacement = __state; 68 | } 69 | 70 | #if DEBUG 71 | static readonly Stopwatch timer = new(); 72 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.LoadBlueprints))] 73 | [HarmonyPrefix] 74 | static void OwlcatModification_LoadBlueprints_Prefix(OwlcatModification __instance) 75 | { 76 | Main.Logger.Log($"{__instance.Manifest.UniqueName} {nameof(OwlcatModification.LoadBlueprints)} start"); 77 | timer.Restart(); 78 | } 79 | 80 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.LoadBlueprints))] 81 | [HarmonyPostfix] 82 | static void OwlcatModification_LoadBlueprints_Postfix(OwlcatModification __instance) 83 | { 84 | timer.Stop(); 85 | Main.Logger.Log($"{__instance.Manifest.UniqueName} {nameof(OwlcatModification.LoadBlueprints)} time = {timer.ElapsedMilliseconds}ms"); 86 | } 87 | #endif 88 | static readonly MethodInfo LoadedBlueprints_TryGetValue = typeof(Dictionary).GetMethod(nameof(Dictionary.TryGetValue)); 89 | 90 | // BlueprintsCache.Load will now call OwlcatModificationsManager.Instance.OnResourceLoaded when a blueprint's guid is not found in the cache. 91 | // This allows OMM mods to add their blueprints when they are requested instead of all at once. 92 | // UMM mods can also do this by subscribing to the OnDemandBlueprints.BeforeResourceLoad event 93 | [HarmonyPatch(typeof(BlueprintsCache), nameof(BlueprintsCache.Load))] 94 | [HarmonyTranspiler] 95 | static IEnumerable BlueprintsCache_Load_Transpiler(IEnumerable instructions, ILGenerator ilGen) 96 | { 97 | var matcher = new CodeMatcher(instructions, ilGen); 98 | matcher = matcher 99 | .MatchStartForward( 100 | CodeMatch.LoadsLocal(), 101 | CodeMatch.Branches(), 102 | new(OpCodes.Ldnull), 103 | CodeMatch.StoresLocal(), 104 | new(ci => ci.opcode == OpCodes.Leave_S || ci.opcode == OpCodes.Leave)); 105 | 106 | var leaveAndReturnNull = matcher.InstructionsWithOffsets(2, 4); 107 | 108 | matcher = matcher.SetAndAdvance(OpCodes.Nop, null); 109 | 110 | var keyNotInCacheTarget = (Label)matcher.Operand; 111 | 112 | matcher = matcher 113 | .SetOpcodeAndAdvance(OpCodes.Br_S) 114 | .MatchEndBackwards( 115 | CodeMatch.Calls(LoadedBlueprints_TryGetValue), 116 | CodeMatch.Branches()) 117 | .SetOperandAndAdvance(keyNotInCacheTarget) 118 | .MatchEndForward( 119 | CodeMatch.LoadsLocal(), 120 | new(ci => ci.opcode == OpCodes.Isinst), 121 | new(ci => ci.opcode == OpCodes.Dup), 122 | CodeMatch.Branches(), 123 | new(ci => ci.opcode == OpCodes.Pop), 124 | CodeMatch.LoadsLocal(), 125 | CodeMatch.StoresLocal()) 126 | .Advance(1); 127 | 128 | matcher = matcher 129 | .InsertAndAdvance([new(matcher.InstructionAt(-2))]); 130 | 131 | matcher = matcher 132 | .InsertBranchAndAdvance(OpCodes.Brtrue_S, matcher.Pos) 133 | .Insert(leaveAndReturnNull); 134 | 135 | return matcher.InstructionEnumeration(); 136 | } 137 | 138 | public static readonly Dictionary> ModBlueprints = []; 139 | public static string? GetModBlueprintGuid(string path, OwlcatModification mod) 140 | { 141 | try 142 | { 143 | using var file = File.OpenText(path); 144 | using var reader = new JsonTextReader(file); 145 | 146 | while (reader.Read() && reader.Value is not "AssetId") { } 147 | 148 | var assetId = reader.ReadAsString(); 149 | #if DEBUG 150 | mod.Logger.Log($"Register blueprint {assetId} = {path}"); 151 | #endif 152 | 153 | if (string.IsNullOrEmpty(assetId)) 154 | return null; 155 | 156 | ModBlueprints.GetOrAdd(mod).Add(assetId, path); 157 | return assetId; 158 | } 159 | catch(Exception ex) 160 | { 161 | mod.Logger.Error($"Exception while reading blueprint {path}"); 162 | mod.Logger.Exception(ex); 163 | } 164 | 165 | return null; 166 | } 167 | 168 | static readonly MethodInfo BlueprintJsonWrapper_Load = AccessTools.Method(typeof(BlueprintJsonWrapper), nameof(BlueprintJsonWrapper.Load)); 169 | static readonly FieldInfo OwlcatModification_Blueprints_Field = AccessTools.Field(typeof(OwlcatModification), nameof(OwlcatModification.Blueprints)); 170 | static readonly MethodInfo HashSet_Add = typeof(HashSet).GetMethod(nameof(HashSet.Add)); 171 | 172 | // Don't try to deserialize blueprints here, just read the AssetId and add to the ModBlueprints dictionary 173 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.LoadBlueprints))] 174 | [HarmonyTranspiler] 175 | static IEnumerable OwlcatModification_LoadBlueprints_Transpiler(IEnumerable instructions, ILGenerator ilGen) 176 | { 177 | var matcher = new CodeMatcher(instructions, ilGen); 178 | 179 | matcher = matcher 180 | .MatchStartForward( 181 | CodeMatch.IsLdarg(0), 182 | CodeMatch.LoadsField(OwlcatModification_Blueprints_Field), 183 | CodeMatch.LoadsLocal(), 184 | new(_ => true), 185 | CodeMatch.Calls(HashSet_Add)); 186 | 187 | var end = matcher.Pos; 188 | 189 | matcher = matcher 190 | .InsertAndAdvance( 191 | new CodeInstruction(OpCodes.Ldarg_0), 192 | CodeInstruction.Call((string path, OwlcatModification mod) => GetModBlueprintGuid(path, mod)), 193 | CodeInstruction.StoreLocal(1)) 194 | .Advance(2) 195 | .SetAndAdvance(OpCodes.Ldloc_1, null) 196 | .SetAndAdvance(OpCodes.Nop, null) 197 | .Advance(1); 198 | 199 | // Might be unnecessary to add this null check here? 200 | // --- 201 | matcher = matcher 202 | .InsertBranchAndAdvance(OpCodes.Br_S, matcher.Pos) 203 | .Insert(new CodeInstruction(OpCodes.Pop)); 204 | var ifNull = matcher.Pos; 205 | matcher = matcher 206 | .Advance(-2) 207 | .InsertBranch(OpCodes.Brfalse_S, ifNull) 208 | .Insert(new CodeInstruction(OpCodes.Dup)); 209 | // --- 210 | 211 | matcher = matcher 212 | .MatchStartBackwards(CodeMatch.Calls(BlueprintJsonWrapper_Load)); 213 | 214 | while (matcher.Pos < end) 215 | matcher = matcher.SetAndAdvance(OpCodes.Nop, null); 216 | 217 | return matcher.InstructionEnumeration(); 218 | } 219 | 220 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.OnResourceLoaded))] 221 | [HarmonyPrefix] 222 | static void OwlcatModification_OnResourceLoaded_Prefix(ref object? resource, string guid, OwlcatModification __instance, out SimpleBlueprint? __state) 223 | { 224 | __state = null; 225 | 226 | if (resource is null 227 | && __instance.Blueprints.Contains(guid) 228 | && ModBlueprints.GetOrAdd(__instance).TryGetValue(guid, out var path)) 229 | { 230 | __instance.Logger.Log($"Adding blueprint {path}"); 231 | 232 | try 233 | { 234 | var blueprint = BlueprintJsonWrapper.Load(path).Data; 235 | blueprint.OnEnable(); 236 | 237 | resource = __state = ResourcesLibrary.BlueprintsCache.AddCachedBlueprint(BlueprintGuid.Parse(guid), blueprint); 238 | } 239 | catch (Exception ex) 240 | { 241 | __instance.Logger.Error($"Exception while loading blueprint {path}"); 242 | __instance.Logger.Exception(ex); 243 | } 244 | } 245 | } 246 | 247 | // Need to use __state here because the original method sets replacement to null 248 | [HarmonyPatch(typeof(OwlcatModification), nameof(OwlcatModification.OnResourceLoaded))] 249 | [HarmonyPostfix] 250 | static void OwlcatModification_OnResourceLoaded_Postfix(ref object? replacement, SimpleBlueprint? __state) => replacement ??= __state; 251 | } 252 | --------------------------------------------------------------------------------