NPC wake / sleep cleanup (#5679)

This commit is contained in:
metalgearsloth
2021-12-05 14:08:35 +11:00
committed by GitHub
parent ae65418c52
commit 5d63411113
10 changed files with 175 additions and 217 deletions

View File

@@ -22,7 +22,7 @@ namespace Content.IntegrationTests.Tests
{ {
var options = new ServerContentIntegrationOption() var options = new ServerContentIntegrationOption()
{ {
CVarOverrides = {{CCVars.AIMaxUpdates.Name, int.MaxValue.ToString()}} CVarOverrides = {{CCVars.NPCMaxUpdates.Name, int.MaxValue.ToString()}}
}; };
var server = StartServer(options); var server = StartServer(options);

View File

@@ -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.Movement.Components;
using Content.Shared.Roles; using Content.Shared.Roles;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -19,6 +20,29 @@ namespace Content.Server.AI.Components
public override string Name => "AiController"; public override string Name => "AiController";
// TODO: Need to ECS a lot more of the AI first before we can ECS this
/// <summary>
/// Whether the AI is actively iterated.
/// </summary>
public bool Awake
{
get => _awake;
set
{
if (_awake == value) return;
_awake = value;
if (_awake)
EntitySystem.Get<NPCSystem>().WakeNPC(this);
else
EntitySystem.Get<NPCSystem>().SleepNPC(this);
}
}
[DataField("awake")]
private bool _awake = true;
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
[DataField("startingGear")] [DataField("startingGear")]
public string? StartingGearPrototype { get; set; } public string? StartingGearPrototype { get; set; }

View File

@@ -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
{
/// <summary>
/// Handles NPCs running every tick.
/// </summary>
[UsedImplicitly]
internal class AiSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
/// <summary>
/// To avoid iterating over dead AI continuously they can wake and sleep themselves when necessary.
/// </summary>
private readonly HashSet<AiControllerComponent> _awakeAi = new();
// To avoid modifying awakeAi while iterating over it.
private readonly List<SleepAiMessage> _queuedSleepMessages = new();
private readonly List<MobStateChangedMessage> _queuedMobStateMessages = new();
public bool IsAwake(AiControllerComponent npc) => _awakeAi.Contains(npc);
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SleepAiMessage>(HandleAiSleep);
SubscribeLocalEvent<MobStateChangedMessage>(MobStateChanged);
}
/// <inheritdoc />
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<AiControllerComponent>();
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<AiControllerComponent>())
{
return;
}
_queuedMobStateMessages.Add(message);
}
}
}

View File

@@ -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
{
/// <summary>
/// Handles NPCs running every tick.
/// </summary>
[UsedImplicitly]
internal class NPCSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
/// <summary>
/// To avoid iterating over dead AI continuously they can wake and sleep themselves when necessary.
/// </summary>
private readonly HashSet<AiControllerComponent> _awakeNPCs = new();
/// <summary>
/// Whether any NPCs are allowed to run at all.
/// </summary>
public bool Enabled { get; set; } = true;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AiControllerComponent, MobStateChangedEvent>(OnMobStateChange);
SubscribeLocalEvent<AiControllerComponent, ComponentInit>(OnNPCInit);
SubscribeLocalEvent<AiControllerComponent, ComponentShutdown>(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);
}
/// <summary>
/// Allows the NPC to actively be updated.
/// </summary>
/// <param name="component"></param>
public void WakeNPC(AiControllerComponent component)
{
_awakeNPCs.Add(component);
}
/// <summary>
/// Stops the NPC from actively being updated.
/// </summary>
/// <param name="component"></param>
public void SleepNPC(AiControllerComponent component)
{
_awakeNPCs.Remove(component);
}
/// <inheritdoc />
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;
}
}
}
}

View File

@@ -1,24 +0,0 @@
using Content.Server.AI.Components;
using Robust.Shared.GameObjects;
namespace Content.Server.AI
{
/// <summary>
/// Indicates whether an AI should be updated by the AiSystem or not.
/// Useful to sleep AI when they die or otherwise should be inactive.
/// </summary>
internal sealed class SleepAiMessage : EntityEventArgs
{
/// <summary>
/// Sleep or awake.
/// </summary>
public bool Sleep { get; }
public AiControllerComponent Component { get; }
public SleepAiMessage(AiControllerComponent component, bool sleep)
{
Component = component;
Sleep = sleep;
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
using System.Threading; using System.Threading;
using Content.Server.AI.Components; using Content.Server.AI.Components;
using Content.Server.AI.EntitySystems;
using Content.Server.AI.LoadBalancer; using Content.Server.AI.LoadBalancer;
using Content.Server.AI.Operators; using Content.Server.AI.Operators;
using Content.Server.AI.Utility.Actions; using Content.Server.AI.Utility.Actions;
@@ -59,33 +60,13 @@ namespace Content.Server.AI.Utility.AiLogic
private CancellationTokenSource? _actionCancellation; private CancellationTokenSource? _actionCancellation;
/// <summary>
/// If we can't do anything then stop thinking; should probably use ActionBlocker instead
/// </summary>
private bool _isDead;
/*public void AfterDeserialization()
{
if (BehaviorSets.Count > 0)
{
var behaviorManager = IoCManager.Resolve<INpcBehaviorManager>();
foreach (var bSet in BehaviorSets)
{
behaviorManager.AddBehaviorSet(this, bSet, false);
}
behaviorManager.RebuildActions(this);
}
}*/
protected override void Initialize() protected override void Initialize()
{ {
if (BehaviorSets.Count > 0) if (BehaviorSets.Count > 0)
{ {
var behaviorManager = IoCManager.Resolve<INpcBehaviorManager>(); var behaviorManager = IoCManager.Resolve<INpcBehaviorManager>();
behaviorManager.RebuildActions(this); behaviorManager.RebuildActions(this);
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(this, false)); EntitySystem.Get<NPCSystem>().WakeNPC(this);
} }
base.Initialize(); base.Initialize();
@@ -103,27 +84,6 @@ namespace Content.Server.AI.Utility.AiLogic
CurrentAction = null; CurrentAction = null;
} }
public void MobStateChanged(MobStateChangedMessage message)
{
var oldDeadState = _isDead;
_isDead = message.Component.IsIncapacitated();
if (oldDeadState != _isDead)
{
var entityManager = IoCManager.Resolve<IEntityManager>();
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() private void ReceivedAction()
{ {
if (_actionRequest == null) if (_actionRequest == null)

View File

@@ -84,9 +84,9 @@ namespace Content.Server.AI.Utility
if (rebuild) if (rebuild)
RebuildActions(npc); RebuildActions(npc);
if (npc.BehaviorSets.Count == 1 && !EntitySystem.Get<AiSystem>().IsAwake(npc)) if (npc.BehaviorSets.Count == 1 && !npc.Awake)
{ {
_entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(npc, false)); EntitySystem.Get<NPCSystem>().WakeNPC(npc);
} }
} }
@@ -113,9 +113,9 @@ namespace Content.Server.AI.Utility
if (rebuild) if (rebuild)
RebuildActions(npc); RebuildActions(npc);
if (npc.BehaviorSets.Count == 0 && EntitySystem.Get<AiSystem>().IsAwake(npc)) if (npc.BehaviorSets.Count == 0 && npc.Awake)
{ {
_entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(npc, true)); EntitySystem.Get<NPCSystem>().SleepNPC(npc);
} }
} }

View File

@@ -300,12 +300,13 @@ namespace Content.Shared.CCVar
CVarDef.Create("hud.fps_counter_visible", false, CVar.CLIENTONLY | CVar.ARCHIVE); CVarDef.Create("hud.fps_counter_visible", false, CVar.CLIENTONLY | CVar.ARCHIVE);
/* /*
* AI * NPCs
*/ */
public static readonly CVarDef<int> AIMaxUpdates = public static readonly CVarDef<int> NPCMaxUpdates =
CVarDef.Create("ai.maxupdates", 64); CVarDef.Create("npc.max_updates", 64);
public static readonly CVarDef<bool> NPCEnabled = CVarDef.Create("npc.enabled", true);
/* /*
* Net * Net

View File

@@ -311,11 +311,8 @@ namespace Content.Shared.MobState.Components
state.EnterState(OwnerUid, Owner.EntityManager); state.EnterState(OwnerUid, Owner.EntityManager);
state.UpdateState(OwnerUid, threshold, Owner.EntityManager); state.UpdateState(OwnerUid, threshold, Owner.EntityManager);
var message = new MobStateChangedMessage(this, old, state); var message = new MobStateChangedEvent(this, old, state);
#pragma warning disable 618 Owner.EntityManager.EventBus.RaiseLocalEvent(OwnerUid, message);
SendMessage(message);
#pragma warning restore 618
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, message);
Dirty(); Dirty();
} }

View File

@@ -4,11 +4,9 @@ using Robust.Shared.GameObjects;
namespace Content.Shared.MobState namespace Content.Shared.MobState
{ {
#pragma warning disable 618 public class MobStateChangedEvent : EntityEventArgs
public class MobStateChangedMessage : ComponentMessage
#pragma warning restore 618
{ {
public MobStateChangedMessage( public MobStateChangedEvent(
MobStateComponent component, MobStateComponent component,
IMobState? oldMobState, IMobState? oldMobState,
IMobState currentMobState) IMobState currentMobState)