diff --git a/Content.IntegrationTests/Tests/AI/AiControllerTest.cs b/Content.IntegrationTests/Tests/AI/AiControllerTest.cs deleted file mode 100644 index e5b3916208..0000000000 --- a/Content.IntegrationTests/Tests/AI/AiControllerTest.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Content.Server.GameObjects.Components.Movement; -using Content.Shared.Utility; -using NUnit.Framework; -using Robust.Server.AI; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; -using Robust.Shared.Reflection; - -namespace Content.IntegrationTests.Tests.AI -{ - [TestFixture] - [TestOf(typeof(AiControllerTest))] - public class AiControllerTest : ContentIntegrationTest - { - [Test] - public async Task AiProcessorNamesValidTest() - { - var server = StartServerDummyTicker(); - - server.Assert(() => - { - var entityManager = IoCManager.Resolve(); - var prototypeManager = IoCManager.Resolve(); - var reflectionManager = IoCManager.Resolve(); - var mapManager = IoCManager.Resolve(); - - mapManager.CreateMap(new MapId(1)); - var grid = mapManager.CreateGrid(new MapId(1)); - - var processorNames = new List(); - - // Verify they all have the required attribute - foreach (var processor in reflectionManager.GetAllChildren(typeof(AiLogicProcessor))) - { - var attrib = (AiLogicProcessorAttribute) Attribute.GetCustomAttribute(processor, typeof(AiLogicProcessorAttribute)); - Assert.That(attrib != null, $"No AiLogicProcessorAttribute found on {processor.Name}"); - processorNames.Add(attrib.SerializeName); - } - - foreach (var entity in prototypeManager.EnumeratePrototypes()) - { - var comps = entity.Components; - - if (!comps.ContainsKey("AiController")) continue; - - var aiEntity = entityManager.SpawnEntity(entity.ID, grid.ToCoordinates()); - var aiController = aiEntity.GetComponent(); - Assert.That(processorNames.Contains(aiController.LogicName), $"Could not find valid processor named {aiController.LogicName} on entity {entity.ID}"); - } - }); - - await server.WaitIdleAsync(); - } - } -} diff --git a/Content.IntegrationTests/Tests/AI/BehaviorSetsTest.cs b/Content.IntegrationTests/Tests/AI/BehaviorSetsTest.cs new file mode 100644 index 0000000000..683996524a --- /dev/null +++ b/Content.IntegrationTests/Tests/AI/BehaviorSetsTest.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Content.Server.AI.Utility; +using Content.Server.AI.Utility.Actions; +using NUnit.Framework; +using Robust.Shared.Prototypes; +using Robust.Shared.Reflection; +using YamlDotNet.RepresentationModel; + +namespace Content.IntegrationTests.Tests.AI +{ + [TestFixture] + [TestOf(typeof(BehaviorSetPrototype))] + public class BehaviorSetsTest : ContentIntegrationTest + { + [Test] + public async Task TestBehaviorSets() + { + var options = new ServerIntegrationOptions(); + var server = StartServerDummyTicker(options); + await server.WaitIdleAsync(); + + var protoManager = server.ResolveDependency(); + var reflectionManager = server.ResolveDependency(); + + Dictionary> behaviorSets = new(); + + // Test that all BehaviorSet actions exist. + await server.WaitAssertion(() => + { + foreach (var proto in protoManager.EnumeratePrototypes()) + { + behaviorSets[proto.ID] = proto.Actions.ToList(); + + foreach (var action in proto.Actions) + { + if (!reflectionManager.TryLooseGetType(action, out var actionType) || + !typeof(IAiUtility).IsAssignableFrom(actionType)) + { + Assert.Fail($"Action {action} is not valid within BehaviorSet {proto.ID}"); + } + } + } + }); + + // Test that all BehaviorSets on NPCs exist. + await server.WaitAssertion(() => + { + foreach (var entity in protoManager.EnumeratePrototypes()) + { + if (!entity.Components.TryGetValue("UtilityAI", out var npcNode)) continue; + var sets = npcNode["behaviorSets"]; + + foreach (var entry in (YamlSequenceNode) sets) + { + Assert.That(behaviorSets.ContainsKey(entry.ToString()), $"BehaviorSet {entry} in entity {entity.ID} not found"); + } + } + }); + } + } +} diff --git a/Content.IntegrationTests/Tests/AI/SpriteTest.cs b/Content.IntegrationTests/Tests/SpriteTest.cs similarity index 97% rename from Content.IntegrationTests/Tests/AI/SpriteTest.cs rename to Content.IntegrationTests/Tests/SpriteTest.cs index 12f5f58474..6544b10ec9 100644 --- a/Content.IntegrationTests/Tests/AI/SpriteTest.cs +++ b/Content.IntegrationTests/Tests/SpriteTest.cs @@ -7,7 +7,7 @@ using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Prototypes; -namespace Content.IntegrationTests.Tests.AI +namespace Content.IntegrationTests.Tests { [TestFixture] public sealed class SpriteTest : ContentIntegrationTest @@ -21,7 +21,7 @@ namespace Content.IntegrationTests.Tests.AI var (client, server) = await StartConnectedServerClientPair(); await client.WaitIdleAsync(); await server.WaitIdleAsync(); - + var resc = client.ResolveDependency(); var entityManager = client.ResolveDependency(); var mapManager = client.ResolveDependency(); @@ -58,4 +58,4 @@ namespace Content.IntegrationTests.Tests.AI await client.WaitIdleAsync(); } } -} \ No newline at end of file +} diff --git a/Content.Server/AI/Operators/AiOperator.cs b/Content.Server/AI/Operators/AiOperator.cs index 975eb90e1b..cf7a264bd2 100644 --- a/Content.Server/AI/Operators/AiOperator.cs +++ b/Content.Server/AI/Operators/AiOperator.cs @@ -8,7 +8,7 @@ namespace Content.Server.AI.Operators public bool HasShutdown { get; private set; } /// - /// Called once when the AiLogicProcessor starts this action + /// Called once when the NPC starts this action /// /// true if it hasn't started up previously public virtual bool Startup() @@ -24,7 +24,7 @@ namespace Content.Server.AI.Operators } /// - /// Called once when the AiLogicProcessor is done with this action if the outcome is successful or fails. + /// Called once when the NPC is done with this action if the outcome is successful or fails. /// public virtual bool Shutdown(Outcome outcome) { diff --git a/Content.Server/AI/Utility/Actions/Clothing/Gloves/EquipGloves.cs b/Content.Server/AI/Utility/Actions/Clothing/Gloves/EquipGloves.cs index 8d0658c7ff..a7a28e4913 100644 --- a/Content.Server/AI/Utility/Actions/Clothing/Gloves/EquipGloves.cs +++ b/Content.Server/AI/Utility/Actions/Clothing/Gloves/EquipGloves.cs @@ -13,27 +13,21 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Gloves { public sealed class EquipGloves : UtilityAction { - private readonly IEntity _entity; - - public EquipGloves(IEntity owner, IEntity entity, float weight) : base(owner) - { - _entity = entity; - Bonus = weight; - } + public IEntity Target { get; set; } public override void SetupOperators(Blackboard context) { ActionOperators = new Queue(new AiOperator[] { - new EquipEntityOperator(Owner, _entity), - new UseItemInInventoryOperator(Owner, _entity), + new EquipEntityOperator(Owner, Target), + new UseItemInInventoryOperator(Owner, Target), }); } protected override void UpdateBlackboard(Blackboard context) { base.UpdateBlackboard(context); - context.GetState().SetValue(_entity); + context.GetState().SetValue(Target); } protected override IReadOnlyCollection> GetConsiderations(Blackboard context) diff --git a/Content.Server/AI/Utility/Actions/Clothing/Gloves/PickUpGloves.cs b/Content.Server/AI/Utility/Actions/Clothing/Gloves/PickUpGloves.cs index 744ddf0c1c..184af45b0f 100644 --- a/Content.Server/AI/Utility/Actions/Clothing/Gloves/PickUpGloves.cs +++ b/Content.Server/AI/Utility/Actions/Clothing/Gloves/PickUpGloves.cs @@ -14,25 +14,19 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Gloves { public sealed class PickUpGloves : UtilityAction { - private readonly IEntity _entity; - - public PickUpGloves(IEntity owner, IEntity entity, float weight) : base(owner) - { - _entity = entity; - Bonus = weight; - } + public IEntity Target { get; set; } public override void SetupOperators(Blackboard context) { - ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence; } protected override void UpdateBlackboard(Blackboard context) { base.UpdateBlackboard(context); - context.GetState().SetValue(_entity); + context.GetState().SetValue(Target); } - + protected override IReadOnlyCollection> GetConsiderations(Blackboard context) { var considerationsManager = IoCManager.Resolve(); diff --git a/Content.Server/AI/Utility/Actions/Clothing/Head/EquipHead.cs b/Content.Server/AI/Utility/Actions/Clothing/Head/EquipHead.cs index 893b3daa48..b1a3a376fe 100644 --- a/Content.Server/AI/Utility/Actions/Clothing/Head/EquipHead.cs +++ b/Content.Server/AI/Utility/Actions/Clothing/Head/EquipHead.cs @@ -13,27 +13,21 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Head { public sealed class EquipHead : UtilityAction { - private readonly IEntity _entity; - - public EquipHead(IEntity owner, IEntity entity, float weight) : base(owner) - { - _entity = entity; - Bonus = weight; - } + public IEntity Target { get; set; } public override void SetupOperators(Blackboard context) { ActionOperators = new Queue(new AiOperator[] { - new EquipEntityOperator(Owner, _entity), - new UseItemInInventoryOperator(Owner, _entity), + new EquipEntityOperator(Owner, Target), + new UseItemInInventoryOperator(Owner, Target), }); } protected override void UpdateBlackboard(Blackboard context) { base.UpdateBlackboard(context); - context.GetState().SetValue(_entity); + context.GetState().SetValue(Target); } protected override IReadOnlyCollection> GetConsiderations(Blackboard context) diff --git a/Content.Server/AI/Utility/Actions/Clothing/Head/PickUpHead.cs b/Content.Server/AI/Utility/Actions/Clothing/Head/PickUpHead.cs index fc93ab6313..86e5d28493 100644 --- a/Content.Server/AI/Utility/Actions/Clothing/Head/PickUpHead.cs +++ b/Content.Server/AI/Utility/Actions/Clothing/Head/PickUpHead.cs @@ -14,23 +14,17 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Head { public sealed class PickUpHead : UtilityAction { - private readonly IEntity _entity; - - public PickUpHead(IEntity owner, IEntity entity, float weight) : base(owner) - { - _entity = entity; - Bonus = weight; - } + public IEntity Target { get; set; } public override void SetupOperators(Blackboard context) { - ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence; } protected override void UpdateBlackboard(Blackboard context) { base.UpdateBlackboard(context); - context.GetState().SetValue(_entity); + context.GetState().SetValue(Target); } protected override IReadOnlyCollection> GetConsiderations(Blackboard context) diff --git a/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/EquipOuterClothing.cs b/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/EquipOuterClothing.cs index df2d3068e0..5e1afb7d91 100644 --- a/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/EquipOuterClothing.cs +++ b/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/EquipOuterClothing.cs @@ -13,27 +13,21 @@ namespace Content.Server.AI.Utility.Actions.Clothing.OuterClothing { public sealed class EquipOuterClothing : UtilityAction { - private readonly IEntity _entity; - - public EquipOuterClothing(IEntity owner, IEntity entity, float weight) : base(owner) - { - _entity = entity; - Bonus = weight; - } + public IEntity Target { get; set; } public override void SetupOperators(Blackboard context) { ActionOperators = new Queue(new AiOperator[] { - new EquipEntityOperator(Owner, _entity), - new UseItemInInventoryOperator(Owner, _entity), + new EquipEntityOperator(Owner, Target), + new UseItemInInventoryOperator(Owner, Target), }); } protected override void UpdateBlackboard(Blackboard context) { base.UpdateBlackboard(context); - context.GetState().SetValue(_entity); + context.GetState().SetValue(Target); } protected override IReadOnlyCollection> GetConsiderations(Blackboard context) diff --git a/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/PickUpOuterClothing.cs b/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/PickUpOuterClothing.cs index 4b23ab4fa1..ee74b40c76 100644 --- a/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/PickUpOuterClothing.cs +++ b/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/PickUpOuterClothing.cs @@ -14,23 +14,17 @@ namespace Content.Server.AI.Utility.Actions.Clothing.OuterClothing { public sealed class PickUpOuterClothing : UtilityAction { - private readonly IEntity _entity; - - public PickUpOuterClothing(IEntity owner, IEntity entity, float weight) : base(owner) - { - _entity = entity; - Bonus = weight; - } + public IEntity Target { get; set; } public override void SetupOperators(Blackboard context) { - ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence; } protected override void UpdateBlackboard(Blackboard context) { base.UpdateBlackboard(context); - context.GetState().SetValue(_entity); + context.GetState().SetValue(Target); } protected override IReadOnlyCollection> GetConsiderations(Blackboard context) diff --git a/Content.Server/AI/Utility/Actions/Clothing/Shoes/EquipShoes.cs b/Content.Server/AI/Utility/Actions/Clothing/Shoes/EquipShoes.cs index af63780559..6ba9ca0d41 100644 --- a/Content.Server/AI/Utility/Actions/Clothing/Shoes/EquipShoes.cs +++ b/Content.Server/AI/Utility/Actions/Clothing/Shoes/EquipShoes.cs @@ -13,27 +13,21 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Shoes { public sealed class EquipShoes : UtilityAction { - private readonly IEntity _entity; - - public EquipShoes(IEntity owner, IEntity entity, float weight) : base(owner) - { - _entity = entity; - Bonus = weight; - } + public IEntity Target { get; set; } public override void SetupOperators(Blackboard context) { ActionOperators = new Queue(new AiOperator[] { - new EquipEntityOperator(Owner, _entity), - new UseItemInInventoryOperator(Owner, _entity), + new EquipEntityOperator(Owner, Target), + new UseItemInInventoryOperator(Owner, Target), }); } protected override void UpdateBlackboard(Blackboard context) { base.UpdateBlackboard(context); - context.GetState().SetValue(_entity); + context.GetState().SetValue(Target); } protected override IReadOnlyCollection> GetConsiderations(Blackboard context) diff --git a/Content.Server/AI/Utility/Actions/Clothing/Shoes/PickUpShoes.cs b/Content.Server/AI/Utility/Actions/Clothing/Shoes/PickUpShoes.cs index afe505873a..bc11486eaf 100644 --- a/Content.Server/AI/Utility/Actions/Clothing/Shoes/PickUpShoes.cs +++ b/Content.Server/AI/Utility/Actions/Clothing/Shoes/PickUpShoes.cs @@ -14,23 +14,17 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Shoes { public sealed class PickUpShoes : UtilityAction { - private readonly IEntity _entity; - - public PickUpShoes(IEntity owner, IEntity entity, float weight) : base(owner) - { - _entity = entity; - Bonus = weight; - } + public IEntity Target { get; set; } public override void SetupOperators(Blackboard context) { - ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence; } protected override void UpdateBlackboard(Blackboard context) { base.UpdateBlackboard(context); - context.GetState().SetValue(_entity); + context.GetState().SetValue(Target); } protected override IReadOnlyCollection> GetConsiderations(Blackboard context) diff --git a/Content.Server/AI/Utility/Actions/Combat/Melee/EquipMelee.cs b/Content.Server/AI/Utility/Actions/Combat/Melee/EquipMelee.cs index 19a7610a24..da40762efd 100644 --- a/Content.Server/AI/Utility/Actions/Combat/Melee/EquipMelee.cs +++ b/Content.Server/AI/Utility/Actions/Combat/Melee/EquipMelee.cs @@ -15,27 +15,21 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee { public sealed class EquipMelee : UtilityAction { - private readonly IEntity _entity; - - public EquipMelee(IEntity owner, IEntity entity, float weight) : base(owner) - { - _entity = entity; - Bonus = weight; - } + public IEntity Target { get; set; } public override void SetupOperators(Blackboard context) { ActionOperators = new Queue(new AiOperator[] { - new EquipEntityOperator(Owner, _entity) + new EquipEntityOperator(Owner, Target) }); } protected override void UpdateBlackboard(Blackboard context) { base.UpdateBlackboard(context); - context.GetState().SetValue(_entity); - context.GetState().SetValue(_entity); + context.GetState().SetValue(Target); + context.GetState().SetValue(Target); } protected override IReadOnlyCollection> GetConsiderations(Blackboard context) diff --git a/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeWeaponAttackEntity.cs b/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeWeaponAttackEntity.cs index 4b2d2d054c..7f22e786b2 100644 --- a/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeWeaponAttackEntity.cs +++ b/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeWeaponAttackEntity.cs @@ -21,13 +21,7 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee { public sealed class MeleeWeaponAttackEntity : UtilityAction { - private readonly IEntity _entity; - - public MeleeWeaponAttackEntity(IEntity owner, IEntity entity, float weight) : base(owner) - { - _entity = entity; - Bonus = weight; - } + public IEntity Target { get; set; } public override void SetupOperators(Blackboard context) { @@ -35,26 +29,26 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee var equipped = context.GetState().GetValue(); if (equipped != null && equipped.TryGetComponent(out MeleeWeaponComponent meleeWeaponComponent)) { - moveOperator = new MoveToEntityOperator(Owner, _entity, meleeWeaponComponent.Range - 0.01f); + moveOperator = new MoveToEntityOperator(Owner, Target, meleeWeaponComponent.Range - 0.01f); } else { // TODO: Abort - moveOperator = new MoveToEntityOperator(Owner, _entity); + moveOperator = new MoveToEntityOperator(Owner, Target); } ActionOperators = new Queue(new AiOperator[] { moveOperator, - new SwingMeleeWeaponOperator(Owner, _entity), + new SwingMeleeWeaponOperator(Owner, Target), }); } protected override void UpdateBlackboard(Blackboard context) { base.UpdateBlackboard(context); - context.GetState().SetValue(_entity); - context.GetState().SetValue(_entity); + context.GetState().SetValue(Target); + context.GetState().SetValue(Target); var equipped = context.GetState().GetValue(); context.GetState().SetValue(equipped); } diff --git a/Content.Server/AI/Utility/Actions/Combat/Melee/PickUpMeleeWeapon.cs b/Content.Server/AI/Utility/Actions/Combat/Melee/PickUpMeleeWeapon.cs index dde7602dd8..a988d7c8d6 100644 --- a/Content.Server/AI/Utility/Actions/Combat/Melee/PickUpMeleeWeapon.cs +++ b/Content.Server/AI/Utility/Actions/Combat/Melee/PickUpMeleeWeapon.cs @@ -15,24 +15,18 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee { public sealed class PickUpMeleeWeapon : UtilityAction { - private readonly IEntity _entity; - - public PickUpMeleeWeapon(IEntity owner, IEntity entity, float weight) : base(owner) - { - _entity = entity; - Bonus = weight; - } + public IEntity Target { get; set; } public override void SetupOperators(Blackboard context) { - ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence; } protected override void UpdateBlackboard(Blackboard context) { base.UpdateBlackboard(context); - context.GetState().SetValue(_entity); - context.GetState().SetValue(_entity); + context.GetState().SetValue(Target); + context.GetState().SetValue(Target); } protected override IReadOnlyCollection> GetConsiderations(Blackboard context) diff --git a/Content.Server/AI/Utility/Actions/Combat/Melee/UnarmedAttackEntity.cs b/Content.Server/AI/Utility/Actions/Combat/Melee/UnarmedAttackEntity.cs index fead7a4db8..46ae25d0e6 100644 --- a/Content.Server/AI/Utility/Actions/Combat/Melee/UnarmedAttackEntity.cs +++ b/Content.Server/AI/Utility/Actions/Combat/Melee/UnarmedAttackEntity.cs @@ -19,40 +19,34 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee { public sealed class UnarmedAttackEntity : UtilityAction { - private readonly IEntity _entity; - - public UnarmedAttackEntity(IEntity owner, IEntity entity, float weight) : base(owner) - { - _entity = entity; - Bonus = weight; - } + public IEntity Target { get; set; } public override void SetupOperators(Blackboard context) { MoveToEntityOperator moveOperator; if (Owner.TryGetComponent(out UnarmedCombatComponent unarmedCombatComponent)) { - moveOperator = new MoveToEntityOperator(Owner, _entity, unarmedCombatComponent.Range - 0.01f); + moveOperator = new MoveToEntityOperator(Owner, Target, unarmedCombatComponent.Range - 0.01f); } // I think it's possible for this to happen given planning is time-sliced? // TODO: At this point we should abort else { - moveOperator = new MoveToEntityOperator(Owner, _entity); + moveOperator = new MoveToEntityOperator(Owner, Target); } ActionOperators = new Queue(new AiOperator[] { moveOperator, - new UnarmedCombatOperator(Owner, _entity), + new UnarmedCombatOperator(Owner, Target), }); } protected override void UpdateBlackboard(Blackboard context) { base.UpdateBlackboard(context); - context.GetState().SetValue(_entity); - context.GetState().SetValue(_entity); + context.GetState().SetValue(Target); + context.GetState().SetValue(Target); // Can just set ourselves as entity given unarmed just inherits from meleeweapon context.GetState().SetValue(Owner); } diff --git a/Content.Server/AI/Utility/Actions/IAiUtility.cs b/Content.Server/AI/Utility/Actions/IAiUtility.cs index 6eef451235..c09989707c 100644 --- a/Content.Server/AI/Utility/Actions/IAiUtility.cs +++ b/Content.Server/AI/Utility/Actions/IAiUtility.cs @@ -1,7 +1,17 @@ +using Robust.Shared.GameObjects; + namespace Content.Server.AI.Utility.Actions { public interface IAiUtility { + /// + /// NPC this action is attached to. + /// + IEntity Owner { get; set; } + + /// + /// Highest possible score for this action. + /// float Bonus { get; } } } diff --git a/Content.Server/AI/Utility/Actions/Idle/CloseLastEntityStorage.cs b/Content.Server/AI/Utility/Actions/Idle/CloseLastEntityStorage.cs index 703b2f0429..6dfb1821a9 100644 --- a/Content.Server/AI/Utility/Actions/Idle/CloseLastEntityStorage.cs +++ b/Content.Server/AI/Utility/Actions/Idle/CloseLastEntityStorage.cs @@ -21,17 +21,15 @@ namespace Content.Server.AI.Utility.Actions.Idle public sealed class CloseLastEntityStorage : UtilityAction { public override float Bonus => IdleBonus + 0.01f; - - public CloseLastEntityStorage(IEntity owner) : base(owner) {} public override void SetupOperators(Blackboard context) { var lastStorage = context.GetState().GetValue(); - + ActionOperators = new Queue(new AiOperator[] { new MoveToEntityOperator(Owner, lastStorage), - new CloseLastStorageOperator(Owner), + new CloseLastStorageOperator(Owner), }); } diff --git a/Content.Server/AI/Utility/Actions/Idle/WanderAndWait.cs b/Content.Server/AI/Utility/Actions/Idle/WanderAndWait.cs index 6f491d2fdd..e1784ceff9 100644 --- a/Content.Server/AI/Utility/Actions/Idle/WanderAndWait.cs +++ b/Content.Server/AI/Utility/Actions/Idle/WanderAndWait.cs @@ -23,8 +23,6 @@ namespace Content.Server.AI.Utility.Actions.Idle public override bool CanOverride => false; public override float Bonus => 1.0f; - public WanderAndWait(IEntity owner) : base(owner) {} - public override void SetupOperators(Blackboard context) { var robustRandom = IoCManager.Resolve(); diff --git a/Content.Server/AI/Utility/Actions/Nutrition/Drink/PickUpDrink.cs b/Content.Server/AI/Utility/Actions/Nutrition/Drink/PickUpDrink.cs index f2ec596184..8a7fc8ae15 100644 --- a/Content.Server/AI/Utility/Actions/Nutrition/Drink/PickUpDrink.cs +++ b/Content.Server/AI/Utility/Actions/Nutrition/Drink/PickUpDrink.cs @@ -14,23 +14,17 @@ namespace Content.Server.AI.Utility.Actions.Nutrition.Drink { public sealed class PickUpDrink : UtilityAction { - private readonly IEntity _entity; - - public PickUpDrink(IEntity owner, IEntity entity, float weight) : base(owner) - { - _entity = entity; - Bonus = weight; - } + public IEntity Target { get; set; } public override void SetupOperators(Blackboard context) { - ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence; } protected override void UpdateBlackboard(Blackboard context) { base.UpdateBlackboard(context); - context.GetState().SetValue(_entity); + context.GetState().SetValue(Target); } protected override IReadOnlyCollection> GetConsiderations(Blackboard context) diff --git a/Content.Server/AI/Utility/Actions/Nutrition/Drink/UseDrinkInInventory.cs b/Content.Server/AI/Utility/Actions/Nutrition/Drink/UseDrinkInInventory.cs index 4f71379169..a9cfc2a334 100644 --- a/Content.Server/AI/Utility/Actions/Nutrition/Drink/UseDrinkInInventory.cs +++ b/Content.Server/AI/Utility/Actions/Nutrition/Drink/UseDrinkInInventory.cs @@ -15,27 +15,21 @@ namespace Content.Server.AI.Utility.Actions.Nutrition.Drink { public sealed class UseDrinkInInventory : UtilityAction { - private readonly IEntity _entity; - - public UseDrinkInInventory(IEntity owner, IEntity entity, float weight) : base(owner) - { - _entity = entity; - Bonus = weight; - } + public IEntity Target { get; set; } public override void SetupOperators(Blackboard context) { ActionOperators = new Queue(new AiOperator[] { - new EquipEntityOperator(Owner, _entity), - new UseDrinkInInventoryOperator(Owner, _entity), + new EquipEntityOperator(Owner, Target), + new UseDrinkInInventoryOperator(Owner, Target), }); } protected override void UpdateBlackboard(Blackboard context) { base.UpdateBlackboard(context); - context.GetState().SetValue(_entity); + context.GetState().SetValue(Target); } protected override IReadOnlyCollection> GetConsiderations(Blackboard context) diff --git a/Content.Server/AI/Utility/Actions/Nutrition/Food/PickUpFood.cs b/Content.Server/AI/Utility/Actions/Nutrition/Food/PickUpFood.cs index 2c5ae8e219..8aa6a5be90 100644 --- a/Content.Server/AI/Utility/Actions/Nutrition/Food/PickUpFood.cs +++ b/Content.Server/AI/Utility/Actions/Nutrition/Food/PickUpFood.cs @@ -14,23 +14,17 @@ namespace Content.Server.AI.Utility.Actions.Nutrition.Food { public sealed class PickUpFood : UtilityAction { - private readonly IEntity _entity; - - public PickUpFood(IEntity owner, IEntity entity, float weight) : base(owner) - { - _entity = entity; - Bonus = weight; - } + public IEntity Target { get; set; } public override void SetupOperators(Blackboard context) { - ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence; } protected override void UpdateBlackboard(Blackboard context) { base.UpdateBlackboard(context); - context.GetState().SetValue(_entity); + context.GetState().SetValue(Target); } protected override IReadOnlyCollection> GetConsiderations(Blackboard context) diff --git a/Content.Server/AI/Utility/Actions/Nutrition/Food/UseFoodInInventory.cs b/Content.Server/AI/Utility/Actions/Nutrition/Food/UseFoodInInventory.cs index 6e813ccc7c..dc7462c5ab 100644 --- a/Content.Server/AI/Utility/Actions/Nutrition/Food/UseFoodInInventory.cs +++ b/Content.Server/AI/Utility/Actions/Nutrition/Food/UseFoodInInventory.cs @@ -15,27 +15,21 @@ namespace Content.Server.AI.Utility.Actions.Nutrition.Food { public sealed class UseFoodInInventory : UtilityAction { - private readonly IEntity _entity; - - public UseFoodInInventory(IEntity owner, IEntity entity, float weight) : base(owner) - { - _entity = entity; - Bonus = weight; - } + public IEntity Target { get; set; } public override void SetupOperators(Blackboard context) { ActionOperators = new Queue(new AiOperator[] { - new EquipEntityOperator(Owner, _entity), - new UseFoodInInventoryOperator(Owner, _entity), + new EquipEntityOperator(Owner, Target), + new UseFoodInInventoryOperator(Owner, Target), }); } protected override void UpdateBlackboard(Blackboard context) { base.UpdateBlackboard(context); - context.GetState().SetValue(_entity); + context.GetState().SetValue(Target); } protected override IReadOnlyCollection> GetConsiderations(Blackboard context) diff --git a/Content.Server/AI/Utility/Actions/Test/MoveRightAndLeftTen.cs b/Content.Server/AI/Utility/Actions/Test/MoveRightAndLeftTen.cs index 82b8c0e738..57ea73172b 100644 --- a/Content.Server/AI/Utility/Actions/Test/MoveRightAndLeftTen.cs +++ b/Content.Server/AI/Utility/Actions/Test/MoveRightAndLeftTen.cs @@ -17,8 +17,6 @@ namespace Content.Server.AI.Utility.Actions.Test { public override bool CanOverride => false; - public MoveRightAndLeftTen(IEntity owner) : base(owner) {} - public override void SetupOperators(Blackboard context) { var currentPosition = Owner.Transform.Coordinates; diff --git a/Content.Server/AI/Utility/Actions/UtilityAction.cs b/Content.Server/AI/Utility/Actions/UtilityAction.cs index 2ab431acf7..5ff9e5f5a6 100644 --- a/Content.Server/AI/Utility/Actions/UtilityAction.cs +++ b/Content.Server/AI/Utility/Actions/UtilityAction.cs @@ -24,7 +24,7 @@ namespace Content.Server.AI.Utility.Actions /// Threshold doesn't necessarily mean we'll do an action at a higher threshold; /// if it's really un-optimal (i.e. low score) then we'll also check lower tiers /// - public virtual float Bonus { get; protected set; } = IdleBonus; + public virtual float Bonus { get; set; } = IdleBonus; // For GW2 they had the bonuses close together but IMO it feels better when they're more like discrete tiers. // These are just baselines to make mass-updates easier; actions can do whatever @@ -36,7 +36,7 @@ namespace Content.Server.AI.Utility.Actions public const float CombatBonus = 30.0f; public const float DangerBonus = 50.0f; - protected IEntity Owner { get; } + public IEntity Owner { get; set; } /// /// All the considerations are multiplied together to get the final score; a consideration of 0.0 means the action is not possible. @@ -58,10 +58,8 @@ namespace Content.Server.AI.Utility.Actions /// protected virtual void UpdateBlackboard(Blackboard context) {} - protected UtilityAction(IEntity owner) - { - Owner = owner; - } + // Needs to be able to be instantiated without args via typefactory. + public UtilityAction() {} public virtual void Shutdown() {} diff --git a/Content.Server/AI/Utility/AiLogic/Civilian.cs b/Content.Server/AI/Utility/AiLogic/Civilian.cs deleted file mode 100644 index 03c621e8a2..0000000000 --- a/Content.Server/AI/Utility/AiLogic/Civilian.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Content.Server.AI.Utility.BehaviorSets; -using JetBrains.Annotations; -using Robust.Server.AI; - -namespace Content.Server.AI.Utility.AiLogic -{ - [AiLogicProcessor("Civilian")] - [UsedImplicitly] - public sealed class Civilian : UtilityAi - { - public override void Setup() - { - base.Setup(); - AddBehaviorSet(new ClothingBehaviorSet(SelfEntity), false); - AddBehaviorSet(new HungerBehaviorSet(SelfEntity), false); - AddBehaviorSet(new ThirstBehaviorSet(SelfEntity), false); - AddBehaviorSet(new IdleBehaviorSet(SelfEntity), false); - SortActions(); - } - } -} diff --git a/Content.Server/AI/Utility/AiLogic/Mimic.cs b/Content.Server/AI/Utility/AiLogic/Mimic.cs deleted file mode 100644 index bc8ecb9f28..0000000000 --- a/Content.Server/AI/Utility/AiLogic/Mimic.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Server.AI.Utility.BehaviorSets; -using JetBrains.Annotations; -using Robust.Server.AI; - -namespace Content.Server.AI.Utility.AiLogic -{ - [AiLogicProcessor("Mimic")] - [UsedImplicitly] - public sealed class Mimic : UtilityAi - { - public override void Setup() - { - base.Setup(); - AddBehaviorSet(new UnarmedAttackPlayersBehaviorSet(SelfEntity), false); - SortActions(); - } - } -} \ No newline at end of file diff --git a/Content.Server/AI/Utility/AiLogic/PathingDummy.cs b/Content.Server/AI/Utility/AiLogic/PathingDummy.cs deleted file mode 100644 index 3c493021b3..0000000000 --- a/Content.Server/AI/Utility/AiLogic/PathingDummy.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Server.AI.Utility.BehaviorSets; -using JetBrains.Annotations; -using Robust.Server.AI; - -namespace Content.Server.AI.Utility.AiLogic -{ - [AiLogicProcessor("PathingDummy")] - [UsedImplicitly] - public sealed class PathingDummy : UtilityAi - { - public override void Setup() - { - base.Setup(); - BehaviorSets.Add(typeof(PathingDummyBehaviorSet), new PathingDummyBehaviorSet(SelfEntity)); - SortActions(); - } - } -} diff --git a/Content.Server/AI/Utility/AiLogic/Spirate.cs b/Content.Server/AI/Utility/AiLogic/Spirate.cs deleted file mode 100644 index 9a8935f57f..0000000000 --- a/Content.Server/AI/Utility/AiLogic/Spirate.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Content.Server.AI.Utility.BehaviorSets; -using JetBrains.Annotations; -using Robust.Server.AI; - -namespace Content.Server.AI.Utility.AiLogic -{ - [AiLogicProcessor("Spirate")] - [UsedImplicitly] - public sealed class Spirate : UtilityAi - { - public override void Setup() - { - base.Setup(); - AddBehaviorSet(new ClothingBehaviorSet(SelfEntity), false); - AddBehaviorSet(new HungerBehaviorSet(SelfEntity), false); - AddBehaviorSet(new ThirstBehaviorSet(SelfEntity), false); - AddBehaviorSet(new IdleBehaviorSet(SelfEntity), false); - AddBehaviorSet(new SpirateBehaviorSet(SelfEntity), false); - SortActions(); - } - } -} diff --git a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs index cb2936ebba..45a88d0a0f 100644 --- a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs +++ b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs @@ -3,23 +3,28 @@ using System.Collections.Generic; using System.Threading; using Content.Server.AI.Operators; using Content.Server.AI.Utility.Actions; -using Content.Server.AI.Utility.BehaviorSets; using Content.Server.AI.WorldState; using Content.Server.AI.WorldState.States.Utility; +using Content.Server.GameObjects.Components.Movement; using Content.Server.GameObjects.EntitySystems.AI; using Content.Server.GameObjects.EntitySystems.AI.LoadBalancer; using Content.Server.GameObjects.EntitySystems.JobQueues; using Content.Shared.GameObjects.Components.Mobs.State; -using Robust.Server.AI; +using Content.Shared.GameObjects.Components.Movement; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; -using Robust.Shared.Utility; +using Robust.Shared.Serialization; namespace Content.Server.AI.Utility.AiLogic { - public abstract class UtilityAi : AiLogicProcessor + // TODO: Need to split out the IMover stuff for NPC to a generic one that can be used for hoomans as well. + [RegisterComponent] + [ComponentReference(typeof(AiControllerComponent)), ComponentReference(typeof(IMoverComponent))] + internal sealed class UtilityAi : AiControllerComponent { + public override string Name => "UtilityAI"; + // TODO: Look at having ParallelOperators (probably no more than that as then you'd have a full-blown BT) // Also RepeatOperators (e.g. if we're following an entity keep repeating MoveToEntity) private AiActionSystem _planner; @@ -27,10 +32,11 @@ namespace Content.Server.AI.Utility.AiLogic private Blackboard _blackboard; /// - /// The sum of all BehaviorSets gives us what actions the AI can take + /// The sum of all BehaviorSets gives us what actions the AI can take /// - public Dictionary BehaviorSets { get; } = new(); - private readonly List _availableActions = new(); + public HashSet BehaviorSets { get; } = new(); + + public List AvailableActions { get; set; } = new(); /// /// The currently running action; most importantly are the operators. @@ -52,86 +58,39 @@ namespace Content.Server.AI.Utility.AiLogic private CancellationTokenSource _actionCancellation; /// - /// If we can't do anything then stop thinking; should probably use ActionBlocker instead + /// If we can't do anything then stop thinking; should probably use ActionBlocker instead /// - private bool _isDead = false; + private bool _isDead; - // These 2 methods will be used eventually if / when we get a director AI - public void AddBehaviorSet(T behaviorSet, bool sort = true) where T : BehaviorSet + public override void ExposeData(ObjectSerializer serializer) { - if (BehaviorSets.TryAdd(typeof(T), behaviorSet) && sort) - { - SortActions(); - } + base.ExposeData(serializer); + var bSets = serializer.ReadDataField("behaviorSets", new List()); - if (BehaviorSets.Count == 1 && !EntitySystem.Get().IsAwake(this)) + if (bSets.Count > 0) { - IoCManager.Resolve() - .EventBus - .RaiseEvent(EventSource.Local, new SleepAiMessage(this, false)); - } - } + var behaviorManager = IoCManager.Resolve(); - public void RemoveBehaviorSet(Type behaviorSet) - { - DebugTools.Assert(behaviorSet.IsAssignableFrom(typeof(BehaviorSet))); - - if (BehaviorSets.ContainsKey(behaviorSet)) - { - BehaviorSets.Remove(behaviorSet); - SortActions(); - } - - if (BehaviorSets.Count == 0) - { - IoCManager.Resolve() - .EventBus - .RaiseEvent(EventSource.Local, new SleepAiMessage(this, true)); - } - } - - /// - /// Whenever the behavior sets are changed we'll re-sort the actions by bonus - /// - protected void SortActions() - { - _availableActions.Clear(); - foreach (var set in BehaviorSets.Values) - { - foreach (var action in set.Actions) + foreach (var bSet in bSets) { - var found = false; - - for (var i = 0; i < _availableActions.Count; i++) - { - if (_availableActions[i].Bonus < action.Bonus) - { - _availableActions.Insert(i, action); - found = true; - break; - } - } - - if (!found) - { - _availableActions.Add(action); - } + behaviorManager.AddBehaviorSet(this, bSet, false); } + + behaviorManager.RebuildActions(this); } - - _availableActions.Reverse(); } - public override void Setup() + public override void Initialize() { - base.Setup(); + base.Initialize(); _planCooldownRemaining = PlanCooldown; - _blackboard = new Blackboard(SelfEntity); - _planner = IoCManager.Resolve().GetEntitySystem(); + _blackboard = new Blackboard(Owner); + _planner = EntitySystem.Get(); } - public override void Shutdown() + public override void OnRemove() { + base.OnRemove(); var currentOp = CurrentAction?.ActionOperators.Peek(); currentOp?.Shutdown(Outcome.Failed); CurrentAction?.Shutdown(); @@ -190,6 +149,8 @@ namespace Content.Server.AI.Utility.AiLogic public override void Update(float frameTime) { + base.Update(frameTime); + // If we asked for a new action we don't want to dump the existing one. if (_actionRequest != null) { @@ -210,7 +171,7 @@ namespace Content.Server.AI.Utility.AiLogic { _planCooldownRemaining = PlanCooldown; _actionCancellation = new CancellationTokenSource(); - _actionRequest = _planner.RequestAction(new AiActionRequest(SelfEntity.Uid, _blackboard, _availableActions), _actionCancellation); + _actionRequest = _planner.RequestAction(new AiActionRequest(Owner.Uid, _blackboard, AvailableActions), _actionCancellation); return; } diff --git a/Content.Server/AI/Utility/AiLogic/Xeno.cs b/Content.Server/AI/Utility/AiLogic/Xeno.cs deleted file mode 100644 index b658d0cfb0..0000000000 --- a/Content.Server/AI/Utility/AiLogic/Xeno.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Content.Server.AI.Utility.BehaviorSets; -using JetBrains.Annotations; -using Robust.Server.AI; - -namespace Content.Server.AI.Utility.AiLogic -{ - [AiLogicProcessor("Xeno")] - [UsedImplicitly] - public sealed class Xeno : UtilityAi - { - public override void Setup() - { - base.Setup(); - AddBehaviorSet(new IdleBehaviorSet(SelfEntity), false); - AddBehaviorSet(new UnarmedAttackPlayersBehaviorSet(SelfEntity), false); - SortActions(); - } - } -} \ No newline at end of file diff --git a/Content.Server/AI/Utility/BehaviorSetPrototype.cs b/Content.Server/AI/Utility/BehaviorSetPrototype.cs new file mode 100644 index 0000000000..c4ef4d6d39 --- /dev/null +++ b/Content.Server/AI/Utility/BehaviorSetPrototype.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using YamlDotNet.RepresentationModel; + +namespace Content.Server.AI.Utility +{ + [Prototype("behaviorSet")] + public class BehaviorSetPrototype : IPrototype + { + /// + /// Name of the BehaviorSet. + /// + public string ID { get; private set; } + + /// + /// Actions that this BehaviorSet grants to the entity. + /// + public IReadOnlyList Actions { get; private set; } + + public void LoadFrom(YamlMappingNode mapping) + { + var serializer = YamlObjectSerializer.NewReader(mapping); + serializer.DataField(this, x => x.ID, "id", string.Empty); + serializer.DataField(this, x => x.Actions, "actions", new List()); + } + } +} diff --git a/Content.Server/AI/Utility/BehaviorSets/BehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/BehaviorSet.cs deleted file mode 100644 index 59bb26dd24..0000000000 --- a/Content.Server/AI/Utility/BehaviorSets/BehaviorSet.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using Content.Server.AI.Utility.Actions; -using Robust.Shared.GameObjects; - -namespace Content.Server.AI.Utility.BehaviorSets -{ - /// - /// AKA DecisionMaker in IAUS. Just a group of actions that can be dynamically added or taken away from an AI. - /// - public abstract class BehaviorSet - { - protected IEntity Owner; - - public BehaviorSet(IEntity owner) - { - Owner = owner; - } - - public IEnumerable Actions { get; protected set; } - } -} diff --git a/Content.Server/AI/Utility/BehaviorSets/ClothingBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/ClothingBehaviorSet.cs deleted file mode 100644 index 2008026a08..0000000000 --- a/Content.Server/AI/Utility/BehaviorSets/ClothingBehaviorSet.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Content.Server.AI.Utility.Actions; -using Content.Server.AI.Utility.ExpandableActions.Clothing.Gloves; -using Content.Server.AI.Utility.ExpandableActions.Clothing.Head; -using Content.Server.AI.Utility.ExpandableActions.Clothing.OuterClothing; -using Content.Server.AI.Utility.ExpandableActions.Clothing.Shoes; -using Robust.Shared.GameObjects; - -namespace Content.Server.AI.Utility.BehaviorSets -{ - public sealed class ClothingBehaviorSet : BehaviorSet - { - public ClothingBehaviorSet(IEntity owner) : base(owner) - { - Actions = new IAiUtility[] - { - new EquipAnyHeadExp(), - new EquipAnyOuterClothingExp(), - new EquipAnyGlovesExp(), - new EquipAnyShoesExp(), - new PickUpAnyNearbyHeadExp(), - new PickUpAnyNearbyOuterClothingExp(), - new PickUpAnyNearbyGlovesExp(), - new PickUpAnyNearbyShoesExp(), - }; - } - } -} diff --git a/Content.Server/AI/Utility/BehaviorSets/HungerBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/HungerBehaviorSet.cs deleted file mode 100644 index dc6920d4bf..0000000000 --- a/Content.Server/AI/Utility/BehaviorSets/HungerBehaviorSet.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Server.AI.Utility.Actions; -using Content.Server.AI.Utility.ExpandableActions.Nutrition; -using Robust.Shared.GameObjects; - -namespace Content.Server.AI.Utility.BehaviorSets -{ - public sealed class HungerBehaviorSet : BehaviorSet - { - public HungerBehaviorSet(IEntity owner) : base(owner) - { - Actions = new IAiUtility[] - { - new PickUpNearbyFoodExp(), - new UseFoodInInventoryExp(), - }; - } - } -} diff --git a/Content.Server/AI/Utility/BehaviorSets/IdleBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/IdleBehaviorSet.cs deleted file mode 100644 index dc9640b35f..0000000000 --- a/Content.Server/AI/Utility/BehaviorSets/IdleBehaviorSet.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Server.AI.Utility.Actions; -using Content.Server.AI.Utility.Actions.Idle; -using Robust.Shared.GameObjects; - -namespace Content.Server.AI.Utility.BehaviorSets -{ - public class IdleBehaviorSet : BehaviorSet - { - public IdleBehaviorSet(IEntity owner) : base(owner) - { - Actions = new IAiUtility[] - { - new CloseLastEntityStorage(Owner), - new WanderAndWait(Owner), - }; - } - } -} diff --git a/Content.Server/AI/Utility/BehaviorSets/PathingDummyBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/PathingDummyBehaviorSet.cs deleted file mode 100644 index f11cf9a099..0000000000 --- a/Content.Server/AI/Utility/BehaviorSets/PathingDummyBehaviorSet.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Content.Server.AI.Utility.Actions; -using Content.Server.AI.Utility.Actions.Test; -using Robust.Shared.GameObjects; - -namespace Content.Server.AI.Utility.BehaviorSets -{ - public sealed class PathingDummyBehaviorSet : BehaviorSet - { - public PathingDummyBehaviorSet(IEntity owner) : base(owner) - { - Actions = new IAiUtility[] - { - new MoveRightAndLeftTen(owner), - }; - } - } -} diff --git a/Content.Server/AI/Utility/BehaviorSets/SpirateBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/SpirateBehaviorSet.cs deleted file mode 100644 index 7013390fbd..0000000000 --- a/Content.Server/AI/Utility/BehaviorSets/SpirateBehaviorSet.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Content.Server.AI.Utility.Actions; -using Content.Server.AI.Utility.ExpandableActions.Combat.Melee; -using Robust.Shared.GameObjects; - -namespace Content.Server.AI.Utility.BehaviorSets -{ - public sealed class SpirateBehaviorSet : BehaviorSet - { - public SpirateBehaviorSet(IEntity owner) : base(owner) - { - Actions = new IAiUtility[] - { - // TODO: Reload Ballistic - // TODO: Ideally long-term we should just store the weapons in backpack - new EquipMeleeExp(), - new PickUpMeleeWeaponExp(), - new MeleeAttackNearbyExp(), - }; - } - } -} diff --git a/Content.Server/AI/Utility/BehaviorSets/ThirstBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/ThirstBehaviorSet.cs deleted file mode 100644 index 64cb60a751..0000000000 --- a/Content.Server/AI/Utility/BehaviorSets/ThirstBehaviorSet.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Server.AI.Utility.Actions; -using Content.Server.AI.Utility.ExpandableActions.Nutrition; -using Robust.Shared.GameObjects; - -namespace Content.Server.AI.Utility.BehaviorSets -{ - public sealed class ThirstBehaviorSet : BehaviorSet - { - public ThirstBehaviorSet(IEntity owner) : base(owner) - { - Actions = new IAiUtility[] - { - new PickUpNearbyDrinkExp(), - new UseDrinkInInventoryExp(), - }; - } - } -} diff --git a/Content.Server/AI/Utility/BehaviorSets/UnarmedAttackPlayersBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/UnarmedAttackPlayersBehaviorSet.cs deleted file mode 100644 index 0dd77c6025..0000000000 --- a/Content.Server/AI/Utility/BehaviorSets/UnarmedAttackPlayersBehaviorSet.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Content.Server.AI.Utility.Actions; -using Content.Server.AI.Utility.ExpandableActions.Combat.Melee; -using Robust.Shared.GameObjects; - -namespace Content.Server.AI.Utility.BehaviorSets -{ - public sealed class UnarmedAttackPlayersBehaviorSet : BehaviorSet - { - public UnarmedAttackPlayersBehaviorSet(IEntity owner) : base(owner) - { - Actions = new IAiUtility[] - { - new UnarmedAttackNearbyPlayerExp(), - }; - } - } -} \ No newline at end of file diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/EquipAnyGlovesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/EquipAnyGlovesExp.cs index f828ab89b8..288406654d 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/EquipAnyGlovesExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/EquipAnyGlovesExp.cs @@ -40,7 +40,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Gloves if (entity.TryGetComponent(out ClothingComponent clothing) && (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.GLOVES) != 0) { - yield return new EquipGloves(owner, entity, Bonus); + yield return new EquipGloves {Owner = owner, Target = entity, Bonus = Bonus}; } } } diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/PickUpAnyNearbyGlovesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/PickUpAnyNearbyGlovesExp.cs index 6128442684..9a1c6a9c53 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/PickUpAnyNearbyGlovesExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/PickUpAnyNearbyGlovesExp.cs @@ -38,7 +38,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Gloves if (entity.TryGetComponent(out ClothingComponent clothing) && (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.GLOVES) != 0) { - yield return new PickUpGloves(owner, entity, Bonus); + yield return new PickUpGloves {Owner = owner, Target = entity, Bonus = Bonus}; } } } diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/EquipAnyHeadExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/EquipAnyHeadExp.cs index 2f05138436..427585ce50 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/EquipAnyHeadExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/EquipAnyHeadExp.cs @@ -39,7 +39,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Head if (entity.TryGetComponent(out ClothingComponent clothing) && (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.HEAD) != 0) { - yield return new EquipHead(owner, entity, Bonus); + yield return new EquipHead {Owner = owner, Target = entity, Bonus = Bonus}; } } } diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/PickUpAnyNearbyHeadExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/PickUpAnyNearbyHeadExp.cs index a5642f0944..1095cd8704 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/PickUpAnyNearbyHeadExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/PickUpAnyNearbyHeadExp.cs @@ -38,7 +38,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Head if (entity.TryGetComponent(out ClothingComponent clothing) && (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.HEAD) != 0) { - yield return new PickUpHead(owner, entity, Bonus); + yield return new PickUpHead() {Owner = owner, Target = entity, Bonus = Bonus}; } } } diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/EquipAnyOuterClothingExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/EquipAnyOuterClothingExp.cs index 31aaa0ae94..27d516b8dd 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/EquipAnyOuterClothingExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/EquipAnyOuterClothingExp.cs @@ -40,7 +40,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.OuterClothing if (entity.TryGetComponent(out ClothingComponent clothing) && (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.OUTERCLOTHING) != 0) { - yield return new EquipOuterClothing(owner, entity, Bonus); + yield return new EquipOuterClothing() {Owner = owner, Target = entity, Bonus = Bonus}; } } } diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/PickUpAnyNearbyOuterClothingExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/PickUpAnyNearbyOuterClothingExp.cs index f216285e72..f04551b21a 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/PickUpAnyNearbyOuterClothingExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/PickUpAnyNearbyOuterClothingExp.cs @@ -39,7 +39,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.OuterClothing if (entity.TryGetComponent(out ClothingComponent clothing) && (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.OUTERCLOTHING) != 0) { - yield return new PickUpOuterClothing(owner, entity, Bonus); + yield return new PickUpOuterClothing() {Owner = owner, Target = entity, Bonus = Bonus}; } } } diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/EquipAnyShoesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/EquipAnyShoesExp.cs index ab0a5a2a7b..09d60fd8a8 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/EquipAnyShoesExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/EquipAnyShoesExp.cs @@ -40,7 +40,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Shoes if (entity.TryGetComponent(out ClothingComponent clothing) && (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.SHOES) != 0) { - yield return new EquipShoes(owner, entity, Bonus); + yield return new EquipShoes() {Owner = owner, Target = entity, Bonus = Bonus}; } } } diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/PickUpAnyNearbyShoesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/PickUpAnyNearbyShoesExp.cs index 0e25c67fb7..6779ef85ca 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/PickUpAnyNearbyShoesExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/PickUpAnyNearbyShoesExp.cs @@ -39,7 +39,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Shoes if (entity.TryGetComponent(out ClothingComponent clothing) && (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.SHOES) != 0) { - yield return new PickUpShoes(owner, entity, Bonus); + yield return new PickUpShoes {Owner = owner, Target = entity, Bonus = Bonus}; } } } diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/EquipMeleeExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/EquipMeleeExp.cs index 996ff8c1ec..86ab1b81a5 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/EquipMeleeExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/EquipMeleeExp.cs @@ -37,8 +37,8 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee { continue; } - - yield return new EquipMelee(owner, entity, Bonus); + + yield return new EquipMelee() {Owner = owner, Target = entity, Bonus = Bonus}; } } } diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyExp.cs index b825065e01..8b70caabf6 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyExp.cs @@ -39,7 +39,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee foreach (var target in EntitySystem.Get() .GetNearbyHostiles(owner, controller.VisionRadius)) { - yield return new MeleeWeaponAttackEntity(owner, target, Bonus); + yield return new MeleeWeaponAttackEntity() {Owner = owner, Target = target, Bonus = Bonus}; } } } diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/PickUpMeleeWeaponExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/PickUpMeleeWeaponExp.cs index c91e0b065b..9b57beed6b 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/PickUpMeleeWeaponExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/PickUpMeleeWeaponExp.cs @@ -35,7 +35,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee foreach (var entity in context.GetState().GetValue()) { - yield return new PickUpMeleeWeapon(owner, entity, Bonus); + yield return new PickUpMeleeWeapon() {Owner = owner, Target = entity, Bonus = Bonus}; } } } diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyPlayerExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyHostilesExp.cs similarity index 89% rename from Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyPlayerExp.cs rename to Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyHostilesExp.cs index 803c335444..08085fefb8 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyPlayerExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyHostilesExp.cs @@ -13,7 +13,7 @@ using Robust.Shared.IoC; namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee { - public sealed class UnarmedAttackNearbyPlayerExp : ExpandableUtilityAction + public sealed class UnarmedAttackNearbyHostilesExp : ExpandableUtilityAction { public override float Bonus => UtilityAction.CombatBonus; @@ -39,7 +39,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee foreach (var target in EntitySystem.Get() .GetNearbyHostiles(owner, controller.VisionRadius)) { - yield return new UnarmedAttackEntity(owner, target, Bonus); + yield return new UnarmedAttackEntity() {Owner = owner, Target = target, Bonus = Bonus}; } } } diff --git a/Content.Server/AI/Utility/ExpandableActions/ExpandableUtilityAction.cs b/Content.Server/AI/Utility/ExpandableActions/ExpandableUtilityAction.cs index d8f291c9f8..de2c6549f7 100644 --- a/Content.Server/AI/Utility/ExpandableActions/ExpandableUtilityAction.cs +++ b/Content.Server/AI/Utility/ExpandableActions/ExpandableUtilityAction.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Content.Server.AI.Utility.Actions; using Content.Server.AI.WorldState; +using Robust.Shared.GameObjects; namespace Content.Server.AI.Utility.ExpandableActions { @@ -11,6 +12,8 @@ namespace Content.Server.AI.Utility.ExpandableActions /// public abstract class ExpandableUtilityAction : IAiUtility { + public IEntity Owner { get; set; } + public abstract float Bonus { get; } /// diff --git a/Content.Server/AI/Utility/ExpandableActions/Nutrition/PickUpNearbyDrinkExp.cs b/Content.Server/AI/Utility/ExpandableActions/Nutrition/PickUpNearbyDrinkExp.cs index c2558bcbc7..aa44497970 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Nutrition/PickUpNearbyDrinkExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Nutrition/PickUpNearbyDrinkExp.cs @@ -32,7 +32,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Nutrition foreach (var entity in context.GetState().GetValue()) { - yield return new PickUpDrink(owner, entity, Bonus); + yield return new PickUpDrink() {Owner = owner, Target = entity, Bonus = Bonus}; } } } diff --git a/Content.Server/AI/Utility/ExpandableActions/Nutrition/PickUpNearbyFoodExp.cs b/Content.Server/AI/Utility/ExpandableActions/Nutrition/PickUpNearbyFoodExp.cs index e3a3409aea..2166a7c703 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Nutrition/PickUpNearbyFoodExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Nutrition/PickUpNearbyFoodExp.cs @@ -32,7 +32,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Nutrition foreach (var entity in context.GetState().GetValue()) { - yield return new PickUpFood(owner, entity, Bonus); + yield return new PickUpFood {Owner = owner, Target = entity, Bonus = Bonus}; } } } diff --git a/Content.Server/AI/Utility/ExpandableActions/Nutrition/UseDrinkInInventoryExp.cs b/Content.Server/AI/Utility/ExpandableActions/Nutrition/UseDrinkInInventoryExp.cs index 06574721c6..57f492c629 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Nutrition/UseDrinkInInventoryExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Nutrition/UseDrinkInInventoryExp.cs @@ -15,7 +15,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Nutrition public sealed class UseDrinkInInventoryExp : ExpandableUtilityAction { public override float Bonus => UtilityAction.NeedsBonus; - + protected override IEnumerable> GetCommonConsiderations(Blackboard context) { var considerationsManager = IoCManager.Resolve(); @@ -35,8 +35,8 @@ namespace Content.Server.AI.Utility.ExpandableActions.Nutrition { continue; } - - yield return new UseDrinkInInventory(owner, entity, Bonus); + + yield return new UseDrinkInInventory {Owner = owner, Target = entity, Bonus = Bonus}; } } } diff --git a/Content.Server/AI/Utility/ExpandableActions/Nutrition/UseFoodInInventoryExp.cs b/Content.Server/AI/Utility/ExpandableActions/Nutrition/UseFoodInInventoryExp.cs index 1f919b56a3..0b07d9bfb5 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Nutrition/UseFoodInInventoryExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Nutrition/UseFoodInInventoryExp.cs @@ -35,8 +35,8 @@ namespace Content.Server.AI.Utility.ExpandableActions.Nutrition { continue; } - - yield return new UseFoodInInventory(owner, entity, Bonus); + + yield return new UseFoodInInventory() {Owner = owner, Target = entity, Bonus = Bonus}; } } } diff --git a/Content.Server/AI/Utility/NpcBehaviorManager.cs b/Content.Server/AI/Utility/NpcBehaviorManager.cs new file mode 100644 index 0000000000..3fc13290b9 --- /dev/null +++ b/Content.Server/AI/Utility/NpcBehaviorManager.cs @@ -0,0 +1,172 @@ +#nullable enable +using System; +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.AiLogic; +using Content.Server.GameObjects.EntitySystems.AI; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Prototypes; +using Robust.Shared.Reflection; + +namespace Content.Server.AI.Utility +{ + internal interface INpcBehaviorManager + { + void Initialize(); + + void AddBehaviorSet(UtilityAi npc, string behaviorSet, bool rebuild = true); + + void RemoveBehaviorSet(UtilityAi npc, string behaviorSet, bool rebuild = true); + + void RebuildActions(UtilityAi npc); + } + + /// + /// Handles BehaviorSets and adding / removing behaviors to NPCs + /// + internal sealed class NpcBehaviorManager : INpcBehaviorManager + { + [Dependency] private readonly IDynamicTypeFactory _typeFactory = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + + private readonly NpcActionComparer _comparer = new(); + + private Dictionary> _behaviorSets = new(); + + public void Initialize() + { + IoCManager.InjectDependencies(this); + var protoManager = IoCManager.Resolve(); + var reflectionManager = IoCManager.Resolve(); + + foreach (var bSet in protoManager.EnumeratePrototypes()) + { + var actions = new List(); + + foreach (var act in bSet.Actions) + { + if (!reflectionManager.TryLooseGetType(act, out var parsedType) || + !typeof(IAiUtility).IsAssignableFrom(parsedType)) + { + Logger.Error($"Unable to parse AI action for {act}"); + } + else + { + actions.Add(parsedType); + } + } + + _behaviorSets[bSet.ID] = actions; + } + } + + /// + /// Adds the BehaviorSet to the NPC. + /// + /// + /// + /// Set to false if you want to manually rebuild it after bulk updates. + public void AddBehaviorSet(UtilityAi npc, string behaviorSet, bool rebuild = true) + { + if (!_behaviorSets.ContainsKey(behaviorSet)) + { + Logger.Error($"Tried to add BehaviorSet {behaviorSet} to {npc} but no such BehaviorSet found!"); + return; + } + + if (!npc.BehaviorSets.Add(behaviorSet)) + { + Logger.Error($"Tried to add BehaviorSet {behaviorSet} to {npc} which already has the BehaviorSet!"); + return; + } + + if (rebuild) + RebuildActions(npc); + + if (npc.BehaviorSets.Count == 1 && !EntitySystem.Get().IsAwake(npc)) + { + _entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(npc, false)); + } + } + + /// + /// Removes the BehaviorSet from the NPC. + /// + /// + /// + /// Set to false if yo uwant to manually rebuild it after bulk updates. + public void RemoveBehaviorSet(UtilityAi npc, string behaviorSet, bool rebuild = true) + { + if (!_behaviorSets.TryGetValue(behaviorSet, out var actions)) + { + Logger.Error($"Tried to remove BehaviorSet {behaviorSet} from {npc} but no such BehaviorSet found!"); + return; + } + + if (!npc.BehaviorSets.Remove(behaviorSet)) + { + Logger.Error($"Tried to remove BehaviorSet {behaviorSet} from {npc} but it doesn't have that BehaviorSet!"); + return; + } + + if (rebuild) + RebuildActions(npc); + + if (npc.BehaviorSets.Count == 0 && EntitySystem.Get().IsAwake(npc)) + { + _entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(npc, true)); + } + } + + /// + /// Clear our actions and re-instantiate them from our BehaviorSets. + /// Will ensure each action is unique. + /// + /// + public void RebuildActions(UtilityAi npc) + { + npc.AvailableActions.Clear(); + foreach (var bSet in npc.BehaviorSets) + { + foreach (var action in GetActions(bSet)) + { + if (npc.AvailableActions.Contains(action)) continue; + // Setup + action.Owner = npc.Owner; + + // Ad to actions. + npc.AvailableActions.Add(action); + } + } + + SortActions(npc); + } + + private IEnumerable GetActions(string behaviorSet) + { + foreach (var action in _behaviorSets[behaviorSet]) + { + yield return (IAiUtility) _typeFactory.CreateInstance(action); + } + } + + /// + /// Whenever the behavior sets are changed we'll re-sort the actions by bonus + /// + private void SortActions(UtilityAi npc) + { + npc.AvailableActions.Sort(_comparer); + } + + private class NpcActionComparer : Comparer + { + public override int Compare(IAiUtility? x, IAiUtility? y) + { + if (x == null || y == null) return 0; + return y.Bonus.CompareTo(x.Bonus); + } + } + } +} diff --git a/Content.Server/AI/Utility/UtilityAiHelpers.cs b/Content.Server/AI/Utility/UtilityAiHelpers.cs index b8f0e02036..68cd796fd1 100644 --- a/Content.Server/AI/Utility/UtilityAiHelpers.cs +++ b/Content.Server/AI/Utility/UtilityAiHelpers.cs @@ -14,7 +14,7 @@ namespace Content.Server.AI.Utility return null; } - if (aiControllerComponent.Processor is UtilityAi utilityAi) + if (aiControllerComponent is UtilityAi utilityAi) { return utilityAi.Blackboard; } @@ -22,4 +22,4 @@ namespace Content.Server.AI.Utility return null; } } -} \ No newline at end of file +} diff --git a/Content.Server/Commands/AI/AddAiCommand.cs b/Content.Server/Commands/AI/AddAiCommand.cs index 382da7b2c6..6d7a401c3c 100644 --- a/Content.Server/Commands/AI/AddAiCommand.cs +++ b/Content.Server/Commands/AI/AddAiCommand.cs @@ -1,5 +1,7 @@ #nullable enable using Content.Server.Administration; +using Content.Server.AI.Utility; +using Content.Server.AI.Utility.AiLogic; using Content.Server.GameObjects.Components.Movement; using Content.Server.GameObjects.EntitySystems.AI; using Content.Shared.Administration; @@ -15,41 +17,48 @@ namespace Content.Server.Commands.AI { public string Command => "addai"; public string Description => "Add an ai component with a given processor to an entity."; - public string Help => "Usage: addai " - + "\n processorId: Class that inherits AiLogicProcessor and has an AiLogicProcessor attribute." - + "\n entityID: Uid of entity to add the AiControllerComponent to. Open its VV menu to find this."; + public string Help => "Usage: addai ..." + + "\n entityID: Uid of entity to add the AiControllerComponent to. Open its VV menu to find this." + + "\n behaviorSet: Name of a behaviorset to add to the component on initialize."; public void Execute(IConsoleShell shell, string argStr, string[] args) { - if(args.Length != 2) + if(args.Length < 1) { shell.WriteLine("Wrong number of args."); return; } - var processorId = args[0]; - var entId = new EntityUid(int.Parse(args[1])); - var ent = IoCManager.Resolve().GetEntity(entId); - var aiSystem = EntitySystem.Get(); + var entId = new EntityUid(int.Parse(args[0])); - if (!aiSystem.ProcessorTypeExists(processorId)) + if (!IoCManager.Resolve().TryGetEntity(entId, out var ent)) { - shell.WriteLine("Invalid processor type. Processor must inherit AiLogicProcessor and have an AiLogicProcessor attribute."); + shell.WriteLine($"Unable to find entity with uid {entId}"); return; } + if (ent.HasComponent()) { shell.WriteLine("Entity already has an AI component."); return; } + // TODO: IMover refffaaccctttooorrr if (ent.HasComponent()) { ent.RemoveComponent(); } - var comp = ent.AddComponent(); - comp.LogicName = processorId; + var comp = ent.AddComponent(); + var behaviorManager = IoCManager.Resolve(); + + for (var i = 1; i < args.Length; i++) + { + var bSet = args[i]; + behaviorManager.AddBehaviorSet(comp, bSet, false); + } + + behaviorManager.RebuildActions(comp); shell.WriteLine("AI component added."); } } diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs index 3268b29171..96f18c4133 100644 --- a/Content.Server/EntryPoint.cs +++ b/Content.Server/EntryPoint.cs @@ -1,4 +1,5 @@ using Content.Server.Administration; +using Content.Server.AI.Utility; using Content.Server.AI.Utility.Considerations; using Content.Server.AI.WorldState; using Content.Server.Database; @@ -92,6 +93,7 @@ namespace Content.Server IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); _euiManager.Initialize(); } diff --git a/Content.Server/GameObjects/Components/Movement/AiControllerComponent.cs b/Content.Server/GameObjects/Components/Movement/AiControllerComponent.cs index 26c5553f9c..e846046e46 100644 --- a/Content.Server/GameObjects/Components/Movement/AiControllerComponent.cs +++ b/Content.Server/GameObjects/Components/Movement/AiControllerComponent.cs @@ -17,27 +17,10 @@ namespace Content.Server.GameObjects.Components.Movement [RegisterComponent, ComponentReference(typeof(IMoverComponent))] public class AiControllerComponent : Component, IMoverComponent { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IGameTicker _gameTicker = default!; - - private string? _logicName; private float _visionRadius; public override string Name => "AiController"; - [ViewVariables(VVAccess.ReadWrite)] - public string? LogicName - { - get => _logicName; - set - { - _logicName = value; - Processor = null!; - } - } - - public UtilityAi? Processor { get; set; } - [ViewVariables(VVAccess.ReadWrite)] public string? StartingGearPrototype { get; set; } @@ -55,8 +38,6 @@ namespace Content.Server.GameObjects.Components.Movement // This component requires a physics component. Owner.EnsureComponent(); - - EntitySystem.Get().ProcessorInitialize(this); } protected override void Startup() @@ -65,10 +46,12 @@ namespace Content.Server.GameObjects.Components.Movement if (StartingGearPrototype != null) { - var startingGear = _prototypeManager.Index(StartingGearPrototype); - _gameTicker.EquipStartingGear(Owner, startingGear, null); - } + var gameTicker = IoCManager.Resolve(); + var protoManager = IoCManager.Resolve(); + var startingGear = protoManager.Index(StartingGearPrototype); + gameTicker.EquipStartingGear(Owner, startingGear, null); + } } /// @@ -76,7 +59,6 @@ namespace Content.Server.GameObjects.Components.Movement { base.ExposeData(serializer); - serializer.DataField(ref _logicName, "logic", null); serializer.DataReadWriteFunction( "startingGear", null, @@ -85,12 +67,6 @@ namespace Content.Server.GameObjects.Components.Movement serializer.DataField(ref _visionRadius, "vision", 8.0f); } - protected override void Shutdown() - { - base.Shutdown(); - Processor?.Shutdown(); - } - /// /// Movement speed (m/s) that the entity walks, after modifiers /// @@ -155,5 +131,7 @@ namespace Content.Server.GameObjects.Components.Movement public void SetVelocityDirection(Direction direction, ushort subTick, bool enabled) { } public void SetSprinting(ushort subTick, bool walking) { } + + public virtual void Update(float frameTime) {} } } diff --git a/Content.Server/GameObjects/EntitySystems/AI/AiSystem.cs b/Content.Server/GameObjects/EntitySystems/AI/AiSystem.cs index 3098ef00a1..65793cd21f 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/AiSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/AiSystem.cs @@ -1,41 +1,41 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; using Content.Server.AI.Utility.AiLogic; using Content.Server.GameObjects.Components.Movement; using Content.Shared.GameObjects.Components.Mobs.State; using Content.Shared; using JetBrains.Annotations; -using Robust.Server.AI; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; +using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Utility; namespace Content.Server.GameObjects.EntitySystems.AI { + /// + /// Handles NPCs running every tick. + /// [UsedImplicitly] internal class AiSystem : EntitySystem { [Dependency] private readonly IConfigurationManager _configurationManager = default!; - [Dependency] private readonly IDynamicTypeFactory _typeFactory = default!; - [Dependency] private readonly IReflectionManager _reflectionManager = default!; - - private readonly Dictionary _processorTypes = new(); /// /// To avoid iterating over dead AI continuously they can wake and sleep themselves when necessary. /// - private readonly HashSet _awakeAi = new(); + private readonly HashSet _awakeAi = new(); // To avoid modifying awakeAi while iterating over it. private readonly List _queuedSleepMessages = new(); private readonly List _queuedMobStateMessages = new(); - public bool IsAwake(AiLogicProcessor processor) => _awakeAi.Contains(processor); + public bool IsAwake(AiControllerComponent npc) => _awakeAi.Contains(npc); /// public override void Initialize() @@ -43,15 +43,6 @@ namespace Content.Server.GameObjects.EntitySystems.AI base.Initialize(); SubscribeLocalEvent(HandleAiSleep); SubscribeLocalEvent(MobStateChanged); - - var processors = _reflectionManager.GetAllChildren(); - foreach (var processor in processors) - { - var att = (AiLogicProcessorAttribute) Attribute.GetCustomAttribute(processor, typeof(AiLogicProcessorAttribute))!; - // Tests should pick this up - DebugTools.AssertNotNull(att); - _processorTypes.Add(att.SerializeName, processor); - } } /// @@ -63,13 +54,14 @@ namespace Content.Server.GameObjects.EntitySystems.AI foreach (var message in _queuedMobStateMessages) { + // TODO: Need to generecise this but that will be part of a larger cleanup later anyway. if (message.Entity.Deleted || - !message.Entity.TryGetComponent(out AiControllerComponent? controller)) + !message.Entity.TryGetComponent(out UtilityAi? controller)) { continue; } - controller.Processor?.MobStateChanged(message); + controller.MobStateChanged(message); } _queuedMobStateMessages.Clear(); @@ -79,14 +71,14 @@ namespace Content.Server.GameObjects.EntitySystems.AI switch (message.Sleep) { case true: - if (_awakeAi.Count == cvarMaxUpdates && _awakeAi.Contains(message.Processor)) + if (_awakeAi.Count == cvarMaxUpdates && _awakeAi.Contains(message.Component)) { Logger.Warning($"Under AI limit again: {_awakeAi.Count - 1} / {cvarMaxUpdates}"); } - _awakeAi.Remove(message.Processor); + _awakeAi.Remove(message.Component); break; case false: - _awakeAi.Add(message.Processor); + _awakeAi.Add(message.Component); if (_awakeAi.Count > cvarMaxUpdates) { @@ -97,16 +89,17 @@ namespace Content.Server.GameObjects.EntitySystems.AI } _queuedSleepMessages.Clear(); - var toRemove = new List(); + var toRemove = new List(); var maxUpdates = Math.Min(_awakeAi.Count, cvarMaxUpdates); var count = 0; - foreach (var processor in _awakeAi) + foreach (var npc in _awakeAi) { - if (processor.SelfEntity.Deleted || - !processor.SelfEntity.HasComponent()) + if (npc.Paused) continue; + + if (npc.Deleted) { - toRemove.Add(processor); + toRemove.Add(npc); continue; } @@ -115,7 +108,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI break; } - processor.Update(frameTime); + npc.Update(frameTime); count++; } @@ -139,32 +132,5 @@ namespace Content.Server.GameObjects.EntitySystems.AI _queuedMobStateMessages.Add(message); } - - /// - /// Will start up the controller's processor if not already done so. - /// Also add them to the awakeAi for updates. - /// - /// - public void ProcessorInitialize(AiControllerComponent controller) - { - if (controller.Processor != null || controller.LogicName == null) return; - controller.Processor = CreateProcessor(controller.LogicName); - controller.Processor.SelfEntity = controller.Owner; - controller.Processor.Setup(); - _awakeAi.Add(controller.Processor); - } - - private UtilityAi CreateProcessor(string name) - { - if (_processorTypes.TryGetValue(name, out var type)) - { - return (UtilityAi)_typeFactory.CreateInstance(type); - } - - // processor needs to inherit AiLogicProcessor, and needs an AiLogicProcessorAttribute to define the YAML name - throw new ArgumentException($"Processor type {name} could not be found.", nameof(name)); - } - - public bool ProcessorTypeExists(string name) => _processorTypes.ContainsKey(name); } } diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/ReachableArgs.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/ReachableArgs.cs index 51efb703ae..6cedbf3176 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/ReachableArgs.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/ReachableArgs.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +#nullable enable +using System.Collections.Generic; using Content.Server.GameObjects.Components.Access; using Content.Server.GameObjects.Components.Movement; using Robust.Shared.GameObjects; @@ -26,7 +27,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible public static ReachableArgs GetArgs(IEntity entity) { var collisionMask = 0; - if (entity.TryGetComponent(out IPhysicsComponent physics)) + if (entity.TryGetComponent(out IPhysicsComponent? physics)) { collisionMask = physics.CollisionMask; } diff --git a/Content.Server/GameObjects/EntitySystems/AI/SleepAiMessage.cs b/Content.Server/GameObjects/EntitySystems/AI/SleepAiMessage.cs index 28f6674a80..614e2c27f9 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/SleepAiMessage.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/SleepAiMessage.cs @@ -1,4 +1,4 @@ -using Robust.Server.AI; +using Content.Server.GameObjects.Components.Movement; using Robust.Shared.GameObjects; namespace Content.Server.GameObjects.EntitySystems.AI @@ -13,12 +13,12 @@ namespace Content.Server.GameObjects.EntitySystems.AI /// Sleep or awake. /// public bool Sleep { get; } - public AiLogicProcessor Processor { get; } - - public SleepAiMessage(AiLogicProcessor processor, bool sleep) + public AiControllerComponent Component { get; } + + public SleepAiMessage(AiControllerComponent component, bool sleep) { - Processor = processor; + Component = component; Sleep = sleep; } } -} \ No newline at end of file +} diff --git a/Content.Server/ServerContentIoC.cs b/Content.Server/ServerContentIoC.cs index 70e2c06869..37c09b73a4 100644 --- a/Content.Server/ServerContentIoC.cs +++ b/Content.Server/ServerContentIoC.cs @@ -1,4 +1,5 @@ using Content.Server.Administration; +using Content.Server.AI.Utility; using Content.Server.AI.Utility.Considerations; using Content.Server.AI.WorldState; using Content.Server.Chat; @@ -60,6 +61,7 @@ namespace Content.Server IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml index e873ffd610..7a2e10f021 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml @@ -6,8 +6,10 @@ drawdepth: Mobs suffix: AI components: - - type: AiController - logic: Xeno + - type: UtilityAI + behaviorSets: + - Idle + - UnarmedAttackHostiles - type: AiFactionTag factions: - SimpleHostile diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/dummy_npcs.yml b/Resources/Prototypes/Entities/Mobs/NPCs/dummy_npcs.yml index acd6b639f6..9c34ca0192 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/dummy_npcs.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/dummy_npcs.yml @@ -7,5 +7,6 @@ drawdepth: Mobs suffix: AI components: - - type: AiController - logic: PathingDummy + - type: UtilityAI + behaviorSets: + - PathingDummy diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/human.yml b/Resources/Prototypes/Entities/Mobs/NPCs/human.yml index 9282d2f284..bef2f79c47 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/human.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/human.yml @@ -7,8 +7,12 @@ drawdepth: Mobs suffix: AI components: - - type: AiController - logic: Civilian + - type: UtilityAI + behaviorSets: + - Clothing + - Hunger + - Thirst + - Idle startingGear: AssistantGear - type: AiFactionTag factions: @@ -22,5 +26,10 @@ description: Yarr! suffix: AI components: - - type: AiController - logic: Spirate + - type: UtilityAI + behaviorSets: + - Clothing + - Hunger + - Thirst + - Idle + - Spirate diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml b/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml index b43397de2f..b5e6555362 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml @@ -7,8 +7,9 @@ drawdepth: Mobs suffix: AI components: - - type: AiController - logic: Mimic + - type: UtilityAI + behaviorSets: + - UnarmedAttackHostiles - type: AiFactionTag factions: - SimpleHostile diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml index 69e591833f..a57140e2fe 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml @@ -5,8 +5,12 @@ drawdepth: Mobs suffix: AI components: - - type: AiController - logic: Civilian + - type: UtilityAI + behaviorSets: + - Clothing + - Hunger + - Thirst + - Idle - type: AiFactionTag factions: - SimpleNeutral diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index d2fe9c1157..f248214647 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -7,8 +7,10 @@ drawdepth: Mobs suffix: AI components: - - type: AiController - logic: Xeno + - type: UtilityAI + behaviorSets: + - Idle + - UnarmedAttackHostiles - type: AiFactionTag factions: - Xeno diff --git a/Resources/Prototypes/NPCs/behavior_sets.yml b/Resources/Prototypes/NPCs/behavior_sets.yml new file mode 100644 index 0000000000..481df2f256 --- /dev/null +++ b/Resources/Prototypes/NPCs/behavior_sets.yml @@ -0,0 +1,46 @@ +- type: behaviorSet + id: Clothing + actions: + - EquipAnyHeadExp + - EquipAnyOuterClothingExp + - EquipAnyGlovesExp + - EquipAnyShoesExp + - PickUpAnyNearbyHeadExp + - PickUpAnyNearbyOuterClothingExp + - PickUpAnyNearbyGlovesExp + - PickUpAnyNearbyShoesExp + +- type: behaviorSet + id: Hunger + actions: + - PickUpNearbyFoodExp + - UseFoodInInventoryExp + +- type: behaviorSet + id: Idle + actions: + - CloseLastEntityStorage + - WanderAndWait + +- type: behaviorSet + id: PathingDummy + actions: + - MoveRightAndLeftTen + +- type: behaviorSet + id: Spirate + actions: + - EquipMeleeExp + - PickUpMeleeWeaponExp + - MeleeAttackNearbyExp + +- type: behaviorSet + id: Thirst + actions: + - PickUpNearbyDrinkExp + - UseDrinkInInventoryExp + +- type: behaviorSet + id: UnarmedAttackHostiles + actions: + - UnarmedAttackNearbyHostilesExp \ No newline at end of file diff --git a/RobustToolbox b/RobustToolbox index e2a4dcdff1..eb3a815d48 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit e2a4dcdff16ec7a712fe6098938415cedc7120d6 +Subproject commit eb3a815d48e91308c72c218996a0fa2da14a4267