* ECS

* A

* parity

* Remove dummy update

* abs

* thanks rider
This commit is contained in:
metalgearsloth
2022-07-25 14:57:33 +10:00
committed by GitHub
parent 3fb9b4a480
commit aad6a22a6a
40 changed files with 482 additions and 531 deletions

View File

@@ -48,7 +48,7 @@ namespace Content.IntegrationTests.Tests.AI
{ {
foreach (var entity in protoManager.EnumeratePrototypes<EntityPrototype>()) foreach (var entity in protoManager.EnumeratePrototypes<EntityPrototype>())
{ {
if (!entity.TryGetComponent<UtilityAi>("UtilityAI", out var npcNode)) continue; if (!entity.TryGetComponent<UtilityNPCComponent>("UtilityAI", out var npcNode)) continue;
foreach (var entry in npcNode.BehaviorSets) foreach (var entry in npcNode.BehaviorSets)
{ {

View File

@@ -1,5 +1,6 @@
using Content.Server.Administration; using Content.Server.Administration;
using Content.Server.AI.Components; using Content.Server.AI.Components;
using Content.Server.AI.EntitySystems;
using Content.Server.AI.Utility; using Content.Server.AI.Utility;
using Content.Server.AI.Utility.AiLogic; using Content.Server.AI.Utility.AiLogic;
using Content.Shared.Administration; using Content.Shared.Administration;
@@ -35,22 +36,22 @@ namespace Content.Server.AI.Commands
return; return;
} }
if (_entities.HasComponent<AiControllerComponent>(entId)) if (_entities.HasComponent<NPCComponent>(entId))
{ {
shell.WriteLine("Entity already has an AI component."); shell.WriteLine("Entity already has an AI component.");
return; return;
} }
var comp = _entities.AddComponent<UtilityAi>(entId); var comp = _entities.AddComponent<UtilityNPCComponent>(entId);
var behaviorManager = IoCManager.Resolve<INpcBehaviorManager>(); var npcSystem = IoCManager.Resolve<IEntityManager>().EntitySysManager.GetEntitySystem<NPCSystem>();
for (var i = 1; i < args.Length; i++) for (var i = 1; i < args.Length; i++)
{ {
var bSet = args[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."); shell.WriteLine("AI component added.");
} }
} }

View File

@@ -0,0 +1,7 @@
namespace Content.Server.AI.Components;
/// <summary>
/// Added to NPCs that are actively being updated.
/// </summary>
[RegisterComponent]
public sealed class ActiveNPCComponent : Component {}

View File

@@ -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
/// <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)]
public float VisionRadius
{
get => _visionRadius;
set => _visionRadius = value;
}
/// <summary>
/// Is the entity Sprinting (running)?
/// </summary>
[ViewVariables]
public bool Sprinting { get; } = true;
/// <summary>
/// Calculated linear velocity direction of the entity.
/// </summary>
[ViewVariables]
public Vector2 VelocityDir { get; set; }
public virtual void Update(float frameTime) {}
}
}

View File

@@ -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.
/// <summary>
/// Contains all of the world data for a particular NPC in terms of how it sees the world.
/// </summary>
//[ViewVariables, DataField("blackboardA")]
//public Dictionary<string, object> BlackboardA = new();
public float VisionRadius => 7f;
}
}

View File

@@ -0,0 +1,42 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.AI.Components;
namespace Content.Server.AI.EntitySystems;
public sealed partial class NPCSystem
{
/*
/// <summary>
/// Tries to get the blackboard data for a particular key. Returns default if not found
/// </summary>
public T? GetValueOrDefault<T>(NPCComponent component, string key)
{
if (component.BlackboardA.TryGetValue(key, out var value))
{
return (T) value;
}
return default;
}
/// <summary>
/// Tries to get the blackboard data for a particular key.
/// </summary>
public bool TryGetValue<T>(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";
}

View File

@@ -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<string, List<Type>> _behaviorSets = new();
private readonly AiActionJobQueue _aiRequestQueue = new();
private void InitializeUtility()
{
SubscribeLocalEvent<UtilityNPCComponent, ComponentStartup>(OnUtilityStartup);
foreach (var bSet in _prototypeManager.EnumeratePrototypes<BehaviorSetPrototype>())
{
var actions = new List<Type>();
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<ActiveNPCComponent, UtilityNPCComponent>())
{
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<LastUtilityScoreState>().SetValue(0.0f);
}
break;
case Outcome.Continuing:
break;
case Outcome.Failed:
component.CurrentAction.Shutdown();
component.CurrentAction = null;
component._blackboard.GetState<LastUtilityScoreState>().SetValue(0.0f);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
/// <summary>
/// Adds the BehaviorSet to the NPC.
/// </summary>
/// <param name="npc"></param>
/// <param name="behaviorSet"></param>
/// <param name="rebuild">Set to false if you want to manually rebuild it after bulk updates.</param>
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);
}
/// <summary>
/// Removes the BehaviorSet from the NPC.
/// </summary>
/// <param name="npc"></param>
/// <param name="behaviorSet"></param>
/// <param name="rebuild">Set to false if yo uwant to manually rebuild it after bulk updates.</param>
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);
}
/// <summary>
/// Clear our actions and re-instantiate them from our BehaviorSets.
/// Will ensure each action is unique.
/// </summary>
/// <param name="npc"></param>
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<IAiUtility> GetActions(string behaviorSet)
{
foreach (var action in _behaviorSets[behaviorSet])
{
yield return (IAiUtility) _typeFactory.CreateInstance(action);
}
}
/// <summary>
/// Whenever the behavior sets are changed we'll re-sort the actions by bonus
/// </summary>
private void SortActions(UtilityNPCComponent npc)
{
npc.AvailableActions.Sort(_comparer);
}
private sealed class NpcActionComparer : Comparer<IAiUtility>
{
public override int Compare(IAiUtility? x, IAiUtility? y)
{
if (x == null || y == null) return 0;
return y.Bonus.CompareTo(x.Bonus);
}
}
}

View File

@@ -1,10 +1,10 @@
using System.Linq;
using Content.Server.AI.Components; using Content.Server.AI.Components;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.MobState; using Content.Shared.MobState;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Random; using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
namespace Content.Server.AI.EntitySystems namespace Content.Server.AI.EntitySystems
{ {
@@ -12,120 +12,103 @@ namespace Content.Server.AI.EntitySystems
/// Handles NPCs running every tick. /// Handles NPCs running every tick.
/// </summary> /// </summary>
[UsedImplicitly] [UsedImplicitly]
internal sealed class NPCSystem : EntitySystem public sealed partial class NPCSystem : EntitySystem
{ {
[Dependency] private readonly IConfigurationManager _configurationManager = default!; [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!;
/// <summary> private ISawmill _sawmill = default!;
/// To avoid iterating over dead AI continuously they can wake and sleep themselves when necessary.
/// </summary>
private readonly HashSet<AiControllerComponent> _awakeNPCs = new();
/// <summary> /// <summary>
/// Whether any NPCs are allowed to run at all. /// Whether any NPCs are allowed to run at all.
/// </summary> /// </summary>
public bool Enabled { get; set; } = true; public bool Enabled { get; set; } = true;
private int _maxUpdates;
private int _count;
/// <inheritdoc /> /// <inheritdoc />
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<AiControllerComponent, MobStateChangedEvent>(OnMobStateChange); _sawmill = Logger.GetSawmill("npc");
SubscribeLocalEvent<AiControllerComponent, ComponentInit>(OnNPCInit); InitializeUtility();
SubscribeLocalEvent<AiControllerComponent, ComponentShutdown>(OnNPCShutdown); SubscribeLocalEvent<NPCComponent, MobStateChangedEvent>(OnMobStateChange);
SubscribeLocalEvent<NPCComponent, ComponentInit>(OnNPCInit);
SubscribeLocalEvent<NPCComponent, ComponentShutdown>(OnNPCShutdown);
_configurationManager.OnValueChanged(CCVars.NPCEnabled, SetEnabled, true); _configurationManager.OnValueChanged(CCVars.NPCEnabled, SetEnabled, true);
_configurationManager.OnValueChanged(CCVars.NPCMaxUpdates, SetMaxUpdates, true);
var maxUpdates = _configurationManager.GetCVar(CCVars.NPCMaxUpdates);
if (maxUpdates < 1024)
_awakeNPCs.EnsureCapacity(maxUpdates);
} }
private void SetMaxUpdates(int obj) => _maxUpdates = obj;
private void SetEnabled(bool value) => Enabled = value; private void SetEnabled(bool value) => Enabled = value;
public override void Shutdown() public override void Shutdown()
{ {
base.Shutdown(); base.Shutdown();
_configurationManager.UnsubValueChanged(CCVars.NPCEnabled, SetEnabled); _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; WakeNPC(component);
_awakeNPCs.Add(component);
} }
private void OnNPCShutdown(EntityUid uid, AiControllerComponent component, ComponentShutdown args) private void OnNPCShutdown(EntityUid uid, NPCComponent component, ComponentShutdown args)
{ {
_awakeNPCs.Remove(component); SleepNPC(component);
}
/// <summary>
/// Is the NPC awake and updating?
/// </summary>
public bool IsAwake(NPCComponent component, ActiveNPCComponent? active = null)
{
return Resolve(component.Owner, ref active, false);
} }
/// <summary> /// <summary>
/// Allows the NPC to actively be updated. /// Allows the NPC to actively be updated.
/// </summary> /// </summary>
/// <param name="component"></param> public void WakeNPC(NPCComponent component)
public void WakeNPC(AiControllerComponent component)
{ {
_awakeNPCs.Add(component); _sawmill.Debug($"Waking {ToPrettyString(component.Owner)}");
EnsureComp<ActiveNPCComponent>(component.Owner);
} }
/// <summary> /// <summary>
/// Stops the NPC from actively being updated. /// Stops the NPC from actively being updated.
/// </summary> /// </summary>
/// <param name="component"></param> public void SleepNPC(NPCComponent component)
public void SleepNPC(AiControllerComponent component)
{ {
_awakeNPCs.Remove(component); _sawmill.Debug($"Sleeping {ToPrettyString(component.Owner)}");
RemComp<ActiveNPCComponent>(component.Owner);
} }
/// <inheritdoc /> /// <inheritdoc />
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
base.Update(frameTime);
if (!Enabled) return; if (!Enabled) return;
var cvarMaxUpdates = _configurationManager.GetCVar(CCVars.NPCMaxUpdates); _count = 0;
UpdateUtility(frameTime);
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);
}
} }
private void OnMobStateChange(EntityUid uid, AiControllerComponent component, MobStateChangedEvent args) private void OnMobStateChange(EntityUid uid, NPCComponent component, MobStateChangedEvent args)
{ {
switch (args.CurrentMobState) switch (args.CurrentMobState)
{ {
case DamageState.Alive: case DamageState.Alive:
component.Awake = true; WakeNPC(component);
break; break;
case DamageState.Critical: case DamageState.Critical:
case DamageState.Dead: case DamageState.Dead:
component.Awake = false; SleepNPC(component);
break; break;
} }
} }

View File

@@ -35,7 +35,7 @@ namespace Content.Server.AI.LoadBalancer
var entity = _request.Context.GetState<SelfState>().GetValue(); var entity = _request.Context.GetState<SelfState>().GetValue();
if (!IoCManager.Resolve<IEntityManager>().HasComponent<AiControllerComponent>(entity)) if (!IoCManager.Resolve<IEntityManager>().HasComponent<NPCComponent>(entity))
{ {
return null; return null;
} }

View File

@@ -1,27 +0,0 @@
using System.Threading;
using Content.Server.CPUJob.JobQueues.Queues;
namespace Content.Server.AI.LoadBalancer
{
/// <summary>
/// This will queue up an AI's request for an action and give it one when possible
/// </summary>
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();
}
}
}

View File

@@ -1,4 +1,5 @@
using Content.Server.AI.Components; using Content.Server.AI.Components;
using Content.Server.AI.EntitySystems;
using Content.Shared.Access.Systems; using Content.Shared.Access.Systems;
using Robust.Shared.Physics; using Robust.Shared.Physics;
@@ -33,7 +34,7 @@ namespace Content.Server.AI.Pathfinding.Accessible
var accessSystem = EntitySystem.Get<AccessReaderSystem>(); var accessSystem = EntitySystem.Get<AccessReaderSystem>();
var access = accessSystem.FindAccessTags(entity); var access = accessSystem.FindAccessTags(entity);
var visionRadius = entMan.GetComponent<AiControllerComponent>(entity).VisionRadius; var visionRadius = entMan.GetComponent<NPCComponent>(entity).VisionRadius;
return new ReachableArgs(visionRadius, access, collisionMask); return new ReachableArgs(visionRadius, access, collisionMask);
} }

View File

@@ -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!;
/// <summary>
/// The sum of all BehaviorSets gives us what actions the AI can take
/// </summary>
[DataField("behaviorSets", customTypeSerializer:typeof(PrototypeIdHashSetSerializer<BehaviorSetPrototype>))]
public HashSet<string> BehaviorSets { get; } = new();
public List<IAiUtility> AvailableActions { get; set; } = new();
/// <summary>
/// The currently running action; most importantly are the operators.
/// </summary>
public UtilityAction? CurrentAction { get; private set; }
/// <summary>
/// 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.
/// </summary>
public float PlanCooldown { get; } = 0.5f;
private float _planCooldownRemaining;
/// <summary>
/// If we've requested a plan then wait patiently for the action
/// </summary>
private AiActionRequestJob? _actionRequest;
private CancellationTokenSource? _actionCancellation;
protected override void Initialize()
{
if (BehaviorSets.Count > 0)
{
var behaviorManager = IoCManager.Resolve<INpcBehaviorManager>();
behaviorManager.RebuildActions(this);
EntitySystem.Get<NPCSystem>().WakeNPC(this);
}
base.Initialize();
_planCooldownRemaining = PlanCooldown;
_blackboard = new Blackboard(Owner);
_planner = EntitySystem.Get<AiActionSystem>();
}
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<LastUtilityScoreState>().SetValue(0.0f);
}
break;
case Outcome.Continuing:
break;
case Outcome.Failed:
CurrentAction.Shutdown();
CurrentAction = null;
_blackboard.GetState<LastUtilityScoreState>().SetValue(0.0f);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@@ -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!;
/// <summary>
/// The sum of all BehaviorSets gives us what actions the AI can take
/// </summary>
[DataField("behaviorSets", customTypeSerializer:typeof(PrototypeIdHashSetSerializer<BehaviorSetPrototype>))]
public HashSet<string> BehaviorSets { get; } = new();
public List<IAiUtility> AvailableActions { get; set; } = new();
/// <summary>
/// The currently running action; most importantly are the operators.
/// </summary>
public UtilityAction? CurrentAction { get; set; }
/// <summary>
/// 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.
/// </summary>
public float PlanCooldown { get; } = 0.5f;
public float _planCooldownRemaining;
/// <summary>
/// If we've requested a plan then wait patiently for the action
/// </summary>
public AiActionRequestJob? _actionRequest;
public CancellationTokenSource? _actionCancellation;
}
}

View File

@@ -27,7 +27,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
public override IEnumerable<UtilityAction> GetActions(Blackboard context) public override IEnumerable<UtilityAction> GetActions(Blackboard context)
{ {
var owner = context.GetState<SelfState>().GetValue(); var owner = context.GetState<SelfState>().GetValue();
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(owner, out AiControllerComponent? controller)) if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(owner, out NPCComponent? controller))
{ {
throw new InvalidOperationException(); throw new InvalidOperationException();
} }

View File

@@ -27,7 +27,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
public override IEnumerable<UtilityAction> GetActions(Blackboard context) public override IEnumerable<UtilityAction> GetActions(Blackboard context)
{ {
var owner = context.GetState<SelfState>().GetValue(); var owner = context.GetState<SelfState>().GetValue();
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(owner, out AiControllerComponent? controller)) if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(owner, out NPCComponent? controller))
{ {
throw new InvalidOperationException(); throw new InvalidOperationException();
} }

View File

@@ -29,7 +29,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Bots
public override IEnumerable<UtilityAction> GetActions(Blackboard context) public override IEnumerable<UtilityAction> GetActions(Blackboard context)
{ {
var owner = context.GetState<SelfState>().GetValue(); var owner = context.GetState<SelfState>().GetValue();
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(owner, out AiControllerComponent? controller)) if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(owner, out NPCComponent? controller))
{ {
throw new InvalidOperationException(); throw new InvalidOperationException();
} }

View File

@@ -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);
}
/// <summary>
/// Handles BehaviorSets and adding / removing behaviors to NPCs
/// </summary>
internal sealed class NpcBehaviorManager : INpcBehaviorManager
{
[Dependency] private readonly IDynamicTypeFactory _typeFactory = default!;
private readonly NpcActionComparer _comparer = new();
private Dictionary<string, List<Type>> _behaviorSets = new();
public void Initialize()
{
IoCManager.InjectDependencies(this);
var protoManager = IoCManager.Resolve<IPrototypeManager>();
var reflectionManager = IoCManager.Resolve<IReflectionManager>();
foreach (var bSet in protoManager.EnumeratePrototypes<BehaviorSetPrototype>())
{
var actions = new List<Type>();
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;
}
}
/// <summary>
/// Adds the BehaviorSet to the NPC.
/// </summary>
/// <param name="npc"></param>
/// <param name="behaviorSet"></param>
/// <param name="rebuild">Set to false if you want to manually rebuild it after bulk updates.</param>
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<NPCSystem>().WakeNPC(npc);
}
}
/// <summary>
/// Removes the BehaviorSet from the NPC.
/// </summary>
/// <param name="npc"></param>
/// <param name="behaviorSet"></param>
/// <param name="rebuild">Set to false if yo uwant to manually rebuild it after bulk updates.</param>
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<NPCSystem>().SleepNPC(npc);
}
}
/// <summary>
/// Clear our actions and re-instantiate them from our BehaviorSets.
/// Will ensure each action is unique.
/// </summary>
/// <param name="npc"></param>
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<IAiUtility> GetActions(string behaviorSet)
{
foreach (var action in _behaviorSets[behaviorSet])
{
yield return (IAiUtility) _typeFactory.CreateInstance(action);
}
}
/// <summary>
/// Whenever the behavior sets are changed we'll re-sort the actions by bonus
/// </summary>
private void SortActions(UtilityAi npc)
{
npc.AvailableActions.Sort(_comparer);
}
private sealed class NpcActionComparer : Comparer<IAiUtility>
{
public override int Compare(IAiUtility? x, IAiUtility? y)
{
if (x == null || y == null) return 0;
return y.Bonus.CompareTo(x.Bonus);
}
}
}
}

View File

@@ -8,12 +8,12 @@ namespace Content.Server.AI.Utility
{ {
public static Blackboard? GetBlackboard(EntityUid entity) public static Blackboard? GetBlackboard(EntityUid entity)
{ {
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(entity, out AiControllerComponent? aiControllerComponent)) if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(entity, out NPCComponent? aiControllerComponent))
{ {
return null; return null;
} }
if (aiControllerComponent is UtilityAi utilityAi) if (aiControllerComponent is UtilityNPCComponent utilityAi)
{ {
return utilityAi.Blackboard; return utilityAi.Blackboard;
} }

View File

@@ -17,7 +17,7 @@ namespace Content.Server.AI.WorldState.States.Clothing
var result = new List<EntityUid>(); var result = new List<EntityUid>();
var entMan = IoCManager.Resolve<IEntityManager>(); var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller)) if (!entMan.TryGetComponent(Owner, out NPCComponent? controller))
{ {
return result; return result;
} }

View File

@@ -15,7 +15,7 @@ namespace Content.Server.AI.WorldState.States.Combat.Nearby
var result = new List<EntityUid>(); var result = new List<EntityUid>();
var entMan = IoCManager.Resolve<IEntityManager>(); var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller)) if (!entMan.TryGetComponent(Owner, out NPCComponent? controller))
{ {
return result; return result;
} }

View File

@@ -15,7 +15,7 @@ namespace Content.Server.AI.WorldState.States.Mobs
var result = new List<EntityUid>(); var result = new List<EntityUid>();
var entMan = IoCManager.Resolve<IEntityManager>(); var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller)) if (!entMan.TryGetComponent(Owner, out NPCComponent? controller))
{ {
return result; return result;
} }

View File

@@ -15,7 +15,7 @@ namespace Content.Server.AI.WorldState.States.Mobs
var result = new List<EntityUid>(); var result = new List<EntityUid>();
var entMan = IoCManager.Resolve<IEntityManager>(); var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller)) if (!entMan.TryGetComponent(Owner, out NPCComponent? controller))
{ {
return result; return result;
} }

View File

@@ -17,7 +17,7 @@ namespace Content.Server.AI.WorldState.States.Nutrition
var result = new List<EntityUid>(); var result = new List<EntityUid>();
var entMan = IoCManager.Resolve<IEntityManager>(); var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller)) if (!entMan.TryGetComponent(Owner, out NPCComponent? controller))
{ {
return result; return result;
} }

View File

@@ -17,7 +17,7 @@ namespace Content.Server.AI.WorldState.States.Nutrition
var result = new List<EntityUid>(); var result = new List<EntityUid>();
var entMan = IoCManager.Resolve<IEntityManager>(); var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent(Owner, out AiControllerComponent? controller)) if (!entMan.TryGetComponent(Owner, out NPCComponent? controller))
{ {
return result; return result;
} }

View File

@@ -125,7 +125,6 @@ namespace Content.Server.Entry
IoCManager.Resolve<BlackboardManager>().Initialize(); IoCManager.Resolve<BlackboardManager>().Initialize();
IoCManager.Resolve<ConsiderationsManager>().Initialize(); IoCManager.Resolve<ConsiderationsManager>().Initialize();
IoCManager.Resolve<IAdminManager>().Initialize(); IoCManager.Resolve<IAdminManager>().Initialize();
IoCManager.Resolve<INpcBehaviorManager>().Initialize();
IoCManager.Resolve<IAfkManager>().Initialize(); IoCManager.Resolve<IAfkManager>().Initialize();
IoCManager.Resolve<RulesManager>().Initialize(); IoCManager.Resolve<RulesManager>().Initialize();
_euiManager.Initialize(); _euiManager.Initialize();

View File

@@ -48,7 +48,6 @@ namespace Content.Server.IoC
IoCManager.Register<IAdminManager, AdminManager>(); IoCManager.Register<IAdminManager, AdminManager>();
IoCManager.Register<EuiManager, EuiManager>(); IoCManager.Register<EuiManager, EuiManager>();
IoCManager.Register<IVoteManager, VoteManager>(); IoCManager.Register<IVoteManager, VoteManager>();
IoCManager.Register<INpcBehaviorManager, NpcBehaviorManager>();
IoCManager.Register<IPlayerLocator, PlayerLocator>(); IoCManager.Register<IPlayerLocator, PlayerLocator>();
IoCManager.Register<IAfkManager, AfkManager>(); IoCManager.Register<IAfkManager, AfkManager>();
IoCManager.Register<IGameMapManager, GameMapManager>(); IoCManager.Register<IGameMapManager, GameMapManager>();

View File

@@ -44,7 +44,7 @@ namespace Content.Server.Mind.Commands
public static void MakeSentient(EntityUid uid, IEntityManager entityManager) public static void MakeSentient(EntityUid uid, IEntityManager entityManager)
{ {
entityManager.RemoveComponent<AiControllerComponent>(uid); entityManager.RemoveComponent<NPCComponent>(uid);
entityManager.EnsureComponent<MindComponent>(uid); entityManager.EnsureComponent<MindComponent>(uid);
entityManager.EnsureComponent<InputMoverComponent>(uid); entityManager.EnsureComponent<InputMoverComponent>(uid);

View File

@@ -139,7 +139,7 @@
Piercing: 1 Piercing: 1
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- UnarmedAttackHostiles - UnarmedAttackHostiles
- Idle - Idle
@@ -670,7 +670,7 @@
Blunt: 10 Blunt: 10
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- UnarmedAttackHostiles - UnarmedAttackHostiles
- type: AiFactionTag - type: AiFactionTag
@@ -1151,7 +1151,7 @@
baseSprintSpeed : 7 baseSprintSpeed : 7
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- UnarmedAttackHostiles - UnarmedAttackHostiles
- type: AiFactionTag - type: AiFactionTag
@@ -1319,7 +1319,7 @@
- Xeno - Xeno
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- Idle - Idle
- UnarmedAttackHostiles - UnarmedAttackHostiles

View File

@@ -6,7 +6,7 @@
components: components:
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- Idle - Idle
- UnarmedAttackHostiles - UnarmedAttackHostiles

View File

@@ -6,7 +6,7 @@
components: components:
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- Idle - Idle
- UnarmedAttackHostiles - UnarmedAttackHostiles

View File

@@ -8,7 +8,7 @@
components: components:
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
startingGear: PassengerGear startingGear: PassengerGear
behaviorSets: behaviorSets:
- PathingDummy - PathingDummy

View File

@@ -6,7 +6,7 @@
components: components:
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- Clothing - Clothing
- Hunger - Hunger
@@ -26,7 +26,7 @@
components: components:
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- Clothing - Clothing
- Hunger - Hunger

View File

@@ -9,7 +9,7 @@
- FootstepSound - FootstepSound
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- UnarmedAttackHostiles - UnarmedAttackHostiles
- type: AiFactionTag - type: AiFactionTag

View File

@@ -84,7 +84,7 @@
Slash: 5 Slash: 5
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- UnarmedAttackHostiles - UnarmedAttackHostiles
- type: AiFactionTag - type: AiFactionTag

View File

@@ -125,7 +125,7 @@
baseSprintSpeed : 3.5 baseSprintSpeed : 3.5
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- Idle - Idle
- UnarmedAttackHostiles - UnarmedAttackHostiles

View File

@@ -39,7 +39,7 @@
netsync: false netsync: false
- type: Recyclable - type: Recyclable
safe: false safe: false
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- Idle - Idle
- type: AiFactionTag - type: AiFactionTag
@@ -99,7 +99,7 @@
- type: SpamEmitSound - type: SpamEmitSound
sound: sound:
collection: BikeHorn collection: BikeHorn
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- Idle - Idle
- type: Sprite - type: Sprite
@@ -139,7 +139,7 @@
drawdepth: Mobs drawdepth: Mobs
sprite: Mobs/Silicon/Bots/cleanbot.rsi sprite: Mobs/Silicon/Bots/cleanbot.rsi
state: cleanbot state: cleanbot
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- CleanBot - CleanBot
- type: Drain - type: Drain

View File

@@ -14,7 +14,7 @@
Acidic: [Touch, Ingestion] Acidic: [Touch, Ingestion]
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
# - Clothing # - Clothing
- Idle - Idle
@@ -159,7 +159,7 @@
components: components:
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- Idle - Idle
# - Hunger TODO: eating on the floor and fix weird AI endless stomach # - Hunger TODO: eating on the floor and fix weird AI endless stomach

View File

@@ -6,7 +6,7 @@
components: components:
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- Idle - Idle
- UnarmedAttackHostiles - UnarmedAttackHostiles

View File

@@ -12,7 +12,7 @@
canDisarm: true canDisarm: true
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- Idle - Idle
- UnarmedAttackHostiles - UnarmedAttackHostiles
@@ -351,7 +351,7 @@
gender: male gender: male
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- Idle - Idle
- UnarmedAttackHostiles - UnarmedAttackHostiles

View File

@@ -45,7 +45,7 @@
Slash: 7 Slash: 7
- type: InputMover - type: InputMover
- type: MobMover - type: MobMover
- type: UtilityAI - type: UtilityNPC
behaviorSets: behaviorSets:
- Idle - Idle
- type: AiFactionTag - type: AiFactionTag