ECS NPCs (#9941)
* ECS * A * parity * Remove dummy update * abs * thanks rider
This commit is contained in:
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
Content.Server/AI/Components/ActiveNPCComponent.cs
Normal file
7
Content.Server/AI/Components/ActiveNPCComponent.cs
Normal 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 {}
|
||||||
@@ -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) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17
Content.Server/AI/Components/NPCComponent.cs
Normal file
17
Content.Server/AI/Components/NPCComponent.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Content.Server/AI/EntitySystems/NPCSystem.Blackboard.cs
Normal file
42
Content.Server/AI/EntitySystems/NPCSystem.Blackboard.cs
Normal 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";
|
||||||
|
}
|
||||||
284
Content.Server/AI/EntitySystems/NPCSystem.Utility.cs
Normal file
284
Content.Server/AI/EntitySystems/NPCSystem.Utility.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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++)
|
private void OnMobStateChange(EntityUid uid, NPCComponent component, MobStateChangedEvent args)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
46
Content.Server/AI/Utility/AiLogic/UtilityNPCComponent.cs
Normal file
46
Content.Server/AI/Utility/AiLogic/UtilityNPCComponent.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
components:
|
components:
|
||||||
- type: InputMover
|
- type: InputMover
|
||||||
- type: MobMover
|
- type: MobMover
|
||||||
- type: UtilityAI
|
- type: UtilityNPC
|
||||||
behaviorSets:
|
behaviorSets:
|
||||||
- Idle
|
- Idle
|
||||||
- UnarmedAttackHostiles
|
- UnarmedAttackHostiles
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
components:
|
components:
|
||||||
- type: InputMover
|
- type: InputMover
|
||||||
- type: MobMover
|
- type: MobMover
|
||||||
- type: UtilityAI
|
- type: UtilityNPC
|
||||||
behaviorSets:
|
behaviorSets:
|
||||||
- Idle
|
- Idle
|
||||||
- UnarmedAttackHostiles
|
- UnarmedAttackHostiles
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
components:
|
components:
|
||||||
- type: InputMover
|
- type: InputMover
|
||||||
- type: MobMover
|
- type: MobMover
|
||||||
- type: UtilityAI
|
- type: UtilityNPC
|
||||||
behaviorSets:
|
behaviorSets:
|
||||||
- Idle
|
- Idle
|
||||||
- UnarmedAttackHostiles
|
- UnarmedAttackHostiles
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user