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
There's a couple of things going on here.
Sentence ids
The
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
It seems like
Localization
You can see the weird string at the start of the
Sentence Variables
Moving on, you can also see in the xml that we have access to properties of the conversation itself via
In order to inject these variables, you need to use
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
In C#, all tags inherit from
The
Using the same xml as posted at the top, the
And that xml again, for the whole picture:
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.
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: