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)