Modding Conversations - research

Users who are viewing this thread

I am trying to get a handle on the BL conversations engine - how it works and how to mod it. The following is what I am piecing together as I go down the rabbit hole. If anyone has input or sees me get anything wrong, please let me know - I'll update this first post with new info.

XML Files

Firstly, NPC conversations are stored as a bunch of conversations in the [ModuleName]/ModuleData/comment_strings.xml file. They look a little something like this:

XML:
    <string id="str_comment_noble_introduces_self_and_clan.empire" text="{=q5nQGimE}I am {CONVERSATION_CHARACTER.LINK}, of the house of {CLAN_NAME}. Though one should not be too proud of one's lineage, I am glad to say that we have always taken seriously our duty to protect the common folk of the Empire.">
      <tags>
        <tag tag_name="EmpireTag" />
        <tag tag_name="MercyTag" weight="1" />
      </tags>
    </string>

There's a couple of things going on here.

Sentence ids

The id property is used to identify the use of the conversation sentence. These id's are registered in the code and are used to pick which sentence you display

C#:
private bool conversation_lord_introduction_on_condition()

...

string id;

if (Hero.OneToOneConversationHero.Clan.IsMinorFaction)
{
    id = "str_comment_minor_faction_leader_introduces_self";
}

else if (!Hero.OneToOneConversationHero.Clan.IsMapFaction && Hero.OneToOneConversationHero.MapFaction.Leader == Hero.OneToOneConversationHero)
{
    id = "str_comment_liege_introduces_self";
}

else if (!Hero.OneToOneConversationHero.Clan.IsMapFaction && Hero.OneToOneConversationHero.MapFaction.Culture == Hero.OneToOneConversationHero.CharacterObject.Culture && Hero.OneToOneConversationHero.Clan.Renown >= 200f)
{
    id = "str_comment_noble_introduces_self_and_clan";
}

else
{
    id = "str_comment_noble_introduces_self";
}

TextObject textObject = Campaign.Current.ConversationManager.FindMatchingTextOrNull(id, CharacterObject.OneToOneConversationCharacter);

In the above method, the characters involved in the conversation are queried in order to figure out which string to select from the xml file. The id is selected and then passed to Campaign.Current.ConversationManager.FindMatchingTextOrNull which kind of wraps Game.Current.GameTextManager.GetGameText, with some caveats for randomly matching sentence variations.

It seems like TextObject and GameText are at the nuts-and-bolts level of the system, with the ConversationManager and CampaignGameStarter classes managing the logic of selection.

Localization

You can see the weird string at the start of the text property ({=q5nQGimE}). That is used for localization. When you are adding new languages to your conversations, the engine tries to match that id string up with any string in the Modules/[ModuleName]/ModuleData/Languages directory. The localization system will try to match the strings by checking your localization settings and matching the folder/filename/idstring of the given conversation sentence. Note, the engine will prepend std_ onto the filename when looking for matching files. If you have a look in the Modules/Native/ModuleData/Languages folder you'll see the pattern.

Sentence Variables

Moving on, you can also see in the xml that we have access to properties of the conversation itself via {BRACED_CAPITAL_UNDERSCORE} template variables: text="{=q5nQGimE}I am [B]{CONVERSATION_CHARACTER.LINK}[/B], of the house of [B]{CLAN_NAME}[/B]. Though one should not be too proud of one's lineage, I am glad to say that we have always taken seriously our duty to protect the common folk of the Empire." .

In order to inject these variables, you need to use TextObject.SetTextVariable(string TEMPLATE_VARIABLE_NAME, string toPrint) , as you can see here:

C#:
                    TextObject textObject = new TextObject("{=driH06vI}I need more recruits in {SETTLEMENT}'s garrison. Since I'll be elsewhere... maybe you can recruit {NUMBER_OF_TROOP_TO_BE_RECRUITED} {TROOP_TYPE} and bring them to the garrison for me?", null);
                    textObject.SetTextVariable("SETTLEMENT", this._settlement.Name);
                    textObject.SetTextVariable("TROOP_TYPE", this.NeededTroopType.Name);
                    textObject.SetTextVariable("NUMBER_OF_TROOP_TO_BE_RECRUITED", this.NumberOfTroopToBeRecruited);
                    return textObject;

There may also be variables that are globally available...

Tags (or, how to say the same thing in different ways)

Next up, you can see that the conversation xml node has a <tag> node. These tags tie directly to C# classes, which act as kind of an extended enum with a predicate that the engine uses to decide on what to say next. This is what the engine uses to pick, say, an enemy troops response text instead of a friendly troops response text. Or a greeting from a lord that hates you vs a lord you haven't met yet. Let's dig into this.

In C#, all tags inherit from TaleWorlds.CampaignSystem.Conversation.Tags.ConversationTag. That is an abstract class, so we can likely add our own tags by extending it properly. This base class only has two functions we need to implement:

C#:
        public abstract string StringId { get; }
        public abstract bool IsApplicableTo(CharacterObject character);

The StringId function simply defines a getter, the value of which is used to match the tag_name attribute in the xml file, while the IsApplicableTo allows us to run a function on the passed CharacterObject and return a bool. This is how the conversation system decides if a given tag is valid. NOTE: the CharacterObject passed to the IsApplicableTo function is the character saying the line. You can, however, query the current player by using Hero.MainHero.

Using the same xml as posted at the top, the <tag tag_name="EmpireTag" /> is linked to the class TaleWorlds.CampaignSystem.Conversation.Tags.EmpireTag. Here is the content of that c# class:

C#:
using System;

namespace TaleWorlds.CampaignSystem.Conversation.Tags
{

    public class EmpireTag : ConversationTag
    {

        public override string StringId
        {
            get
            {
                return "EmpireTag";
            }
        }

        public override bool IsApplicableTo(CharacterObject character)
        {
            return character.Culture.StringId == "empire";
        }
    }
}

And that xml again, for the whole picture:

XML:
    <string id="str_comment_noble_introduces_self_and_clan.empire" text="{=q5nQGimE}I am {CONVERSATION_CHARACTER.LINK}, of the house of {CLAN_NAME}. Though one should not be too proud of one's lineage, I am glad to say that we have always taken seriously our duty to protect the common folk of the Empire.">
      <tags>
        <tag tag_name="EmpireTag" />
        <tag tag_name="MercyTag" weight="1" />
      </tags>
    </string>

So looking at this, we have a code-level way to provide the conversation system with a simple query that will give our conversations some variety.
 
Last edited:
Hey there, good post thanks. I've spent some time getting my ahead around the basics (just adding tavern dialogues for new wanderers), I'll share what I've learned.

There's a couple of things going on here. I'm not sure exactly what the id property does as I haven't been able to find much reference to it in the code.

The ids are used to pick the right string to use when there are conditions to take account of, eg the one you mention
(str_comment_noble_introduces_self_and_clan) can be seen in Sandbox.LordConversationCampaignBehavior:

C#:
private bool conversation_lord_introduction_on_condition() 

...

string id;

if (Hero.OneToOneConversationHero.Clan.IsMinorFaction)
{
id = "str_comment_minor_faction_leader_introduces_self";
}

else if (!Hero.OneToOneConversationHero.Clan.IsMapFaction && Hero.OneToOneConversationHero.MapFaction.Leader == Hero.OneToOneConversationHero)
{
id = "str_comment_liege_introduces_self";
}

else if (!Hero.OneToOneConversationHero.Clan.IsMapFaction && Hero.OneToOneConversationHero.MapFaction.Culture == Hero.OneToOneConversationHero.CharacterObject.Culture && Hero.OneToOneConversationHero.Clan.Renown >= 200f)
{
id = "str_comment_noble_introduces_self_and_clan";
}

else
{
id = "str_comment_noble_introduces_self";
}

TextObject textObject = Campaign.Current.ConversationManager.FindMatchingTextOrNull(id,             CharacterObject.OneToOneConversationCharacter);

Meanwhile these things - {=q5nQGimE} are localization keys (that might not be the right term) - if the game sees one at the start of a string while it's running in a localized mode, it will ignore whatever else is in that string and instead look in the Languages folder to try and match the key for a string in the right language.

Which is very relevant when copy/pasting from the vanilla xmls/decompiled dlls!! I made a mod that added a few new wanderers and left those keys in, not knowing what they were; the mod worked fine when running the game normally, but the moment you turn on a localization all my new wanderers vanish, which confused me to bits. Turns out, they were still technically out there, you just couldn't see them - their names had been changed back into the vanilla names from the templates I had started with.

Note that even if the specific localization you're running doesn't have any content for that particular key, the game will default to the 'std' localization, which is just all the English content repeated. Which is important - running the game normally (in english) and running the game localized (in english) means reading two different sets of strings. Compare Sandbox//ModuleData/companion_strings.xml with Sandbox/ModuleData/Languages/std_companion_strings_xml.xml. There are two sources of truth, which could potentially lead to some wtf moments.

Also I'll just plug my mod, pardon me. If I say it might be useful as a resource, I think I'll get away with it - Nexus link. It might be useful as a resource.
 
Very interesting. Thanks for the research and detailed explanation!

Curious that the English version of the text wasn’t simply used as the unique key for the other localizations.

Using two versions of the English text is taking on unnecessary risk too. That’s a recipe for mismatch.

How do you generate the keys (i.e. {hUrFfhji} ) for your mod text? Are there tools within code to prevent or identify collisions?
 
I really haven't been able to link the ids in the xml files to the ids in the decompiled c# code. I'm thinking it gets parsed and generated by a tool somewhere. I would really like to know if the ids in my xml are the same as those in everyone elses. Could you check for the xml entry I posted above in your xml files and confirm? Maybe these get generated on install or smth...

Edit: just saw mr_mouflons reply. Good info thanks! Does this mean that, when adding new dialogue, you need to copy+paste it into the Languages folders std_blah.xml file, as well as your original xml?

Also, I am trying in particular to add a new question to ask village npcs, but I can't find the place that links the npc to the conversation question. Ideas? I'll take a look at that mod as well, thanks!
 
Last edited:
Also, I am trying in particular to add a new question to ask village npcs, but I can't find the place that links the npc to the conversation question. Ideas? I'll take a look at that mod as well, thanks!

I’m locked down without access to steam and haven’t seen the code base, but it appears tale used an agent system with behaviors. Search for entity classes for village npc agents and then trace the associated behaviors. For question and response, look into the events I’ve seen in the behavior classes. It could also be strongly dependent on the model-view-viewmodel framework too. Those are all wild ass guesses.
 
Does this mean that, when adding new dialogue, you need to copy+paste it into the Languages folders std_blah.xml file, as well as your original xml?

Unless you're planning to translate your mod to multiple languages, I think the best thing to do is just remove the localization key entirely. Then the game has no choices to make and simply returns the string it is given, whether the user is running a localization or not.
 
This is really helpful information. I had been wondering what all the {} stuff was and it's good to finally understand that a bit better. Has anyone managed to create a custom recruitment conversation for their companions yet? I have a working companion example but the recruitment conversation gives a bunch of error text about a missing backstory.

My conversation XML looks like this:
Code:
<?xml version="1.0" encoding="utf-8"?>
<base xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" type="string">
<!-- Original file copied from: ...\Modules\SandBox\ModuleData\wanderer_strings.xml -->
<!-- File renamed to try and avoid overwrite issues. -->

  <strings>
    <!-- Example of a custom wanderer recruitment conversation. -->
    <!-- This conversation is required for wanderer id="CompanionExample" -->

    <string id="prebackstory.CompanionExample" text="You want to know about me?" />
    <string id="backstory_a.CompanionExample" text="I'm an example of a custom wanderer." />
    <string id="backstory_b.CompanionExample" text="This conversation is just a template." />
    <string id="backstory_c.CompanionExample" text="It was created to help with mod development." />
    <string id="response_1.CompanionExample" text="Interesting, this is response 1." />
    <string id="response_2.CompanionExample" text="Hmm, this is response 2." />
    <string id="backstory_d.CompanionExample" text="Yes, well. It appears this example works." />

  </strings>
</base>
So far as I understand it I need to define my companion, the companion's recruitment conversation, and reference both the the SubModule.xml, all of which I have done. But there must be something more of the XML above must be wrong. Does anyone in this thread have any thoughts on what I've missed?
 
I'm getting quite close here.

I decompiled the 'Ask Lords Location' mod on nexus: https://www.nexusmods.com/mountandblade2bannerlord/mods/260 and had a look how they were loading in their questions. Here's the meat of it:

C#:
        public void OnSessionLaunched(CampaignGameStarter campaignGameStarter)
        {
            campaignGameStarter.AddPlayerLine("Find_Lord_P_0", "lord_talk_ask_something_2", "Find_Lord_D_0", "{=b0m2DxeG}Could you tell me where the nobles of the Kingdom are?", new ConversationSentence.OnConditionDelegate(this.conversation_player_has_question_on_condition), new ConversationSentence.OnConsequenceDelegate(this.OnFindOptionClicked), 101, null, null);
            campaignGameStarter.AddDialogLine("Find_Lord_D_0", "Find_Lord_D_0", "lord_pretalk", "{=CX6JHwbB}I'll have my scribe write down their whereabouts for you.", null, null, 100, null);
        }

The 'AddPlayerLine' method registers a new line that the player can say at some point in the game. Here is the method signature:

C#:
public ConversationSentence AddPlayerLine(
    string id,
    string inputToken,
    string outputToken,
    string text,
    ConversationSentence.OnConditionDelegate conditionDelegate,
    ConversationSentence.OnConsequenceDelegate consequenceDelegate,
    int priority = 100,
    ConversationSentence.OnClickableConditionDelegate clickableConditionDelegate = null,
    ConversationSentence.OnPersuasionOptionDelegate persuasionOptionDelegate = null
)

The 'id' links the dialog line to an xml entry (I think) and it needs to be provided even if you aren't loading xml files (you can write the strings straight into the code).

I think 'inputToken' is used to define the state that allows the line to be displayed. Looking through the Sandbox modules xml I can see a bunch of input tokens that relate to gameplay state:
Code:
member_intelgathering_3
merchant_quest_4b2
player_siege_ask_surrender

The 'outputToken' is similar, but it defines the state that will be assumed once the sentence has been passed. An outputToken becomes the inputToken for the next sentence in the conversation.

'text' is simply the string that will be displayed. Internally this is passed to TaleWorlds TextObject class that handles a bunch of gameplay-related stuff (and is a very important piece in this puzzle)

'onConditionDelegate' is a reference to a function that returns a bool depending on gameplay state. This is used to mask the given sentence depending on some condition. So the inputToken could be referencing a conversation with a bandit, but the onCondition could be that the player has a certain amount of criminal score, therefore the bandit will say the sentence.

'onConsequenceDelegate' is a method that runs when the player clicks the sentence to say it during the conversation. This is often where you would start a quest or have a lords relationship change, or any other action that should happen in the game once that option has been clicked.

I'm not sure what the priority is, I assume it's used to change the order that the option is shown - maybe a higher priority is shown at the top of the list, for example.

I'm not sure what 'clickableConditionDelegate' does exactly but it also takes a function.

I'm thinking that 'persuasionOptionDelegate' is to do with the persuasion system, such as when you are asking to court a lord of the opposite gender.

So the main issue I am having right now is finding a good list of the inputTokens so that I can start to place my conversations in the correct locations. The 'Ask Lord Location' mod uses the inputToken 'lord_talk_ask_something_2' to place a question under the 'I have a quick question' section when talking to lords. Trawling through the code I have found all kinds of input tokens (I believe they are the same as the "istate" property in conversation xml nodes) but I have only been able to get a handful to work.
 
Last edited:
@RapidFire thanks for that, the mod does work and examining it reveals the conversation text is loaded in the DLL OnGameStart event handler. I had been trying to do this over the weekend, but the game kept crashing. Examining the difference between my conversations and the mod author reveals that <!-- comments --> outside the <strings> tags will crash the loader.

Example 1: My old conversation XML that crashed when loading:
Code:
<?xml version="1.0" encoding="utf-8"?>
<base xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" type="string">
<!-- Original file copied from: ...\Modules\SandBox\ModuleData\wanderer_strings.xml -->
<!-- File renamed to try and avoid overwrite issues. -->

  <strings>
    <!-- Example of a custom wanderer recruitment conversation. -->
    <!-- This conversation is required for wanderer id="CompanionExample" -->

    <string id="prebackstory.CompanionExample" text="{=jPGOlC7O}You want to know about me?" />
    <string id="backstory_a.CompanionExample" text="{=3yK4uXyt}I'm an example of a custom wanderer." />
    <string id="backstory_b.CompanionExample" text="{=OYOwasMe}This conversation is just a template." />
    <string id="backstory_c.CompanionExample" text="{=JfTTbPag}It was created to help with mod development." />
    <string id="response_1.CompanionExample" text="{=R36Bl55h}Interesting, this is response 1." />
    <string id="response_2.CompanionExample" text="{=oXnqNsvz}Hmm, this is response 2." />
    <string id="backstory_d.CompanionExample" text="{=b1sCl4SI}Yes, well. It appears this example works." />

  </strings>
</base>

Example 2: My new conversation XML that works when loaded in OnGameStart:
Code:
<?xml version="1.0" encoding="utf-8"?>
<base xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" type="string">
    <strings>
    <!-- Original example copied from: ...\Modules\SandBox\ModuleData\companion_strings.xml -->
    <!-- File renamed to avoid overwrite issues. -->
    
    <!-- Example of a custom wanderer recruitment conversation. -->
    <!-- This conversation is required for wanderer id="CompanionExample" -->
        <string id="prebackstory.CompanionExample" text="You want to know about me?" />
        <string id="backstory_a.CompanionExample" text="I'm an example of a custom wanderer." />
        <string id="backstory_b.CompanionExample" text="This conversation is just a template." />
        <string id="backstory_c.CompanionExample" text="It was created to help with mod development." />
        <string id="response_1.CompanionExample" text="Interesting, this is response 1." />
        <string id="response_2.CompanionExample" text="Hmm, this is response 2." />
        <string id="backstory_d.CompanionExample" text="Yes, well. It appears this example works." />
        
    </strings>
</base>

So it seems the SubModule.xml file is still very much in early access and won't work to load all our XMLs, but at least it's easy to load our own. The code for which is:
Code:
        protected override void OnGameStart(Game game, IGameStarter gameStarter)
        {
            // Called 1st after choosing (Resume Game, Campaign, Custom Battle) from the main menu.
            // The game does not load strings from XML, so for now custom conversation text must be loaded manually.
            try
            {
                CampaignGameStarter campaignGameStarter;
                if (game.GameType is Campaign)
                {
                    campaignGameStarter = (gameStarter as CampaignGameStarter);
                    campaignGameStarter.LoadGameTexts(BasePath.Name + "Modules/YourModName/ModuleData/YourCustomStrings.xml");
                    InformationManager.DisplayMessage(new InformationMessage("XML Loaded."));
                }
            }
            catch (Exception e)
            {
                InformationManager.DisplayMessage(new InformationMessage("XML failed to load:" + e.Message));
            }
        }
 
Great find! And good to see you catching exceptions - these silent crashes are killing me.

There is also a "LoadConversations" method that appears to load the xml file as well as some extra hooking-up of onConditions and conversationCallbacks at the same time. Take a look at TaleWorlds.CampaignSystem ConversationManager.LoadConversations(). You can see it being called in TaleWorlds.CampaignSystem.SandBox SandBoxManager.AddDialogs(). I think this is a fancier way of loading in the conversations as opposed to creating them in the code, but I'm not sure what advantages it offers (if it's cached or something, or just designed so you can modify the xml without having to recompile the code)

I'm starting to see a general pattern of how these modules are being loaded in and a general event cycle that we should be able to hook into. I think I'll do a write-up on this stuff sometime soon as it extends past just conversations and to the general module setup, and I haven't found a resource that explains it really well yet.

Also, I've come full circle to the ConversationSentence class, which really is the bones of this system. It turns out that those AddDialogLine methods really just expect an instance of the ConversationSentence class - you can even construct a ConversationSentence and then pass it straight to the AddDialogLine method, as that method has an overload for that. It's probably a good idea to dig into the ConversationSentence class to get an idea of its capabilities - there's stuff in here about sentence variations and it also references the system that handles those inputTokens from earlier.

I'm definitely getting somewhere though! I just made a tiny mod that allows me to insult my barkeep ?
 
I just made a tiny mod that allows me to insult my barkeep

Brilliant! Now make it so we can pick up tavern wenches and steal horses in town. Ye Olde GTA style. Thanks again for nudging me in the right direction. Next stop for me: physics materials, collisions, and particle systems. I want a horse that makes sparks when its hooves hit the ground, and to be the only person in Calradia whose fists can explode enemies.?
 
Is it possible to add dialogue to a specific lord instead of all of them?
Yes. Each conversation line has an "onConditionDelegate" which is a function that returns a bool which outlines when to show the line. You can use that delegate to query Hero.oneToOneConversationHero to decide if the lord you are talking to is the one you want.

Note that if you're talking to a non-hero character (ie a townsperson) then the above variable will return NULL so make sure to check for that before performing operations on it.
 
@RapidFire thanks for that, the mod does work and examining it reveals the conversation text is loaded in the DLL OnGameStart event handler. I had been trying to do this over the weekend, but the game kept crashing. Examining the difference between my conversations and the mod author reveals that <!-- comments --> outside the <strings> tags will crash the loader.

Example 1: My old conversation XML that crashed when loading:
Code:
<?xml version="1.0" encoding="utf-8"?>
<base xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" type="string">
<!-- Original file copied from: ...\Modules\SandBox\ModuleData\wanderer_strings.xml -->
<!-- File renamed to try and avoid overwrite issues. -->

  <strings>
    <!-- Example of a custom wanderer recruitment conversation. -->
    <!-- This conversation is required for wanderer id="CompanionExample" -->

    <string id="prebackstory.CompanionExample" text="{=jPGOlC7O}You want to know about me?" />
    <string id="backstory_a.CompanionExample" text="{=3yK4uXyt}I'm an example of a custom wanderer." />
    <string id="backstory_b.CompanionExample" text="{=OYOwasMe}This conversation is just a template." />
    <string id="backstory_c.CompanionExample" text="{=JfTTbPag}It was created to help with mod development." />
    <string id="response_1.CompanionExample" text="{=R36Bl55h}Interesting, this is response 1." />
    <string id="response_2.CompanionExample" text="{=oXnqNsvz}Hmm, this is response 2." />
    <string id="backstory_d.CompanionExample" text="{=b1sCl4SI}Yes, well. It appears this example works." />

  </strings>
</base>

Example 2: My new conversation XML that works when loaded in OnGameStart:
Code:
<?xml version="1.0" encoding="utf-8"?>
<base xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" type="string">
    <strings>
    <!-- Original example copied from: ...\Modules\SandBox\ModuleData\companion_strings.xml -->
    <!-- File renamed to avoid overwrite issues. -->
   
    <!-- Example of a custom wanderer recruitment conversation. -->
    <!-- This conversation is required for wanderer id="CompanionExample" -->
        <string id="prebackstory.CompanionExample" text="You want to know about me?" />
        <string id="backstory_a.CompanionExample" text="I'm an example of a custom wanderer." />
        <string id="backstory_b.CompanionExample" text="This conversation is just a template." />
        <string id="backstory_c.CompanionExample" text="It was created to help with mod development." />
        <string id="response_1.CompanionExample" text="Interesting, this is response 1." />
        <string id="response_2.CompanionExample" text="Hmm, this is response 2." />
        <string id="backstory_d.CompanionExample" text="Yes, well. It appears this example works." />
       
    </strings>
</base>

So it seems the SubModule.xml file is still very much in early access and won't work to load all our XMLs, but at least it's easy to load our own. The code for which is:
Code:
        protected override void OnGameStart(Game game, IGameStarter gameStarter)
        {
            // Called 1st after choosing (Resume Game, Campaign, Custom Battle) from the main menu.
            // The game does not load strings from XML, so for now custom conversation text must be loaded manually.
            try
            {
                CampaignGameStarter campaignGameStarter;
                if (game.GameType is Campaign)
                {
                    campaignGameStarter = (gameStarter as CampaignGameStarter);
                    campaignGameStarter.LoadGameTexts(BasePath.Name + "Modules/YourModName/ModuleData/YourCustomStrings.xml");
                    InformationManager.DisplayMessage(new InformationMessage("XML Loaded."));
                }
            }
            catch (Exception e)
            {
                InformationManager.DisplayMessage(new InformationMessage("XML failed to load:" + e.Message));
            }
        }
Wait so where would I put this in the actual Submodule?
 
@KingBas20 the game will not load any companion recruitment conversations referenced in the SubModule.xml file. You must create a DLL mod and load the XML file containing your custom conversation text during the OnGameStart event in your C# code.

For my mod, I have my companions defined within:
"Mount & Blade II Bannerlord\Modules\ModuleName\ModuleData\cust_companions.xml"

My companion recruitment conversations are defined within:
"Mount & Blade II Bannerlord\Modules\ModuleName\ModuleData\cust_conversations.xml"

Both files are referenced within:
"Mount & Blade II Bannerlord\Modules\ModuleName\SubModule.xml"

But the game only loads "cust_companions.xml" and ignores "cust_conversations.xml" resulting in an error message about missing text when talking to a companion in the tavern. To fix this I load "cust_conversations.xml" using CampaignGameStarter.LoadGameTexts() during the OnGameStart() event in my module DLLs main class which inherits from MBSubModuleBase.

If you follow the tutorial here this will show how to create a DLL and correctly reference it within SubModule.xml. At step 3 of the Programming section of that tutorial, you will have a main class called "MySubModule" and the tutorial will ask you to override the OnSubModuleLoad() method. At this point you can ignore the rest of the tutorial and instead paste this code into your main class:
C#:
        protected override void OnGameStart(Game game, IGameStarter gameStarter)
        {
            // Called 1st after choosing (Resume Game, Campaign, Custom Battle) from the main menu.
            // The game does not load strings from XML, so for now custom conversation text must be loaded manually.
            try
            {
                CampaignGameStarter campaignGameStarter;
                if (game.GameType is Campaign)
                {
                    campaignGameStarter = (gameStarter as CampaignGameStarter);
                    campaignGameStarter.LoadGameTexts(BasePath.Name + "Modules/YourModName/ModuleData/YourCustomStrings.xml");
                    InformationManager.DisplayMessage(new InformationMessage("XML Loaded."));
                }
            }
            catch (Exception e)
            {
                InformationManager.DisplayMessage(new InformationMessage("XML failed to load:" + e.Message));
            }
        }

Make sure to update "YourCustomStrings" to whatever you call the file with your recruitment conversation text.
 
Very interesting. Thanks for the research and detailed explanation!

Curious that the English version of the text wasn’t simply used as the unique key for the other localizations.

Using two versions of the English text is taking on unnecessary risk too. That’s a recipe for mismatch.

How do you generate the keys (i.e. {hUrFfhji} ) for your mod text? Are there tools within code to prevent or identify collisions?

{=*} will dynamically create a unique ID so you don't have to come up with your own ID
 
Back
Top Bottom