then
Open the folder (single click in the tree-view on left, or double click on the folder in asset view in middle):
NOTE - Almost everything we do follows the same pattern => RightClick -> Create -> Something, name it, then edit it.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PortraitInjector
{
[OwlcatModificationEnterPoint]
public static void ModEntryPoint() {
}
}
Add more code to PortraitInjector.cs
- you can either delete the current lines then copy/paste all the text below, or add just the new lines:
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using Kingmaker.Blueprints.JsonSystem;
using Kingmaker.Modding;
using UnityEngine;
public class Main
{
public static OwlcatModification ModDetails;
public static Harmony HarmonyHandle;
public static void Log(string message) => ModDetails.Logger.Log(message);
public static void Log(string fmt, params object[] args) => ModDetails.Logger.Log(fmt, args);
[OwlcatModificationEnterPoint]
public static void ModEntryPoint(OwlcatModification modDetails) {
// Save the details so we can access them later
ModDetails = modDetails;
// Create a new harmony instance with our unique name
HarmonyHandle = new Harmony(modDetails.Manifest.UniqueName);
// Ask harmony to find all [HarmonyPatch] methods in this assembly and apply them
HarmonyHandle.PatchAll(Assembly.GetExecutingAssembly());
}
}
[HarmonyPatch(typeof(BlueprintsCache), nameof(BlueprintsCache.Init))]
public static class AddBlueprintsInCode {
[HarmonyPostfix]
public static void Init() {
Main.Log("Adding portraits");
}
}
public ReturnCode Run()
{
var msBuildPath = @"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe";
string projectPath = Path.Combine(
Directory.GetCurrentDirectory(),
"BubbleLesson0_Portraits.csproj" //THIS MUST MATCH YOUR CSPROJ FILE
);
var settings = m_BuildParameters.GetScriptCompilationSettings();
string outputFolder = m_BuildParameters.GetOutputFilePathForIdentifier(BuilderConsts.OutputAssemblies);
if (File.Exists(msBuildPath) && File.Exists(projectPath))
{
ProcessStartInfo msBuildInfo =
new ProcessStartInfo(msBuildPath, $"{projectPath} /p:OutputPath={outputFolder}");
var msBuild = Process.Start(msBuildInfo);
msBuild.WaitForExit();
}
else {
var results = PlayerBuildInterface.CompilePlayerScripts(settings, outputFolder);
if (results.assemblies == null || !results.assemblies.Any())
{
return ReturnCode.Error;
}
}
string[] asmdefGuids = AssetDatabase.FindAssets("t:Asmdef", new[] {m_ModificationParameters.ScriptsPath});
string[] asmdefNames = asmdefGuids
.Select(AssetDatabase.GUIDToAssetPath)
.Select(AssetDatabase.LoadAssetAtPath<AssemblyDefinitionAsset>)
.Select(GetAssemblyName)
.ToArray();
foreach (string filePath in Directory.GetFiles(outputFolder))
{
if (asmdefNames.All(i => !filePath.Contains(i)))
{
File.Delete(filePath);
}
}
return ReturnCode.Success;
}
We also need to make sure any stuff we add will be bundled even without a reference, edit Assets/Editor/Build/Tasks/PrepareBundles.cs
:
string assetPath = AssetDatabase.GUIDToAssetPath(assetGuid);
string bundleName = m_LayoutManager.GetBundleForAssetPath(assetPath, m_ModificationParameters.TargetFolderName);
if (bundleName == null)
{
continue;
}
if (!m_Tracker.UpdateInfo(assetPath))
{
return ReturnCode.Canceled;
}
string assetPath = AssetDatabase.GUIDToAssetPath(assetGuid);
string bundleName = m_LayoutManager.GetBundleForAssetPath(assetPath, m_ModificationParameters.TargetFolderName);
if (bundleName == null || bundleName.EndsWith("_content"))
{
bundleName = "bubblelessons_all";
}
if (!m_Tracker.UpdateInfo(assetPath))
{
return ReturnCode.Canceled;
}
- Copy the
Build/BubbleTeaches_Lesson0Portraits
folder from theBuilder
folder touser-folder/AppData/LocalLow/Owlcat Games/Pathfinder Wrath Of The Righteous/Modifications
- Add your modification to user-folder/AppData/LocalLow/Owlcat Games/Pathfinder Wrath Of The Righteous/OwlcatModificationManagerSettings.json
{
"EnabledModifications": ["BubbleTeaches_Lesson0Portraits"]
}
- Run Pathfinder: Wrath of the Righteous
Open the log file: user-folder/AppData/LocalLow/Owlcat Games/Pathfinder Wrath Of The Righteous/GameLogFull.txt
, search for Bubble
, you should see:
[55.3343 - Mods]: Load modifications' descriptors: C:/Users/worce/AppData/LocalLow/Owlcat Games/Pathfinder Wrath Of The Righteous\Modifications
[55.3413 - Mods]: Apply modification: BubbleTeaches_Lesson0Portraits (C:/Users/worce/AppData/LocalLow/Owlcat Games/Pathfinder Wrath Of The Righteous\Modifications\BubbleTeaches_Lesson0Portraits)
[55.3443 - Mods]: Load assembly: C:/Users/worce/AppData/LocalLow/Owlcat Games/Pathfinder Wrath Of The Righteous\Modifications\BubbleTeaches_Lesson0Portraits\Assemblies\BubbleLesson0_Portraits.dll
[55.3463 - Mods]: Bundle found: C:/Users/worce/AppData/LocalLow/Owlcat Games/Pathfinder Wrath Of The Righteous\Modifications\BubbleTeaches_Lesson0Portraits\Bundles\BubbleTeaches_Lesson0Portraits_BlueprintDirectReferences
[55.3483 - Mods]: Initialize assembly: BubbleLesson0_Portraits, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
[55.4953 - BubbleTeaches_Lesson0Portraits]: Adding portraits
hashtag-success
We need a "GUID" for the blueprint, this is just a (very) unique name to make sure if someone else adds a blueprint we don't clash.
There are a bunch of ways to generate GUIDs, for the moment just go to https://guidgenerator.com/online-guid-generator.aspx and click generate, you should get something like:
024261bd-c6e9-4e55-a521-0fe4eb4f8xyz
- NOTE: This is intentionally invalid so you don't copy it.
[HarmonyPatch(typeof(BlueprintsCache), nameof(BlueprintsCache.Init))]
public static class AddBlueprintsInCode {
[HarmonyPostfix]
public static void Init() {
Main.Log("Adding portraits");
var myPortrait = new BlueprintPortrait {
name = "mewsifer-portrait",
AssetGuid = BlueprintGuid.Parse("PUT-YOUR-GUID-HERE"),
Data = new PortraitData {
PortraitCategory = Kingmaker.Enums.PortraitCategory.Wrath,
m_FullLengthImage = new SpriteLink { AssetId = "" },
m_HalfLengthImage = new SpriteLink { AssetId = "" },
m_PortraitImage = new SpriteLink { AssetId = "" },
}
};
}
}
here is an example: mewsifer-portraits.zip
or you can generate your own using: http://www.notra.fr/portrait/pathfinder_wotr
Download the zip, open it, and copy the three images into the Content
folder of your mod:
Now we need some magic "AssetID", Owlcat has a right-click for this but it copies stuff we don't need, so let's open Assets\Editor\ToolsMenu.cs
[MenuItem("Assets/Modification Tools/Copy guid only", priority = 2)]
private static void CopyAssetGuidOnly()
{
var obj = Selection.activeObject;
if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out string guid, out long fileId))
{
GUIUtility.systemCopyBuffer = guid;
}
else
{
GUIUtility.systemCopyBuffer = $"Can't find guid for asset '{AssetDatabase.GetAssetPath(obj)}'";
}
}
Now right click on your first sprite get the Asset id, then paste it into our script, repeat for the other two sprites:
[HarmonyPatch(typeof(BlueprintsCache), nameof(BlueprintsCache.Init))]
public static class AddBlueprintsInCode {
[HarmonyPostfix]
public static void Init() {
Main.Log("Adding portraits");
var myPortrait = new BlueprintPortrait {
name = "mewsifer-portrait",
AssetGuid = BlueprintGuid.Parse("YOUR-GUID-HERE"),
Data = new PortraitData {
PortraitCategory = Kingmaker.Enums.PortraitCategory.Wrath,
m_FullLengthImage = new SpriteLink { AssetId = "Fulllength-ASSET-ID" },
m_HalfLengthImage = new SpriteLink { AssetId = "Medium-ASSET-ID" },
m_PortraitImage = new SpriteLink { AssetId = "Small-ASSET-ID" },
}
};
}
}
Now we need to add our blueprint to the library, and then make sure the game will select it in the right place.
Alas, we cannot. We can't touch owlcat's special privates (without blueprint patching which bleh).
Unless? 😳👉👈
There are two ways we can do this, the first is easier to start with but annoying if we want to use it often (reflection).
The second is to replace the Assets\PathfinderAssemblies\Assembly-CSharp.dll
with a publicized assembly. Use bing to find out how to publicize, but it's not hard.
If you publicize you need to check unsafe
code in the asmdef and will need to exit vscode/whatever C# editor you are using and re-open the script
HOWEVER: for the sake of this tutorial we are just gonna use reflection because it's slightly more foolproof:
[HarmonyPatch(typeof(BlueprintsCache), nameof(BlueprintsCache.Init))]
public static class AddBlueprintsInCode {
[HarmonyPostfix]
public static void Init() {
Main.Log("Adding portraits");
var myPortrait = new BlueprintPortrait {
name = "mewsifer-portrait",
AssetGuid = BlueprintGuid.Parse("YOUR-GUID-HERE"),
Data = new PortraitData {
PortraitCategory = Kingmaker.Enums.PortraitCategory.Wrath,
m_FullLengthImage = new SpriteLink { AssetId = "Fulllength-ASSET-ID" },
m_HalfLengthImage = new SpriteLink { AssetId = "Medium-ASSET-ID" },
m_PortraitImage = new SpriteLink { AssetId = "Small-ASSET-ID" },
}
};
ResourcesLibrary.BlueprintsCache.AddCachedBlueprint(myPortrait.AssetGuid, myPortrait);
var field = typeof(CharGenRoot).GetField("m_Portraits", BindingFlags.Instance | BindingFlags.NonPublic);
var charGen = BlueprintRoot.Instance.CharGen;
BlueprintPortraitReference[] portraitArray = (BlueprintPortraitReference[])field.GetValue(charGen);
portraitArray = portraitArray.AddToArray(myPortrait.ToReference<BlueprintPortraitReference>());
field.SetValue(charGen, portraitArray);
}
}
Build the mod, then copy the build folder to the install location (updating any current files), then run
(yes I need to hoover)