diff --git a/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/EquipmentSetterMissionLogic.cs b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/EquipmentSetterMissionLogic.cs new file mode 100644 index 0000000..a0c1793 --- /dev/null +++ b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/EquipmentSetterMissionLogic.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using Bannerlord.ExpandedTemplate.Domain.EquipmentPool; +using Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.MissionLogic.EquipmentSetters; +using SandBox.Missions.MissionLogics; +using TaleWorlds.CampaignSystem; +using TaleWorlds.Core; +using TaleWorlds.MountAndBlade; + +namespace Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.MissionLogic; + +public class EquipmentSetterMissionLogic : TaleWorlds.MountAndBlade.MissionLogic +{ + private readonly HeroEquipmentSetter _heroEquipmentSetter; + private readonly TroopEquipmentPoolSetter _troopEquipmentPoolSetter; + private readonly IGetEquipmentPool _getEquipmentPool; + private readonly CharacterEquipmentRosterReference _characterEquipmentRosterReference; + + private readonly Dictionary _nativeTroopEquipmentRosters = new(); + private readonly Dictionary _nativeHeroEquipment = new(); + + public EquipmentSetterMissionLogic(HeroEquipmentSetter heroEquipmentSetter, + TroopEquipmentPoolSetter troopEquipmentPoolSetter, IGetEquipmentPool getEquipmentPool, + CharacterEquipmentRosterReference characterEquipmentRosterReference) + { + _heroEquipmentSetter = heroEquipmentSetter; + _troopEquipmentPoolSetter = troopEquipmentPoolSetter; + _getEquipmentPool = getEquipmentPool; + _characterEquipmentRosterReference = characterEquipmentRosterReference; + } + + public override void OnBehaviorInitialize() + { + base.OnBehaviorInitialize(); + + _nativeTroopEquipmentRosters.Clear(); + _nativeHeroEquipment.Clear(); + } + + public override void OnAgentCreated(Agent agent) + { + MBEquipmentRoster? agentEquipmentRoster = + _characterEquipmentRosterReference.GetEquipmentRoster(agent.Character); + + if (agentEquipmentRoster is null) return; + if (!CanOverrideEquipment(agent)) return; + + base.OnAgentCreated(agent); + + string id = agent.Character.StringId; + if (agent.Character is CharacterObject characterObject) + id = characterObject.OriginalCharacter?.StringId ?? id; + + var equipmentPool = _getEquipmentPool.GetTroopEquipmentPool(id); + if (equipmentPool.IsEmpty()) + equipmentPool = _getEquipmentPool.GetTroopEquipmentPool(agentEquipmentRoster.StringId); + + if (agent.IsHero) + { + _nativeHeroEquipment[agent.Character.StringId] = agent.Character.Equipment.Clone(); + _heroEquipmentSetter.SetEquipmentFromEquipmentPool(agent, equipmentPool); + } + else + { + _nativeTroopEquipmentRosters[agent.Character.StringId] = agentEquipmentRoster; + _troopEquipmentPoolSetter.SetEquipmentPool(agent, equipmentPool); + } + } + + public override void OnAgentBuild(Agent agent, Banner banner) + { + if (!CanOverrideEquipment(agent)) return; + + base.OnAgentBuild(agent, banner); + + if (agent.IsHero) + _heroEquipmentSetter.SetEquipment(agent, _nativeHeroEquipment[agent.Character.StringId]); + else + _troopEquipmentPoolSetter.SetEquipmentPool(agent, + _nativeTroopEquipmentRosters[agent.Character.StringId]); + } + + private static bool CanOverrideEquipment(IAgent agent) + { + return (Mission.Current?.GetMissionBehavior() is not null || + Mission.Current?.GetMissionBehavior() is not null) && + agent?.Character is not null && + !(Clan.PlayerClan?.Heroes?.Exists( + hero => hero?.StringId is not null && agent?.Character?.StringId == hero.StringId) ?? true); + } +} \ No newline at end of file diff --git a/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/EquipmentSetters/CharacterEquipmentRosterReference.cs b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/EquipmentSetters/CharacterEquipmentRosterReference.cs new file mode 100644 index 0000000..559cce3 --- /dev/null +++ b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/EquipmentSetters/CharacterEquipmentRosterReference.cs @@ -0,0 +1,46 @@ +using System.Reflection; +using Bannerlord.ExpandedTemplate.Domain.Logging.Port; +using TaleWorlds.Core; + +namespace Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.MissionLogic.EquipmentSetters; + +public class CharacterEquipmentRosterReference +{ + private static readonly FieldInfo? EquipmentRosterField = + typeof(BasicCharacterObject).GetField("_equipmentRoster", BindingFlags.NonPublic | BindingFlags.Instance)!; + + public CharacterEquipmentRosterReference(ILoggerFactory loggerFactory) + { + ILogger logger = loggerFactory.CreateLogger(); + + if (EquipmentRosterField is null || EquipmentRosterField.FieldType != typeof(MBEquipmentRoster)) + logger.Error( + "BasicCharacterObject's _mbEquipmentRoster field could not be found preventing equipment pool override"); + } + + /// + /// Returns the equipment roster reference of a characterObject + /// + /// + /// + /// the internal MBEquipmentRoster object from the characterObject object or + /// null if the characterObject is null or if the under the hood reflection implementation fails due to + /// a game update) + /// + public MBEquipmentRoster? GetEquipmentRoster(BasicCharacterObject characterObject) + { + if (characterObject is null) return null; + return (MBEquipmentRoster?)EquipmentRosterField?.GetValue(characterObject); + } + + /// + /// Sets the equipment roster reference of a characterObject + /// + /// + /// + public void SetEquipmentRoster(BasicCharacterObject characterObject, MBEquipmentRoster mbEquipmentRoster) + { + if (characterObject is null) return; + EquipmentRosterField?.SetValue(characterObject, mbEquipmentRoster); + } +} \ No newline at end of file diff --git a/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/EquipmentSetters/HeroEquipmentSetter.cs b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/EquipmentSetters/HeroEquipmentSetter.cs new file mode 100644 index 0000000..ecbf719 --- /dev/null +++ b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/EquipmentSetters/HeroEquipmentSetter.cs @@ -0,0 +1,59 @@ +using Bannerlord.ExpandedTemplate.Domain.EquipmentPool; +using Bannerlord.ExpandedTemplate.Domain.EquipmentPool.Model; +using Bannerlord.ExpandedTemplate.Domain.Logging.Port; +using Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.EquipmentPools.Mappers; +using TaleWorlds.Core; +using Equipment = TaleWorlds.Core.Equipment; + +namespace Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.MissionLogic.EquipmentSetters; + +public class HeroEquipmentSetter +{ + private readonly IGetEquipment _getEquipment; + private readonly EquipmentMapper _equipmentMapper; + private readonly CharacterEquipmentRosterReference _characterEquipmentRosterReference; + private readonly ILogger _logger; + + public HeroEquipmentSetter(IGetEquipment getEquipment, EquipmentMapper equipmentMapper, + CharacterEquipmentRosterReference characterEquipmentRosterReference, ILoggerFactory loggerFactory) + { + _getEquipment = getEquipment; + _equipmentMapper = equipmentMapper; + _characterEquipmentRosterReference = characterEquipmentRosterReference; + _logger = loggerFactory.CreateLogger(); + } + + public void SetEquipmentFromEquipmentPool(IAgent agent, EquipmentPool equipmentPool) + { + MBEquipmentRoster? agentEquipmentRoster = + _characterEquipmentRosterReference.GetEquipmentRoster(agent.Character); + + if (agentEquipmentRoster is null) return; + + var equipment = _getEquipment.GetEquipmentFromEquipmentPool(equipmentPool); + if (equipment is null) + SetEquipment(agent, new Equipment()); + else + SetEquipment(agent, + _equipmentMapper.Map(equipment, agentEquipmentRoster)); + } + + public void SetEquipment(IAgent agent, Equipment? equipment) + { + if (agent?.Character?.Equipment is null) + { + _logger.Error( + "Expected a hero Agent to have a non-nullable Character field with a non-nullable Equipment field"); + return; + } + + if (equipment is null) + { + _logger.Error( + $"Could find any equipment for ${agent.Character.StringId}"); + return; + } + + agent.Character.Equipment.FillFrom(equipment); + } +} \ No newline at end of file diff --git a/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/EquipmentSetters/TroopEquipmentPoolSetter.cs b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/EquipmentSetters/TroopEquipmentPoolSetter.cs new file mode 100644 index 0000000..faf98b2 --- /dev/null +++ b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/EquipmentSetters/TroopEquipmentPoolSetter.cs @@ -0,0 +1,37 @@ +using Bannerlord.ExpandedTemplate.Domain.EquipmentPool.Model; +using Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.EquipmentPools.Mappers; +using TaleWorlds.Core; + +namespace Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.MissionLogic.EquipmentSetters; + +public class TroopEquipmentPoolSetter +{ + private readonly EquipmentPoolsMapper _equipmentPoolsMapper; + private readonly CharacterEquipmentRosterReference _characterEquipmentRosterReference; + + public TroopEquipmentPoolSetter(EquipmentPoolsMapper equipmentPoolsMapper, + CharacterEquipmentRosterReference characterEquipmentRosterReference) + + { + _equipmentPoolsMapper = equipmentPoolsMapper; + _characterEquipmentRosterReference = characterEquipmentRosterReference; + } + + public void SetEquipmentPool(IAgent agent, EquipmentPool equipmentPool) + { + if (agent?.Character is null) return; + + MBEquipmentRoster? agentEquipmentRoster = + _characterEquipmentRosterReference.GetEquipmentRoster(agent.Character); + + if (agentEquipmentRoster is null) return; + + SetEquipmentPool(agent, _equipmentPoolsMapper.MapEquipmentPool(equipmentPool, agentEquipmentRoster)); + } + + public void SetEquipmentPool(IAgent agent, MBEquipmentRoster equipmentRoster) + { + if (agent?.Character is null) return; + _characterEquipmentRosterReference.SetEquipmentRoster(agent.Character, equipmentRoster); + } +} \ No newline at end of file diff --git a/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/ForceCivilianEquipmentSetter.cs b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/ForceCivilianEquipmentMissionLogic.cs old mode 100755 new mode 100644 similarity index 83% rename from Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/ForceCivilianEquipmentSetter.cs rename to Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/ForceCivilianEquipmentMissionLogic.cs index eedeab0..030b87b --- a/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/ForceCivilianEquipmentSetter.cs +++ b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/ForceCivilianEquipmentMissionLogic.cs @@ -4,7 +4,7 @@ namespace Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.MissionLogic { - public class ForceCivilianEquipmentSetter : TaleWorlds.MountAndBlade.MissionLogic + public class ForceCivilianEquipmentMissionLogic : TaleWorlds.MountAndBlade.MissionLogic { public override void OnAgentCreated(Agent agent) { diff --git a/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/MissionSpawnEquipmentPoolSetter.cs b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/MissionSpawnEquipmentPoolSetter.cs deleted file mode 100755 index 85f6743..0000000 --- a/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/MissionSpawnEquipmentPoolSetter.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System.Collections.Generic; -using System.Reflection; -using Bannerlord.ExpandedTemplate.Domain.EquipmentPool; -using Bannerlord.ExpandedTemplate.Domain.Logging.Port; -using Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.EquipmentPools.Mappers; -using SandBox.Missions.MissionLogics; -using TaleWorlds.CampaignSystem; -using TaleWorlds.Core; -using TaleWorlds.MountAndBlade; - -namespace Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.MissionLogic -{ - public class MissionSpawnEquipmentPoolSetter : TaleWorlds.MountAndBlade.MissionLogic - { - private readonly FieldInfo? _equipmentRosterField = - typeof(BasicCharacterObject).GetField("_equipmentRoster", BindingFlags.NonPublic | BindingFlags.Instance)!; - - private readonly IGetEquipmentPool _getEquipmentPool; - private readonly IGetEquipment _getEquipment; - private readonly EquipmentPoolsMapper _equipmentPoolsMapper; - private readonly EquipmentMapper _equipmentMapper; - private readonly ILogger _logger; - - private readonly Dictionary _nativeEquipmentPools = new(); - - public MissionSpawnEquipmentPoolSetter(IGetEquipmentPool getEquipmentPool, IGetEquipment getEquipment, - EquipmentPoolsMapper equipmentPoolsMapper, EquipmentMapper equipmentMapper, ILoggerFactory loggerFactory) - { - _getEquipmentPool = getEquipmentPool; - _getEquipment = getEquipment; - _equipmentPoolsMapper = equipmentPoolsMapper; - _equipmentMapper = equipmentMapper; - _logger = loggerFactory.CreateLogger(); - - - if (_equipmentRosterField is null || _equipmentRosterField.FieldType != typeof(MBEquipmentRoster)) - _logger.Error( - "BasicCharacterObject's _mbEquipmentRoster field could not be found preventing equipment pool override in friendly missions"); - } - - public override void OnBehaviorInitialize() - { - base.OnBehaviorInitialize(); - - _nativeEquipmentPools.Clear(); - } - - public override void OnAgentCreated(Agent agent) - { - if (_equipmentRosterField is null) return; - if (!CanOverrideEquipment(agent)) return; - - base.OnAgentCreated(agent); - - var equipmentRoster = (MBEquipmentRoster)_equipmentRosterField.GetValue(agent.Character); - - string id = agent.Character.StringId; - if (agent.Character is CharacterObject characterObject) - id = characterObject.OriginalCharacter?.StringId ?? id; - - _nativeEquipmentPools[agent.Character.StringId] = equipmentRoster; - - var equipmentPool = _getEquipmentPool.GetTroopEquipmentPool(id); - if (equipmentPool.IsEmpty()) - equipmentPool = _getEquipmentPool.GetTroopEquipmentPool(equipmentRoster.StringId); - - - if (agent.IsHero) - { - var equipment = _getEquipment.GetEquipmentFromEquipmentPool(equipmentPool); - if (equipment is null) - OverrideHeroEquipment(agent, new Equipment()); - else - OverrideHeroEquipment(agent, - _equipmentMapper.Map(equipment, equipmentRoster)); - } - else - { - OverrideTroopEquipment(agent, - _equipmentPoolsMapper.MapEquipmentPool(equipmentPool, equipmentRoster)); - } - } - - public override void OnAgentBuild(Agent agent, Banner banner) - { - if (_equipmentRosterField is null) return; - if (!CanOverrideEquipment(agent)) return; - - base.OnAgentBuild(agent, banner); - - OverrideTroopEquipment(agent, _nativeEquipmentPools[agent.Character.StringId]); - } - - private bool CanOverrideEquipment(IAgent agent) - { - return (Mission.Current?.GetMissionBehavior() is not null || - Mission.Current?.GetMissionBehavior() is not null) && - agent?.Character is not null && - !(Clan.PlayerClan?.Heroes?.Exists( - hero => hero?.StringId is not null && agent?.Character?.StringId == hero.StringId) ?? true); - } - - private void OverrideTroopEquipment(IAgent agent, MBEquipmentRoster equipmentPool) - { - _equipmentRosterField?.SetValue(agent.Character, equipmentPool); - } - - private void OverrideHeroEquipment(IAgent agent, Equipment? equipment) - { - if (agent.Character?.Equipment is null) - { - _logger.Error( - "Expected a hero Agent to have a non-nullable Character field with a non-nullable Equipment field"); - return; - } - - if (equipment is null) - { - _logger.Error( - $"Could find any equipment for ${agent.Character.StringId}"); - return; - } - - agent.Character.Equipment.FillFrom(equipment); - } - } -} \ No newline at end of file diff --git a/Bannerlord.ExpandedTemplate.Integration/SubModule.cs b/Bannerlord.ExpandedTemplate.Integration/SubModule.cs index 804fabc..d6b0005 100644 --- a/Bannerlord.ExpandedTemplate.Integration/SubModule.cs +++ b/Bannerlord.ExpandedTemplate.Integration/SubModule.cs @@ -14,6 +14,7 @@ using Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.EquipmentPools.Mappers; using Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.EquipmentPools.Providers; using Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.MissionLogic; +using Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.MissionLogic.EquipmentSetters; using Bannerlord.ExpandedTemplate.Integration.Xml; using TaleWorlds.CampaignSystem; using TaleWorlds.Core; @@ -27,8 +28,8 @@ public class SubModule : MBSubModuleBase private readonly ILoggerFactory _loggerFactory; private readonly ICacheProvider _cacheProvider; - private ForceCivilianEquipmentSetter _forceCivilianEquipmentSetter; - private MissionSpawnEquipmentPoolSetter _missionSpawnEquipmentPoolSetter; + private ForceCivilianEquipmentMissionLogic _forceCivilianEquipmentMissionLogic; + private EquipmentSetterMissionLogic _equipmentSetterMissionLogic; public SubModule() { @@ -97,17 +98,22 @@ private void HandleEquipmentSpawnDependencies() troopSiegeEquipmentProvider, troopCivilianEquipmentProvider, equipmentPicker, _loggerFactory); var getEquipment = new GetEquipment(random); - _forceCivilianEquipmentSetter = new ForceCivilianEquipmentSetter(); - _missionSpawnEquipmentPoolSetter = - new MissionSpawnEquipmentPoolSetter(getEquipmentPool, getEquipment, equipmentPoolMapper, - equipmentMapper, - _loggerFactory); + var characterEquipmentRosterReference = new CharacterEquipmentRosterReference(_loggerFactory); + var heroEquipmentSetter = new HeroEquipmentSetter(getEquipment, equipmentMapper, + characterEquipmentRosterReference, _loggerFactory); + var troopEquipmentPoolSetter = new TroopEquipmentPoolSetter(equipmentPoolMapper, + characterEquipmentRosterReference); + + _forceCivilianEquipmentMissionLogic = new ForceCivilianEquipmentMissionLogic(); + _equipmentSetterMissionLogic = + new EquipmentSetterMissionLogic(heroEquipmentSetter, troopEquipmentPoolSetter, getEquipmentPool, + characterEquipmentRosterReference); } private void AddEquipmentSpawnMissionBehaviour(Mission mission) { - mission.AddMissionBehavior(_forceCivilianEquipmentSetter); - mission.AddMissionBehavior(_missionSpawnEquipmentPoolSetter); + mission.AddMissionBehavior(_forceCivilianEquipmentMissionLogic); + mission.AddMissionBehavior(_equipmentSetterMissionLogic); } #endregion