diff --git a/Content.IntegrationTests/Tests/AI/BehaviorSetsTest.cs b/Content.IntegrationTests/Tests/AI/BehaviorSetsTest.cs index 4f89e79791..1f21343c15 100644 --- a/Content.IntegrationTests/Tests/AI/BehaviorSetsTest.cs +++ b/Content.IntegrationTests/Tests/AI/BehaviorSetsTest.cs @@ -48,7 +48,7 @@ namespace Content.IntegrationTests.Tests.AI { foreach (var entity in protoManager.EnumeratePrototypes()) { - if (!entity.TryGetComponent("UtilityAI", out var npcNode)) continue; + if (!entity.TryGetComponent("UtilityAI", out var npcNode)) continue; foreach (var entry in npcNode.BehaviorSets) { diff --git a/Content.Server/AI/Commands/AddAiCommand.cs b/Content.Server/AI/Commands/AddAiCommand.cs index af7e463289..d56996a704 100644 --- a/Content.Server/AI/Commands/AddAiCommand.cs +++ b/Content.Server/AI/Commands/AddAiCommand.cs @@ -1,5 +1,6 @@ using Content.Server.Administration; using Content.Server.AI.Components; +using Content.Server.AI.EntitySystems; using Content.Server.AI.Utility; using Content.Server.AI.Utility.AiLogic; using Content.Shared.Administration; @@ -35,22 +36,22 @@ namespace Content.Server.AI.Commands return; } - if (_entities.HasComponent(entId)) + if (_entities.HasComponent(entId)) { shell.WriteLine("Entity already has an AI component."); return; } - var comp = _entities.AddComponent(entId); - var behaviorManager = IoCManager.Resolve(); + var comp = _entities.AddComponent(entId); + var npcSystem = IoCManager.Resolve().EntitySysManager.GetEntitySystem(); for (var i = 1; i < args.Length; i++) { var bSet = args[i]; - behaviorManager.AddBehaviorSet(comp, bSet, false); + npcSystem.AddBehaviorSet(comp, bSet, false); } - behaviorManager.RebuildActions(comp); + npcSystem.RebuildActions(comp); shell.WriteLine("AI component added."); } } diff --git a/Content.Server/AI/Components/ActiveNPCComponent.cs b/Content.Server/AI/Components/ActiveNPCComponent.cs new file mode 100644 index 0000000000..410c07e5f3 --- /dev/null +++ b/Content.Server/AI/Components/ActiveNPCComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.AI.Components; + +/// +/// Added to NPCs that are actively being updated. +/// +[RegisterComponent] +public sealed class ActiveNPCComponent : Component {} diff --git a/Content.Server/AI/Components/AiControllerComponent.cs b/Content.Server/AI/Components/AiControllerComponent.cs deleted file mode 100644 index 68dac19adf..0000000000 --- a/Content.Server/AI/Components/AiControllerComponent.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Content.Server.AI.EntitySystems; -using Content.Server.Station.Systems; -using Content.Shared.Movement.Components; -using Content.Shared.Roles; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; - -namespace Content.Server.AI.Components -{ - [RegisterComponent] - [Virtual] - public class AiControllerComponent : Component - { - [DataField("logic")] private float _visionRadius = 8.0f; - - // TODO: Need to ECS a lot more of the AI first before we can ECS this - /// - /// Whether the AI is actively iterated. - /// - public bool Awake - { - get => _awake; - set - { - if (_awake == value) return; - - _awake = value; - - if (_awake) - EntitySystem.Get().WakeNPC(this); - else - EntitySystem.Get().SleepNPC(this); - } - } - - [DataField("awake")] - private bool _awake = true; - - [ViewVariables(VVAccess.ReadWrite)] - public float VisionRadius - { - get => _visionRadius; - set => _visionRadius = value; - } - - /// - /// Is the entity Sprinting (running)? - /// - [ViewVariables] - public bool Sprinting { get; } = true; - - /// - /// Calculated linear velocity direction of the entity. - /// - [ViewVariables] - public Vector2 VelocityDir { get; set; } - - public virtual void Update(float frameTime) {} - } -} diff --git a/Content.Server/AI/Components/NPCComponent.cs b/Content.Server/AI/Components/NPCComponent.cs new file mode 100644 index 0000000000..97e229f422 --- /dev/null +++ b/Content.Server/AI/Components/NPCComponent.cs @@ -0,0 +1,17 @@ +using Content.Server.AI.EntitySystems; + +namespace Content.Server.AI.Components +{ + [Access(typeof(NPCSystem))] + public abstract class NPCComponent : Component + { + // TODO: Soon. I didn't realise how much effort it was to deprecate the old one. + /// + /// Contains all of the world data for a particular NPC in terms of how it sees the world. + /// + //[ViewVariables, DataField("blackboardA")] + //public Dictionary BlackboardA = new(); + + public float VisionRadius => 7f; + } +} diff --git a/Content.Server/AI/EntitySystems/NPCSystem.Blackboard.cs b/Content.Server/AI/EntitySystems/NPCSystem.Blackboard.cs new file mode 100644 index 0000000000..b818aeb1a5 --- /dev/null +++ b/Content.Server/AI/EntitySystems/NPCSystem.Blackboard.cs @@ -0,0 +1,42 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Server.AI.Components; + +namespace Content.Server.AI.EntitySystems; + +public sealed partial class NPCSystem +{ + /* + /// + /// Tries to get the blackboard data for a particular key. Returns default if not found + /// + public T? GetValueOrDefault(NPCComponent component, string key) + { + if (component.BlackboardA.TryGetValue(key, out var value)) + { + return (T) value; + } + + return default; + } + + /// + /// Tries to get the blackboard data for a particular key. + /// + public bool TryGetValue(NPCComponent component, string key, [NotNullWhen(true)] out T? value) + { + if (component.BlackboardA.TryGetValue(key, out var data)) + { + value = (T) data; + return true; + } + + value = default; + return false; + } + + /* + * Constants to make development easier + */ + + public const string VisionRadius = "VisionRadius"; +} diff --git a/Content.Server/AI/EntitySystems/NPCSystem.Utility.cs b/Content.Server/AI/EntitySystems/NPCSystem.Utility.cs new file mode 100644 index 0000000000..10f96b3660 --- /dev/null +++ b/Content.Server/AI/EntitySystems/NPCSystem.Utility.cs @@ -0,0 +1,284 @@ +using System.Runtime.ExceptionServices; +using System.Threading; +using Content.Server.AI.Components; +using Content.Server.AI.LoadBalancer; +using Content.Server.AI.Operators; +using Content.Server.AI.Utility; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.AiLogic; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Utility; +using Content.Server.CPUJob.JobQueues; +using Content.Server.CPUJob.JobQueues.Queues; +using Robust.Shared.Prototypes; +using Robust.Shared.Reflection; + +namespace Content.Server.AI.EntitySystems; + +public sealed partial class NPCSystem +{ + /* + * Handles Utility AI, implemented via IAUS + */ + + private readonly NpcActionComparer _comparer = new(); + + private Dictionary> _behaviorSets = new(); + + private readonly AiActionJobQueue _aiRequestQueue = new(); + + private void InitializeUtility() + { + SubscribeLocalEvent(OnUtilityStartup); + + foreach (var bSet in _prototypeManager.EnumeratePrototypes()) + { + var actions = new List(); + + foreach (var act in bSet.Actions) + { + if (!_reflectionManager.TryLooseGetType(act, out var parsedType) || + !typeof(IAiUtility).IsAssignableFrom(parsedType)) + { + _sawmill.Error($"Unable to parse AI action for {act}"); + } + else + { + actions.Add(parsedType); + } + } + + _behaviorSets[bSet.ID] = actions; + } + } + + private void OnUtilityStartup(EntityUid uid, UtilityNPCComponent component, ComponentStartup args) + { + if (component.BehaviorSets.Count > 0) + { + RebuildActions(component); + } + + component._planCooldownRemaining = component.PlanCooldown; + component._blackboard = new Blackboard(component.Owner); + } + + public AiActionRequestJob RequestAction(UtilityNPCComponent component, AiActionRequest request, CancellationTokenSource cancellationToken) + { + var job = new AiActionRequestJob(0.002, request, cancellationToken.Token); + // AI should already know if it shouldn't request again + _aiRequestQueue.EnqueueJob(job); + return job; + } + + private void UpdateUtility(float frameTime) + { + foreach (var (_, comp) in EntityQuery()) + { + if (_count >= _maxUpdates) break; + + Update(comp, frameTime); + _count++; + } + + _aiRequestQueue.Process(); + } + + private void ReceivedAction(UtilityNPCComponent component) + { + if (component._actionRequest == null) + { + return; + } + + switch (component._actionRequest.Exception) + { + case null: + break; + default: + _sawmill.Fatal(component._actionRequest.Exception.ToString()); + ExceptionDispatchInfo.Capture(component._actionRequest.Exception).Throw(); + // The code never actually reaches here, because the above throws. + // This is to tell the compiler that the flow never leaves here. + throw component._actionRequest.Exception; + } + var action = component._actionRequest.Result; + component._actionRequest = null; + // Actions with lower scores should be implicitly dumped by GetAction + // If we're not allowed to replace the action with an action of the same type then dump. + if (action == null || !action.CanOverride && component.CurrentAction?.GetType() == action.GetType()) + { + return; + } + + var currentOp = component.CurrentAction?.ActionOperators.Peek(); + if (currentOp != null && currentOp.HasStartup) + { + currentOp.Shutdown(Outcome.Failed); + } + + component.CurrentAction = action; + action.SetupOperators(component._blackboard); + } + + private void Update(UtilityNPCComponent component, float frameTime) + { + // If we asked for a new action we don't want to dump the existing one. + if (component._actionRequest != null) + { + if (component._actionRequest.Status != JobStatus.Finished) + { + return; + } + + ReceivedAction(component); + // Do something next tick + return; + } + + component._planCooldownRemaining -= frameTime; + + // Might find a better action while we're doing one already + if (component._planCooldownRemaining <= 0.0f) + { + component._planCooldownRemaining = component.PlanCooldown; + component._actionCancellation = new CancellationTokenSource(); + component._actionRequest = RequestAction(component, new AiActionRequest(component.Owner, component._blackboard, component.AvailableActions), component._actionCancellation); + + return; + } + + // When we spawn in we won't get an action for a bit + if (component.CurrentAction == null) + { + return; + } + + var outcome = component.CurrentAction.Execute(frameTime); + + switch (outcome) + { + case Outcome.Success: + if (component.CurrentAction.ActionOperators.Count == 0) + { + component.CurrentAction.Shutdown(); + component.CurrentAction = null; + // Nothing to compare new action to + component._blackboard.GetState().SetValue(0.0f); + } + break; + case Outcome.Continuing: + break; + case Outcome.Failed: + component.CurrentAction.Shutdown(); + component.CurrentAction = null; + component._blackboard.GetState().SetValue(0.0f); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Adds the BehaviorSet to the NPC. + /// + /// + /// + /// Set to false if you want to manually rebuild it after bulk updates. + public void AddBehaviorSet(UtilityNPCComponent npc, string behaviorSet, bool rebuild = true) + { + if (!_behaviorSets.ContainsKey(behaviorSet)) + { + _sawmill.Error($"Tried to add BehaviorSet {behaviorSet} to {npc} but no such BehaviorSet found!"); + return; + } + + if (!npc.BehaviorSets.Add(behaviorSet)) + { + _sawmill.Error($"Tried to add BehaviorSet {behaviorSet} to {npc} which already has the BehaviorSet!"); + return; + } + + if (rebuild) + RebuildActions(npc); + + if (npc.BehaviorSets.Count == 1 && !IsAwake(npc)) + WakeNPC(npc); + } + + /// + /// Removes the BehaviorSet from the NPC. + /// + /// + /// + /// Set to false if yo uwant to manually rebuild it after bulk updates. + public void RemoveBehaviorSet(UtilityNPCComponent 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) + SleepNPC(npc); + } + + /// + /// Clear our actions and re-instantiate them from our BehaviorSets. + /// Will ensure each action is unique. + /// + /// + public void RebuildActions(UtilityNPCComponent 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(UtilityNPCComponent npc) + { + npc.AvailableActions.Sort(_comparer); + } + + private sealed 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/EntitySystems/NPCSystem.cs b/Content.Server/AI/EntitySystems/NPCSystem.cs index da4a7191fa..25da7d8621 100644 --- a/Content.Server/AI/EntitySystems/NPCSystem.cs +++ b/Content.Server/AI/EntitySystems/NPCSystem.cs @@ -1,10 +1,10 @@ -using System.Linq; using Content.Server.AI.Components; using Content.Shared.CCVar; using Content.Shared.MobState; using JetBrains.Annotations; using Robust.Shared.Configuration; -using Robust.Shared.Random; +using Robust.Shared.Prototypes; +using Robust.Shared.Reflection; namespace Content.Server.AI.EntitySystems { @@ -12,120 +12,103 @@ namespace Content.Server.AI.EntitySystems /// Handles NPCs running every tick. /// [UsedImplicitly] - internal sealed class NPCSystem : EntitySystem + public sealed partial class NPCSystem : EntitySystem { [Dependency] private readonly IConfigurationManager _configurationManager = default!; - [Dependency] private readonly IRobustRandom _robustRandom = default!; + [Dependency] private readonly IDynamicTypeFactory _typeFactory = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IReflectionManager _reflectionManager = default!; - /// - /// To avoid iterating over dead AI continuously they can wake and sleep themselves when necessary. - /// - private readonly HashSet _awakeNPCs = new(); + private ISawmill _sawmill = default!; /// /// Whether any NPCs are allowed to run at all. /// public bool Enabled { get; set; } = true; + private int _maxUpdates; + + private int _count; + /// public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnMobStateChange); - SubscribeLocalEvent(OnNPCInit); - SubscribeLocalEvent(OnNPCShutdown); + _sawmill = Logger.GetSawmill("npc"); + InitializeUtility(); + SubscribeLocalEvent(OnMobStateChange); + SubscribeLocalEvent(OnNPCInit); + SubscribeLocalEvent(OnNPCShutdown); _configurationManager.OnValueChanged(CCVars.NPCEnabled, SetEnabled, true); - - var maxUpdates = _configurationManager.GetCVar(CCVars.NPCMaxUpdates); - - if (maxUpdates < 1024) - _awakeNPCs.EnsureCapacity(maxUpdates); + _configurationManager.OnValueChanged(CCVars.NPCMaxUpdates, SetMaxUpdates, true); } + private void SetMaxUpdates(int obj) => _maxUpdates = obj; private void SetEnabled(bool value) => Enabled = value; public override void Shutdown() { base.Shutdown(); _configurationManager.UnsubValueChanged(CCVars.NPCEnabled, SetEnabled); + _configurationManager.UnsubValueChanged(CCVars.NPCMaxUpdates, SetMaxUpdates); } - private void OnNPCInit(EntityUid uid, AiControllerComponent component, ComponentInit args) + private void OnNPCInit(EntityUid uid, NPCComponent component, ComponentInit args) { - if (!component.Awake) return; - - _awakeNPCs.Add(component); + WakeNPC(component); } - private void OnNPCShutdown(EntityUid uid, AiControllerComponent component, ComponentShutdown args) + private void OnNPCShutdown(EntityUid uid, NPCComponent component, ComponentShutdown args) { - _awakeNPCs.Remove(component); + SleepNPC(component); + } + + /// + /// Is the NPC awake and updating? + /// + public bool IsAwake(NPCComponent component, ActiveNPCComponent? active = null) + { + return Resolve(component.Owner, ref active, false); } /// /// Allows the NPC to actively be updated. /// - /// - public void WakeNPC(AiControllerComponent component) + public void WakeNPC(NPCComponent component) { - _awakeNPCs.Add(component); + _sawmill.Debug($"Waking {ToPrettyString(component.Owner)}"); + EnsureComp(component.Owner); } /// /// Stops the NPC from actively being updated. /// - /// - public void SleepNPC(AiControllerComponent component) + public void SleepNPC(NPCComponent component) { - _awakeNPCs.Remove(component); + _sawmill.Debug($"Sleeping {ToPrettyString(component.Owner)}"); + RemComp(component.Owner); } /// public override void Update(float frameTime) { + base.Update(frameTime); if (!Enabled) return; - var cvarMaxUpdates = _configurationManager.GetCVar(CCVars.NPCMaxUpdates); - - if (cvarMaxUpdates <= 0) return; - - var npcs = _awakeNPCs.ToArray(); - var startIndex = 0; - - // If we're overcap we'll just update randomly so they all still at least do something - // Didn't randomise the array (even though it'd probably be better) because god damn that'd be expensive. - if (npcs.Length > cvarMaxUpdates) - { - startIndex = _robustRandom.Next(npcs.Length); - } - - for (var i = 0; i < npcs.Length; i++) - { - MetaDataComponent? metadata = null; - var index = (i + startIndex) % npcs.Length; - var npc = npcs[index]; - - if (Deleted(npc.Owner, metadata)) - continue; - - // Probably gets resolved in deleted for us already - if (Paused(npc.Owner, metadata)) - continue; - - npc.Update(frameTime); - } + _count = 0; + UpdateUtility(frameTime); } - private void OnMobStateChange(EntityUid uid, AiControllerComponent component, MobStateChangedEvent args) + private void OnMobStateChange(EntityUid uid, NPCComponent component, MobStateChangedEvent args) { switch (args.CurrentMobState) { case DamageState.Alive: - component.Awake = true; + WakeNPC(component); break; case DamageState.Critical: case DamageState.Dead: - component.Awake = false; + SleepNPC(component); break; } } diff --git a/Content.Server/AI/LoadBalancer/AiActionRequestJob.cs b/Content.Server/AI/LoadBalancer/AiActionRequestJob.cs index 64c0b6808e..452aca7961 100644 --- a/Content.Server/AI/LoadBalancer/AiActionRequestJob.cs +++ b/Content.Server/AI/LoadBalancer/AiActionRequestJob.cs @@ -35,7 +35,7 @@ namespace Content.Server.AI.LoadBalancer var entity = _request.Context.GetState().GetValue(); - if (!IoCManager.Resolve().HasComponent(entity)) + if (!IoCManager.Resolve().HasComponent(entity)) { return null; } diff --git a/Content.Server/AI/LoadBalancer/AiActionSystem.cs b/Content.Server/AI/LoadBalancer/AiActionSystem.cs deleted file mode 100644 index f6110ab268..0000000000 --- a/Content.Server/AI/LoadBalancer/AiActionSystem.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Threading; -using Content.Server.CPUJob.JobQueues.Queues; - -namespace Content.Server.AI.LoadBalancer -{ - /// - /// This will queue up an AI's request for an action and give it one when possible - /// - public sealed class AiActionSystem : EntitySystem - { - private readonly AiActionJobQueue _aiRequestQueue = new(); - - public AiActionRequestJob RequestAction(AiActionRequest request, CancellationTokenSource cancellationToken) - { - var job = new AiActionRequestJob(0.002, request, cancellationToken.Token); - // AI should already know if it shouldn't request again - _aiRequestQueue.EnqueueJob(job); - return job; - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - _aiRequestQueue.Process(); - } - } -} diff --git a/Content.Server/AI/Pathfinding/Accessible/ReachableArgs.cs b/Content.Server/AI/Pathfinding/Accessible/ReachableArgs.cs index 9df5c9cd1f..c1f4544c62 100644 --- a/Content.Server/AI/Pathfinding/Accessible/ReachableArgs.cs +++ b/Content.Server/AI/Pathfinding/Accessible/ReachableArgs.cs @@ -1,4 +1,5 @@ using Content.Server.AI.Components; +using Content.Server.AI.EntitySystems; using Content.Shared.Access.Systems; using Robust.Shared.Physics; @@ -33,7 +34,7 @@ namespace Content.Server.AI.Pathfinding.Accessible var accessSystem = EntitySystem.Get(); var access = accessSystem.FindAccessTags(entity); - var visionRadius = entMan.GetComponent(entity).VisionRadius; + var visionRadius = entMan.GetComponent(entity).VisionRadius; return new ReachableArgs(visionRadius, access, collisionMask); } diff --git a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs deleted file mode 100644 index 4eb4e47a71..0000000000 --- a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs +++ /dev/null @@ -1,176 +0,0 @@ -using Content.Server.AI.Components; -using Content.Server.AI.EntitySystems; -using Content.Server.AI.LoadBalancer; -using Content.Server.AI.Operators; -using Content.Server.AI.Utility.Actions; -using Content.Server.AI.WorldState; -using Content.Server.AI.WorldState.States.Utility; -using Content.Server.CPUJob.JobQueues; -using Content.Shared.Movement.Components; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; -using System.Runtime.ExceptionServices; -using System.Threading; - -namespace Content.Server.AI.Utility.AiLogic -{ - // TODO: Need to split out the IMover stuff for NPC to a generic one that can be used for hoomans as well. - [RegisterComponent] - [ComponentProtoName("UtilityAI")] - [ComponentReference(typeof(AiControllerComponent))] - public sealed class UtilityAi : AiControllerComponent - { - // 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 = default!; - public Blackboard Blackboard => _blackboard; - private Blackboard _blackboard = default!; - - /// - /// The sum of all BehaviorSets gives us what actions the AI can take - /// - [DataField("behaviorSets", customTypeSerializer:typeof(PrototypeIdHashSetSerializer))] - public HashSet BehaviorSets { get; } = new(); - - public List AvailableActions { get; set; } = new(); - - /// - /// The currently running action; most importantly are the operators. - /// - public UtilityAction? CurrentAction { get; private set; } - - /// - /// How frequently we can re-plan. If an AI's in combat you could decrease the cooldown, - /// or if there's no players nearby increase it. - /// - public float PlanCooldown { get; } = 0.5f; - private float _planCooldownRemaining; - - /// - /// If we've requested a plan then wait patiently for the action - /// - private AiActionRequestJob? _actionRequest; - - private CancellationTokenSource? _actionCancellation; - - protected override void Initialize() - { - if (BehaviorSets.Count > 0) - { - var behaviorManager = IoCManager.Resolve(); - behaviorManager.RebuildActions(this); - EntitySystem.Get().WakeNPC(this); - } - - base.Initialize(); - _planCooldownRemaining = PlanCooldown; - _blackboard = new Blackboard(Owner); - _planner = EntitySystem.Get(); - } - - protected override void OnRemove() - { - base.OnRemove(); - var currentOp = CurrentAction?.ActionOperators.Peek(); - currentOp?.Shutdown(Outcome.Failed); - CurrentAction?.Shutdown(); - CurrentAction = null; - } - - private void ReceivedAction() - { - if (_actionRequest == null) - { - return; - } - - switch (_actionRequest.Exception) - { - case null: - break; - default: - Logger.FatalS("ai", _actionRequest.Exception.ToString()); - ExceptionDispatchInfo.Capture(_actionRequest.Exception).Throw(); - // The code never actually reaches here, because the above throws. - // This is to tell the compiler that the flow never leaves here. - throw _actionRequest.Exception; - } - var action = _actionRequest.Result; - _actionRequest = null; - // Actions with lower scores should be implicitly dumped by GetAction - // If we're not allowed to replace the action with an action of the same type then dump. - if (action == null || !action.CanOverride && CurrentAction?.GetType() == action.GetType()) - { - return; - } - - var currentOp = CurrentAction?.ActionOperators.Peek(); - if (currentOp != null && currentOp.HasStartup) - { - currentOp.Shutdown(Outcome.Failed); - } - - CurrentAction = action; - action.SetupOperators(_blackboard); - } - - 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) - { - if (_actionRequest.Status != JobStatus.Finished) - { - return; - } - - ReceivedAction(); - // Do something next tick - return; - } - - _planCooldownRemaining -= frameTime; - - // Might find a better action while we're doing one already - if (_planCooldownRemaining <= 0.0f) - { - _planCooldownRemaining = PlanCooldown; - _actionCancellation = new CancellationTokenSource(); - _actionRequest = _planner.RequestAction(new AiActionRequest(Owner, _blackboard, AvailableActions), _actionCancellation); - - return; - } - - // When we spawn in we won't get an action for a bit - if (CurrentAction == null) - { - return; - } - - var outcome = CurrentAction.Execute(frameTime); - - switch (outcome) - { - case Outcome.Success: - if (CurrentAction.ActionOperators.Count == 0) - { - CurrentAction.Shutdown(); - CurrentAction = null; - // Nothing to compare new action to - _blackboard.GetState().SetValue(0.0f); - } - break; - case Outcome.Continuing: - break; - case Outcome.Failed: - CurrentAction.Shutdown(); - CurrentAction = null; - _blackboard.GetState().SetValue(0.0f); - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - } -} diff --git a/Content.Server/AI/Utility/AiLogic/UtilityNPCComponent.cs b/Content.Server/AI/Utility/AiLogic/UtilityNPCComponent.cs new file mode 100644 index 0000000000..880512a8d4 --- /dev/null +++ b/Content.Server/AI/Utility/AiLogic/UtilityNPCComponent.cs @@ -0,0 +1,46 @@ +using Content.Server.AI.Components; +using Content.Server.AI.LoadBalancer; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.WorldState; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; +using System.Threading; +using Content.Server.AI.EntitySystems; + +namespace Content.Server.AI.Utility.AiLogic +{ + [RegisterComponent, Access(typeof(NPCSystem))] + [ComponentReference(typeof(NPCComponent))] + public sealed class UtilityNPCComponent : NPCComponent + { + public Blackboard Blackboard => _blackboard; + public Blackboard _blackboard = default!; + + /// + /// The sum of all BehaviorSets gives us what actions the AI can take + /// + [DataField("behaviorSets", customTypeSerializer:typeof(PrototypeIdHashSetSerializer))] + public HashSet BehaviorSets { get; } = new(); + + public List AvailableActions { get; set; } = new(); + + /// + /// The currently running action; most importantly are the operators. + /// + public UtilityAction? CurrentAction { get; set; } + + /// + /// How frequently we can re-plan. If an AI's in combat you could decrease the cooldown, + /// or if there's no players nearby increase it. + /// + public float PlanCooldown { get; } = 0.5f; + + public float _planCooldownRemaining; + + /// + /// If we've requested a plan then wait patiently for the action + /// + public AiActionRequestJob? _actionRequest; + + public CancellationTokenSource? _actionCancellation; + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyExp.cs index 55dd33bf86..4f2db88e76 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyExp.cs @@ -27,7 +27,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee public override IEnumerable GetActions(Blackboard context) { var owner = context.GetState().GetValue(); - if (!IoCManager.Resolve().TryGetComponent(owner, out AiControllerComponent? controller)) + if (!IoCManager.Resolve().TryGetComponent(owner, out NPCComponent? controller)) { throw new InvalidOperationException(); } diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyHostilesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyHostilesExp.cs index bd8631ffeb..cf2b6120a2 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyHostilesExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyHostilesExp.cs @@ -27,7 +27,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee public override IEnumerable GetActions(Blackboard context) { var owner = context.GetState().GetValue(); - if (!IoCManager.Resolve().TryGetComponent(owner, out AiControllerComponent? controller)) + if (!IoCManager.Resolve().TryGetComponent(owner, out NPCComponent? controller)) { throw new InvalidOperationException(); } diff --git a/Content.Server/AI/Utility/ExpendableActions/Bots/BufferNearbyPuddlesExp.cs b/Content.Server/AI/Utility/ExpendableActions/Bots/BufferNearbyPuddlesExp.cs index d3cff226ec..0b7f56536e 100644 --- a/Content.Server/AI/Utility/ExpendableActions/Bots/BufferNearbyPuddlesExp.cs +++ b/Content.Server/AI/Utility/ExpendableActions/Bots/BufferNearbyPuddlesExp.cs @@ -29,7 +29,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Bots public override IEnumerable GetActions(Blackboard context) { var owner = context.GetState().GetValue(); - if (!IoCManager.Resolve().TryGetComponent(owner, out AiControllerComponent? controller)) + if (!IoCManager.Resolve().TryGetComponent(owner, out NPCComponent? controller)) { throw new InvalidOperationException(); } diff --git a/Content.Server/AI/Utility/NpcBehaviorManager.cs b/Content.Server/AI/Utility/NpcBehaviorManager.cs deleted file mode 100644 index 4e40203fa5..0000000000 --- a/Content.Server/AI/Utility/NpcBehaviorManager.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Content.Server.AI.EntitySystems; -using Content.Server.AI.Utility.Actions; -using Content.Server.AI.Utility.AiLogic; -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!; - - 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 && !npc.Awake) - { - EntitySystem.Get().WakeNPC(npc); - } - } - - /// - /// 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 && npc.Awake) - { - EntitySystem.Get().SleepNPC(npc); - } - } - - /// - /// 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 sealed 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 b7b92f2111..7d46a71fd7 100644 --- a/Content.Server/AI/Utility/UtilityAiHelpers.cs +++ b/Content.Server/AI/Utility/UtilityAiHelpers.cs @@ -8,12 +8,12 @@ namespace Content.Server.AI.Utility { public static Blackboard? GetBlackboard(EntityUid entity) { - if (!IoCManager.Resolve().TryGetComponent(entity, out AiControllerComponent? aiControllerComponent)) + if (!IoCManager.Resolve().TryGetComponent(entity, out NPCComponent? aiControllerComponent)) { return null; } - if (aiControllerComponent is UtilityAi utilityAi) + if (aiControllerComponent is UtilityNPCComponent utilityAi) { return utilityAi.Blackboard; } diff --git a/Content.Server/AI/WorldState/States/Clothing/NearbyClothingState.cs b/Content.Server/AI/WorldState/States/Clothing/NearbyClothingState.cs index d335c9a34d..b92f675cb7 100644 --- a/Content.Server/AI/WorldState/States/Clothing/NearbyClothingState.cs +++ b/Content.Server/AI/WorldState/States/Clothing/NearbyClothingState.cs @@ -17,7 +17,7 @@ namespace Content.Server.AI.WorldState.States.Clothing var result = new List(); var entMan = IoCManager.Resolve(); - if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller)) + if (!entMan.TryGetComponent(Owner, out NPCComponent? controller)) { return result; } diff --git a/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyMeleeWeapons.cs b/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyMeleeWeapons.cs index 34f11d4c29..f95e9461e2 100644 --- a/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyMeleeWeapons.cs +++ b/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyMeleeWeapons.cs @@ -15,7 +15,7 @@ namespace Content.Server.AI.WorldState.States.Combat.Nearby var result = new List(); var entMan = IoCManager.Resolve(); - if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller)) + if (!entMan.TryGetComponent(Owner, out NPCComponent? controller)) { return result; } diff --git a/Content.Server/AI/WorldState/States/Mobs/NearbyBodiesState.cs b/Content.Server/AI/WorldState/States/Mobs/NearbyBodiesState.cs index b4128ac7ca..3b1ea76830 100644 --- a/Content.Server/AI/WorldState/States/Mobs/NearbyBodiesState.cs +++ b/Content.Server/AI/WorldState/States/Mobs/NearbyBodiesState.cs @@ -15,7 +15,7 @@ namespace Content.Server.AI.WorldState.States.Mobs var result = new List(); var entMan = IoCManager.Resolve(); - if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller)) + if (!entMan.TryGetComponent(Owner, out NPCComponent? controller)) { return result; } diff --git a/Content.Server/AI/WorldState/States/Mobs/NearbyPlayersState.cs b/Content.Server/AI/WorldState/States/Mobs/NearbyPlayersState.cs index 0edc69377b..438db57262 100644 --- a/Content.Server/AI/WorldState/States/Mobs/NearbyPlayersState.cs +++ b/Content.Server/AI/WorldState/States/Mobs/NearbyPlayersState.cs @@ -15,7 +15,7 @@ namespace Content.Server.AI.WorldState.States.Mobs var result = new List(); var entMan = IoCManager.Resolve(); - if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller)) + if (!entMan.TryGetComponent(Owner, out NPCComponent? controller)) { return result; } diff --git a/Content.Server/AI/WorldState/States/Nutrition/NearbyDrinkState.cs b/Content.Server/AI/WorldState/States/Nutrition/NearbyDrinkState.cs index db0ebfff84..446b875653 100644 --- a/Content.Server/AI/WorldState/States/Nutrition/NearbyDrinkState.cs +++ b/Content.Server/AI/WorldState/States/Nutrition/NearbyDrinkState.cs @@ -17,7 +17,7 @@ namespace Content.Server.AI.WorldState.States.Nutrition var result = new List(); var entMan = IoCManager.Resolve(); - if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller)) + if (!entMan.TryGetComponent(Owner, out NPCComponent? controller)) { return result; } diff --git a/Content.Server/AI/WorldState/States/Nutrition/NearbyFoodState.cs b/Content.Server/AI/WorldState/States/Nutrition/NearbyFoodState.cs index ae25a92645..c1ab2c4479 100644 --- a/Content.Server/AI/WorldState/States/Nutrition/NearbyFoodState.cs +++ b/Content.Server/AI/WorldState/States/Nutrition/NearbyFoodState.cs @@ -17,7 +17,7 @@ namespace Content.Server.AI.WorldState.States.Nutrition var result = new List(); var entMan = IoCManager.Resolve(); - if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller)) + if (!entMan.TryGetComponent(Owner, out NPCComponent? controller)) { return result; } diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index 77dd716eaa..f8bc19cd43 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -125,7 +125,6 @@ namespace Content.Server.Entry IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); _euiManager.Initialize(); diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index d2f12665e9..876cb7b7be 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -48,7 +48,6 @@ namespace Content.Server.IoC IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); - IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); diff --git a/Content.Server/Mind/Commands/MakeSentientCommand.cs b/Content.Server/Mind/Commands/MakeSentientCommand.cs index 8f7624574d..2da02dce5c 100644 --- a/Content.Server/Mind/Commands/MakeSentientCommand.cs +++ b/Content.Server/Mind/Commands/MakeSentientCommand.cs @@ -44,7 +44,7 @@ namespace Content.Server.Mind.Commands public static void MakeSentient(EntityUid uid, IEntityManager entityManager) { - entityManager.RemoveComponent(uid); + entityManager.RemoveComponent(uid); entityManager.EnsureComponent(uid); entityManager.EnsureComponent(uid); diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 0c9dfbe472..fd7cfda3ef 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -139,7 +139,7 @@ Piercing: 1 - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: - UnarmedAttackHostiles - Idle @@ -670,7 +670,7 @@ Blunt: 10 - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: - UnarmedAttackHostiles - type: AiFactionTag @@ -1151,7 +1151,7 @@ baseSprintSpeed : 7 - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: - UnarmedAttackHostiles - type: AiFactionTag @@ -1319,7 +1319,7 @@ - Xeno - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: - Idle - UnarmedAttackHostiles diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/bear.yml b/Resources/Prototypes/Entities/Mobs/NPCs/bear.yml index 815791ab46..5624ba715d 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/bear.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/bear.yml @@ -6,7 +6,7 @@ components: - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: - Idle - UnarmedAttackHostiles diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml index d9a63ebfb1..78738555a0 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml @@ -6,7 +6,7 @@ components: - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: - Idle - UnarmedAttackHostiles diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/dummy_npcs.yml b/Resources/Prototypes/Entities/Mobs/NPCs/dummy_npcs.yml index 3ff41caf96..9efba23017 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/dummy_npcs.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/dummy_npcs.yml @@ -8,7 +8,7 @@ components: - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC startingGear: PassengerGear behaviorSets: - PathingDummy diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/human.yml b/Resources/Prototypes/Entities/Mobs/NPCs/human.yml index cbea8d7c98..944ebe15c2 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/human.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/human.yml @@ -6,7 +6,7 @@ components: - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: - Clothing - Hunger @@ -26,7 +26,7 @@ components: - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: - Clothing - Hunger diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml b/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml index 55f156f1fb..a38bb308ad 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml @@ -9,7 +9,7 @@ - FootstepSound - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: - UnarmedAttackHostiles - type: AiFactionTag diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml index 62d9d39650..b2ef6daf01 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml @@ -84,7 +84,7 @@ Slash: 5 - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: - UnarmedAttackHostiles - type: AiFactionTag diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml index 1ebf4f3c45..9dda412f0a 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml @@ -125,7 +125,7 @@ baseSprintSpeed : 3.5 - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: - Idle - UnarmedAttackHostiles diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml index b7fd334b12..579025fce7 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml @@ -39,7 +39,7 @@ netsync: false - type: Recyclable safe: false - - type: UtilityAI + - type: UtilityNPC behaviorSets: - Idle - type: AiFactionTag @@ -99,7 +99,7 @@ - type: SpamEmitSound sound: collection: BikeHorn - - type: UtilityAI + - type: UtilityNPC behaviorSets: - Idle - type: Sprite @@ -139,7 +139,7 @@ drawdepth: Mobs sprite: Mobs/Silicon/Bots/cleanbot.rsi state: cleanbot - - type: UtilityAI + - type: UtilityNPC behaviorSets: - CleanBot - type: Drain diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml index 8564e71cf1..993c76d55c 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml @@ -14,7 +14,7 @@ Acidic: [Touch, Ingestion] - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: # - Clothing - Idle @@ -159,7 +159,7 @@ components: - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: - Idle # - Hunger TODO: eating on the floor and fix weird AI endless stomach diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml b/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml index 0b722bb951..8985d204bb 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml @@ -6,7 +6,7 @@ components: - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: - Idle - UnarmedAttackHostiles diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index 540dc9295b..bd5d0328c6 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -12,7 +12,7 @@ canDisarm: true - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: - Idle - UnarmedAttackHostiles @@ -351,7 +351,7 @@ gender: male - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: - Idle - UnarmedAttackHostiles diff --git a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml index 7cc6f244e2..d545b72218 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml @@ -45,7 +45,7 @@ Slash: 7 - type: InputMover - type: MobMover - - type: UtilityAI + - type: UtilityNPC behaviorSets: - Idle - type: AiFactionTag