From cf3c0f5eecce4528851fce72a2ad5d742002606c Mon Sep 17 00:00:00 2001 From: ouyangfang Date: Tue, 11 Aug 2026 20:53:10 +0800 Subject: [PATCH 1/6] Update SpellbookMerge for WotR 2.7 and add zhCN --- README.md | 8 +++- SpellbookMerge/Info.json | 6 +-- SpellbookMerge/Localization/zhCN.json | 36 +++++++++++++++ SpellbookMerge/Main.cs | 4 +- SpellbookMerge/Patches/LegacyMerge.cs | 31 ++++++++----- SpellbookMerge/Patches/MythicProgression.cs | 44 +++++++++++++++++- SpellbookMerge/Resources.cs | 50 ++++++++++++++++----- SpellbookMerge/SpellbookMerge.csproj | 10 ++++- 8 files changed, 160 insertions(+), 29 deletions(-) create mode 100644 SpellbookMerge/Localization/zhCN.json diff --git a/README.md b/README.md index 2b07c12..622ea52 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ This is a mod for Pathfinder Wrath of the Righteous. It adds additional spellbook merging options. +## Compatibility + +This branch updates the mod for the current 2.7.x game data and Unity Mod Manager 0.32.x. The mythic spellbook merge patches now discover all known base-game spellbooks, so Aeon, Azata, Demon and Trickster can use the same merge behavior as Angel and Lich. If a custom merge cannot be applied safely, the game’s native merge implementation is left in place. + +The release also includes Simplified Chinese strings in `Localization/zhCN.json`. The mod keeps its English fallback when another locale is selected. + ## Patch 1.3 Owlcat introduced several changes to the Spellbook Merging system in patch 1.3, that just broke the mod. @@ -164,4 +170,4 @@ File an issue report if that's not the case. I would like to give my sincerest thanks to: 1. Owlcat for this amazing game. -2. My fellow modders from discord for inspiration and answering my questions. Particularly Narria Cabarius, Vek17, Bubbles and ArcaneTrixter. \ No newline at end of file +2. My fellow modders from discord for inspiration and answering my questions. Particularly Narria Cabarius, Vek17, Bubbles and ArcaneTrixter. diff --git a/SpellbookMerge/Info.json b/SpellbookMerge/Info.json index 7081d8c..738a001 100644 --- a/SpellbookMerge/Info.json +++ b/SpellbookMerge/Info.json @@ -2,9 +2,9 @@ "Id": "SpellbookMerge", "DisplayName": "Spellbook Merge", "Author": "vikigenius", - "Version": "1.7.1", - "ManagerVersion": "0.21.3", + "Version": "1.8.0", + "ManagerVersion": "0.32.2", "Requirements": [], "AssemblyName": "SpellbookMerge.dll", "EntryMethod": "SpellbookMerge.Main.Load" -} \ No newline at end of file +} diff --git a/SpellbookMerge/Localization/zhCN.json b/SpellbookMerge/Localization/zhCN.json new file mode 100644 index 0000000..8b3ee07 --- /dev/null +++ b/SpellbookMerge/Localization/zhCN.json @@ -0,0 +1,36 @@ +{ + "strings": [ + { + "Key": "AeonIncorporateSpellbook.Name", + "Value": "神话法术书" + }, + { + "Key": "AeonIncorporateSpellbook.Description", + "Value": "在神话阶层3时,御衡者获得施放神话法术的能力。你可以选择将其并入现有法术书,或将其作为独立法术书。" + }, + { + "Key": "AzataIncorporateSpellbook.Name", + "Value": "神话法术书" + }, + { + "Key": "AzataIncorporateSpellbook.Description", + "Value": "在神话阶层3时,灵使获得施放神话法术的能力。你可以选择将其并入现有法术书,或将其作为独立法术书。" + }, + { + "Key": "DemonIncorporateSpellbook.Name", + "Value": "神话法术书" + }, + { + "Key": "DemonIncorporateSpellbook.Description", + "Value": "在神话阶层3时,恶魔获得施放神话法术的能力。你可以选择将其并入现有法术书,或将其作为独立法术书。" + }, + { + "Key": "TricksterIncorporateSpellbook.Name", + "Value": "神话法术书" + }, + { + "Key": "TricksterIncorporateSpellbook.Description", + "Value": "在神话阶层3时,诡计大师获得施放神话法术的能力。你可以选择将其并入现有法术书,或将其作为独立法术书。" + } + ] +} diff --git a/SpellbookMerge/Main.cs b/SpellbookMerge/Main.cs index 19da57c..2aa214f 100644 --- a/SpellbookMerge/Main.cs +++ b/SpellbookMerge/Main.cs @@ -20,7 +20,7 @@ public static class Main private static bool Load(UnityModManager.ModEntry modEntry) { ModEntry = modEntry; - UserConfigDir = ModEntry.Path + "UserSettings"; + UserConfigDir = Path.Combine(ModEntry.Path, "UserSettings"); Directory.CreateDirectory(UserConfigDir); ModSettings.OverrideFrom(UserConfigDir); ModEntry.OnToggle = OnToggle; @@ -63,4 +63,4 @@ public static void LogError(string message) { PFLog.Mods.Error(message); } } -} \ No newline at end of file +} diff --git a/SpellbookMerge/Patches/LegacyMerge.cs b/SpellbookMerge/Patches/LegacyMerge.cs index 3369322..746a60d 100644 --- a/SpellbookMerge/Patches/LegacyMerge.cs +++ b/SpellbookMerge/Patches/LegacyMerge.cs @@ -20,8 +20,19 @@ private static class ApplySpellbookApplyPatch private static bool Prefix(LevelUpState state, UnitDescriptor unit) { if (!Main.Enabled) return true; - Apply(state, unit); - return false; + try + { + Apply(state, unit); + return false; + } + catch (Exception e) + { + // A third-party class/archetype can have a spellbook shape that this legacy + // compatibility path does not know about. Let the game's implementation run + // in that case instead of breaking level-up for the whole character. + Main.LogException(e, "Spellbook Merge failed while applying a spellbook level; falling back to the game implementation."); + return true; + } } public static void Apply(LevelUpState state, UnitDescriptor unit) @@ -30,17 +41,17 @@ public static void Apply(LevelUpState state, UnitDescriptor unit) { return; } - var component = state.SelectedClass.GetComponent(); - if (component != null && component.Levels.Contains(state.NextClassLevel)) - { - return; - } + var component = state.SelectedClass.GetComponent(); + if (component != null && component.Levels.Contains(state.NextClassLevel)) + { + return; + } var classData = unit.Progression.GetClassData(state.SelectedClass); if (classData?.Spellbook == null) return; - Spellbook spellbook = unit.DemandSpellbook(classData.Spellbook); + Spellbook spellbook = unit.DemandSpellbook(classData.Spellbook); if (state.SelectedClass.Spellbook && state.SelectedClass.Spellbook != classData.Spellbook) { - var spellbook2 = unit.Spellbooks.FirstOrDefault(s => s.Blueprint == state.SelectedClass.Spellbook); + var spellbook2 = unit.Spellbooks.FirstOrDefault(s => s.Blueprint == state.SelectedClass.Spellbook); if (spellbook2 != null) { foreach (AbilityData abilityData in spellbook2.GetAllKnownSpells()) @@ -85,4 +96,4 @@ public static void Apply(LevelUpState state, UnitDescriptor unit) } } } -} \ No newline at end of file +} diff --git a/SpellbookMerge/Patches/MythicProgression.cs b/SpellbookMerge/Patches/MythicProgression.cs index 5a79d9a..0f24c4c 100644 --- a/SpellbookMerge/Patches/MythicProgression.cs +++ b/SpellbookMerge/Patches/MythicProgression.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using HarmonyLib; using Kingmaker.Blueprints; using Kingmaker.Blueprints.Classes; @@ -24,11 +25,35 @@ private static void Postfix() PatchTricksterProgression(); PatchAngelAllowedMerges(); PatchLichAllowedMerges(); + PatchAllModAllowedMerges(); + } + + private static void PatchAllModAllowedMerges() + { + var spellbooks = Resources.SpellbookBlueprints.AllSpellbooks + .Where(spellbook => spellbook != null) + .Select(spellbook => spellbook.ToReference()) + .ToArray(); + if (spellbooks.Length == 0) return; + + PatchModAllowedMerges("AeonIncorporateSpellbook", spellbooks); + PatchModAllowedMerges("AzataIncorporateSpellbook", spellbooks); + PatchModAllowedMerges("DemonIncorporateSpellbook", spellbooks); + PatchModAllowedMerges("TricksterIncorporateSpellbook", spellbooks); + } + + private static void PatchModAllowedMerges(string name, BlueprintSpellbookReference[] spellbooks) + { + var feature = Resources.TryGetModBlueprint(name); + if (feature == null) return; + feature.m_AllowedSpellbooks = spellbooks; + Main.Log($"Patched {name} to allow {spellbooks.Length} spellbooks"); } private static void PatchAngelAllowedMerges() { var angelIncorporateSpellbookFeature = Resources.MythicMergeBlueprints.AngelIncorporateSpellbook; + if (angelIncorporateSpellbookFeature == null || angelIncorporateSpellbookFeature.m_AllowedSpellbooks == null) return; var angelAllowedMerges = new List(angelIncorporateSpellbookFeature.m_AllowedSpellbooks); var additionalMerges = new List { @@ -40,7 +65,14 @@ private static void PatchAngelAllowedMerges() Resources.SpellbookBlueprints.SageSpellbook.ToReference(), Resources.SpellbookBlueprints.CrossbloodedSpellbook.ToReference(), }; - angelAllowedMerges.AddRange(additionalMerges); + foreach (var spellbook in additionalMerges) + { + if (!angelAllowedMerges.Any(existing => existing.Guid == spellbook.Guid)) angelAllowedMerges.Add(spellbook); + } + angelAllowedMerges.AddRange(Resources.SpellbookBlueprints.AllSpellbooks + .Where(spellbook => spellbook != null) + .Select(spellbook => spellbook.ToReference()) + .Where(spellbook => !angelAllowedMerges.Any(existing => existing.Guid == spellbook.Guid))); angelIncorporateSpellbookFeature.m_AllowedSpellbooks = angelAllowedMerges.ToArray(); Main.Log("Patched Angel allowed spellbook merges"); } @@ -48,6 +80,7 @@ private static void PatchAngelAllowedMerges() private static void PatchLichAllowedMerges() { var lichIncorporateSpellbookFeature = Resources.MythicMergeBlueprints.LichIncorporateSpellbook; + if (lichIncorporateSpellbookFeature == null || lichIncorporateSpellbookFeature.m_AllowedSpellbooks == null) return; var lichAllowedMerges = new List(lichIncorporateSpellbookFeature.m_AllowedSpellbooks); var additionalMerges = new List { @@ -57,7 +90,14 @@ private static void PatchLichAllowedMerges() Resources.SpellbookBlueprints.ClericSpellbook.ToReference(), }; - lichAllowedMerges.AddRange(additionalMerges); + foreach (var spellbook in additionalMerges) + { + if (!lichAllowedMerges.Any(existing => existing.Guid == spellbook.Guid)) lichAllowedMerges.Add(spellbook); + } + lichAllowedMerges.AddRange(Resources.SpellbookBlueprints.AllSpellbooks + .Where(spellbook => spellbook != null) + .Select(spellbook => spellbook.ToReference()) + .Where(spellbook => !lichAllowedMerges.Any(existing => existing.Guid == spellbook.Guid))); lichIncorporateSpellbookFeature.m_AllowedSpellbooks = lichAllowedMerges.ToArray(); Main.Log("Patched Lich allowed spellbook merges"); } diff --git a/SpellbookMerge/Resources.cs b/SpellbookMerge/Resources.cs index 1c0b42d..0ceae6f 100644 --- a/SpellbookMerge/Resources.cs +++ b/SpellbookMerge/Resources.cs @@ -4,6 +4,7 @@ using Kingmaker.Blueprints.Classes; using Kingmaker.Blueprints.Classes.Spells; using Kingmaker.Localization; +using Kingmaker.Localization.Shared; namespace SpellbookMerge { @@ -43,6 +44,19 @@ internal static class SpellbookBlueprints public static BlueprintSpellbook ClericSpellbook => TryGetBlueprint("4673d19a0cf2fab4f885cc4d1353da33")!; public static BlueprintSpellbook ShamanSpellbook => TryGetBlueprint("44f16931dabdff643bfe2a48138e769f")!; + // Keep the merge choices in one place. This list contains every base-game spellbook + // known to the 2.7 data set; using it for each mythic path is what makes the mod's + // advertised "any class" behavior consistent instead of path-dependent. + public static BlueprintSpellbook[] AllSpellbooks => new[] + { + InquisitorSpellbook, WarPriestSpellbook, MagusSpellbook, BardSpellbook, + SkaldSpellbook, SwordSaintSpellbook, BloodRagerSpellbook, AlchemistSpellbook, + EldritchScoundrelSpellbook, HunterSpellbook, SorcererSpellbook, DruidSpellbook, + PaladinSpellbook, WizardSpellbook, ExploiterWizardSpellbook, WitchSpellbook, + EldritchScionSpellbook, ArcanistSpellbook, SageSpellbook, CrossbloodedSpellbook, + OracleSpellbook, ClericSpellbook, ShamanSpellbook + }; + } internal static class SpellListBlueprints @@ -77,9 +91,23 @@ internal static class SpellTableBlueprints private static readonly Dictionary ModBlueprints = new Dictionary(); - // All localized strings created in this mod, mapped to their localized key. Populated by CreateString. + // Localized strings are keyed by their actual key, rather than their English text. The old + // implementation reused the first key for all four mythic paths because their display name + // is identical; that made external localization packs unable to address the other entries. private static Dictionary textToLocalizedString = new Dictionary(); + private static readonly Dictionary ChineseStrings = new Dictionary + { + { "AeonIncorporateSpellbook.Name", "神话法术书" }, + { "AzataIncorporateSpellbook.Name", "神话法术书" }, + { "DemonIncorporateSpellbook.Name", "神话法术书" }, + { "TricksterIncorporateSpellbook.Name", "神话法术书" }, + { "AeonIncorporateSpellbook.Description", "在神话阶层3时,御衡者获得施放神话{g|Encyclopedia:Spell}法术{/g}的能力。你可以选择将其并入现有法术书,或将其作为独立法术书。" }, + { "AzataIncorporateSpellbook.Description", "在神话阶层3时,灵使获得施放神话{g|Encyclopedia:Spell}法术{/g}的能力。你可以选择将其并入现有法术书,或将其作为独立法术书。" }, + { "DemonIncorporateSpellbook.Description", "在神话阶层3时,恶魔获得施放神话{g|Encyclopedia:Spell}法术{/g}的能力。你可以选择将其并入现有法术书,或将其作为独立法术书。" }, + { "TricksterIncorporateSpellbook.Description", "在神话阶层3时,诡计大师获得施放神话{g|Encyclopedia:Spell}法术{/g}的能力。你可以选择将其并入现有法术书,或将其作为独立法术书。" }, + }; + private static void AddBlueprint(SimpleBlueprint blueprint) { AddBlueprint(blueprint, blueprint.AssetGuid); } @@ -108,22 +136,24 @@ private static void AddBlueprint(SimpleBlueprint blueprint, BlueprintGuid assetI } public static LocalizedString CreateString(string key, string value) { - // See if we used the text previously. - // (It's common for many features to use the same localized text. - // In that case, we reuse the old entry instead of making a new one.) - if (textToLocalizedString.TryGetValue(value, out LocalizedString localized)) { + if (textToLocalizedString.TryGetValue(key, out LocalizedString localized)) { return localized; } + var localizedValue = value; + if (LocalizationManager.CurrentLocale == Locale.zhCN && ChineseStrings.TryGetValue(key, out var chineseValue)) { + localizedValue = chineseValue; + } var current = LocalizationManager.CurrentPack?.GetText(key, false); - if (current != "" && current != value) { - Main.LogDebug($"Info: duplicate localized string `{key}`, different text."); + if (current != "" && current != value && current != localizedValue) { + Main.LogDebug($"Info: keeping existing localized string `{key}`."); + localizedValue = current; } - LocalizationManager.CurrentPack?.PutString(key, value); + LocalizationManager.CurrentPack?.PutString(key, localizedValue); localized = new LocalizedString { m_ShouldProcess = false, m_Key = key }; - textToLocalizedString[value] = localized; + textToLocalizedString[key] = localized; return localized; } @@ -145,4 +175,4 @@ public static LocalizedString CreateString(string key, string value) { return value as T; } } -} \ No newline at end of file +} diff --git a/SpellbookMerge/SpellbookMerge.csproj b/SpellbookMerge/SpellbookMerge.csproj index a6525d7..16427df 100644 --- a/SpellbookMerge/SpellbookMerge.csproj +++ b/SpellbookMerge/SpellbookMerge.csproj @@ -44,6 +44,9 @@ PreserveNewest + + PreserveNewest + @@ -89,10 +92,15 @@ - + + + + + + From 50366da6fb700c227bcd0d91b9e4de8873ff44f6 Mon Sep 17 00:00:00 2001 From: ouyangfang Date: Tue, 11 Aug 2026 21:50:51 +0800 Subject: [PATCH 2/6] Access mythic spellbook fields compatibly --- .../Features/IncorporateSpellbook.cs | 50 +++++++++---------- SpellbookMerge/Patches/MythicProgression.cs | 20 +++++--- SpellbookMerge/Resources.cs | 43 ++++++++++++++++ 3 files changed, 81 insertions(+), 32 deletions(-) diff --git a/SpellbookMerge/Features/IncorporateSpellbook.cs b/SpellbookMerge/Features/IncorporateSpellbook.cs index 684535d..f14dc74 100644 --- a/SpellbookMerge/Features/IncorporateSpellbook.cs +++ b/SpellbookMerge/Features/IncorporateSpellbook.cs @@ -12,7 +12,7 @@ public static void AddAeonIncorporateSpellbookFeature() bp.SetName("Mythic Spellbook"); bp.SetDescription("At 3rd rank, Aeon receives the ability to cast mythic {g|Encyclopedia:Spell}spells{/g}. They can either choose to take it as part of an existing spellbook, or as a standalone spellbook."); - bp.m_AllowedSpellbooks = new[] + Resources.SetAllowedSpellbooks(bp, new[] { Resources.SpellbookBlueprints.InquisitorSpellbook.ToReference(), Resources.SpellbookBlueprints.WarPriestSpellbook.ToReference(), @@ -32,12 +32,12 @@ public static void AddAeonIncorporateSpellbookFeature() Resources.SpellbookBlueprints.ClericSpellbook.ToReference(), Resources.SpellbookBlueprints.WitchSpellbook.ToReference(), - }; - bp.m_MythicSpellList = Resources.SpellListBlueprints.AeonSpellList.ToReference(); + }); // TODO This is just there for compatibility with Owlcat new changes in 1.3. We are not using it yet, since we reverted to the old merging behavior - bp.m_SpellKnownForSpontaneous = Resources.SpellTableBlueprints.MythicSpontaneousSpellsKnownTable - .ToReference(); - bp.IsClassFeature = true; + Resources.ConfigureMythicSpellbook( + bp, + Resources.SpellListBlueprints.AeonSpellList.ToReference(), + Resources.SpellTableBlueprints.MythicSpontaneousSpellsKnownTable.ToReference()); }); } @@ -48,7 +48,7 @@ public static void AddAzataIncorporateSpellbookFeature() bp.SetName("Mythic Spellbook"); bp.SetDescription("At 3rd rank, Azata receives the ability to cast mythic {g|Encyclopedia:Spell}spells{/g}. They can either choose to take it as part of an existing spellbook, or as a standalone spellbook."); - bp.m_AllowedSpellbooks = new[] + Resources.SetAllowedSpellbooks(bp, new[] { Resources.SpellbookBlueprints.MagusSpellbook.ToReference(), Resources.SpellbookBlueprints.BardSpellbook.ToReference(), @@ -68,12 +68,12 @@ public static void AddAzataIncorporateSpellbookFeature() Resources.SpellbookBlueprints.ShamanSpellbook.ToReference(), Resources.SpellbookBlueprints.ClericSpellbook.ToReference(), Resources.SpellbookBlueprints.WitchSpellbook.ToReference(), - }; - bp.m_MythicSpellList = Resources.SpellListBlueprints.AzataSpellList.ToReference(); + }); // TODO This is just there for compatibility with Owlcat new changes in 1.3. We are not using it yet, since we reverted to the old merging behavior - bp.m_SpellKnownForSpontaneous = Resources.SpellTableBlueprints.MythicSpontaneousSpellsKnownTable - .ToReference(); - bp.IsClassFeature = true; + Resources.ConfigureMythicSpellbook( + bp, + Resources.SpellListBlueprints.AzataSpellList.ToReference(), + Resources.SpellTableBlueprints.MythicSpontaneousSpellsKnownTable.ToReference()); }); } @@ -84,7 +84,7 @@ public static void AddDemonIncorporateSpellbookFeature() bp.SetName("Mythic Spellbook"); bp.SetDescription("At 3rd rank, Demon receives the ability to cast mythic {g|Encyclopedia:Spell}spells{/g}. They can either choose to take it as part of an existing spellbook, or as a standalone spellbook."); - bp.m_AllowedSpellbooks = new[] + Resources.SetAllowedSpellbooks(bp, new[] { Resources.SpellbookBlueprints.MagusSpellbook.ToReference(), Resources.SpellbookBlueprints.SwordSaintSpellbook.ToReference(), @@ -103,12 +103,12 @@ public static void AddDemonIncorporateSpellbookFeature() Resources.SpellbookBlueprints.WizardSpellbook.ToReference(), Resources.SpellbookBlueprints.DruidSpellbook.ToReference(), Resources.SpellbookBlueprints.WitchSpellbook.ToReference(), - }; - bp.m_MythicSpellList = Resources.SpellListBlueprints.DemonSpellList.ToReference(); + }); // TODO This is just there for compatibility with Owlcat new changes in 1.3. We are not using it yet, since we reverted to the old merging behavior - bp.m_SpellKnownForSpontaneous = Resources.SpellTableBlueprints.MythicSpontaneousSpellsKnownTable - .ToReference(); - bp.IsClassFeature = true; + Resources.ConfigureMythicSpellbook( + bp, + Resources.SpellListBlueprints.DemonSpellList.ToReference(), + Resources.SpellTableBlueprints.MythicSpontaneousSpellsKnownTable.ToReference()); }); } @@ -119,7 +119,7 @@ public static void AddTricksterIncorporateSpellbookFeature() bp.SetName("Mythic Spellbook"); bp.SetDescription("At 3rd rank, Trickster receives the ability to cast mythic {g|Encyclopedia:Spell}spells{/g}. They can either choose to take it as part of an existing spellbook, or as a standalone spellbook."); - bp.m_AllowedSpellbooks = new[] + Resources.SetAllowedSpellbooks(bp, new[] { Resources.SpellbookBlueprints.MagusSpellbook.ToReference(), Resources.SpellbookBlueprints.SwordSaintSpellbook.ToReference(), @@ -138,14 +138,14 @@ public static void AddTricksterIncorporateSpellbookFeature() Resources.SpellbookBlueprints.ClericSpellbook.ToReference(), Resources.SpellbookBlueprints.WizardSpellbook.ToReference(), Resources.SpellbookBlueprints.DruidSpellbook.ToReference(), - }; - bp.m_MythicSpellList = Resources.SpellListBlueprints.TricksterSpellList.ToReference(); + }); // TODO This is just there for compatibility with Owlcat new changes in 1.3. We are not using it yet, since we reverted to the old merging behavior - bp.m_SpellKnownForSpontaneous = Resources.SpellTableBlueprints.MythicSpontaneousSpellsKnownTable - .ToReference(); - bp.IsClassFeature = true; + Resources.ConfigureMythicSpellbook( + bp, + Resources.SpellListBlueprints.TricksterSpellList.ToReference(), + Resources.SpellTableBlueprints.MythicSpontaneousSpellsKnownTable.ToReference()); }); } } -} \ No newline at end of file +} diff --git a/SpellbookMerge/Patches/MythicProgression.cs b/SpellbookMerge/Patches/MythicProgression.cs index 0f24c4c..237828b 100644 --- a/SpellbookMerge/Patches/MythicProgression.cs +++ b/SpellbookMerge/Patches/MythicProgression.cs @@ -46,15 +46,18 @@ private static void PatchModAllowedMerges(string name, BlueprintSpellbookReferen { var feature = Resources.TryGetModBlueprint(name); if (feature == null) return; - feature.m_AllowedSpellbooks = spellbooks; + Resources.SetAllowedSpellbooks(feature, spellbooks); Main.Log($"Patched {name} to allow {spellbooks.Length} spellbooks"); } private static void PatchAngelAllowedMerges() { var angelIncorporateSpellbookFeature = Resources.MythicMergeBlueprints.AngelIncorporateSpellbook; - if (angelIncorporateSpellbookFeature == null || angelIncorporateSpellbookFeature.m_AllowedSpellbooks == null) return; - var angelAllowedMerges = new List(angelIncorporateSpellbookFeature.m_AllowedSpellbooks); + var existingAngelSpellbooks = angelIncorporateSpellbookFeature == null + ? null + : Resources.GetAllowedSpellbooks(angelIncorporateSpellbookFeature); + if (angelIncorporateSpellbookFeature == null || existingAngelSpellbooks == null) return; + var angelAllowedMerges = new List(existingAngelSpellbooks); var additionalMerges = new List { Resources.SpellbookBlueprints.PaladinSpellbook.ToReference(), @@ -73,15 +76,18 @@ private static void PatchAngelAllowedMerges() .Where(spellbook => spellbook != null) .Select(spellbook => spellbook.ToReference()) .Where(spellbook => !angelAllowedMerges.Any(existing => existing.Guid == spellbook.Guid))); - angelIncorporateSpellbookFeature.m_AllowedSpellbooks = angelAllowedMerges.ToArray(); + Resources.SetAllowedSpellbooks(angelIncorporateSpellbookFeature, angelAllowedMerges.ToArray()); Main.Log("Patched Angel allowed spellbook merges"); } private static void PatchLichAllowedMerges() { var lichIncorporateSpellbookFeature = Resources.MythicMergeBlueprints.LichIncorporateSpellbook; - if (lichIncorporateSpellbookFeature == null || lichIncorporateSpellbookFeature.m_AllowedSpellbooks == null) return; - var lichAllowedMerges = new List(lichIncorporateSpellbookFeature.m_AllowedSpellbooks); + var existingLichSpellbooks = lichIncorporateSpellbookFeature == null + ? null + : Resources.GetAllowedSpellbooks(lichIncorporateSpellbookFeature); + if (lichIncorporateSpellbookFeature == null || existingLichSpellbooks == null) return; + var lichAllowedMerges = new List(existingLichSpellbooks); var additionalMerges = new List { Resources.SpellbookBlueprints.DruidSpellbook.ToReference(), @@ -98,7 +104,7 @@ private static void PatchLichAllowedMerges() .Where(spellbook => spellbook != null) .Select(spellbook => spellbook.ToReference()) .Where(spellbook => !lichAllowedMerges.Any(existing => existing.Guid == spellbook.Guid))); - lichIncorporateSpellbookFeature.m_AllowedSpellbooks = lichAllowedMerges.ToArray(); + Resources.SetAllowedSpellbooks(lichIncorporateSpellbookFeature, lichAllowedMerges.ToArray()); Main.Log("Patched Lich allowed spellbook merges"); } diff --git a/SpellbookMerge/Resources.cs b/SpellbookMerge/Resources.cs index 0ceae6f..852aa3b 100644 --- a/SpellbookMerge/Resources.cs +++ b/SpellbookMerge/Resources.cs @@ -5,6 +5,7 @@ using Kingmaker.Blueprints.Classes.Spells; using Kingmaker.Localization; using Kingmaker.Localization.Shared; +using HarmonyLib; namespace SpellbookMerge { @@ -17,6 +18,48 @@ internal static class MythicMergeBlueprints public static BlueprintFeatureSelectMythicSpellbook LichIncorporateSpellbook => TryGetBlueprint("3f16e9caf7c683c40884c7c455ed26af")!; } + + private static readonly System.Reflection.FieldInfo AllowedSpellbooksField = + AccessTools.Field(typeof(BlueprintFeatureSelectMythicSpellbook), "m_AllowedSpellbooks"); + + public static BlueprintSpellbookReference[]? GetAllowedSpellbooks(BlueprintFeatureSelectMythicSpellbook feature) + { + return AllowedSpellbooksField?.GetValue(feature) as BlueprintSpellbookReference[]; + } + + public static void SetAllowedSpellbooks(BlueprintFeatureSelectMythicSpellbook feature, BlueprintSpellbookReference[] spellbooks) + { + SetMember(feature, "m_AllowedSpellbooks", spellbooks); + } + + public static void ConfigureMythicSpellbook( + BlueprintFeatureSelectMythicSpellbook feature, + BlueprintSpellListReference mythicSpellList, + BlueprintSpellsTableReference spellsKnownForSpontaneous) + { + SetMember(feature, "m_MythicSpellList", mythicSpellList); + SetMember(feature, "m_SpellKnownForSpontaneous", spellsKnownForSpontaneous); + SetMember(feature, "IsClassFeature", true); + } + + private static void SetMember(object target, string name, object value) + { + var field = AccessTools.Field(target.GetType(), name); + if (field != null) + { + field.SetValue(target, value); + return; + } + + var property = AccessTools.Property(target.GetType(), name); + if (property?.SetMethod != null) + { + property.SetValue(target, value, null); + return; + } + + Main.LogError($"Could not find member `{name}` on {target.GetType().FullName}"); + } internal static class SpellbookBlueprints { From ea641b169ef8809b243200466544706fd7c3588e Mon Sep 17 00:00:00 2001 From: ouyangfang Date: Tue, 11 Aug 2026 22:13:49 +0800 Subject: [PATCH 3/6] Avoid private blueprint field access at runtime --- SpellbookMerge/Extensions/ExtensionMethods.cs | 8 +-- SpellbookMerge/Patches/MythicProgression.cs | 40 +++++++++---- SpellbookMerge/Resources.cs | 58 ++++++++++++++----- 3 files changed, 76 insertions(+), 30 deletions(-) diff --git a/SpellbookMerge/Extensions/ExtensionMethods.cs b/SpellbookMerge/Extensions/ExtensionMethods.cs index 66b2e70..a692bd5 100644 --- a/SpellbookMerge/Extensions/ExtensionMethods.cs +++ b/SpellbookMerge/Extensions/ExtensionMethods.cs @@ -8,16 +8,16 @@ namespace SpellbookMerge.Extensions public static class ExtensionMethods { public static void SetName(this BlueprintUnitFact feature, String name) { - feature.m_DisplayName = Resources.CreateString(feature.name + ".Name", name); + Resources.SetMember(feature, "m_DisplayName", Resources.CreateString(feature.name + ".Name", name)); } public static void SetDescription(this BlueprintUnitFact feature, LocalizedString description) { - feature.m_Description = description; + Resources.SetMember(feature, "m_Description", description); } public static void SetDescription(this BlueprintUnitFact feature, String description) { var taggedDescription = DescriptionTools.TagEncyclopediaEntries(description); - feature.m_Description = Resources.CreateString(feature.name + ".Description", taggedDescription); + Resources.SetMember(feature, "m_Description", Resources.CreateString(feature.name + ".Description", taggedDescription)); } } -} \ No newline at end of file +} diff --git a/SpellbookMerge/Patches/MythicProgression.cs b/SpellbookMerge/Patches/MythicProgression.cs index 237828b..b069bd6 100644 --- a/SpellbookMerge/Patches/MythicProgression.cs +++ b/SpellbookMerge/Patches/MythicProgression.cs @@ -113,9 +113,13 @@ private static void PatchAeonProgression() var aeonProgression = Resources.ProgressionBlueprints.AeonProgression; var aeonIncorporateSpellbookFeature = Resources.TryGetModBlueprint("AeonIncorporateSpellbook"); - aeonProgression.LevelEntries[0].m_Features - .Add(aeonIncorporateSpellbookFeature.ToReference()); - Main.Log("Patched Aeon Progression"); + if (Resources.AddToCollectionMember( + aeonProgression.LevelEntries[0], + "m_Features", + aeonIncorporateSpellbookFeature.ToReference())) + { + Main.Log("Patched Aeon Progression"); + } } private static void PatchAzataProgression() @@ -123,9 +127,13 @@ private static void PatchAzataProgression() var azataProgression = Resources.ProgressionBlueprints.AzataProgression; var azataIncorporateSpellbookFeature = Resources.TryGetModBlueprint("AzataIncorporateSpellbook"); - azataProgression.LevelEntries[0].m_Features - .Add(azataIncorporateSpellbookFeature.ToReference()); - Main.Log("Patched Azata Progression"); + if (Resources.AddToCollectionMember( + azataProgression.LevelEntries[0], + "m_Features", + azataIncorporateSpellbookFeature.ToReference())) + { + Main.Log("Patched Azata Progression"); + } } private static void PatchDemonProgression() @@ -133,9 +141,13 @@ private static void PatchDemonProgression() var demonProgression = Resources.ProgressionBlueprints.DemonProgression; var demonIncorporateSpellbookFeature = Resources.TryGetModBlueprint("DemonIncorporateSpellbook"); - demonProgression.LevelEntries[0].m_Features - .Add(demonIncorporateSpellbookFeature.ToReference()); - Main.Log("Patched Demon Progression"); + if (Resources.AddToCollectionMember( + demonProgression.LevelEntries[0], + "m_Features", + demonIncorporateSpellbookFeature.ToReference())) + { + Main.Log("Patched Demon Progression"); + } } private static void PatchTricksterProgression() @@ -143,9 +155,13 @@ private static void PatchTricksterProgression() var tricksterProgression = Resources.ProgressionBlueprints.TricksterProgression; var tricksterIncorporateSpellbookFeature = Resources.TryGetModBlueprint("TricksterIncorporateSpellbook"); - tricksterProgression.LevelEntries[0].m_Features - .Add(tricksterIncorporateSpellbookFeature.ToReference()); - Main.Log("Patched Trickster Progression"); + if (Resources.AddToCollectionMember( + tricksterProgression.LevelEntries[0], + "m_Features", + tricksterIncorporateSpellbookFeature.ToReference())) + { + Main.Log("Patched Trickster Progression"); + } } } } diff --git a/SpellbookMerge/Resources.cs b/SpellbookMerge/Resources.cs index 852aa3b..1676462 100644 --- a/SpellbookMerge/Resources.cs +++ b/SpellbookMerge/Resources.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using Kingmaker.Blueprints; using Kingmaker.Blueprints.Classes; @@ -19,12 +20,9 @@ internal static class MythicMergeBlueprints TryGetBlueprint("3f16e9caf7c683c40884c7c455ed26af")!; } - private static readonly System.Reflection.FieldInfo AllowedSpellbooksField = - AccessTools.Field(typeof(BlueprintFeatureSelectMythicSpellbook), "m_AllowedSpellbooks"); - public static BlueprintSpellbookReference[]? GetAllowedSpellbooks(BlueprintFeatureSelectMythicSpellbook feature) { - return AllowedSpellbooksField?.GetValue(feature) as BlueprintSpellbookReference[]; + return GetMember(feature, "m_AllowedSpellbooks") as BlueprintSpellbookReference[]; } public static void SetAllowedSpellbooks(BlueprintFeatureSelectMythicSpellbook feature, BlueprintSpellbookReference[] spellbooks) @@ -42,23 +40,56 @@ public static void ConfigureMythicSpellbook( SetMember(feature, "IsClassFeature", true); } - private static void SetMember(object target, string name, object value) + public static object? GetMember(object target, string name) + { + var field = AccessTools.Field(target.GetType(), name); + if (field != null) + { + return field.GetValue(target); + } + + var property = AccessTools.Property(target.GetType(), name); + var getter = property?.GetGetMethod(true); + if (getter != null) + { + return getter.Invoke(target, null); + } + + Main.LogError($"Could not find readable member `{name}` on {target.GetType().FullName}"); + return null; + } + + public static bool SetMember(object target, string name, object? value) { var field = AccessTools.Field(target.GetType(), name); if (field != null) { field.SetValue(target, value); - return; + return true; } var property = AccessTools.Property(target.GetType(), name); - if (property?.SetMethod != null) + var setter = property?.GetSetMethod(true); + if (setter != null) + { + setter.Invoke(target, new[] { value }); + return true; + } + + Main.LogError($"Could not find writable member `{name}` on {target.GetType().FullName}"); + return false; + } + + public static bool AddToCollectionMember(object target, string name, object value) + { + if (GetMember(target, name) is IList collection) { - property.SetValue(target, value, null); - return; + collection.Add(value); + return true; } - Main.LogError($"Could not find member `{name}` on {target.GetType().FullName}"); + Main.LogError($"Member `{name}` on {target.GetType().FullName} is not a mutable collection"); + return false; } internal static class SpellbookBlueprints @@ -192,10 +223,9 @@ public static LocalizedString CreateString(string key, string value) { localizedValue = current; } LocalizationManager.CurrentPack?.PutString(key, localizedValue); - localized = new LocalizedString { - m_ShouldProcess = false, - m_Key = key - }; + localized = new LocalizedString(); + SetMember(localized, "m_ShouldProcess", false); + SetMember(localized, "m_Key", key); textToLocalizedString[key] = localized; return localized; } From 693f9972f47d08edf6bff6e72d352eec32a9b6d6 Mon Sep 17 00:00:00 2001 From: ouyangfang Date: Tue, 11 Aug 2026 22:16:54 +0800 Subject: [PATCH 4/6] Invoke private spellbook helper through reflection --- SpellbookMerge/Patches/LegacyMerge.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/SpellbookMerge/Patches/LegacyMerge.cs b/SpellbookMerge/Patches/LegacyMerge.cs index 746a60d..c0adcae 100644 --- a/SpellbookMerge/Patches/LegacyMerge.cs +++ b/SpellbookMerge/Patches/LegacyMerge.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Reflection; using HarmonyLib; using Kingmaker.Blueprints; using Kingmaker.Blueprints.Classes.Spells; @@ -16,6 +17,11 @@ public class LegacyMerge // ReSharper disable once UnusedType.Local private static class ApplySpellbookApplyPatch { + private static readonly MethodInfo TryApplyCustomSpellsMethod = AccessTools.Method( + typeof(ApplySpellbook), + "TryApplyCustomSpells", + new[] { typeof(Spellbook), typeof(AddCustomSpells), typeof(LevelUpState), typeof(UnitDescriptor) }); + // ReSharper disable once UnusedMember.Local private static bool Prefix(LevelUpState state, UnitDescriptor unit) { @@ -91,7 +97,11 @@ public static void Apply(LevelUpState state, UnitDescriptor unit) } foreach (AddCustomSpells customSpells in spellbook.Blueprint.GetComponents()) { - ApplySpellbook.TryApplyCustomSpells(spellbook, customSpells, state, unit); + if (TryApplyCustomSpellsMethod == null) + { + throw new MissingMethodException(typeof(ApplySpellbook).FullName, "TryApplyCustomSpells"); + } + TryApplyCustomSpellsMethod.Invoke(null, new object[] { spellbook, customSpells, state, unit }); } } } From 0ed34a02af18b65487d42e3621794afa7a20157e Mon Sep 17 00:00:00 2001 From: ouyangfang Date: Tue, 11 Aug 2026 22:29:17 +0800 Subject: [PATCH 5/6] Invalidate cached mythic spellbook choices --- SpellbookMerge/Patches/MythicProgression.cs | 2 +- SpellbookMerge/Resources.cs | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/SpellbookMerge/Patches/MythicProgression.cs b/SpellbookMerge/Patches/MythicProgression.cs index b069bd6..927fe6d 100644 --- a/SpellbookMerge/Patches/MythicProgression.cs +++ b/SpellbookMerge/Patches/MythicProgression.cs @@ -47,7 +47,7 @@ private static void PatchModAllowedMerges(string name, BlueprintSpellbookReferen var feature = Resources.TryGetModBlueprint(name); if (feature == null) return; Resources.SetAllowedSpellbooks(feature, spellbooks); - Main.Log($"Patched {name} to allow {spellbooks.Length} spellbooks"); + Main.Log($"Patched {name} to allow {spellbooks.Length} spellbooks; generated {feature.Items.Count()} selection items"); } private static void PatchAngelAllowedMerges() diff --git a/SpellbookMerge/Resources.cs b/SpellbookMerge/Resources.cs index 1676462..e505422 100644 --- a/SpellbookMerge/Resources.cs +++ b/SpellbookMerge/Resources.cs @@ -28,6 +28,10 @@ internal static class MythicMergeBlueprints public static void SetAllowedSpellbooks(BlueprintFeatureSelectMythicSpellbook feature, BlueprintSpellbookReference[] spellbooks) { SetMember(feature, "m_AllowedSpellbooks", spellbooks); + // Items is built lazily and cached by the game. Whenever the allowed spellbooks are + // changed, invalidate that cache so the level-up UI can rebuild the class-specific + // choices (for example, "Mythic Spellbook (Wizard)"). + SetMember(feature, "m_CachedItems", null); } public static void ConfigureMythicSpellbook( @@ -204,8 +208,11 @@ private static void AddBlueprint(SimpleBlueprint blueprint, BlueprintGuid assetI name = name, AssetGuid = Main.ModSettings.Blueprints.GetGuiD(name) }; - AddBlueprint(result); init?.Invoke(result); + // Configure the blueprint before OnEnable runs. Newer game versions may build and + // cache selection items during registration; registering first would permanently + // cache an empty allowed-spellbook list. + AddBlueprint(result); return result; } From 2990769ac377d4088be833c4be7493ba3cd7d958 Mon Sep 17 00:00:00 2001 From: ouyangfang Date: Tue, 11 Aug 2026 22:52:57 +0800 Subject: [PATCH 6/6] Support runtime replacement spellbooks in merge choices --- .../Features/IncorporateSpellbook.cs | 9 +- .../Patches/DynamicSpellbookChoices.cs | 69 +++++++++++ SpellbookMerge/Patches/LegacyMerge.cs | 109 ------------------ 3 files changed, 74 insertions(+), 113 deletions(-) create mode 100644 SpellbookMerge/Patches/DynamicSpellbookChoices.cs delete mode 100644 SpellbookMerge/Patches/LegacyMerge.cs diff --git a/SpellbookMerge/Features/IncorporateSpellbook.cs b/SpellbookMerge/Features/IncorporateSpellbook.cs index f14dc74..aa55fa8 100644 --- a/SpellbookMerge/Features/IncorporateSpellbook.cs +++ b/SpellbookMerge/Features/IncorporateSpellbook.cs @@ -33,7 +33,8 @@ public static void AddAeonIncorporateSpellbookFeature() Resources.SpellbookBlueprints.WitchSpellbook.ToReference(), }); - // TODO This is just there for compatibility with Owlcat new changes in 1.3. We are not using it yet, since we reverted to the old merging behavior + // Use the current game's native mythic-spellbook merge path. It handles the + // selected replacement spellbook, caster-level growth, and mythic spell choices. Resources.ConfigureMythicSpellbook( bp, Resources.SpellListBlueprints.AeonSpellList.ToReference(), @@ -69,7 +70,7 @@ public static void AddAzataIncorporateSpellbookFeature() Resources.SpellbookBlueprints.ClericSpellbook.ToReference(), Resources.SpellbookBlueprints.WitchSpellbook.ToReference(), }); - // TODO This is just there for compatibility with Owlcat new changes in 1.3. We are not using it yet, since we reverted to the old merging behavior + // Use the current game's native mythic-spellbook merge path. Resources.ConfigureMythicSpellbook( bp, Resources.SpellListBlueprints.AzataSpellList.ToReference(), @@ -104,7 +105,7 @@ public static void AddDemonIncorporateSpellbookFeature() Resources.SpellbookBlueprints.DruidSpellbook.ToReference(), Resources.SpellbookBlueprints.WitchSpellbook.ToReference(), }); - // TODO This is just there for compatibility with Owlcat new changes in 1.3. We are not using it yet, since we reverted to the old merging behavior + // Use the current game's native mythic-spellbook merge path. Resources.ConfigureMythicSpellbook( bp, Resources.SpellListBlueprints.DemonSpellList.ToReference(), @@ -139,7 +140,7 @@ public static void AddTricksterIncorporateSpellbookFeature() Resources.SpellbookBlueprints.WizardSpellbook.ToReference(), Resources.SpellbookBlueprints.DruidSpellbook.ToReference(), }); - // TODO This is just there for compatibility with Owlcat new changes in 1.3. We are not using it yet, since we reverted to the old merging behavior + // Use the current game's native mythic-spellbook merge path. Resources.ConfigureMythicSpellbook( bp, Resources.SpellListBlueprints.TricksterSpellList.ToReference(), diff --git a/SpellbookMerge/Patches/DynamicSpellbookChoices.cs b/SpellbookMerge/Patches/DynamicSpellbookChoices.cs new file mode 100644 index 0000000..6216d47 --- /dev/null +++ b/SpellbookMerge/Patches/DynamicSpellbookChoices.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using Kingmaker.Blueprints; +using Kingmaker.Blueprints.Classes; +using Kingmaker.Blueprints.Classes.Spells; +using Kingmaker.UnitLogic; + +namespace SpellbookMerge.Patches +{ + /// + /// Adds the spellbooks actually owned by the unit to mythic merge selections. + /// + /// The game filters BlueprintFeatureSelectMythicSpellbook.Items by exact blueprint + /// identity. Archetypes and other mods commonly replace a class spellbook with a new + /// blueprint, so a hard-coded list containing the base Wizard spellbook cannot produce + /// a choice for a character using a replacement Wizard spellbook. + /// + public static class DynamicSpellbookChoices + { + [HarmonyPatch( + typeof(BlueprintFeatureSelectMythicSpellbook), + nameof(BlueprintFeatureSelectMythicSpellbook.ExtractSelectionItems))] + private static class ExtractSelectionItemsPatch + { + // ReSharper disable once UnusedMember.Local + private static void Prefix( + BlueprintFeatureSelectMythicSpellbook __instance, + UnitDescriptor previewUnit) + { + if (!Main.Enabled || previewUnit == null) return; + + var allowedSpellbooks = new List( + Resources.GetAllowedSpellbooks(__instance) + ?? Array.Empty()); + var addedSpellbooks = new List(); + + foreach (var spellbook in previewUnit.Spellbooks) + { + var blueprint = spellbook?.Blueprint; + if (blueprint == null || blueprint.IsMythic || blueprint.CharacterClass == null) + { + continue; + } + + var reference = blueprint.ToReference(); + if (allowedSpellbooks.Any(existing => + existing != null && existing.Guid == reference.Guid)) + { + continue; + } + + allowedSpellbooks.Add(reference); + addedSpellbooks.Add(blueprint); + } + + if (addedSpellbooks.Count == 0) return; + + // This also invalidates the game's cached selection items, allowing the + // replacement blueprint to become a "Mythic Spellbook (Class)" option. + Resources.SetAllowedSpellbooks(__instance, allowedSpellbooks.ToArray()); + Main.Log( + "Added runtime mythic merge choices: " + + string.Join(", ", addedSpellbooks.Select(bp => $"{bp.name} [{bp.AssetGuid}]"))); + } + } + } +} diff --git a/SpellbookMerge/Patches/LegacyMerge.cs b/SpellbookMerge/Patches/LegacyMerge.cs deleted file mode 100644 index c0adcae..0000000 --- a/SpellbookMerge/Patches/LegacyMerge.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using HarmonyLib; -using Kingmaker.Blueprints; -using Kingmaker.Blueprints.Classes.Spells; -using Kingmaker.UnitLogic; -using Kingmaker.UnitLogic.Abilities; -using Kingmaker.UnitLogic.Class.LevelUp; -using Kingmaker.UnitLogic.Class.LevelUp.Actions; - -namespace SpellbookMerge.Patches -{ - public class LegacyMerge - { - [HarmonyPatch(typeof(ApplySpellbook), nameof(ApplySpellbook.Apply), new Type[] {typeof(LevelUpState), typeof(UnitDescriptor)})] - // ReSharper disable once UnusedType.Local - private static class ApplySpellbookApplyPatch - { - private static readonly MethodInfo TryApplyCustomSpellsMethod = AccessTools.Method( - typeof(ApplySpellbook), - "TryApplyCustomSpells", - new[] { typeof(Spellbook), typeof(AddCustomSpells), typeof(LevelUpState), typeof(UnitDescriptor) }); - - // ReSharper disable once UnusedMember.Local - private static bool Prefix(LevelUpState state, UnitDescriptor unit) - { - if (!Main.Enabled) return true; - try - { - Apply(state, unit); - return false; - } - catch (Exception e) - { - // A third-party class/archetype can have a spellbook shape that this legacy - // compatibility path does not know about. Let the game's implementation run - // in that case instead of breaking level-up for the whole character. - Main.LogException(e, "Spellbook Merge failed while applying a spellbook level; falling back to the game implementation."); - return true; - } - } - - public static void Apply(LevelUpState state, UnitDescriptor unit) - { - if (state.SelectedClass == null) - { - return; - } - var component = state.SelectedClass.GetComponent(); - if (component != null && component.Levels.Contains(state.NextClassLevel)) - { - return; - } - var classData = unit.Progression.GetClassData(state.SelectedClass); - if (classData?.Spellbook == null) return; - Spellbook spellbook = unit.DemandSpellbook(classData.Spellbook); - if (state.SelectedClass.Spellbook && state.SelectedClass.Spellbook != classData.Spellbook) - { - var spellbook2 = unit.Spellbooks.FirstOrDefault(s => s.Blueprint == state.SelectedClass.Spellbook); - if (spellbook2 != null) - { - foreach (AbilityData abilityData in spellbook2.GetAllKnownSpells()) - { - spellbook.AddKnown(abilityData.SpellLevel, abilityData.Blueprint); - } - unit.DeleteSpellbook(state.SelectedClass.Spellbook); - } - } - int casterLevel = spellbook.CasterLevel; - spellbook.AddLevelFromClass(classData.CharacterClass); - int casterLevel2 = spellbook.CasterLevel; - SpellSelectionData spellSelectionData = state.DemandSpellSelection(spellbook.Blueprint, spellbook.Blueprint.SpellList); - if (spellbook.Blueprint.SpellsKnown != null) - { - for (int i = 0; i <= 10; i++) - { - BlueprintSpellsTable spellsKnown = spellbook.Blueprint.SpellsKnown; - int num = spellsKnown.GetCount(casterLevel, i); - int num2 = spellsKnown.GetCount(casterLevel2, i); - spellSelectionData.SetLevelSpells(i, Math.Max(0, num2 - num)); - } - } - int maxSpellLevel = spellbook.MaxSpellLevel; - if (spellbook.Blueprint.SpellsPerLevel > 0) - { - if (casterLevel == 0) - { - spellSelectionData.SetExtraSpells(0, maxSpellLevel); - spellSelectionData.ExtraByStat = true; - spellSelectionData.UpdateMaxLevelSpells(unit); - } - else - { - spellSelectionData.SetExtraSpells(spellbook.Blueprint.SpellsPerLevel, maxSpellLevel); - } - } - foreach (AddCustomSpells customSpells in spellbook.Blueprint.GetComponents()) - { - if (TryApplyCustomSpellsMethod == null) - { - throw new MissingMethodException(typeof(ApplySpellbook).FullName, "TryApplyCustomSpells"); - } - TryApplyCustomSpellsMethod.Invoke(null, new object[] { spellbook, customSpells, state, unit }); - } - } - } - } -}