From 5d63411113b6cb54c49b244542ae8806c35edd1a Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sun, 5 Dec 2021 14:08:35 +1100 Subject: [PATCH] NPC wake / sleep cleanup (#5679) --- Content.IntegrationTests/Tests/EntityTest.cs | 2 +- .../AI/Components/AiControllerComponent.cs | 26 +++- Content.Server/AI/EntitySystems/AiSystem.cs | 133 ----------------- Content.Server/AI/EntitySystems/NPCSystem.cs | 135 ++++++++++++++++++ Content.Server/AI/SleepAiMessage.cs | 24 ---- .../AI/Utility/AiLogic/UtilityAI.cs | 44 +----- .../AI/Utility/NpcBehaviorManager.cs | 8 +- Content.Shared/CCVar/CCVars.cs | 7 +- .../MobState/Components/MobStateComponent.cs | 7 +- ...ngedMessage.cs => MobStateChangedEvent.cs} | 6 +- 10 files changed, 175 insertions(+), 217 deletions(-) delete mode 100644 Content.Server/AI/EntitySystems/AiSystem.cs create mode 100644 Content.Server/AI/EntitySystems/NPCSystem.cs delete mode 100644 Content.Server/AI/SleepAiMessage.cs rename Content.Shared/MobState/{MobStateChangedMessage.cs => MobStateChangedEvent.cs} (80%) diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs index a889b652cf..b05c0b38e1 100644 --- a/Content.IntegrationTests/Tests/EntityTest.cs +++ b/Content.IntegrationTests/Tests/EntityTest.cs @@ -22,7 +22,7 @@ namespace Content.IntegrationTests.Tests { var options = new ServerContentIntegrationOption() { - CVarOverrides = {{CCVars.AIMaxUpdates.Name, int.MaxValue.ToString()}} + CVarOverrides = {{CCVars.NPCMaxUpdates.Name, int.MaxValue.ToString()}} }; var server = StartServer(options); diff --git a/Content.Server/AI/Components/AiControllerComponent.cs b/Content.Server/AI/Components/AiControllerComponent.cs index 468a245860..630a006cde 100644 --- a/Content.Server/AI/Components/AiControllerComponent.cs +++ b/Content.Server/AI/Components/AiControllerComponent.cs @@ -1,4 +1,5 @@ -using Content.Server.GameTicking; +using Content.Server.AI.EntitySystems; +using Content.Server.GameTicking; using Content.Shared.Movement.Components; using Content.Shared.Roles; using Robust.Shared.GameObjects; @@ -19,6 +20,29 @@ namespace Content.Server.AI.Components public override string Name => "AiController"; + // 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)] [DataField("startingGear")] public string? StartingGearPrototype { get; set; } diff --git a/Content.Server/AI/EntitySystems/AiSystem.cs b/Content.Server/AI/EntitySystems/AiSystem.cs deleted file mode 100644 index 19a94bedfd..0000000000 --- a/Content.Server/AI/EntitySystems/AiSystem.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Collections.Generic; -using Content.Server.AI.Components; -using Content.Server.AI.Utility.AiLogic; -using Content.Shared; -using Content.Shared.CCVar; -using Content.Shared.MobState; -using JetBrains.Annotations; -using Robust.Shared.Configuration; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Log; - -namespace Content.Server.AI.EntitySystems -{ - /// - /// Handles NPCs running every tick. - /// - [UsedImplicitly] - internal class AiSystem : EntitySystem - { - [Dependency] private readonly IConfigurationManager _configurationManager = default!; - - /// - /// To avoid iterating over dead AI continuously they can wake and sleep themselves when necessary. - /// - 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(AiControllerComponent npc) => _awakeAi.Contains(npc); - - /// - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(HandleAiSleep); - SubscribeLocalEvent(MobStateChanged); - } - - /// - public override void Update(float frameTime) - { - var cvarMaxUpdates = _configurationManager.GetCVar(CCVars.AIMaxUpdates); - if (cvarMaxUpdates <= 0) - return; - - 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 UtilityAi? controller)) - { - continue; - } - - controller.MobStateChanged(message); - } - - _queuedMobStateMessages.Clear(); - - foreach (var message in _queuedSleepMessages) - { - switch (message.Sleep) - { - case true: - if (_awakeAi.Count == cvarMaxUpdates && _awakeAi.Contains(message.Component)) - { - Logger.Warning($"Under AI limit again: {_awakeAi.Count - 1} / {cvarMaxUpdates}"); - } - _awakeAi.Remove(message.Component); - break; - case false: - _awakeAi.Add(message.Component); - - if (_awakeAi.Count > cvarMaxUpdates) - { - Logger.Warning($"AI limit exceeded: {_awakeAi.Count} / {cvarMaxUpdates}"); - } - break; - } - } - - _queuedSleepMessages.Clear(); - var toRemove = new List(); - var maxUpdates = Math.Min(_awakeAi.Count, cvarMaxUpdates); - var count = 0; - - foreach (var npc in _awakeAi) - { - if (npc.Deleted) - { - toRemove.Add(npc); - continue; - } - - if (npc.Paused) - continue; - - if (count >= maxUpdates) - { - break; - } - - npc.Update(frameTime); - count++; - } - - foreach (var processor in toRemove) - { - _awakeAi.Remove(processor); - } - } - - private void HandleAiSleep(SleepAiMessage message) - { - _queuedSleepMessages.Add(message); - } - - private void MobStateChanged(MobStateChangedMessage message) - { - if (!message.Entity.HasComponent()) - { - return; - } - - _queuedMobStateMessages.Add(message); - } - } -} diff --git a/Content.Server/AI/EntitySystems/NPCSystem.cs b/Content.Server/AI/EntitySystems/NPCSystem.cs new file mode 100644 index 0000000000..a3e5b518fb --- /dev/null +++ b/Content.Server/AI/EntitySystems/NPCSystem.cs @@ -0,0 +1,135 @@ +using System.Collections.Generic; +using System.Linq; +using Content.Server.AI.Components; +using Content.Server.MobState.States; +using Content.Shared.CCVar; +using Content.Shared.MobState; +using JetBrains.Annotations; +using Robust.Shared.Configuration; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Random; + +namespace Content.Server.AI.EntitySystems +{ + /// + /// Handles NPCs running every tick. + /// + [UsedImplicitly] + internal class NPCSystem : EntitySystem + { + [Dependency] private readonly IConfigurationManager _configurationManager = default!; + [Dependency] private readonly IRobustRandom _robustRandom = default!; + + /// + /// To avoid iterating over dead AI continuously they can wake and sleep themselves when necessary. + /// + private readonly HashSet _awakeNPCs = new(); + + /// + /// Whether any NPCs are allowed to run at all. + /// + public bool Enabled { get; set; } = true; + + /// + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnMobStateChange); + SubscribeLocalEvent(OnNPCInit); + SubscribeLocalEvent(OnNPCShutdown); + _configurationManager.OnValueChanged(CCVars.NPCEnabled, SetEnabled, true); + + var maxUpdates = _configurationManager.GetCVar(CCVars.NPCMaxUpdates); + + if (maxUpdates < 1024) + _awakeNPCs.EnsureCapacity(maxUpdates); + } + + private void SetEnabled(bool value) => Enabled = value; + + public override void Shutdown() + { + base.Shutdown(); + _configurationManager.UnsubValueChanged(CCVars.NPCEnabled, SetEnabled); + } + + private void OnNPCInit(EntityUid uid, AiControllerComponent component, ComponentInit args) + { + if (!component.Awake) return; + + _awakeNPCs.Add(component); + } + + private void OnNPCShutdown(EntityUid uid, AiControllerComponent component, ComponentShutdown args) + { + _awakeNPCs.Remove(component); + } + + /// + /// Allows the NPC to actively be updated. + /// + /// + public void WakeNPC(AiControllerComponent component) + { + _awakeNPCs.Add(component); + } + + /// + /// Stops the NPC from actively being updated. + /// + /// + public void SleepNPC(AiControllerComponent component) + { + _awakeNPCs.Remove(component); + } + + /// + public override void Update(float 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++) + { + var index = (i + startIndex) % npcs.Length; + var npc = npcs[index]; + + if (npc.Deleted) + continue; + + if (npc.Paused) + continue; + + npc.Update(frameTime); + } + } + + private void OnMobStateChange(EntityUid uid, AiControllerComponent component, MobStateChangedEvent args) + { + switch (args.CurrentMobState) + { + case NormalMobState: + component.Awake = true; + break; + case CriticalMobState: + case DeadMobState: + component.Awake = false; + break; + } + } + } +} diff --git a/Content.Server/AI/SleepAiMessage.cs b/Content.Server/AI/SleepAiMessage.cs deleted file mode 100644 index 8e5f81b6b9..0000000000 --- a/Content.Server/AI/SleepAiMessage.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Content.Server.AI.Components; -using Robust.Shared.GameObjects; - -namespace Content.Server.AI -{ - /// - /// Indicates whether an AI should be updated by the AiSystem or not. - /// Useful to sleep AI when they die or otherwise should be inactive. - /// - internal sealed class SleepAiMessage : EntityEventArgs - { - /// - /// Sleep or awake. - /// - public bool Sleep { get; } - public AiControllerComponent Component { get; } - - public SleepAiMessage(AiControllerComponent component, bool sleep) - { - Component = component; - Sleep = sleep; - } - } -} diff --git a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs index b314ffe24e..3e735df191 100644 --- a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs +++ b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Runtime.ExceptionServices; using System.Threading; 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; @@ -59,33 +60,13 @@ namespace Content.Server.AI.Utility.AiLogic private CancellationTokenSource? _actionCancellation; - /// - /// If we can't do anything then stop thinking; should probably use ActionBlocker instead - /// - private bool _isDead; - - /*public void AfterDeserialization() - { - if (BehaviorSets.Count > 0) - { - var behaviorManager = IoCManager.Resolve(); - - foreach (var bSet in BehaviorSets) - { - behaviorManager.AddBehaviorSet(this, bSet, false); - } - - behaviorManager.RebuildActions(this); - } - }*/ - protected override void Initialize() { if (BehaviorSets.Count > 0) { var behaviorManager = IoCManager.Resolve(); behaviorManager.RebuildActions(this); - Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(this, false)); + EntitySystem.Get().WakeNPC(this); } base.Initialize(); @@ -103,27 +84,6 @@ namespace Content.Server.AI.Utility.AiLogic CurrentAction = null; } - public void MobStateChanged(MobStateChangedMessage message) - { - var oldDeadState = _isDead; - _isDead = message.Component.IsIncapacitated(); - - if (oldDeadState != _isDead) - { - var entityManager = IoCManager.Resolve(); - - switch (_isDead) - { - case true: - entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(this, true)); - break; - case false: - entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(this, false)); - break; - } - } - } - private void ReceivedAction() { if (_actionRequest == null) diff --git a/Content.Server/AI/Utility/NpcBehaviorManager.cs b/Content.Server/AI/Utility/NpcBehaviorManager.cs index 8610bc4ed2..f1760ef3e2 100644 --- a/Content.Server/AI/Utility/NpcBehaviorManager.cs +++ b/Content.Server/AI/Utility/NpcBehaviorManager.cs @@ -84,9 +84,9 @@ namespace Content.Server.AI.Utility if (rebuild) RebuildActions(npc); - if (npc.BehaviorSets.Count == 1 && !EntitySystem.Get().IsAwake(npc)) + if (npc.BehaviorSets.Count == 1 && !npc.Awake) { - _entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(npc, false)); + EntitySystem.Get().WakeNPC(npc); } } @@ -113,9 +113,9 @@ namespace Content.Server.AI.Utility if (rebuild) RebuildActions(npc); - if (npc.BehaviorSets.Count == 0 && EntitySystem.Get().IsAwake(npc)) + if (npc.BehaviorSets.Count == 0 && npc.Awake) { - _entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(npc, true)); + EntitySystem.Get().SleepNPC(npc); } } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 3986dfbfd4..a456db86aa 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -300,12 +300,13 @@ namespace Content.Shared.CCVar CVarDef.Create("hud.fps_counter_visible", false, CVar.CLIENTONLY | CVar.ARCHIVE); /* - * AI + * NPCs */ - public static readonly CVarDef AIMaxUpdates = - CVarDef.Create("ai.maxupdates", 64); + public static readonly CVarDef NPCMaxUpdates = + CVarDef.Create("npc.max_updates", 64); + public static readonly CVarDef NPCEnabled = CVarDef.Create("npc.enabled", true); /* * Net diff --git a/Content.Shared/MobState/Components/MobStateComponent.cs b/Content.Shared/MobState/Components/MobStateComponent.cs index be089c6e26..bf2d29a541 100644 --- a/Content.Shared/MobState/Components/MobStateComponent.cs +++ b/Content.Shared/MobState/Components/MobStateComponent.cs @@ -311,11 +311,8 @@ namespace Content.Shared.MobState.Components state.EnterState(OwnerUid, Owner.EntityManager); state.UpdateState(OwnerUid, threshold, Owner.EntityManager); - var message = new MobStateChangedMessage(this, old, state); -#pragma warning disable 618 - SendMessage(message); -#pragma warning restore 618 - Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, message); + var message = new MobStateChangedEvent(this, old, state); + Owner.EntityManager.EventBus.RaiseLocalEvent(OwnerUid, message); Dirty(); } diff --git a/Content.Shared/MobState/MobStateChangedMessage.cs b/Content.Shared/MobState/MobStateChangedEvent.cs similarity index 80% rename from Content.Shared/MobState/MobStateChangedMessage.cs rename to Content.Shared/MobState/MobStateChangedEvent.cs index 5ac381fa77..a862e98cd3 100644 --- a/Content.Shared/MobState/MobStateChangedMessage.cs +++ b/Content.Shared/MobState/MobStateChangedEvent.cs @@ -4,11 +4,9 @@ using Robust.Shared.GameObjects; namespace Content.Shared.MobState { -#pragma warning disable 618 - public class MobStateChangedMessage : ComponentMessage -#pragma warning restore 618 + public class MobStateChangedEvent : EntityEventArgs { - public MobStateChangedMessage( + public MobStateChangedEvent( MobStateComponent component, IMobState? oldMobState, IMobState currentMobState)