Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f4f7064
Initial bandit interactions sync
jordanbrymora Jul 3, 2026
cfa4c15
Merge branch 'development' into BanditInteractionsSync
jordanbrymora Jul 9, 2026
efbb5af
Merge branch 'development' into BanditInteractionsSync
jordanbrymora Jul 22, 2026
f259424
Merge branch 'development' into BanditInteractionsSync
jordanbrymora Jul 23, 2026
e97370f
Merge branch 'development' into BanditInteractionsSync
jordanbrymora Jul 23, 2026
bb9e688
Fix taking bandits as prisoners
jordanbrymora Jul 23, 2026
9723bcc
Remove testing patch
jordanbrymora Jul 23, 2026
91ff4d1
Merge branch 'development' into BanditInteractionsSync
jordanbrymora Jul 23, 2026
39f6ef3
Merge branch 'development' into BanditInteractionsSync
jordanbrymora Jul 23, 2026
ef263d1
Debug command & cleaning
jordanbrymora Jul 23, 2026
c5330fe
Merge branch 'development' into BanditInteractionsSync
jordanbrymora Jul 27, 2026
5023cbb
Merge branch 'development' into BanditInteractionsSync
jordanbrymora Aug 3, 2026
3e36167
Address review comments
jordanbrymora Aug 16, 2026
8100783
Review comment
jordanbrymora Aug 18, 2026
19e0934
Review comments
jordanbrymora Aug 18, 2026
c4f0bd2
Review comment
jordanbrymora Aug 18, 2026
5efaaf1
Merge branch 'development' into BanditInteractionsSync
jordanbrymora Aug 19, 2026
edc872b
Review comments
jordanbrymora Aug 19, 2026
f821229
Fix loot screens no longer showing up
jordanbrymora Aug 21, 2026
76f1bce
Review comments
jordanbrymora Aug 21, 2026
a0b3733
Guard against unresolved battle
jordanbrymora Aug 22, 2026
4f3d7a8
Merge branch 'development' into BanditInteractionsSync
jordanbrymora Aug 26, 2026
a707337
Defer unresolved bandit surrender
jordanbrymora Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions source/GameInterface/Services/Bandits/Commands/BanditsCommands.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Common;
using Common.Logging;
using GameInterface.CoopSessionData;
using Serilog;
using System.Collections.Generic;
using System.Text;
using TaleWorlds.CampaignSystem;
using TaleWorlds.CampaignSystem.CampaignBehaviors;
using static TaleWorlds.Library.CommandLineFunctionality;

namespace GameInterface.Services.Bandits.Commands;

internal class BanditsCommands
{
private static readonly ILogger Logger = LogManager.GetLogger<BanditsCommands>();

/// <summary>
/// View interacted bandits for all players on server and for current player on client
/// </summary>
[CommandLineArgumentFunction("view_interacted_bandits", "coop.debug.bandits")]
public static string ViewInteractedBanditsCommand(List<string> strings)
{
StringBuilder stringBuilder = new StringBuilder();
if (ModInformation.IsServer)
{
if (!ContainerProvider.TryResolve<ICoopSessionProvider>(out var coopSessionProvider)) return "Unable to resolve CoopSessionProvider";

foreach (var playerInteractedBandit in coopSessionProvider.CoopSession.InteractionsPlayerData.PlayerInteractedBandits)
{
if (playerInteractedBandit.Key == null || playerInteractedBandit.Value == null) continue;

stringBuilder.AppendLine($"{playerInteractedBandit.Key}");
foreach (var interactedBandit in playerInteractedBandit.Value)
{
stringBuilder.AppendLine($"{interactedBandit.Key} ({interactedBandit.Value})");
}
}
}
else
{
stringBuilder.AppendLine($"{Hero.MainHero.Name}");
foreach (var interactedBandit in Campaign.Current.GetCampaignBehavior<BanditInteractionsCampaignBehavior>()._interactedBandits)
{
stringBuilder.AppendLine($"{interactedBandit.Key.StringId} ({(int)interactedBandit.Value})");
}
}

string result = stringBuilder.ToString();
if (result.Length > 0)
{
return result;
}
return "Failed to retrieve interacted bandits";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
using Common;
using Common.Logging;
using Common.Messaging;
using Common.Network;
using GameInterface.Services.Bandits.Messages;
using GameInterface.Services.MobileParties.Interfaces;
using GameInterface.Services.MobileParties.Messages;
using GameInterface.Services.ObjectManager;
using Serilog;
using System.Collections.Generic;
using TaleWorlds.CampaignSystem;
using TaleWorlds.CampaignSystem.Actions;
using TaleWorlds.CampaignSystem.CampaignBehaviors;
using TaleWorlds.CampaignSystem.Party;

namespace GameInterface.Services.Bandits.Handlers;

internal class BanditInteractionsHandler : IHandler
{
private static readonly ILogger Logger = LogManager.GetLogger<BanditInteractionsHandler>();

private readonly IMessageBroker messageBroker;
private readonly IObjectManager objectManager;
private readonly INetwork network;
private readonly ISessionInteractionsPlayerDataInterface sessionInteractionsPlayerDataInterface;

public BanditInteractionsHandler(
IMessageBroker messageBroker,
IObjectManager objectManager,
INetwork network,
ISessionInteractionsPlayerDataInterface sessionInteractionsPlayerDataInterface)
{
this.messageBroker = messageBroker;
this.objectManager = objectManager;
this.network = network;
this.sessionInteractionsPlayerDataInterface = sessionInteractionsPlayerDataInterface;

messageBroker.Subscribe<SetPlayerBanditInteraction>(Handle_SetPlayerBanditInteraction);
messageBroker.Subscribe<NetworkSetPlayerBanditInteraction>(Handle_NetworkSetPlayerBanditInteraction);

messageBroker.Subscribe<MobilePartyDestroyed>(Handle_MobilePartyDestroyed);
messageBroker.Subscribe<NetworkBanditPartyDestroyed>(Handle_NetworkBanditPartyDestroyed);

messageBroker.Subscribe<BanditPartyScreenDoneCondition>(Handle_BanditPartyScreenDoneCondition);
messageBroker.Subscribe<NetworkBanditPartyScreenDoneCondition>(Handle_NetworkBanditPartyScreenDoneCondition);

messageBroker.Subscribe<GetBanditMemberAndPrisonerRosters>(Handle_GetBanditMemberAndPrisonerRosters);
messageBroker.Subscribe<NetworkGetBanditMemberAndPrisonerRosters>(Handle_NetworkGetBanditMemberAndPrisonerRosters);

messageBroker.Subscribe<RosterScreenAfterBanditEncounter>(Handle_RosterScreenAfterBanditEncounter);
messageBroker.Subscribe<NetworkRosterScreenAfterBanditEncounter>(Handle_NetworkRosterScreenAfterBanditEncounter);
}

public void Dispose()
{
messageBroker.Unsubscribe<SetPlayerBanditInteraction>(Handle_SetPlayerBanditInteraction);
messageBroker.Unsubscribe<NetworkSetPlayerBanditInteraction>(Handle_NetworkSetPlayerBanditInteraction);

messageBroker.Unsubscribe<MobilePartyDestroyed>(Handle_MobilePartyDestroyed);
messageBroker.Unsubscribe<NetworkBanditPartyDestroyed>(Handle_NetworkBanditPartyDestroyed);

messageBroker.Unsubscribe<BanditPartyScreenDoneCondition>(Handle_BanditPartyScreenDoneCondition);
messageBroker.Unsubscribe<NetworkBanditPartyScreenDoneCondition>(Handle_NetworkBanditPartyScreenDoneCondition);

messageBroker.Unsubscribe<GetBanditMemberAndPrisonerRosters>(Handle_GetBanditMemberAndPrisonerRosters);
messageBroker.Unsubscribe<NetworkGetBanditMemberAndPrisonerRosters>(Handle_NetworkGetBanditMemberAndPrisonerRosters);

messageBroker.Unsubscribe<RosterScreenAfterBanditEncounter>(Handle_RosterScreenAfterBanditEncounter);
messageBroker.Unsubscribe<NetworkRosterScreenAfterBanditEncounter>(Handle_NetworkRosterScreenAfterBanditEncounter);
}

private void Handle_SetPlayerBanditInteraction(MessagePayload<SetPlayerBanditInteraction> obj)
{
var data = obj.What;

GameThread.RunSafe(() =>
{
if (!objectManager.TryGetIdWithLogging(data.MainHero, out var mainHeroId)) return;
if (!objectManager.TryGetIdWithLogging(data.ConversationParty, out var conversationPartyId)) return;

var message = new NetworkSetPlayerBanditInteraction(mainHeroId, conversationPartyId, data.Interaction);
network.SendAll(message);
});
}

private void Handle_NetworkSetPlayerBanditInteraction(MessagePayload<NetworkSetPlayerBanditInteraction> obj)
{
var data = obj.What;

GameThread.RunSafe(() =>
{
// Guard against saving ids that can't be resolved on the server
if (!objectManager.TryGetObjectWithLogging<Hero>(data.MainHeroId, out var _)) return;
if (!objectManager.TryGetObjectWithLogging<MobileParty>(data.ConversationPartyId, out var _)) return;

sessionInteractionsPlayerDataInterface.SetPlayerBanditsInteraction(data.MainHeroId, data.ConversationPartyId, data.Interaction);
});
}

private void Handle_MobilePartyDestroyed(MessagePayload<MobilePartyDestroyed> obj)
{
var data = obj.What;

GameThread.RunSafe(() =>
{
// Don't process anything for destroyed mobile parties that aren't bandits
if (!data.MobileParty.IsBandit) return;

if (!objectManager.TryGetIdWithLogging(data.MobileParty, out var mobilePartyId)) return;

// Update CoopSession data on server
sessionInteractionsPlayerDataInterface.RemoveInteractedBanditsForAllPlayers(mobilePartyId);

var message = new NetworkBanditPartyDestroyed(mobilePartyId);
network.SendAll(message);
});
}

private void Handle_NetworkBanditPartyDestroyed(MessagePayload<NetworkBanditPartyDestroyed> obj)
{
var data = obj.What;

GameThread.RunSafe(() =>
{
if (!TryGetBanditInteractionsBehavior(out var banditInteractionsBehavior)) return;
if (!objectManager.TryGetObjectWithLogging<MobileParty>(data.MobilePartyId, out var mobileParty)) return;

if (banditInteractionsBehavior._interactedBandits.ContainsKey(mobileParty))
{
banditInteractionsBehavior._interactedBandits.Remove(mobileParty);
}
});
}

private void Handle_BanditPartyScreenDoneCondition(MessagePayload<BanditPartyScreenDoneCondition> obj)
{
var data = obj.What;

GameThread.RunSafe(() =>
{
var charactersIds = new List<string>();
foreach (var troopRosterElement in data.RightMemberRoster.data)
{
if (!objectManager.TryGetIdWithLogging(troopRosterElement.Character, out var characterId)) continue;

charactersIds.Add(characterId);
}

var message = new NetworkBanditPartyScreenDoneCondition(charactersIds);
network.SendAll(message);
});
}

private void Handle_NetworkBanditPartyScreenDoneCondition(MessagePayload<NetworkBanditPartyScreenDoneCondition> obj)
{
GameThread.RunSafe(() =>
{
// If list is empty the client will receive a null object. Guard to prevent a NRE
if (obj.What.CharactersIds == null) return;

foreach (var characterId in obj.What.CharactersIds)
Comment thread
ShoT-UPfps marked this conversation as resolved.
{
if (!objectManager.TryGetObjectWithLogging<CharacterObject>(characterId, out var character)) continue;

if (character.IsHero && character.HeroObject.HeroState == Hero.CharacterStates.Fugitive)
{
character.HeroObject.ChangeState(Hero.CharacterStates.Active);
}
}
});
}

private void Handle_GetBanditMemberAndPrisonerRosters(MessagePayload<GetBanditMemberAndPrisonerRosters> obj)
{
var data = obj.What;

GameThread.RunSafe(() =>
{
if (!objectManager.TryGetIdWithLogging(data.PlayerClan, out var playerClanId)) return;
if (!objectManager.TryGetIdWithLogging(data.MainParty, out var mainPartyId)) return;

var partiesIds = new List<string>();
foreach (var party in data.Parties)
{
if (!objectManager.TryGetIdWithLogging(party, out var partyId)) continue;

partiesIds.Add(partyId);
}

var message = new NetworkGetBanditMemberAndPrisonerRosters(playerClanId, mainPartyId, partiesIds, data.DoBanditsJoinPlayerSide);
network.SendAll(message);
});
}

private void Handle_NetworkGetBanditMemberAndPrisonerRosters(MessagePayload<NetworkGetBanditMemberAndPrisonerRosters> obj)
{
var data = obj.What;

GameThread.RunSafe(() =>
{
if (!objectManager.TryGetObjectWithLogging<Clan>(data.PlayerClanId, out var playerClan)) return;
if (!objectManager.TryGetObjectWithLogging<MobileParty>(data.MainPartyId, out var mainParty)) return;

if (data.PartiesIds == null) return;

foreach (var partyId in data.PartiesIds)
Comment thread
ShoT-UPfps marked this conversation as resolved.
{
if (!objectManager.TryGetObjectWithLogging<MobileParty>(partyId, out var mobileParty)) return;

for (int j = mobileParty.PrisonRoster.Count - 1; j > -1; j--)
{
CharacterObject characterAtIndex = mobileParty.PrisonRoster.GetCharacterAtIndex(j);
if (characterAtIndex.IsHero && characterAtIndex.HeroObject.Clan == playerClan)
{
if (data.DoBanditsJoinPlayerSide)
{
EndCaptivityAction.ApplyByPeace(characterAtIndex.HeroObject, null);
}
else
{
EndCaptivityAction.ApplyByReleasedAfterBattle(characterAtIndex.HeroObject);
}
characterAtIndex.HeroObject.ChangeState(Hero.CharacterStates.Active);
AddHeroToPartyAction.Apply(characterAtIndex.HeroObject, mainParty, true);
}
else if (characterAtIndex.IsHero && playerClan.IsAtWarWith(characterAtIndex.HeroObject.Clan))
{
TransferPrisonerAction.Apply(characterAtIndex, mobileParty.Party, mainParty.Party);
}
}
}
});
}

private void Handle_RosterScreenAfterBanditEncounter(MessagePayload<RosterScreenAfterBanditEncounter> obj)
{
var data = obj.What;

GameThread.RunSafe(() =>
{
if (!objectManager.TryGetIdWithLogging(data.MainParty, out var mainPartyId)) return;

var partiesIds = new List<string>();
foreach (var party in data.Parties)
{
if (!objectManager.TryGetIdWithLogging(party, out var partyId)) continue;

partiesIds.Add(partyId);
}

var message = new NetworkRosterScreenAfterBanditEncounter(partiesIds, mainPartyId);
network.SendAll(message);
});
}

private void Handle_NetworkRosterScreenAfterBanditEncounter(MessagePayload<NetworkRosterScreenAfterBanditEncounter> obj)
{
var data = obj.What;

GameThread.RunSafe(() =>
{
if (!objectManager.TryGetObjectWithLogging<MobileParty>(data.MainPartyId, out var mainParty)) return;

if (data.PartiesIds == null) return;

foreach (var partyId in data.PartiesIds)
Comment thread
ShoT-UPfps marked this conversation as resolved.
{
if (!objectManager.TryGetObjectWithLogging<MobileParty>(partyId, out var mobileParty)) return;

CampaignEventDispatcher.Instance.OnBanditPartyRecruited(mobileParty);
DestroyPartyAction.Apply(mainParty.Party, mobileParty);
}
});
}

private bool TryGetBanditInteractionsBehavior(out BanditInteractionsCampaignBehavior banditInteractionsBehavior)
{
banditInteractionsBehavior = Campaign.Current?.GetCampaignBehavior<BanditInteractionsCampaignBehavior>();
if (banditInteractionsBehavior != null) return true;

Logger.Debug("Skipping bandit interactions update because the campaign behavior is unavailable");
return false;
}
}
Loading
Loading