├── .gitignore ├── LICENSE ├── README.md ├── SpellbookMerge.sln ├── SpellbookMerge ├── Config │ ├── Blueprints.cs │ ├── Blueprints.json │ ├── IOverridable.cs │ ├── ModSettings.cs │ ├── PatchSettings.cs │ └── PatchSettings.json ├── Extensions │ └── ExtensionMethods.cs ├── Features │ └── IncorporateSpellbook.cs ├── Info.json ├── Main.cs ├── Patches │ ├── AdditionalBlueprints.cs │ ├── LegacyMerge.cs │ ├── LogBlueprints.cs │ ├── MythicProgression.cs │ └── SpellbookProgression.cs ├── Resources.cs ├── SpellbookMerge.csproj └── Utilities │ └── DescriptionTools.cs └── SpellbookMergeTest ├── ModSettingsTest.cs └── SpellbookMergeTest.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | lib/ 4 | /packages/ 5 | riderModule.iml 6 | /_ReSharper.Caches/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vikash Balasubramanian (vikigenius) 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spellbook Merge 2 | 3 | This is a mod for Pathfinder Wrath of the Righteous. It adds additional spellbook merging options. 4 | 5 | ## Patch 1.3 6 | 7 | Owlcat introduced several changes to the Spellbook Merging system in patch 1.3, that just broke the mod. 8 | To quickly fix the issue I have just undid the spellbook merging changes they introduced and used the old system. 9 | So people that like Owlcat's new system, you will have to wait for a while for me to figure the new system out and patch it. 10 | For people that liked the older merging system, think of this as a feature instead of a bug :) 11 | 12 | ## Warnings 13 | 14 | * This mod creates new blueprints and thus creates a save dependency. Once your save depends on this mod, it is not safe to uninstall it. 15 | 16 | * To be careful, install this mod just before reaching Mythic Rank 3. Test that you get the option to merge spellbooks, if not file a bug report. 17 | 18 | * I have tested this mod only with Aeon-Inquisitor, the other merges are untested (for now). 19 | 20 | * After merging spellbooks your mythic spell progression is tied to your base spellbook. So be careful when taking dips, if you don't progress your base spellbook enough, you might end up not getting the required slots to cast higher level mythic spells. 21 | 22 | * Currently for Casters that learn spells automatically (Druid, Paladin etc.), there is a bug where any new spell slots gained by merging spellbooks do not trigger learning new spells. Reload the save/game to fix the issue. I haven't tested if you can recover them if use respec and keep progressing without learning these spells. 23 | 24 | * There is an incompatibility with Tabletop Tweaks where it modifies the BloodRager spellbook. So if you want to merge BloodRager with mythic paths, disable the specific TTT tweak for it. 25 | 26 | ## Configuration 27 | 28 | The patches to spellbook progressions can be disabled in the json settings file. 29 | In your mod directory for this mod, you should find a UserSettings directory. Open PatchSettings.json and set any spellbook progression patch that you do not want to false. 30 | Note that this only disables the spellbook progression patches, you will still get options to merge spellbooks, you can just chose not to merge if you don't want to. 31 | 32 | ## Mythic Paths 33 | 34 | ### Angel 35 | 36 | Angel can merge with the followin additional spellbooks 37 | * Palading (This needs thorough testing to see if higher level spells are castable and if the progressions are applied correctly. Be warned.) 38 | * Sorcerer 39 | * Wizard 40 | * Arcanist 41 | * Witch 42 | * Sage Sorcerer 43 | * Crossblooded Sorcerer 44 | 45 | ### Lich 46 | 47 | Lich can now merge with the following spellbooks. 48 | 49 | * Druid 50 | * Oracle 51 | * Shaman 52 | * Cleric 53 | 54 | ### Aeon 55 | 56 | Aeon gets to merge spellbooks with the following base spellbooks 57 | 58 | * WarPriest 59 | * Inquisitor 60 | * Druid 61 | * Magus 62 | * Sword Saint 63 | * Hunter 64 | * Eldritch Scion 65 | * Sorcerer 66 | * Wizard 67 | * Arcanist 68 | * Sage Sorcerer 69 | * Cross Blooded Sorcerer 70 | * Oracle 71 | * Bard 72 | * Shaman 73 | * Cleric 74 | * Witch 75 | 76 | ### Azata 77 | Azata gets to merge spellbooks with the following base spellbooks 78 | * Magus 79 | * Bard 80 | * Skald 81 | * Sword Saint 82 | * Sorcerer 83 | * Sage Sorcerer 84 | * Druid 85 | * Hunter 86 | * BloodRager 87 | * Wizard 88 | * ExploiterWizard 89 | * Arcanist 90 | * Eldritch Scion 91 | * Arcanist 92 | * Cross Blooded Sorcerer 93 | * Oracle 94 | * Shaman 95 | * Cleric 96 | * Witch 97 | 98 | ### Demon 99 | Demon gets to merge spellbooks with the following base spellbooks 100 | * BloodRager 101 | * Hunter 102 | * Skald 103 | * Magus 104 | * Sword Saint 105 | * Sorcerer 106 | * Sage Sorcerer 107 | * Bard 108 | * Eldritch Scion 109 | * Cross Blooded Sorcerer 110 | * Arcanist 111 | * Oracle 112 | * Bard 113 | * Shaman 114 | * Cleric 115 | * Wizard 116 | * Druid 117 | * Witch 118 | 119 | ### Trickster 120 | Trickster gets to merge spellbooks with the following base spellbooks 121 | * Alchemist 122 | * Eldritch Scoundrel 123 | * Magus 124 | * Sword Saint 125 | * Bard 126 | * Skald 127 | * Witch 128 | * Eldritch Scion 129 | * Sage Sorcerer 130 | * Sorcerer 131 | * Cross Blooded Sorcerer 132 | * Wizard 133 | * Druid 134 | * Cleric 135 | * Oracle 136 | * Shaman 137 | 138 | 139 | ## Base Spellbooks 140 | 141 | The following spellbooks have had their spell slot progressions patched to allow access to higher level spells from mythic paths. 142 | 143 | * Magus 144 | * Sword Saint 145 | * Alchemist 146 | * BloodRager 147 | * Bard 148 | * Skald 149 | * Inquisitor 150 | * WarPriest 151 | * Paladin 152 | * Hunter (Uses the Bard spellbook) 153 | * Eldritch Scion (Uses the Bard progression) 154 | 155 | 156 | The patched hybrid casters mentioned above can cast 7th level spells post CL 20. 157 | 158 | Paladin progression has been specifically patched for allowing Angel merge. It has not been tested well yet. The progression is tight and slower than an Angel Oracle, but can't be helped, with the current approach. But by my estimation if you stay pure Paladin, you should be able to cast level 10 Angel spells as a merged caster once you reach Mythic 9/10. 159 | File an issue report if that's not the case. 160 | 161 | 162 | ## Acknowledgements 163 | 164 | I would like to give my sincerest thanks to: 165 | 166 | 1. Owlcat for this amazing game. 167 | 2. My fellow modders from discord for inspiration and answering my questions. Particularly Narria Cabarius, Vek17, Bubbles and ArcaneTrixter. -------------------------------------------------------------------------------- /SpellbookMerge.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpellbookMerge", "SpellbookMerge\SpellbookMerge.csproj", "{002703DF-2DB3-4A6D-B236-C574E48531B4}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpellbookMergeTest", "SpellbookMergeTest\SpellbookMergeTest.csproj", "{0D835620-1BB6-4B5B-BEBA-07588F57B4FC}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Release|Any CPU = Release|Any CPU 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {002703DF-2DB3-4A6D-B236-C574E48531B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 14 | {002703DF-2DB3-4A6D-B236-C574E48531B4}.Debug|Any CPU.Build.0 = Debug|Any CPU 15 | {002703DF-2DB3-4A6D-B236-C574E48531B4}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {002703DF-2DB3-4A6D-B236-C574E48531B4}.Release|Any CPU.Build.0 = Release|Any CPU 17 | {0D835620-1BB6-4B5B-BEBA-07588F57B4FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {0D835620-1BB6-4B5B-BEBA-07588F57B4FC}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {0D835620-1BB6-4B5B-BEBA-07588F57B4FC}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {0D835620-1BB6-4B5B-BEBA-07588F57B4FC}.Release|Any CPU.Build.0 = Release|Any CPU 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /SpellbookMerge/Config/Blueprints.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.Blueprints; 2 | using Kingmaker.Utility; 3 | using Newtonsoft.Json; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Reflection; 8 | 9 | 10 | namespace SpellbookMerge.Config 11 | { 12 | public class Blueprints : IOverridable 13 | { 14 | [JsonProperty] 15 | public bool OverrideIds { get; private set; } 16 | 17 | [JsonProperty] 18 | public readonly SortedDictionary NewBlueprints = new SortedDictionary(); 19 | [JsonProperty] 20 | public readonly SortedDictionary AutoGenerated = new SortedDictionary(); 21 | [JsonProperty] 22 | public readonly SortedDictionary UnusedGuids = new SortedDictionary(); 23 | public readonly SortedDictionary UsedGuids = new SortedDictionary(); 24 | private static JsonSerializerSettings? _cachedSettings; 25 | private static JsonSerializerSettings SerializerSettings { 26 | get 27 | { 28 | return _cachedSettings ??= new JsonSerializerSettings 29 | { 30 | CheckAdditionalContent = false, 31 | ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, 32 | DefaultValueHandling = DefaultValueHandling.Include, 33 | FloatParseHandling = FloatParseHandling.Double, 34 | Formatting = Formatting.Indented, 35 | MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead, 36 | MissingMemberHandling = MissingMemberHandling.Ignore, 37 | NullValueHandling = NullValueHandling.Ignore, 38 | ObjectCreationHandling = ObjectCreationHandling.Replace, 39 | PreserveReferencesHandling = PreserveReferencesHandling.None, 40 | StringEscapeHandling = StringEscapeHandling.Default, 41 | }; 42 | } 43 | } 44 | 45 | public BlueprintGuid GetGuiD(string name) { 46 | if (!NewBlueprints.TryGetValue(name, out var id)) { 47 | #if DEBUG 48 | if (!AutoGenerated.TryGetValue(name, out id)) { 49 | id = Guid.NewGuid(); 50 | AutoGenerated.Add(name, id); 51 | Main.LogDebug($"Generated new GUID: {name} - {id}"); 52 | } else { 53 | Main.LogDebug($"WARNING: GUID: {name} - {id} is autogenerated"); 54 | } 55 | #endif 56 | } 57 | UsedGuids[name] = id; 58 | return new BlueprintGuid(id); 59 | } 60 | 61 | public void OverrideFrom(string userConfigDir) 62 | { 63 | var blueprintsFile = Path.Combine(userConfigDir, "Blueprints.json"); 64 | if (!File.Exists(blueprintsFile)) 65 | { 66 | SaveTo(userConfigDir); 67 | return; 68 | } 69 | var loadedBlueprints = FromFile(blueprintsFile); 70 | if (loadedBlueprints!.OverrideIds) 71 | { 72 | OverrideIds = loadedBlueprints.OverrideIds; 73 | loadedBlueprints.NewBlueprints.ForEach(entry => 74 | { 75 | var (key, value) = entry; 76 | if (NewBlueprints.ContainsKey(key)) 77 | { 78 | NewBlueprints[key] = value; 79 | } 80 | }); 81 | } 82 | loadedBlueprints.AutoGenerated.ForEach(entry => 83 | { 84 | var (key, value) = entry; 85 | AutoGenerated[key] = value; 86 | }); 87 | } 88 | 89 | public void SaveTo(string userConfigDir) 90 | { 91 | var blueprintsFile = Path.Combine(userConfigDir, "Blueprints.json"); 92 | File.WriteAllText(blueprintsFile, JsonConvert.SerializeObject(this, SerializerSettings)); 93 | } 94 | 95 | public static Blueprints FromEmbeddedResource() 96 | { 97 | JsonSerializer serializer = JsonSerializer.Create(); 98 | var resourcePath = $"SpellbookMerge.Config.Blueprints.json"; 99 | var assembly = Assembly.GetExecutingAssembly(); 100 | using Stream stream = assembly.GetManifestResourceStream(resourcePath)!; 101 | using StreamReader streamReader = new StreamReader(stream); 102 | using var reader = new JsonTextReader(streamReader); 103 | return serializer.Deserialize(reader)!; 104 | } 105 | 106 | public static Blueprints? FromFile(string filename) 107 | { 108 | return JsonConvert.DeserializeObject(File.ReadAllText(filename)); 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /SpellbookMerge/Config/Blueprints.json: -------------------------------------------------------------------------------- 1 | { 2 | "OverrideIds": false, 3 | "NewBlueprints": { 4 | "AeonIncorporateSpellbook": "2b7027ee-76cb-4c58-b2cf-f0475bc69fbb", 5 | "AzataIncorporateSpellbook": "83385d9f-4d71-4e4e-9461-8703be762a20", 6 | "DemonIncorporateSpellbook": "f3ff8515-355e-4738-b128-c3d01483f1ca", 7 | "TricksterIncorporateSpellbook": "c4ef6975-167d-4cf5-acbf-d66b60e63f9c" 8 | }, 9 | "AutoGenerated": {}, 10 | "UnusedGuiDs": {} 11 | } -------------------------------------------------------------------------------- /SpellbookMerge/Config/IOverridable.cs: -------------------------------------------------------------------------------- 1 | namespace SpellbookMerge.Config 2 | { 3 | public interface IOverridable 4 | { 5 | public void OverrideFrom(string userConfigDir); 6 | public void SaveTo(string userConfigDir); 7 | } 8 | } -------------------------------------------------------------------------------- /SpellbookMerge/Config/ModSettings.cs: -------------------------------------------------------------------------------- 1 | namespace SpellbookMerge.Config 2 | { 3 | public class ModSettings : IOverridable 4 | { 5 | public Blueprints Blueprints { get; private set; } = Blueprints.FromEmbeddedResource(); 6 | public PatchSettings PatchSettings { get; private set; } = PatchSettings.FromEmbeddedResource(); 7 | 8 | public void OverrideFrom(string userConfigDir) 9 | { 10 | Blueprints.OverrideFrom(userConfigDir); 11 | PatchSettings.OverrideFrom(userConfigDir); 12 | } 13 | 14 | public void SaveTo(string userConfigDir) 15 | { 16 | Blueprints.SaveTo(userConfigDir); 17 | PatchSettings.SaveTo(userConfigDir); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /SpellbookMerge/Config/PatchSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Reflection; 5 | using Kingmaker.Utility; 6 | using Newtonsoft.Json; 7 | 8 | namespace SpellbookMerge.Config 9 | { 10 | public class PatchSettings : IOverridable 11 | { 12 | [JsonProperty] 13 | public readonly SortedDictionary SpellProgressionPatches = new SortedDictionary(); 14 | private static JsonSerializerSettings? _cachedSettings; 15 | 16 | public void OverrideFrom(string userConfigDir) 17 | { 18 | var settingsFile = Path.Combine(userConfigDir, "PatchSettings.json"); 19 | if (!File.Exists(settingsFile)) 20 | { 21 | SaveTo(userConfigDir); 22 | return; 23 | } 24 | var loadedSettings = FromFile(settingsFile); 25 | loadedSettings!.SpellProgressionPatches.ForEach(entry => 26 | { 27 | var (key, value) = entry; 28 | if (SpellProgressionPatches.ContainsKey(key)) 29 | { 30 | SpellProgressionPatches[key] = value; 31 | } 32 | }); 33 | } 34 | 35 | public void SaveTo(string userConfigDir) 36 | { 37 | var patchSettingsFile = Path.Combine(userConfigDir, "PatchSettings.json"); 38 | File.WriteAllText(patchSettingsFile, JsonConvert.SerializeObject(this, SerializerSettings)); 39 | } 40 | 41 | private static JsonSerializerSettings SerializerSettings { 42 | get 43 | { 44 | return _cachedSettings ??= new JsonSerializerSettings 45 | { 46 | CheckAdditionalContent = false, 47 | ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, 48 | DefaultValueHandling = DefaultValueHandling.Include, 49 | FloatParseHandling = FloatParseHandling.Double, 50 | Formatting = Formatting.Indented, 51 | MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead, 52 | MissingMemberHandling = MissingMemberHandling.Ignore, 53 | NullValueHandling = NullValueHandling.Ignore, 54 | ObjectCreationHandling = ObjectCreationHandling.Replace, 55 | PreserveReferencesHandling = PreserveReferencesHandling.None, 56 | StringEscapeHandling = StringEscapeHandling.Default, 57 | }; 58 | } 59 | } 60 | 61 | public static PatchSettings FromEmbeddedResource() 62 | { 63 | JsonSerializer serializer = JsonSerializer.Create(); 64 | var resourcePath = $"SpellbookMerge.Config.PatchSettings.json"; 65 | var assembly = Assembly.GetExecutingAssembly(); 66 | using Stream stream = assembly.GetManifestResourceStream(resourcePath)!; 67 | using StreamReader streamReader = new StreamReader(stream); 68 | using var reader = new JsonTextReader(streamReader); 69 | return serializer.Deserialize(reader)!; 70 | } 71 | 72 | public static PatchSettings? FromFile(string filename) 73 | { 74 | return JsonConvert.DeserializeObject(File.ReadAllText(filename)); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /SpellbookMerge/Config/PatchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "SpellProgressionPatches": { 3 | "Inquisitor": true, 4 | "WarPriest": true, 5 | "Magus": true, 6 | "Bard": true, 7 | "Skald": true, 8 | "SwordSaint": true, 9 | "BloodRager": true, 10 | "Alchemist": true, 11 | "Paladin": true 12 | } 13 | } -------------------------------------------------------------------------------- /SpellbookMerge/Extensions/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Kingmaker.Blueprints.Facts; 3 | using Kingmaker.Localization; 4 | using SpellbookMerge.Utilities; 5 | 6 | namespace SpellbookMerge.Extensions 7 | { 8 | public static class ExtensionMethods 9 | { 10 | public static void SetName(this BlueprintUnitFact feature, String name) { 11 | feature.m_DisplayName = Resources.CreateString(feature.name + ".Name", name); 12 | } 13 | 14 | public static void SetDescription(this BlueprintUnitFact feature, LocalizedString description) { 15 | feature.m_Description = description; 16 | } 17 | 18 | public static void SetDescription(this BlueprintUnitFact feature, String description) { 19 | var taggedDescription = DescriptionTools.TagEncyclopediaEntries(description); 20 | feature.m_Description = Resources.CreateString(feature.name + ".Description", taggedDescription); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /SpellbookMerge/Features/IncorporateSpellbook.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.Blueprints; 2 | using Kingmaker.Blueprints.Classes; 3 | using SpellbookMerge.Extensions; 4 | 5 | namespace SpellbookMerge.Features 6 | { 7 | internal static class IncorporateSpellbook { 8 | public static void AddAeonIncorporateSpellbookFeature() 9 | { 10 | Resources.CreateBlueprint("AeonIncorporateSpellbook", bp => 11 | { 12 | bp.SetName("Mythic Spellbook"); 13 | 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."); 14 | 15 | bp.m_AllowedSpellbooks = new[] 16 | { 17 | Resources.SpellbookBlueprints.InquisitorSpellbook.ToReference(), 18 | Resources.SpellbookBlueprints.WarPriestSpellbook.ToReference(), 19 | Resources.SpellbookBlueprints.DruidSpellbook.ToReference(), 20 | Resources.SpellbookBlueprints.MagusSpellbook.ToReference(), 21 | Resources.SpellbookBlueprints.SwordSaintSpellbook.ToReference(), 22 | Resources.SpellbookBlueprints.HunterSpellbook.ToReference(), 23 | Resources.SpellbookBlueprints.EldritchScionSpellbook.ToReference(), 24 | Resources.SpellbookBlueprints.SorcererSpellbook.ToReference(), 25 | Resources.SpellbookBlueprints.WizardSpellbook.ToReference(), 26 | Resources.SpellbookBlueprints.ArcanistSpellbook.ToReference(), 27 | Resources.SpellbookBlueprints.SageSpellbook.ToReference(), 28 | Resources.SpellbookBlueprints.CrossbloodedSpellbook.ToReference(), 29 | Resources.SpellbookBlueprints.OracleSpellbook.ToReference(), 30 | Resources.SpellbookBlueprints.BardSpellbook.ToReference(), 31 | Resources.SpellbookBlueprints.ShamanSpellbook.ToReference(), 32 | Resources.SpellbookBlueprints.ClericSpellbook.ToReference(), 33 | Resources.SpellbookBlueprints.WitchSpellbook.ToReference(), 34 | 35 | }; 36 | bp.m_MythicSpellList = Resources.SpellListBlueprints.AeonSpellList.ToReference(); 37 | // 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 38 | bp.m_SpellKnownForSpontaneous = Resources.SpellTableBlueprints.MythicSpontaneousSpellsKnownTable 39 | .ToReference(); 40 | bp.IsClassFeature = true; 41 | }); 42 | } 43 | 44 | public static void AddAzataIncorporateSpellbookFeature() 45 | { 46 | Resources.CreateBlueprint("AzataIncorporateSpellbook", bp => 47 | { 48 | bp.SetName("Mythic Spellbook"); 49 | 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."); 50 | 51 | bp.m_AllowedSpellbooks = new[] 52 | { 53 | Resources.SpellbookBlueprints.MagusSpellbook.ToReference(), 54 | Resources.SpellbookBlueprints.BardSpellbook.ToReference(), 55 | Resources.SpellbookBlueprints.SkaldSpellbook.ToReference(), 56 | Resources.SpellbookBlueprints.SwordSaintSpellbook.ToReference(), 57 | Resources.SpellbookBlueprints.SorcererSpellbook.ToReference(), 58 | Resources.SpellbookBlueprints.DruidSpellbook.ToReference(), 59 | Resources.SpellbookBlueprints.HunterSpellbook.ToReference(), 60 | Resources.SpellbookBlueprints.BloodRagerSpellbook.ToReference(), 61 | Resources.SpellbookBlueprints.WizardSpellbook.ToReference(), 62 | Resources.SpellbookBlueprints.ExploiterWizardSpellbook.ToReference(), 63 | Resources.SpellbookBlueprints.EldritchScionSpellbook.ToReference(), 64 | Resources.SpellbookBlueprints.ArcanistSpellbook.ToReference(), 65 | Resources.SpellbookBlueprints.SageSpellbook.ToReference(), 66 | Resources.SpellbookBlueprints.CrossbloodedSpellbook.ToReference(), 67 | Resources.SpellbookBlueprints.OracleSpellbook.ToReference(), 68 | Resources.SpellbookBlueprints.ShamanSpellbook.ToReference(), 69 | Resources.SpellbookBlueprints.ClericSpellbook.ToReference(), 70 | Resources.SpellbookBlueprints.WitchSpellbook.ToReference(), 71 | }; 72 | bp.m_MythicSpellList = Resources.SpellListBlueprints.AzataSpellList.ToReference(); 73 | // 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 74 | bp.m_SpellKnownForSpontaneous = Resources.SpellTableBlueprints.MythicSpontaneousSpellsKnownTable 75 | .ToReference(); 76 | bp.IsClassFeature = true; 77 | }); 78 | } 79 | 80 | public static void AddDemonIncorporateSpellbookFeature() 81 | { 82 | Resources.CreateBlueprint("DemonIncorporateSpellbook", bp => 83 | { 84 | bp.SetName("Mythic Spellbook"); 85 | 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."); 86 | 87 | bp.m_AllowedSpellbooks = new[] 88 | { 89 | Resources.SpellbookBlueprints.MagusSpellbook.ToReference(), 90 | Resources.SpellbookBlueprints.SwordSaintSpellbook.ToReference(), 91 | Resources.SpellbookBlueprints.BloodRagerSpellbook.ToReference(), 92 | Resources.SpellbookBlueprints.HunterSpellbook.ToReference(), 93 | Resources.SpellbookBlueprints.SkaldSpellbook.ToReference(), 94 | Resources.SpellbookBlueprints.EldritchScionSpellbook.ToReference(), 95 | Resources.SpellbookBlueprints.SageSpellbook.ToReference(), 96 | Resources.SpellbookBlueprints.SorcererSpellbook.ToReference(), 97 | Resources.SpellbookBlueprints.CrossbloodedSpellbook.ToReference(), 98 | Resources.SpellbookBlueprints.ArcanistSpellbook.ToReference(), 99 | Resources.SpellbookBlueprints.OracleSpellbook.ToReference(), 100 | Resources.SpellbookBlueprints.BardSpellbook.ToReference(), 101 | Resources.SpellbookBlueprints.ShamanSpellbook.ToReference(), 102 | Resources.SpellbookBlueprints.ClericSpellbook.ToReference(), 103 | Resources.SpellbookBlueprints.WizardSpellbook.ToReference(), 104 | Resources.SpellbookBlueprints.DruidSpellbook.ToReference(), 105 | Resources.SpellbookBlueprints.WitchSpellbook.ToReference(), 106 | }; 107 | bp.m_MythicSpellList = Resources.SpellListBlueprints.DemonSpellList.ToReference(); 108 | // 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 109 | bp.m_SpellKnownForSpontaneous = Resources.SpellTableBlueprints.MythicSpontaneousSpellsKnownTable 110 | .ToReference(); 111 | bp.IsClassFeature = true; 112 | }); 113 | } 114 | 115 | public static void AddTricksterIncorporateSpellbookFeature() 116 | { 117 | Resources.CreateBlueprint("TricksterIncorporateSpellbook", bp => 118 | { 119 | bp.SetName("Mythic Spellbook"); 120 | 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."); 121 | 122 | bp.m_AllowedSpellbooks = new[] 123 | { 124 | Resources.SpellbookBlueprints.MagusSpellbook.ToReference(), 125 | Resources.SpellbookBlueprints.SwordSaintSpellbook.ToReference(), 126 | Resources.SpellbookBlueprints.AlchemistSpellbook.ToReference(), 127 | Resources.SpellbookBlueprints.EldritchScoundrelSpellbook.ToReference(), 128 | Resources.SpellbookBlueprints.BardSpellbook.ToReference(), 129 | Resources.SpellbookBlueprints.SkaldSpellbook.ToReference(), 130 | Resources.SpellbookBlueprints.WitchSpellbook.ToReference(), 131 | Resources.SpellbookBlueprints.EldritchScionSpellbook.ToReference(), 132 | Resources.SpellbookBlueprints.SageSpellbook.ToReference(), 133 | Resources.SpellbookBlueprints.SorcererSpellbook.ToReference(), 134 | Resources.SpellbookBlueprints.CrossbloodedSpellbook.ToReference(), 135 | Resources.SpellbookBlueprints.ArcanistSpellbook.ToReference(), 136 | Resources.SpellbookBlueprints.OracleSpellbook.ToReference(), 137 | Resources.SpellbookBlueprints.ShamanSpellbook.ToReference(), 138 | Resources.SpellbookBlueprints.ClericSpellbook.ToReference(), 139 | Resources.SpellbookBlueprints.WizardSpellbook.ToReference(), 140 | Resources.SpellbookBlueprints.DruidSpellbook.ToReference(), 141 | }; 142 | bp.m_MythicSpellList = Resources.SpellListBlueprints.TricksterSpellList.ToReference(); 143 | // 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 144 | bp.m_SpellKnownForSpontaneous = Resources.SpellTableBlueprints.MythicSpontaneousSpellsKnownTable 145 | .ToReference(); 146 | bp.IsClassFeature = true; 147 | }); 148 | } 149 | 150 | } 151 | } -------------------------------------------------------------------------------- /SpellbookMerge/Info.json: -------------------------------------------------------------------------------- 1 | { 2 | "Id": "SpellbookMerge", 3 | "DisplayName": "Spellbook Merge", 4 | "Author": "vikigenius", 5 | "Version": "1.7.1", 6 | "ManagerVersion": "0.21.3", 7 | "Requirements": [], 8 | "AssemblyName": "SpellbookMerge.dll", 9 | "EntryMethod": "SpellbookMerge.Main.Load" 10 | } -------------------------------------------------------------------------------- /SpellbookMerge/Main.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using HarmonyLib; 4 | using Kingmaker; 5 | using Kingmaker.Blueprints.JsonSystem; 6 | using UnityModManagerNet; 7 | using SpellbookMerge.Config; 8 | 9 | namespace SpellbookMerge 10 | { 11 | public static class Main 12 | { 13 | public static bool Enabled { get; private set; } 14 | private static UnityModManager.ModEntry? ModEntry { get; set; } 15 | public static ModSettings ModSettings { get; } = new ModSettings(); 16 | 17 | public static string? UserConfigDir { get; private set; } 18 | 19 | // ReSharper disable once UnusedMember.Local 20 | private static bool Load(UnityModManager.ModEntry modEntry) 21 | { 22 | ModEntry = modEntry; 23 | UserConfigDir = ModEntry.Path + "UserSettings"; 24 | Directory.CreateDirectory(UserConfigDir); 25 | ModSettings.OverrideFrom(UserConfigDir); 26 | ModEntry.OnToggle = OnToggle; 27 | var harmony = new Harmony(ModEntry.Info.Id); 28 | harmony.PatchAll(); 29 | return true; 30 | } 31 | 32 | private static bool OnToggle(UnityModManager.ModEntry modEntry, bool value) 33 | { 34 | Enabled = value; 35 | return true; 36 | } 37 | 38 | public static void Log(string msg) { 39 | ModEntry!.Logger.Log(msg); 40 | } 41 | 42 | [System.Diagnostics.Conditional("DEBUG")] 43 | public static void LogDebug(string msg) { 44 | ModEntry!.Logger.Log(msg); 45 | } 46 | 47 | public static void LogPatch(string action, IScriptableObjectWithAssetId bp) { 48 | Log($"{action}: {bp.AssetGuid} - {bp.name}"); 49 | } 50 | 51 | public static void LogHeader(string msg) { 52 | Log($"--{msg.ToUpper()}--"); 53 | } 54 | 55 | public static void LogException(Exception e, string message) { 56 | Log(message); 57 | Log(e.ToString()); 58 | PFLog.Mods.Error(message); 59 | } 60 | 61 | public static void LogError(string message) { 62 | Log(message); 63 | PFLog.Mods.Error(message); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /SpellbookMerge/Patches/AdditionalBlueprints.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.Blueprints.JsonSystem; 3 | 4 | namespace SpellbookMerge.Patches 5 | { 6 | // ReSharper disable once UnusedType.Global 7 | public class AdditionalBlueprints 8 | { 9 | [HarmonyPatch(typeof(BlueprintsCache), "Init")] 10 | // ReSharper disable once UnusedType.Local 11 | private static class BlueprintsCacheInitPatch 12 | { 13 | private static bool _initialized; 14 | 15 | [HarmonyPriority(Priority.First)] 16 | // ReSharper disable once UnusedMember.Local 17 | private static void Postfix() 18 | { 19 | if (!Main.Enabled || _initialized) return; 20 | _initialized = true; 21 | Main.LogHeader("Adding new Blueprints"); 22 | Features.IncorporateSpellbook.AddAeonIncorporateSpellbookFeature(); 23 | Features.IncorporateSpellbook.AddAzataIncorporateSpellbookFeature(); 24 | Features.IncorporateSpellbook.AddDemonIncorporateSpellbookFeature(); 25 | Features.IncorporateSpellbook.AddTricksterIncorporateSpellbookFeature(); 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /SpellbookMerge/Patches/LegacyMerge.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using HarmonyLib; 4 | using Kingmaker.Blueprints; 5 | using Kingmaker.Blueprints.Classes.Spells; 6 | using Kingmaker.UnitLogic; 7 | using Kingmaker.UnitLogic.Abilities; 8 | using Kingmaker.UnitLogic.Class.LevelUp; 9 | using Kingmaker.UnitLogic.Class.LevelUp.Actions; 10 | 11 | namespace SpellbookMerge.Patches 12 | { 13 | public class LegacyMerge 14 | { 15 | [HarmonyPatch(typeof(ApplySpellbook), nameof(ApplySpellbook.Apply), new Type[] {typeof(LevelUpState), typeof(UnitDescriptor)})] 16 | // ReSharper disable once UnusedType.Local 17 | private static class ApplySpellbookApplyPatch 18 | { 19 | // ReSharper disable once UnusedMember.Local 20 | private static bool Prefix(LevelUpState state, UnitDescriptor unit) 21 | { 22 | if (!Main.Enabled) return true; 23 | Apply(state, unit); 24 | return false; 25 | } 26 | 27 | public static void Apply(LevelUpState state, UnitDescriptor unit) 28 | { 29 | if (state.SelectedClass == null) 30 | { 31 | return; 32 | } 33 | var component = state.SelectedClass.GetComponent(); 34 | if (component != null && component.Levels.Contains(state.NextClassLevel)) 35 | { 36 | return; 37 | } 38 | var classData = unit.Progression.GetClassData(state.SelectedClass); 39 | if (classData?.Spellbook == null) return; 40 | Spellbook spellbook = unit.DemandSpellbook(classData.Spellbook); 41 | if (state.SelectedClass.Spellbook && state.SelectedClass.Spellbook != classData.Spellbook) 42 | { 43 | var spellbook2 = unit.Spellbooks.FirstOrDefault(s => s.Blueprint == state.SelectedClass.Spellbook); 44 | if (spellbook2 != null) 45 | { 46 | foreach (AbilityData abilityData in spellbook2.GetAllKnownSpells()) 47 | { 48 | spellbook.AddKnown(abilityData.SpellLevel, abilityData.Blueprint); 49 | } 50 | unit.DeleteSpellbook(state.SelectedClass.Spellbook); 51 | } 52 | } 53 | int casterLevel = spellbook.CasterLevel; 54 | spellbook.AddLevelFromClass(classData.CharacterClass); 55 | int casterLevel2 = spellbook.CasterLevel; 56 | SpellSelectionData spellSelectionData = state.DemandSpellSelection(spellbook.Blueprint, spellbook.Blueprint.SpellList); 57 | if (spellbook.Blueprint.SpellsKnown != null) 58 | { 59 | for (int i = 0; i <= 10; i++) 60 | { 61 | BlueprintSpellsTable spellsKnown = spellbook.Blueprint.SpellsKnown; 62 | int num = spellsKnown.GetCount(casterLevel, i); 63 | int num2 = spellsKnown.GetCount(casterLevel2, i); 64 | spellSelectionData.SetLevelSpells(i, Math.Max(0, num2 - num)); 65 | } 66 | } 67 | int maxSpellLevel = spellbook.MaxSpellLevel; 68 | if (spellbook.Blueprint.SpellsPerLevel > 0) 69 | { 70 | if (casterLevel == 0) 71 | { 72 | spellSelectionData.SetExtraSpells(0, maxSpellLevel); 73 | spellSelectionData.ExtraByStat = true; 74 | spellSelectionData.UpdateMaxLevelSpells(unit); 75 | } 76 | else 77 | { 78 | spellSelectionData.SetExtraSpells(spellbook.Blueprint.SpellsPerLevel, maxSpellLevel); 79 | } 80 | } 81 | foreach (AddCustomSpells customSpells in spellbook.Blueprint.GetComponents()) 82 | { 83 | ApplySpellbook.TryApplyCustomSpells(spellbook, customSpells, state, unit); 84 | } 85 | } 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /SpellbookMerge/Patches/LogBlueprints.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.Blueprints.JsonSystem; 3 | using Kingmaker.Utility; 4 | 5 | 6 | namespace SpellbookMerge.Patches 7 | { 8 | // ReSharper disable once UnusedType.Global 9 | internal static class LogBlueprints 10 | { 11 | [HarmonyPatch(typeof(BlueprintsCache), "Init")] 12 | // ReSharper disable once UnusedType.Local 13 | private static class BlueprintsLogGuids 14 | { 15 | [HarmonyPriority(Priority.Last)] 16 | // ReSharper disable once UnusedMember.Local 17 | private static void Postfix() { 18 | GenerateUnused(); 19 | Main.ModSettings.Blueprints.SaveTo(Main.UserConfigDir!); 20 | } 21 | 22 | private static void GenerateUnused() { 23 | Main.ModSettings.Blueprints.AutoGenerated.ForEach(entry => 24 | { 25 | var (key, value) = entry; 26 | if (!Main.ModSettings.Blueprints.UsedGuids.ContainsKey(key)) { 27 | Main.ModSettings.Blueprints.UnusedGuids[key] = value; 28 | } 29 | }); 30 | Main.ModSettings.Blueprints.NewBlueprints.ForEach(entry => 31 | { 32 | var (key, value) = entry; 33 | if (!Main.ModSettings.Blueprints.UsedGuids.ContainsKey(key)) { 34 | Main.ModSettings.Blueprints.UnusedGuids[key] = value; 35 | } 36 | }); 37 | } 38 | } 39 | 40 | } 41 | } -------------------------------------------------------------------------------- /SpellbookMerge/Patches/MythicProgression.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using HarmonyLib; 3 | using Kingmaker.Blueprints; 4 | using Kingmaker.Blueprints.Classes; 5 | using Kingmaker.Blueprints.JsonSystem; 6 | 7 | namespace SpellbookMerge.Patches 8 | { 9 | public class MythicProgression 10 | { 11 | [HarmonyPatch(typeof(BlueprintsCache), "Init")] 12 | private static class BlueprintsCacheInitPatch 13 | { 14 | private static bool _initialized; 15 | 16 | private static void Postfix() 17 | { 18 | if (!Main.Enabled || _initialized) return; 19 | _initialized = true; 20 | Main.LogHeader("Patching Mythic Progressions"); 21 | PatchAeonProgression(); 22 | PatchAzataProgression(); 23 | PatchDemonProgression(); 24 | PatchTricksterProgression(); 25 | PatchAngelAllowedMerges(); 26 | PatchLichAllowedMerges(); 27 | } 28 | 29 | private static void PatchAngelAllowedMerges() 30 | { 31 | var angelIncorporateSpellbookFeature = Resources.MythicMergeBlueprints.AngelIncorporateSpellbook; 32 | var angelAllowedMerges = new List(angelIncorporateSpellbookFeature.m_AllowedSpellbooks); 33 | var additionalMerges = new List 34 | { 35 | Resources.SpellbookBlueprints.PaladinSpellbook.ToReference(), 36 | Resources.SpellbookBlueprints.SorcererSpellbook.ToReference(), 37 | Resources.SpellbookBlueprints.WizardSpellbook.ToReference(), 38 | Resources.SpellbookBlueprints.ArcanistSpellbook.ToReference(), 39 | Resources.SpellbookBlueprints.WitchSpellbook.ToReference(), 40 | Resources.SpellbookBlueprints.SageSpellbook.ToReference(), 41 | Resources.SpellbookBlueprints.CrossbloodedSpellbook.ToReference(), 42 | }; 43 | angelAllowedMerges.AddRange(additionalMerges); 44 | angelIncorporateSpellbookFeature.m_AllowedSpellbooks = angelAllowedMerges.ToArray(); 45 | Main.Log("Patched Angel allowed spellbook merges"); 46 | } 47 | 48 | private static void PatchLichAllowedMerges() 49 | { 50 | var lichIncorporateSpellbookFeature = Resources.MythicMergeBlueprints.LichIncorporateSpellbook; 51 | var lichAllowedMerges = new List(lichIncorporateSpellbookFeature.m_AllowedSpellbooks); 52 | var additionalMerges = new List 53 | { 54 | Resources.SpellbookBlueprints.DruidSpellbook.ToReference(), 55 | Resources.SpellbookBlueprints.OracleSpellbook.ToReference(), 56 | Resources.SpellbookBlueprints.ShamanSpellbook.ToReference(), 57 | Resources.SpellbookBlueprints.ClericSpellbook.ToReference(), 58 | 59 | }; 60 | lichAllowedMerges.AddRange(additionalMerges); 61 | lichIncorporateSpellbookFeature.m_AllowedSpellbooks = lichAllowedMerges.ToArray(); 62 | Main.Log("Patched Lich allowed spellbook merges"); 63 | } 64 | 65 | private static void PatchAeonProgression() 66 | { 67 | var aeonProgression = Resources.ProgressionBlueprints.AeonProgression; 68 | var aeonIncorporateSpellbookFeature = 69 | Resources.TryGetModBlueprint("AeonIncorporateSpellbook"); 70 | aeonProgression.LevelEntries[0].m_Features 71 | .Add(aeonIncorporateSpellbookFeature.ToReference()); 72 | Main.Log("Patched Aeon Progression"); 73 | } 74 | 75 | private static void PatchAzataProgression() 76 | { 77 | var azataProgression = Resources.ProgressionBlueprints.AzataProgression; 78 | var azataIncorporateSpellbookFeature = 79 | Resources.TryGetModBlueprint("AzataIncorporateSpellbook"); 80 | azataProgression.LevelEntries[0].m_Features 81 | .Add(azataIncorporateSpellbookFeature.ToReference()); 82 | Main.Log("Patched Azata Progression"); 83 | } 84 | 85 | private static void PatchDemonProgression() 86 | { 87 | var demonProgression = Resources.ProgressionBlueprints.DemonProgression; 88 | var demonIncorporateSpellbookFeature = 89 | Resources.TryGetModBlueprint("DemonIncorporateSpellbook"); 90 | demonProgression.LevelEntries[0].m_Features 91 | .Add(demonIncorporateSpellbookFeature.ToReference()); 92 | Main.Log("Patched Demon Progression"); 93 | } 94 | 95 | private static void PatchTricksterProgression() 96 | { 97 | var tricksterProgression = Resources.ProgressionBlueprints.TricksterProgression; 98 | var tricksterIncorporateSpellbookFeature = 99 | Resources.TryGetModBlueprint("TricksterIncorporateSpellbook"); 100 | tricksterProgression.LevelEntries[0].m_Features 101 | .Add(tricksterIncorporateSpellbookFeature.ToReference()); 102 | Main.Log("Patched Trickster Progression"); 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /SpellbookMerge/Patches/SpellbookProgression.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using HarmonyLib; 4 | using Kingmaker.Blueprints.Classes.Spells; 5 | using Kingmaker.Blueprints.JsonSystem; 6 | 7 | namespace SpellbookMerge.Patches 8 | { 9 | public class SpellbookProgression 10 | { 11 | [HarmonyPatch(typeof(BlueprintsCache), "Init")] 12 | private static class BlueprintsCacheInitPatch 13 | { 14 | private static bool _initialized; 15 | 16 | private static void Postfix() 17 | { 18 | if (!Main.Enabled || _initialized) return; 19 | _initialized = true; 20 | Main.LogHeader("Patching spellbook progressions"); 21 | PatchInquisitorSpellSlotProgression(); 22 | PatchWarPriestSpellSlotProgression(); 23 | PatchMagusSpellSlotProgression(); 24 | PatchBardSpellSlotProgression(); 25 | PatchSkaldSpellSlotProgression(); 26 | PatchSwordSaintSpellSlotProgression(); 27 | PatchBloodRagerSpellSlotProgression(); 28 | PatchAlchemistSpellSlotProgression(); 29 | PatchPaladinSpellSlotProgression(); 30 | } 31 | 32 | private static void PatchHybridCasterSpellProgression(BlueprintSpellsTable hybridCasterSlots) 33 | { 34 | List levels = new List(hybridCasterSlots.Levels); 35 | var additionalSlotTables = new List 36 | { 37 | new[] {0, 5, 5, 5, 5, 5, 5, 1}, 38 | new[] {0, 5, 5, 5, 5, 5, 5, 2}, 39 | new[] {0, 5, 5, 5, 5, 5, 5, 3}, 40 | new[] {0, 5, 5, 5, 5, 5, 5, 4}, 41 | new[] {0, 5, 5, 5, 5, 5, 5, 5}, 42 | }; 43 | levels.AddRange(additionalSlotTables.Select(slots => new SpellsLevelEntry {Count = slots})); 44 | hybridCasterSlots.Levels = levels.ToArray(); 45 | } 46 | 47 | // Patch Inquisitor Spellbook to allow 7th level spells 48 | private static void PatchInquisitorSpellSlotProgression() 49 | { 50 | if (!Main.ModSettings.PatchSettings.SpellProgressionPatches["Inquisitor"]) return; 51 | var inquisitorSpellSlots = Resources.SpellTableBlueprints.InquisitorSpellsTable; 52 | PatchHybridCasterSpellProgression(inquisitorSpellSlots); 53 | Main.Log($"Patched Inquisitor Spell Levels to {inquisitorSpellSlots.Levels.Length}"); 54 | } 55 | 56 | // Patch WarPriest Spellbook to allow 7th level spells 57 | private static void PatchWarPriestSpellSlotProgression() 58 | { 59 | if (!Main.ModSettings.PatchSettings.SpellProgressionPatches["WarPriest"]) return; 60 | var warPriestSpellSlots = Resources.SpellTableBlueprints.WarPriestSpellsTable; 61 | PatchHybridCasterSpellProgression(warPriestSpellSlots); 62 | Main.Log($"Patched WarPriest Spell Levels to {warPriestSpellSlots.Levels.Length}"); 63 | } 64 | 65 | // Patch Bard Spellbook to allow 7th level spells (Also used by Hunter) 66 | private static void PatchBardSpellSlotProgression() 67 | { 68 | if (!Main.ModSettings.PatchSettings.SpellProgressionPatches["Bard"]) return; 69 | var bardSpellSlots = Resources.SpellTableBlueprints.BardSpellsTable; 70 | PatchHybridCasterSpellProgression(bardSpellSlots); 71 | Main.Log($"Patched Bard Spell Levels to {bardSpellSlots.Levels.Length}"); 72 | } 73 | 74 | // Patch Magus Spellbook to allow 7th level spells 75 | private static void PatchMagusSpellSlotProgression() 76 | { 77 | if (!Main.ModSettings.PatchSettings.SpellProgressionPatches["Magus"]) return; 78 | var magusSpellSlots = Resources.SpellTableBlueprints.MagusSpellsTable; 79 | PatchHybridCasterSpellProgression(magusSpellSlots); 80 | Main.Log($"Patched Magus Spell Levels to {magusSpellSlots.Levels.Length}"); 81 | } 82 | 83 | // Patch Skald Spellbook to allow 7th level spells 84 | private static void PatchSkaldSpellSlotProgression() 85 | { 86 | if (!Main.ModSettings.PatchSettings.SpellProgressionPatches["Skald"]) return; 87 | var skaldSpellSlots = Resources.SpellTableBlueprints.SkaldSpellsTable; 88 | PatchHybridCasterSpellProgression(skaldSpellSlots); 89 | Main.Log($"Patched Skald Spell Levels to {skaldSpellSlots.Levels.Length}"); 90 | } 91 | 92 | // Patch SwordSaint Spellbook to allow 7th level spells 93 | private static void PatchSwordSaintSpellSlotProgression() 94 | { 95 | if (!Main.ModSettings.PatchSettings.SpellProgressionPatches["SwordSaint"]) return; 96 | var swordSaintSpellSlots = Resources.SpellTableBlueprints.SwordSaintSpellsTable; 97 | List levels = new List(swordSaintSpellSlots.Levels); 98 | var additionalSlotTables = new List 99 | { 100 | new[] {0, 4, 4, 4, 4, 4, 4, 1}, 101 | new[] {0, 4, 4, 4, 4, 4, 4, 2}, 102 | new[] {0, 4, 4, 4, 4, 4, 4, 3}, 103 | new[] {0, 4, 4, 4, 4, 4, 4, 4}, 104 | }; 105 | levels.AddRange(additionalSlotTables.Select(slots => new SpellsLevelEntry {Count = slots})); 106 | 107 | swordSaintSpellSlots.Levels = levels.ToArray(); 108 | Main.Log($"Patched SwordSaint Spell Levels to {swordSaintSpellSlots.Levels.Length}"); 109 | } 110 | 111 | // Patch BloodRager Spellbook to allow 7th level spells 112 | private static void PatchBloodRagerSpellSlotProgression() 113 | { 114 | if (!Main.ModSettings.PatchSettings.SpellProgressionPatches["BloodRager"]) return; 115 | var bloodRagerSpellSlots = Resources.SpellTableBlueprints.BloodRagerSpellsTable; 116 | List levels = new List(bloodRagerSpellSlots.Levels); 117 | var additionalSlotTables = new List 118 | { 119 | new[] {0, 4, 4, 3, 3, 1}, 120 | new[] {0, 4, 4, 4, 3, 2}, 121 | new[] {0, 4, 4, 4, 4, 3, 1}, 122 | new[] {0, 4, 4, 4, 4, 4, 2}, 123 | new[] {0, 4, 4, 4, 4, 4, 3, 1}, 124 | new[] {0, 4, 4, 4, 4, 4, 4, 2}, 125 | new[] {0, 4, 4, 4, 4, 4, 4, 4}, 126 | }; 127 | levels.AddRange(additionalSlotTables.Select(slots => new SpellsLevelEntry {Count = slots})); 128 | bloodRagerSpellSlots.Levels = levels.ToArray(); 129 | Main.Log($"Patched BloodRager Spell Levels to {bloodRagerSpellSlots.Levels.Length}"); 130 | } 131 | 132 | // Patch Alchemist Spellbook to allow 7th level spells 133 | private static void PatchAlchemistSpellSlotProgression() 134 | { 135 | if (!Main.ModSettings.PatchSettings.SpellProgressionPatches["Alchemist"]) return; 136 | var alchemistSpellSlots = Resources.SpellTableBlueprints.AlchemistSpellsTable; 137 | PatchHybridCasterSpellProgression(alchemistSpellSlots); 138 | Main.Log($"Patched Alchemist Spell Levels to {alchemistSpellSlots.Levels.Length}"); 139 | } 140 | 141 | // Patch Paladin Spellbook to allow 10th level spells 142 | private static void PatchPaladinSpellSlotProgression() 143 | { 144 | if (!Main.ModSettings.PatchSettings.SpellProgressionPatches["Paladin"]) return; 145 | var paladinSpellSlots = Resources.SpellTableBlueprints.PaladinSpellsTable; 146 | paladinSpellSlots.Levels[18].Count = new[] {0, 4, 4, 4, 4, 2, 2}; 147 | paladinSpellSlots.Levels[19].Count = new[] {0, 4, 4, 4, 4, 4, 4}; 148 | paladinSpellSlots.Levels[20].Count = new[] {0, 4, 4, 4, 4, 4, 4, 2}; 149 | 150 | List levels = new List(paladinSpellSlots.Levels); 151 | var additionalSlotTables = new List 152 | { 153 | new[] {0, 4, 4, 4, 4, 4, 4, 4}, 154 | new[] {0, 4, 4, 4, 4, 4, 4, 4, 2}, 155 | new[] {0, 4, 4, 4, 4, 4, 4, 4, 4}, 156 | new[] {0, 4, 4, 4, 4, 4, 4, 4, 4, 2}, 157 | new[] {0, 4, 4, 4, 4, 4, 4, 4, 4, 4}, 158 | new[] {0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 2}, 159 | new[] {0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, 160 | }; 161 | levels.AddRange(additionalSlotTables.Select(slots => new SpellsLevelEntry {Count = slots})); 162 | paladinSpellSlots.Levels = levels.ToArray(); 163 | Main.Log($"Patched Paladin Spell Levels to {paladinSpellSlots.Levels.Length}"); 164 | } 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /SpellbookMerge/Resources.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Kingmaker.Blueprints; 4 | using Kingmaker.Blueprints.Classes; 5 | using Kingmaker.Blueprints.Classes.Spells; 6 | using Kingmaker.Localization; 7 | 8 | namespace SpellbookMerge 9 | { 10 | public static class Resources 11 | { 12 | internal static class MythicMergeBlueprints 13 | { 14 | public static BlueprintFeatureSelectMythicSpellbook AngelIncorporateSpellbook => 15 | TryGetBlueprint("e1fbb0e0e610a3a4d91e5e5284587939")!; 16 | public static BlueprintFeatureSelectMythicSpellbook LichIncorporateSpellbook => 17 | TryGetBlueprint("3f16e9caf7c683c40884c7c455ed26af")!; 18 | } 19 | 20 | internal static class SpellbookBlueprints 21 | { 22 | public static BlueprintSpellbook InquisitorSpellbook => TryGetBlueprint("57fab75111f377248810ece84193a5a5")!; 23 | public static BlueprintSpellbook WarPriestSpellbook => TryGetBlueprint("7d7d51be2948d2544b3c2e1596fd7603")!; 24 | public static BlueprintSpellbook MagusSpellbook => TryGetBlueprint("5d8d04e76dff6c5439de99af0d57be63")!; 25 | public static BlueprintSpellbook BardSpellbook => TryGetBlueprint("bc04fc157a8801d41b877ad0d9af03dd")!; 26 | public static BlueprintSpellbook SkaldSpellbook => TryGetBlueprint("8f159d2f22ced074ea32995eb5a446a0")!; 27 | public static BlueprintSpellbook SwordSaintSpellbook => TryGetBlueprint("682545e11e5306c45b14ca78bcbe3e62")!; 28 | public static BlueprintSpellbook BloodRagerSpellbook => TryGetBlueprint("e19484252c2f80e4a9439b3681b20f00")!; 29 | public static BlueprintSpellbook AlchemistSpellbook => TryGetBlueprint("027d37761f3804042afa96fe3e9086cc")!; 30 | public static BlueprintSpellbook EldritchScoundrelSpellbook => TryGetBlueprint("4f96fb20f06b7494a8b2bd731a70af6c")!; 31 | public static BlueprintSpellbook HunterSpellbook => TryGetBlueprint("885cd422aa357e2409146b38bb1fec51")!; 32 | public static BlueprintSpellbook SorcererSpellbook => TryGetBlueprint("b3db3766a4b605040b366265e2af0e50")!; 33 | public static BlueprintSpellbook DruidSpellbook => TryGetBlueprint("fc78193f68150454483a7eea8b605b71")!; 34 | public static BlueprintSpellbook PaladinSpellbook => TryGetBlueprint("bce4989b070ce924b986bf346f59e885")!; 35 | public static BlueprintSpellbook WizardSpellbook => TryGetBlueprint("5a38c9ac8607890409fcb8f6342da6f4")!; 36 | public static BlueprintSpellbook ExploiterWizardSpellbook => TryGetBlueprint("d09794fb6f93e4a40929a965b434070d")!; 37 | public static BlueprintSpellbook WitchSpellbook => TryGetBlueprint("dd04f9239f655ea438976742728e4909")!; 38 | public static BlueprintSpellbook EldritchScionSpellbook => TryGetBlueprint("e2763fbfdb91920458c4686c3e7ed085")!; 39 | public static BlueprintSpellbook ArcanistSpellbook => TryGetBlueprint("33903fe5c4abeaa45bc249adb9d98848")!; 40 | public static BlueprintSpellbook SageSpellbook => TryGetBlueprint("cc2052732997b654e93eac268a39a0a9")!; 41 | public static BlueprintSpellbook CrossbloodedSpellbook => TryGetBlueprint("cb0be5988031ebe4c947086a1170eacc")!; 42 | public static BlueprintSpellbook OracleSpellbook => TryGetBlueprint("6c03364712b415941a98f74522a81273")!; 43 | public static BlueprintSpellbook ClericSpellbook => TryGetBlueprint("4673d19a0cf2fab4f885cc4d1353da33")!; 44 | public static BlueprintSpellbook ShamanSpellbook => TryGetBlueprint("44f16931dabdff643bfe2a48138e769f")!; 45 | 46 | } 47 | 48 | internal static class SpellListBlueprints 49 | { 50 | public static BlueprintSpellList AeonSpellList => TryGetBlueprint("ca8c6024bd2519f4b97162a3ad286920")!; 51 | public static BlueprintSpellList AzataSpellList => TryGetBlueprint("10c634d2b386d8d41b18a889adb8cd49")!; 52 | public static BlueprintSpellList DemonSpellList => TryGetBlueprint("abb1991bf6e996348bb743471ee7e1c1")!; 53 | public static BlueprintSpellList TricksterSpellList => TryGetBlueprint("7a5ea54564c7d494794f34d0f5a9abb3")!; 54 | } 55 | 56 | internal static class ProgressionBlueprints 57 | { 58 | public static BlueprintProgression AeonProgression => TryGetBlueprint("34b9484b0d5ce9340ae51d2bf9518bbe")!; 59 | public static BlueprintProgression AzataProgression => TryGetBlueprint("9db53de4bf21b564ca1a90ff5bd16586")!; 60 | public static BlueprintProgression DemonProgression => TryGetBlueprint("285fe49f7df8587468f676aa49362213")!; 61 | public static BlueprintProgression TricksterProgression => TryGetBlueprint("cc64789b0cc5df14b90da1ffee7bbeea")!; 62 | } 63 | 64 | internal static class SpellTableBlueprints 65 | { 66 | public static BlueprintSpellsTable MythicSpontaneousSpellsKnownTable => TryGetBlueprint("2d574ccdea8543bda1dffe63b0f16760")!; 67 | public static BlueprintSpellsTable InquisitorSpellsTable => TryGetBlueprint("83d3e15962e5d6949b90b5c226a2b487")!; 68 | public static BlueprintSpellsTable WarPriestSpellsTable => TryGetBlueprint("c73a394ec54adc243aef8ac967e39324")!; 69 | public static BlueprintSpellsTable MagusSpellsTable => TryGetBlueprint("6326b540f7c6a604f9d6f82cc0e2293c")!; 70 | public static BlueprintSpellsTable BardSpellsTable => TryGetBlueprint("0a8eec9ca5c0dc64795243ab3c55d924")!; 71 | public static BlueprintSpellsTable SkaldSpellsTable => TryGetBlueprint("39aeb5d8dafde5a40ba2032dec65db70")!; 72 | public static BlueprintSpellsTable SwordSaintSpellsTable => TryGetBlueprint("b9fdc0b2d37eb9e4298f9163edf5ca82")!; 73 | public static BlueprintSpellsTable BloodRagerSpellsTable => TryGetBlueprint("caf7018942861664ebe87687893ad05d")!; 74 | public static BlueprintSpellsTable AlchemistSpellsTable => TryGetBlueprint("bf4a0a03a45275d438b8c59cd5388259")!; 75 | public static BlueprintSpellsTable PaladinSpellsTable => TryGetBlueprint("9aed4803e424ae8429c392d8fbfb88ff")!; 76 | } 77 | 78 | 79 | private static readonly Dictionary ModBlueprints = new Dictionary(); 80 | // All localized strings created in this mod, mapped to their localized key. Populated by CreateString. 81 | private static Dictionary textToLocalizedString = new Dictionary(); 82 | 83 | private static void AddBlueprint(SimpleBlueprint blueprint) { 84 | AddBlueprint(blueprint, blueprint.AssetGuid); 85 | } 86 | 87 | private static void AddBlueprint(SimpleBlueprint blueprint, BlueprintGuid assetId) { 88 | var loadedBlueprint = ResourcesLibrary.TryGetBlueprint(assetId); 89 | if (loadedBlueprint == null) { 90 | ModBlueprints[assetId] = blueprint; 91 | ResourcesLibrary.BlueprintsCache.AddCachedBlueprint(assetId, blueprint); 92 | blueprint.OnEnable(); 93 | Main.Log($"Added blueprint {assetId}"); 94 | } else { 95 | Main.Log($"Failed to Add: {blueprint.name}"); 96 | Main.Log($"Asset ID: {assetId} already in use by: {loadedBlueprint.name}"); 97 | } 98 | } 99 | 100 | public static T CreateBlueprint(string name, Action? init = null) where T : SimpleBlueprint, new() { 101 | var result = new T { 102 | name = name, 103 | AssetGuid = Main.ModSettings.Blueprints.GetGuiD(name) 104 | }; 105 | AddBlueprint(result); 106 | init?.Invoke(result); 107 | return result; 108 | } 109 | 110 | public static LocalizedString CreateString(string key, string value) { 111 | // See if we used the text previously. 112 | // (It's common for many features to use the same localized text. 113 | // In that case, we reuse the old entry instead of making a new one.) 114 | if (textToLocalizedString.TryGetValue(value, out LocalizedString localized)) { 115 | return localized; 116 | } 117 | var current = LocalizationManager.CurrentPack?.GetText(key, false); 118 | if (current != "" && current != value) { 119 | Main.LogDebug($"Info: duplicate localized string `{key}`, different text."); 120 | } 121 | LocalizationManager.CurrentPack?.PutString(key, value); 122 | localized = new LocalizedString { 123 | m_ShouldProcess = false, 124 | m_Key = key 125 | }; 126 | textToLocalizedString[value] = localized; 127 | return localized; 128 | } 129 | 130 | private static T? TryGetBlueprint(BlueprintGuid id) where T : SimpleBlueprint { 131 | var asset = ResourcesLibrary.TryGetBlueprint(id); 132 | var value = asset as T; 133 | if (value == null) { Main.Log($"COULD NOT LOAD: {id} - {typeof(T)}"); } 134 | return value; 135 | } 136 | 137 | public static T? TryGetBlueprint(string id) where T : SimpleBlueprint { 138 | var assetId = new BlueprintGuid(Guid.Parse(id)); 139 | return TryGetBlueprint(assetId); 140 | } 141 | 142 | public static T? TryGetModBlueprint(string name) where T : SimpleBlueprint { 143 | var assetId = Main.ModSettings.Blueprints.GetGuiD(name); 144 | ModBlueprints.TryGetValue(assetId, out var value); 145 | return value as T; 146 | } 147 | } 148 | } -------------------------------------------------------------------------------- /SpellbookMerge/SpellbookMerge.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net472 4 | 9 5 | enable 6 | 7 | 8 | 9 | true 10 | 11 | 12 | 13 | true 14 | 15 | 16 | 17 | 18 | $(WrathPath)\Wrath_Data\Managed\UnityModManager\0Harmony.dll 19 | False 20 | 21 | 22 | ..\lib\Assembly-CSharp_public.dll 23 | False 24 | 25 | 26 | $(WrathPath)\Wrath_Data\Managed\Newtonsoft.Json.dll 27 | False 28 | 29 | 30 | $(WrathPath)\Wrath_Data\Managed\Owlcat.Runtime.Core.dll 31 | False 32 | 33 | 34 | $(WrathPath)\Wrath_Data\Managed\UnityEngine.CoreModule.dll 35 | False 36 | 37 | 38 | $(WrathPath)\Wrath_Data\Managed\UnityModManager\UnityModManager.dll 39 | False 40 | 41 | 42 | 43 | 44 | 45 | PreserveNewest 46 | 47 | 48 | 49 | 50 | 51 | all 52 | build; native; contentfiles; analyzers; buildtransitive 53 | 54 | 55 | all 56 | build; native; contentfiles; analyzers; buildtransitive 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /SpellbookMerge/Utilities/DescriptionTools.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | 5 | 6 | namespace SpellbookMerge.Utilities 7 | { 8 | internal static class DescriptionTools { 9 | private static readonly EncyclopediaEntry[] EncyclopediaEntries = new EncyclopediaEntry[] { 10 | new() 11 | { 12 | Entry = "Spells", 13 | Patterns = { "Spells?" } 14 | }, 15 | }; 16 | 17 | public static string TagEncyclopediaEntries(string description) { 18 | var result = description; 19 | result = result.StripHTML(); 20 | return EncyclopediaEntries.Aggregate(result, 21 | (current1, entry) => 22 | entry.Patterns.Aggregate(current1, (current, pattern) => current.ApplyTags(pattern, entry))); 23 | } 24 | 25 | private class EncyclopediaEntry { 26 | public string Entry = ""; 27 | public List Patterns = new List(); 28 | 29 | public string Tag(string keyword) { 30 | return $"{{g|Encyclopedia:{Entry}}}{keyword}{{/g}}"; 31 | } 32 | } 33 | 34 | private static string ApplyTags(this string str, string from, EncyclopediaEntry entry) { 35 | var pattern = from.EnforceSolo().ExcludeTagged(); 36 | var matches = Regex.Matches(str, pattern, RegexOptions.IgnoreCase) 37 | .OfType() 38 | .Select(m => m.Value) 39 | .Distinct(); 40 | return matches.Aggregate(str, 41 | (current, match) => Regex.Replace(current, Regex.Escape(match).EnforceSolo().ExcludeTagged(), 42 | entry.Tag(match), RegexOptions.IgnoreCase)); 43 | } 44 | private static string StripHTML(this string str) { 45 | return Regex.Replace(str, "<.*?>", string.Empty); 46 | } 47 | private static string ExcludeTagged(this string str) { 48 | return $"{@"(?]+)"}{str}{@"(?![^\s\.,""'<)]+)"}"; 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /SpellbookMergeTest/ModSettingsTest.cs: -------------------------------------------------------------------------------- 1 | using SpellbookMerge.Config; 2 | using Xunit; 3 | 4 | namespace SpellbookMergeTest 5 | { 6 | public class ModSettingsTest 7 | { 8 | [Fact] 9 | public void BlueprintsTest() 10 | { 11 | var blueprints = Blueprints.FromEmbeddedResource(); 12 | Assert.NotNull(blueprints); 13 | Assert.NotEmpty(blueprints.NewBlueprints); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /SpellbookMergeTest/SpellbookMergeTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net472 5 | 6 | false 7 | 8 | enable 9 | 10 | 9 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | D:\SteamLibrary\steamapps\common\Pathfinder Second Adventure\Wrath_Data\Managed\Assembly-CSharp.dll 33 | 34 | 35 | 36 | 37 | --------------------------------------------------------------------------------