Skip to content

AI MobileParty

Mart Kartasev edited this page Aug 12, 2021 · 25 revisions

The system for MobileParty AI primarily revolves objects derived from CampaignBehaviorBase and PartyComponent.

Control of MobileParty behavior can be managed with either direct control commands in the PartyAI class or using utility system provided through the PartyThinkParams, accessible through the HourlyTickPartyAI campaign event. Either of the two should be used in conjunction with the ticking mechanisms available through CampaignEvents.

TODO: Add diagram overview

Campaign Behavior

All of the behaviors relating to a specific type of MobileParty should be stored as part of a single CampaignBehavior. This is not required from a technical standpoint, but it should be considered a VERY strict recommendation. Separating the code for controlling a behavior into a class makes it easier to manage the complexity of the internal behaviors. It also makes it easier to toggle the behavior as necessary.

Note that this can change depending on what sort of behavior we want to add. It is possible that we need behaviors that will augment the already existing system for ALL parties. In that case we should create a separate CampaignBehavior for a specific type of party behavior we wish add into the game.

Party Component

While the CampaignBehavior contains the logic for controlling the party, the PartyComponent contains data specific to that party type. Moreover, using a specific subclass of PartyComponent determines the type of the MobileParty for the purposes of behavior.

Party components are all inherited from the PartyComponent class and MUST be present in the MobileParty object.

class CustomPartyComponent : PartyComponent

The PartyComponent plays an important role in many of the CampaignBehaviors. As mentioned above, it is the basis for detecting the type of party. There are also important inheritors of this class used in many places by TaleWorlds CampaignBehaviors for calculating scores for the PartyThinkParams. The different types supported by TaleWorlds are accessible as fields on the MobileParty object.

A short excerpt of the Component field logic with some of the more common party Components.

class MobileParty {
    ....

    public CaravanPartyComponent CaravanPartyComponent => this._partyComponent as CaravanPartyComponent;

    public GarrisonPartyComponent GarrisonPartyComponent => this._partyComponent as GarrisonPartyComponent;

    public WarPartyComponent WarPartyComponent => this._partyComponent as WarPartyComponent;

    public VillagerPartyComponent VillagerPartyComponent => this._partyComponent as VillagerPartyComponent;

    public CommonAreaPartyComponent CommonAreaPartyComponent => this._partyComponent as CommonAreaPartyComponent;

    public BanditPartyComponent BanditPartyComponent => this._partyComponent as BanditPartyComponent;

    public LordPartyComponent LordPartyComponent => this._partyComponent as LordPartyComponent;
    
    ....
}

Which in turn are the basis for the checks determining the type of party in the numerous CampaignBehavior calculations throughout the codebase.

class MobileParty {
    ....

    public bool IsLordParty => this.LordPartyComponent != null;

    public bool IsVillager => this.VillagerPartyComponent != null;

    public bool IsMilitia => this.MilitiaPartyComponent != null;

    public bool IsCaravan => this.CaravanPartyComponent != null;

    public bool IsGarrison => this.GarrisonPartyComponent != null;
    
    ....
}

If a party falls into an existing category in terms of behavior, we should use one of the existing components in order to create our parties.

NB! The existing PartyComponents have internal constructors and therefore cannot be easily extended. Therefore if we need customized behavior of the existing types, we need to create them ourselves.

Party creation

Party creation is usually managed through static methods on the respective component. These creation methods are responsible for both the initialization and creation of the party as well as the component itself, attaching the two together in the process. An example is available in any of the party components by TaleWorlds or in ChaosRaidingPartyComponent [#ref required].

The component also contains all the initialization code which determines the party template (XML), size, clan etc. An example from ChaosRaidingPartyComponent.

private void InitializeChaosRaidingParty(MobileParty mobileParty, int partySize)
{
    PartyTemplateObject chaosPartyTemplate = Campaign.Current.ObjectManager.GetObject<PartyTemplateObject>("chaos_cultists");
    mobileParty.Party.MobileParty.InitializeMobileParty(chaosPartyTemplate, Portal.Position2D, 1f, troopNumberLimit: partySize);
    mobileParty.ActualClan = Clan.All.ToList().Find(clan => clan.Name.ToString() == "Chaos Warriors");
    mobileParty.Party.Owner = mobileParty.ActualClan.Leader;
    mobileParty.HomeSettlement = Portal;
    mobileParty.Aggressiveness = 2.0f;
    mobileParty.Party.Visuals.SetMapIconAsDirty();
    mobileParty.ItemRoster.Add(new ItemRosterElement(DefaultItems.Meat, MBRandom.RandomInt(partySize * 10, partySize * 20)));
    mobileParty.SetPartyUsedByQuest(true);
}

The name displayed on the campaign view for the party is determined by the following method.

 public class ChaosRaidingPartyComponent : WarPartyComponent
{
    ...
    
    [CachedData] private TextObject _cachedName;
    
    ...
    
    public override TextObject Name
    {
        get
        {
            if (_cachedName == null)
            {
                _cachedName = new TextObject("Chaos Raiders");
            }
            return _cachedName;
        }
    }
    
    ...
}

Note that TaleWorlds usually caches the name. So that is the recommended practice in our project as well.

Control mechanisms

We have two primary ways of controlling parties on the campaign map: the indirect way with PartyThinkParams and the more direct PartyAI class in conjunction with the MobileParty itself.

Regardless of the method used, some sort of ticking method in needed in order to regularly give and reassess commands for the party. The easiest way to do this is registering to some type of event from CampaignEvents.

PartyThinkParams

PartyThinkParams is an object specific to a given MobileParty in the game. Most parties are controlled using this Class and the related ticking mechanism. The PartyThinkParams object is passed through all the CampaignBehaviors that have registered for the AiHourlyTickEvent.

public class PartyThinkParams
{
    ...
    
    public Dictionary<AIBehaviorTuple, float> AIBehaviorScores;
    
    ...
}

The object AIBehaviorTuple is essentially a list of utility scores for pairs of <AiBehavior, IMapPoint>.

AiBehavior denotes the considered behavior.

IMapPoint a denotes the targeted object. It can be anything that implements the interface, like a Settlement or MobileParty.

Essentially the system considers a large combination of possible actions in combination with different targets and calculates some scalar value to represent them. The values usually lie in a normalized range between [0, 1], with the highest scoring pair determining the final decision. The functions for determining this score can be arbitrary and can provide scores > 1, though this happens rarely. This knowledge can be used for enforcing a type of behavior by providing some comparatively large value for a tuple.

Each of the CampaignBehaviors adds calculation results into the AIBehaviorScores dictionary contained within PartyThinkParams as follows.

    partyThinkParams.AIBehaviorScores[new AIBehaviorTuple(settlement, AiBehavior.RaidSettlement)] = 0.6f;
    partyThinkParams.AIBehaviorScores[new AIBehaviorTuple(mobileParty, AiBehavior.EscortParty)] = 0.9f;

NB!!! Be careful of which combinations you use when adding new scores. While scores are recomputed every tick, they are not removed from the map! If in the above example the "settlement" happens to be a castle, it will cause crashes in some of the CampaignBehaviors from TaleWorlds! This is because only Villages can be raided in the game logic and will cause NullPointerExceptions when Raids are being evaluated on Castles for AI score calculations.

There are also different useful booleans which denote how the chosen action will be carried out. There are some other useful parameters for utility computations as well:

public class PartyThinkParams
{
    ...
    public float CurrentObjectiveValue;
    public bool WillGatherAnArmy;
    public bool DoNotChangeBehavior;
    public float StrengthOfLordsWithoutArmy;
    public float StrengthOfLordsWithArmy;
    public float StrengthOfLordsAtSameClanWithoutArmy;
    ....
}

Event Registration

PartyThinkParams are available as a parameter through the usual principles of registration outlined for CampaignBehaviors.

More specifically, the PartyThinkParams can be accessed by registering for:

CampaignEvents.AiHourlyTickEvent.AddNonSerializedListener(this, HourlyTickPartyAI);

For a method with the following signature:

void HourlyTickPartyAI(MobileParty party, PartyThinkParams partyThinkParams)

Usage tips

The nice effect of registering our methods through CampaignBehaviors is that our event listeners are always added to the bottom of the list of methods all the methods called for a AiHourlyTickEvent. This means that we always see the results of all the previous methods that added values into the PartyThinkParams. I.e we can have the final say over what the final result of PartyThinkParams is.

Therefore, we can easily:

  1. Override scores of previous behavior computations
  2. Add completely new scores, without worrying whether they will be changed
  3. Add scores which are higher than other calculated ones

This means we can effectively achieve the same result as with the direct PartyAI approach, while not disabling behaviors like chasing or running from hostile parties or assisting friendly parties.

The scores are recomputed every time the tick is called so any values will need to written again on every call to the registered method.

PartyAI

The simpler, but less organic way for controlling the MobileParty is by giving it direct commands through the PartyAI class stored in the public Ai variable. Due to the ticking done on most parties, as described above, these commands are quickly overridden by the normal AI ticking process.

Therefore, if we wish to use this approach, we first need to disable the normal decision making process for the party with:

if (!mobileParty.Ai.DoNotMakeNewDecisions) 
{
    mobileParty.Ai.SetDoNotMakeNewDecisions(true);
}

The primary effect of this is that the party will not be included in the normal AI process outlined in the section about PartyThinkParams. Note that this has some consequences which, depending on the circumstances, can be undesirable. For example, the party will not engage support friendly parties that in danger nor engage or run from hostile parties it comes across. It will simply try to execute the given command as directly as possible, ignoring everything else.

Once in this mode, the AI state should be set as follows.

mobileParty.Ai.SetAIState(AIState.PatrollingAroundLocation); //More examples in the AIState enum

The command to execute is issued to the MobileParty object itself.

Here are a few examples. Explore the MobileParty class for other available commands.

mobileParty.SetMovePatrolAroundSettlement(settlement);
mobileParty.SetMoveBesiegeSettlement(settlement);
mobileParty.SetMoveDefendSettlement(settlement);
mobileParty.SetMoveGoToSettlement(settlement);
mobileParty.SetMoveGoToPoint(Vec2);
mobileParty.SetMovePatrolAroundPoint(Vec2);
mobileParty.SetMoveModeHold();
mobileParty.SetMoveEngageParty(targetParty);

In general this method is preferred for very simple scripted behaviors, like quests or other types of events which do not need to interact with the rest of the game's systems. Another use-case is giving initial commands, such that parties do not stand around until their first AITick cycle takes over and makes them act normally. In that latter case we can skip disabling the decision making.

Disabling AI

In addition to DoNotMakeNewDecisions there is another way of toggling AI on and off completely for MobileParties:

mobileParty.Ai.DisableAI();
mobileParty.Ai.EnableAI();

Or the timed variant :

mobileParty.Ai.DisableForHours(int);

This means that the party wont respond to either type of input outlined above. The party will simply stand on the map, or stay in any settlement they were in before the command was given. It will be able to respond to dialogue initiation or participate in battles if part of a garrison. For example, most militias use this approach so they are removed from the AI computation cycles and simply stay in their respective settlements.

Footnotes

In truth the game engine uses the PartyAi class and MobileParty's available commands in order to give commands to the party once the AiHourlyTickEvent cycle is completed. The commands corresponding to the highest utility from PartyThinkParams are given using the same mechanisms outlined in PartyAI. The reason for keeping the two separate in this document is, that for practical purposes, we will be using one method or the other for writing our behaviors. However, it can be useful to know this when investigating some particular edge cases for party behaviors.


The MobileParty itself extends many useful classes and can be used for all types of computations

 public sealed class MobileParty : 
    CampaignObjectBase,
    ILocatable<MobileParty>,
    IMapPoint,
    ITrackableCampaignObject,
    ITrackableBase

The AiHourlyTickEvent is not a true "Hourly" tick event that ticks for every hour in the game. It is instead a PartialHourlyTickEvent which ticks as fast as possible, but AT MOST every hour. We should keep this in mind and try to avoid doing very expensive computations every tick.


Due to the way that that the viewmodel checks for tracked parties, custom party components end up always tracked on creation. A simple way to bypass this is using:

mobileParty.SetPartyUsedByQuest(true);

This might have some unknown side-effects, use with caution or disable after initialization is finished.