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/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/Features/IncorporateSpellbook.cs b/SpellbookMerge/Features/IncorporateSpellbook.cs index 684535d..aa55fa8 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,13 @@ 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; + }); + // 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(), + Resources.SpellTableBlueprints.MythicSpontaneousSpellsKnownTable.ToReference()); }); } @@ -48,7 +49,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 +69,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; + }); + // Use the current game's native mythic-spellbook merge path. + Resources.ConfigureMythicSpellbook( + bp, + Resources.SpellListBlueprints.AzataSpellList.ToReference(), + Resources.SpellTableBlueprints.MythicSpontaneousSpellsKnownTable.ToReference()); }); } @@ -84,7 +85,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 +104,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; + }); + // Use the current game's native mythic-spellbook merge path. + Resources.ConfigureMythicSpellbook( + bp, + Resources.SpellListBlueprints.DemonSpellList.ToReference(), + Resources.SpellTableBlueprints.MythicSpontaneousSpellsKnownTable.ToReference()); }); } @@ -119,7 +120,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 +139,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; + }); + // Use the current game's native mythic-spellbook merge path. + Resources.ConfigureMythicSpellbook( + bp, + Resources.SpellListBlueprints.TricksterSpellList.ToReference(), + Resources.SpellTableBlueprints.MythicSpontaneousSpellsKnownTable.ToReference()); }); } } -} \ No newline at end of file +} 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/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 3369322..0000000 --- a/SpellbookMerge/Patches/LegacyMerge.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Linq; -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 - { - // ReSharper disable once UnusedMember.Local - private static bool Prefix(LevelUpState state, UnitDescriptor unit) - { - if (!Main.Enabled) return true; - Apply(state, unit); - return false; - } - - 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()) - { - ApplySpellbook.TryApplyCustomSpells(spellbook, customSpells, state, unit); - } - } - } - } -} \ No newline at end of file diff --git a/SpellbookMerge/Patches/MythicProgression.cs b/SpellbookMerge/Patches/MythicProgression.cs index 5a79d9a..927fe6d 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,12 +25,39 @@ 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; + Resources.SetAllowedSpellbooks(feature, spellbooks); + Main.Log($"Patched {name} to allow {spellbooks.Length} spellbooks; generated {feature.Items.Count()} selection items"); } private static void PatchAngelAllowedMerges() { var angelIncorporateSpellbookFeature = Resources.MythicMergeBlueprints.AngelIncorporateSpellbook; - 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(), @@ -40,15 +68,26 @@ private static void PatchAngelAllowedMerges() Resources.SpellbookBlueprints.SageSpellbook.ToReference(), Resources.SpellbookBlueprints.CrossbloodedSpellbook.ToReference(), }; - angelAllowedMerges.AddRange(additionalMerges); - angelIncorporateSpellbookFeature.m_AllowedSpellbooks = angelAllowedMerges.ToArray(); + 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))); + Resources.SetAllowedSpellbooks(angelIncorporateSpellbookFeature, angelAllowedMerges.ToArray()); Main.Log("Patched Angel allowed spellbook merges"); } private static void PatchLichAllowedMerges() { var lichIncorporateSpellbookFeature = Resources.MythicMergeBlueprints.LichIncorporateSpellbook; - 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(), @@ -57,8 +96,15 @@ private static void PatchLichAllowedMerges() Resources.SpellbookBlueprints.ClericSpellbook.ToReference(), }; - lichAllowedMerges.AddRange(additionalMerges); - lichIncorporateSpellbookFeature.m_AllowedSpellbooks = lichAllowedMerges.ToArray(); + 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))); + Resources.SetAllowedSpellbooks(lichIncorporateSpellbookFeature, lichAllowedMerges.ToArray()); Main.Log("Patched Lich allowed spellbook merges"); } @@ -67,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() @@ -77,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() @@ -87,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() @@ -97,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 1c0b42d..e505422 100644 --- a/SpellbookMerge/Resources.cs +++ b/SpellbookMerge/Resources.cs @@ -1,9 +1,12 @@ using System; +using System.Collections; using System.Collections.Generic; using Kingmaker.Blueprints; using Kingmaker.Blueprints.Classes; using Kingmaker.Blueprints.Classes.Spells; using Kingmaker.Localization; +using Kingmaker.Localization.Shared; +using HarmonyLib; namespace SpellbookMerge { @@ -16,6 +19,82 @@ internal static class MythicMergeBlueprints public static BlueprintFeatureSelectMythicSpellbook LichIncorporateSpellbook => TryGetBlueprint("3f16e9caf7c683c40884c7c455ed26af")!; } + + public static BlueprintSpellbookReference[]? GetAllowedSpellbooks(BlueprintFeatureSelectMythicSpellbook feature) + { + return GetMember(feature, "m_AllowedSpellbooks") as BlueprintSpellbookReference[]; + } + + 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( + BlueprintFeatureSelectMythicSpellbook feature, + BlueprintSpellListReference mythicSpellList, + BlueprintSpellsTableReference spellsKnownForSpontaneous) + { + SetMember(feature, "m_MythicSpellList", mythicSpellList); + SetMember(feature, "m_SpellKnownForSpontaneous", spellsKnownForSpontaneous); + SetMember(feature, "IsClassFeature", true); + } + + 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 true; + } + + var property = AccessTools.Property(target.GetType(), name); + 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) + { + collection.Add(value); + return true; + } + + Main.LogError($"Member `{name}` on {target.GetType().FullName} is not a mutable collection"); + return false; + } internal static class SpellbookBlueprints { @@ -43,6 +122,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 +169,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); } @@ -102,28 +208,32 @@ 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; } 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); - localized = new LocalizedString { - m_ShouldProcess = false, - m_Key = key - }; - textToLocalizedString[value] = localized; + LocalizationManager.CurrentPack?.PutString(key, localizedValue); + localized = new LocalizedString(); + SetMember(localized, "m_ShouldProcess", false); + SetMember(localized, "m_Key", key); + textToLocalizedString[key] = localized; return localized; } @@ -145,4 +255,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 @@ - + + + + + +