From 7670e2233fc4b342d711916bd6f09903f3504f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl=20Forward?= Date: Sun, 20 Oct 2024 22:44:55 +0200 Subject: [PATCH] feat: pick random equipment from equipment pool for heroes * refactor equipment mapping from equipment pool mapper into a proper mapper for re-use * add usecase for getting a random equipment from a domain equipment pool --- .../EquipmentPool/GetEquipmentShould.cs | 72 ++++++++++++ .../EquipmentPool/GetEquipment.cs | 16 +++ .../EquipmentPool/IGetEquipment.cs | 8 ++ .../EquipmentPools/Mappers/EquipmentMapper.cs | 61 ++++++++++ .../Mappers/EquipmentPoolsMapper.cs | 111 ++++-------------- .../MissionSpawnEquipmentPoolSetter.cs | 70 ++++++++--- 6 files changed, 231 insertions(+), 107 deletions(-) create mode 100644 Bannerlord.ExpandedTemplate.Domain.Tests/EquipmentPool/GetEquipmentShould.cs create mode 100644 Bannerlord.ExpandedTemplate.Domain/EquipmentPool/GetEquipment.cs create mode 100644 Bannerlord.ExpandedTemplate.Domain/EquipmentPool/IGetEquipment.cs create mode 100644 Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/EquipmentPools/Mappers/EquipmentMapper.cs diff --git a/Bannerlord.ExpandedTemplate.Domain.Tests/EquipmentPool/GetEquipmentShould.cs b/Bannerlord.ExpandedTemplate.Domain.Tests/EquipmentPool/GetEquipmentShould.cs new file mode 100644 index 0000000..b92d168 --- /dev/null +++ b/Bannerlord.ExpandedTemplate.Domain.Tests/EquipmentPool/GetEquipmentShould.cs @@ -0,0 +1,72 @@ +using System.Xml.Linq; +using Bannerlord.ExpandedTemplate.Domain.EquipmentPool; +using Bannerlord.ExpandedTemplate.Domain.EquipmentPool.Model; +using Bannerlord.ExpandedTemplate.Domain.EquipmentPool.Util; +using Moq; +using NUnit.Framework; + +namespace Bannerlord.ExpandedTemplate.Domain.Tests.EquipmentPool; + +public class GetEquipmentShould +{ + private Mock _random; + + private IGetEquipment _getEquipment; + + [SetUp] + public void SetUp() + { + _random = new Mock(MockBehavior.Strict); + _getEquipment = new GetEquipment(_random.Object); + } + + [Test] + public void GetFirstEquipmentWhenTwoEquipmentTemplatesAndRandomReturnsZero() + { + var equipment = new List + { + new(XDocument.Parse("")), + new(XDocument.Parse("")) + }; + var equipmentPool = new Domain.EquipmentPool.Model.EquipmentPool(equipment, 0); + _random.Setup(random => random.Next(0, 2)).Returns(0); + + var actualEquipment = _getEquipment.GetEquipmentFromEquipmentPool(equipmentPool); + + Assert.That(actualEquipment, Is.EqualTo(equipment[0])); + } + + [Test] + public void GetSecondEquipmentWhenTwoEquipmentTemplatesAndRandomReturnsOne() + { + var equipment = new List + { + new(XDocument.Parse("")), + new(XDocument.Parse("")) + }; + var equipmentPool = new Domain.EquipmentPool.Model.EquipmentPool(equipment, 0); + _random.Setup(random => random.Next(0, 2)).Returns(1); + + var actualEquipment = _getEquipment.GetEquipmentFromEquipmentPool(equipmentPool); + + Assert.That(actualEquipment, Is.EqualTo(equipment[1])); + } + + [Test] + public void ReturnsNullWhenNoEquipmentTemplates() + { + var equipmentPool = new Domain.EquipmentPool.Model.EquipmentPool(new List(), 0); + + var actualEquipment = _getEquipment.GetEquipmentFromEquipmentPool(equipmentPool); + + Assert.That(actualEquipment, Is.Null); + } + + [Test] + public void ReturnsNullWhenNoEquipmentPool() + { + var actualEquipment = _getEquipment.GetEquipmentFromEquipmentPool(null!); + + Assert.That(actualEquipment, Is.Null); + } +} \ No newline at end of file diff --git a/Bannerlord.ExpandedTemplate.Domain/EquipmentPool/GetEquipment.cs b/Bannerlord.ExpandedTemplate.Domain/EquipmentPool/GetEquipment.cs new file mode 100644 index 0000000..d47ea71 --- /dev/null +++ b/Bannerlord.ExpandedTemplate.Domain/EquipmentPool/GetEquipment.cs @@ -0,0 +1,16 @@ +using Bannerlord.ExpandedTemplate.Domain.EquipmentPool.Model; +using Bannerlord.ExpandedTemplate.Domain.EquipmentPool.Util; + +namespace Bannerlord.ExpandedTemplate.Domain.EquipmentPool; + +public class GetEquipment(IRandom random) : IGetEquipment +{ + public Equipment GetEquipmentFromEquipmentPool(Model.EquipmentPool equipmentPool) + { + if (equipmentPool is null || equipmentPool.GetEquipmentLoadouts().Count == 0) return null; + + var randomIndex = random.Next(0, equipmentPool.GetEquipmentLoadouts().Count); + + return equipmentPool.GetEquipmentLoadouts()[randomIndex]; + } +} \ No newline at end of file diff --git a/Bannerlord.ExpandedTemplate.Domain/EquipmentPool/IGetEquipment.cs b/Bannerlord.ExpandedTemplate.Domain/EquipmentPool/IGetEquipment.cs new file mode 100644 index 0000000..4acb8ed --- /dev/null +++ b/Bannerlord.ExpandedTemplate.Domain/EquipmentPool/IGetEquipment.cs @@ -0,0 +1,8 @@ +using Bannerlord.ExpandedTemplate.Domain.EquipmentPool.Model; + +namespace Bannerlord.ExpandedTemplate.Domain.EquipmentPool; + +public interface IGetEquipment +{ + Equipment? GetEquipmentFromEquipmentPool(Model.EquipmentPool equipmentPool); +} \ No newline at end of file diff --git a/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/EquipmentPools/Mappers/EquipmentMapper.cs b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/EquipmentPools/Mappers/EquipmentMapper.cs new file mode 100644 index 0000000..ed66bec --- /dev/null +++ b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/EquipmentPools/Mappers/EquipmentMapper.cs @@ -0,0 +1,61 @@ +using System; +using System.Xml; +using System.Xml.Linq; +using Bannerlord.ExpandedTemplate.Domain.Logging.Port; +using TaleWorlds.Core; +using TaleWorlds.ObjectSystem; + +namespace Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.EquipmentPools.Mappers; + +public class EquipmentMapper(MBObjectManager mbObjectManager, ILoggerFactory loggerFactory) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + public Equipment Map(Domain.EquipmentPool.Model.Equipment equipment, MBEquipmentRoster bannerlordEquipmentPool) + { + XmlNode? xmlEquipmentNode = MapEquipmentNode(equipment.GetEquipmentNode()); + if (xmlEquipmentNode is null) return null; + + if (xmlEquipmentNode.Name.Equals("EquipmentRoster", StringComparison.InvariantCultureIgnoreCase)) + return AddEquipmentNodeToEquipmentRoster(xmlEquipmentNode, bannerlordEquipmentPool); + + return null; + } + + private XmlNode? MapEquipmentNode(XNode node) + { + if (node is null) return null; + var xmlDocument = new XmlDocument(); + xmlDocument.LoadXml(node.ToString()); + return xmlDocument.DocumentElement; + } + + private Equipment AddEquipmentNodeToEquipmentRoster(XmlNode equipmentRosterNode, + MBEquipmentRoster bannerlordEquipmentPool) + { + var equipmentLoadout = + new Equipment(bool.Parse(equipmentRosterNode.Attributes?["civilian"]?.Value ?? "false")); + equipmentLoadout.Deserialize(mbObjectManager, equipmentRosterNode); + + var nativeEquipmentLoadout = + FindMatchingDomainEquipmentInBannerlordEquipmentPool(bannerlordEquipmentPool, equipmentLoadout); + + if (nativeEquipmentLoadout is null) + { + _logger.Error( + $"Could not find {equipmentLoadout} among native '{bannerlordEquipmentPool.StringId}' equipment roster"); + return null; + } + + return nativeEquipmentLoadout; + } + + private Equipment? FindMatchingDomainEquipmentInBannerlordEquipmentPool(MBEquipmentRoster bannerlordEquipmentPool, + Equipment equipment) + { + if (bannerlordEquipmentPool is null) return null; + + return bannerlordEquipmentPool.AllEquipments.Find(nativeEquipmentLoadout => + nativeEquipmentLoadout.IsEquipmentEqualTo(equipment)); + } +} \ No newline at end of file diff --git a/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/EquipmentPools/Mappers/EquipmentPoolsMapper.cs b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/EquipmentPools/Mappers/EquipmentPoolsMapper.cs index 030a262..2d9bb21 100755 --- a/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/EquipmentPools/Mappers/EquipmentPoolsMapper.cs +++ b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/EquipmentPools/Mappers/EquipmentPoolsMapper.cs @@ -1,105 +1,40 @@ -using System; + using System.Linq; using System.Reflection; -using System.Xml; -using System.Xml.Linq; using Bannerlord.ExpandedTemplate.Domain.EquipmentPool.Model; using Bannerlord.ExpandedTemplate.Domain.Logging.Port; using TaleWorlds.Core; using TaleWorlds.Library; -using TaleWorlds.ObjectSystem; using Equipment = TaleWorlds.Core.Equipment; -namespace Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.EquipmentPools.Mappers -{ - public class EquipmentPoolsMapper - { - private readonly MBObjectManager _mbObjectManager; - private readonly ILogger _logger; - - private readonly FieldInfo _mbEquipmentRosterEquipmentsField = - typeof(MBEquipmentRoster).GetField("_equipments", BindingFlags.NonPublic | BindingFlags.Instance); - - public EquipmentPoolsMapper(MBObjectManager mbObjectManager, ILoggerFactory loggerFactory) - { - _mbObjectManager = mbObjectManager; - _logger = loggerFactory.CreateLogger(); - } - - public MBEquipmentRoster MapEquipmentPool(EquipmentPool equipmentPool, - string equipmentId) - { - var mbEquipmentLoadouts = new MBEquipmentRoster(); - var equipmentNodes = equipmentPool.GetEquipmentLoadouts() - .Select(equipmentLoadout => equipmentLoadout.GetEquipmentNode()); - - foreach (var equipmentLoadoutNode in equipmentNodes) - { - var node = MapNode(equipmentLoadoutNode); - if (node is null) continue; - - if (node.Name.Equals("EquipmentRoster", StringComparison.InvariantCultureIgnoreCase)) - AddEquipmentNodeToEquipmentRoster(node, mbEquipmentLoadouts, equipmentId); - else if (node.Name.Equals("EquipmentSet", StringComparison.InvariantCultureIgnoreCase)) - AddReferencedEquipmentsToPool(node, mbEquipmentLoadouts, equipmentId); - } - - return mbEquipmentLoadouts; - } +namespace Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.EquipmentPools.Mappers; - private XmlNode? MapNode(XNode node) - { - XmlDocument xmlDocument = new XmlDocument(); - xmlDocument.LoadXml(node.ToString()); - return xmlDocument.DocumentElement; - } - - private void AddEquipmentNodeToEquipmentRoster(XmlNode equipmentRosterNode, MBEquipmentRoster equipmentRoster, - string equipmentId) - { - var equipmentLoadout = - new Equipment(bool.Parse(equipmentRosterNode.Attributes?["civilian"]?.Value ?? "false")); - equipmentLoadout.Deserialize(_mbObjectManager, equipmentRosterNode); - - var nativeEquipmentLoadout = FindMatchingEquipment(equipmentId, equipmentLoadout); - - if (nativeEquipmentLoadout is null) - { - _logger.Error($"Could not find {equipmentLoadout} among native '{equipmentId}' equipment roster"); - return; - } +public class EquipmentPoolsMapper +{ + private readonly EquipmentMapper _equipmentMapper; - var equipment = (MBList)_mbEquipmentRosterEquipmentsField.GetValue(equipmentRoster); - equipment.Add(nativeEquipmentLoadout); - } + private readonly FieldInfo? _mbEquipmentRosterEquipmentsField = + typeof(MBEquipmentRoster).GetField("_equipments", BindingFlags.NonPublic | BindingFlags.Instance); - private Equipment? FindMatchingEquipment(string equipmentId, Equipment equipment) - { - var nativeEquipmentPool = _mbObjectManager.GetObject(equipmentId); + public EquipmentPoolsMapper(EquipmentMapper equipmentMapper, ILoggerFactory loggerFactory) + { + ILogger logger = loggerFactory.CreateLogger(); + if (_mbEquipmentRosterEquipmentsField is null) + logger.Error("Could not find the _equipment field in the MBEquipmentRoster class via reflection."); + _equipmentMapper = equipmentMapper; + } - if (nativeEquipmentPool is null) return null; + public MBEquipmentRoster MapEquipmentPool(EquipmentPool equipmentPool, + MBEquipmentRoster equipmentPoolWithAllEquipment) + { + if (_mbEquipmentRosterEquipmentsField is null) return new MBEquipmentRoster(); - // TODO: handle use case when nativeEquipmentPool is not found - return nativeEquipmentPool.AllEquipments.Find(nativeEquipmentLoadout => - nativeEquipmentLoadout.IsEquipmentEqualTo(equipment)); - } - - private void AddReferencedEquipmentsToPool(XmlNode referencedEquipmentNode, MBEquipmentRoster equipmentRoster, - string equipmentId) - { - var id = referencedEquipmentNode.Attributes?["id"]?.Value; - if (string.IsNullOrWhiteSpace(id)) - { - AddEquipmentNodeToEquipmentRoster(referencedEquipmentNode, equipmentRoster, equipmentId); - return; - } + var mbEquipmentLoadouts = new MBEquipmentRoster(); + var equipments = (MBList)_mbEquipmentRosterEquipmentsField.GetValue(mbEquipmentLoadouts); - var referencedId = _mbObjectManager.GetObject(id); - if (referencedId is null) return; + equipmentPool.GetEquipmentLoadouts().ToList().ForEach(equipment => + equipments.Add(_equipmentMapper.Map(equipment, equipmentPoolWithAllEquipment))); - bool.TryParse(referencedEquipmentNode.Attributes?["civilian"]?.Value, out var isCivilian); - // add all referenced equipments from the EquipmentSet node to the roster - equipmentRoster.AddEquipmentRoster(referencedId, isCivilian); - } + return mbEquipmentLoadouts; } } \ No newline at end of file diff --git a/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/MissionSpawnEquipmentPoolSetter.cs b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/MissionSpawnEquipmentPoolSetter.cs index 13cef88..85f6743 100755 --- a/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/MissionSpawnEquipmentPoolSetter.cs +++ b/Bannerlord.ExpandedTemplate.Integration/SetSpawnEquipment/MissionLogic/MissionSpawnEquipmentPoolSetter.cs @@ -12,23 +12,29 @@ namespace Bannerlord.ExpandedTemplate.Integration.SetSpawnEquipment.MissionLogic { public class MissionSpawnEquipmentPoolSetter : TaleWorlds.MountAndBlade.MissionLogic { - private readonly FieldInfo _equipmentRosterField = + 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, - EquipmentPoolsMapper equipmentPoolsMapper, ILoggerFactory loggerFactory) + 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)) - loggerFactory.CreateLogger() - .Error( + _logger.Error( "BasicCharacterObject's _mbEquipmentRoster field could not be found preventing equipment pool override in friendly missions"); } @@ -41,13 +47,12 @@ public override void OnBehaviorInitialize() public override void OnAgentCreated(Agent agent) { - if (!CanOverrideEquipment(agent)) - return; + if (_equipmentRosterField is null) return; + if (!CanOverrideEquipment(agent)) return; base.OnAgentCreated(agent); - var equipmentRoster = - (MBEquipmentRoster)_equipmentRosterField.GetValue(agent.Character); + var equipmentRoster = (MBEquipmentRoster)_equipmentRosterField.GetValue(agent.Character); string id = agent.Character.StringId; if (agent.Character is CharacterObject characterObject) @@ -59,14 +64,27 @@ public override void OnAgentCreated(Agent agent) if (equipmentPool.IsEmpty()) equipmentPool = _getEquipmentPool.GetTroopEquipmentPool(equipmentRoster.StringId); - OverrideTroopEquipment(agent, - _equipmentPoolsMapper.MapEquipmentPool(equipmentPool, 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 (!CanOverrideEquipment(agent)) - return; + if (_equipmentRosterField is null) return; + if (!CanOverrideEquipment(agent)) return; base.OnAgentBuild(agent, banner); @@ -84,12 +102,26 @@ private bool CanOverrideEquipment(IAgent agent) private void OverrideTroopEquipment(IAgent agent, MBEquipmentRoster equipmentPool) { - if (agent.Character.IsHero) - agent.Character.Equipment.FillFrom(equipmentPool.AllEquipments.Count > 0 - ? equipmentPool.AllEquipments[0] - : new Equipment()); - else - _equipmentRosterField.SetValue(agent.Character, 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