Shooting NPCs and more (#18042)
* Add pirate shooting * Shooting working * Basics working * Refactor time * More conversion * Update primitives * Update yml * weh * Building again * Draft * weh * b * Start shutdown * Starting to take form * Code side done * is it worky * Fix prototypes * stuff * Shitty working * Juke events working * Even more cleanup * RTX * Fix interaction combat mode and compquery * GetAmmoCount relays * Fix rotation speed * Juke fixes * fixes * weh * The collision avoidance never ends * Fixes * Pause support * framework * lazy * Fix idling * Fix drip * goobed * Fix takeover shutdown bug * Merge fixes * shitter * Fix carpos
This commit is contained in:
@@ -15,6 +15,6 @@ public sealed class HTNBranch
|
||||
/// <summary>
|
||||
/// Due to how serv3 works we need to defer getting the actual tasks until after they have all been serialized.
|
||||
/// </summary>
|
||||
[DataField("tasks", required: true, customTypeSerializer:typeof(HTNTaskListSerializer))]
|
||||
public List<string> TaskPrototypes = default!;
|
||||
[DataField("tasks", required: true)]
|
||||
public List<HTNTask> Tasks = new();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Threading;
|
||||
using Content.Server.NPC.Components;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
@@ -11,8 +10,8 @@ public sealed class HTNComponent : NPCComponent
|
||||
/// The base task to use for planning
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite),
|
||||
DataField("rootTask", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<HTNCompoundTask>))]
|
||||
public string RootTask = default!;
|
||||
DataField("rootTask", required: true)]
|
||||
public HTNCompoundTask RootTask = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Check any active services for our current plan. This is used to find new targets for example without changing our plan.
|
||||
|
||||
15
Content.Server/NPC/HTN/HTNCompoundPrototype.cs
Normal file
15
Content.Server/NPC/HTN/HTNCompoundPrototype.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a network of multiple tasks. This gets expanded out to its relevant nodes.
|
||||
/// </summary>
|
||||
[Prototype("htnCompound")]
|
||||
public sealed class HTNCompoundPrototype : IPrototype
|
||||
{
|
||||
[IdDataField] public string ID { get; } = string.Empty;
|
||||
|
||||
[DataField("branches", required: true)]
|
||||
public List<HTNBranch> Branches = new();
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a network of multiple tasks. This gets expanded out to its relevant nodes.
|
||||
/// </summary>
|
||||
[Prototype("htnCompound")]
|
||||
public sealed class HTNCompoundTask : HTNTask
|
||||
/// <remarks>
|
||||
/// This just points to a specific htnCompound prototype
|
||||
/// </remarks>
|
||||
public sealed class HTNCompoundTask : HTNTask, IHTNCompound
|
||||
{
|
||||
/// <summary>
|
||||
/// The available branches for this compound task.
|
||||
/// </summary>
|
||||
[DataField("branches", required: true)]
|
||||
public List<HTNBranch> Branches = default!;
|
||||
[DataField("task", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<HTNCompoundPrototype>))]
|
||||
public string Task = string.Empty;
|
||||
}
|
||||
|
||||
@@ -12,16 +12,19 @@ public sealed class HTNPlan
|
||||
/// </summary>
|
||||
public readonly List<Dictionary<string, object>?> Effects;
|
||||
|
||||
public List<int> BranchTraversalRecord;
|
||||
public readonly List<int> BranchTraversalRecord;
|
||||
|
||||
public List<HTNPrimitiveTask> Tasks;
|
||||
|
||||
public int Index = 0;
|
||||
public readonly List<HTNPrimitiveTask> Tasks;
|
||||
|
||||
public HTNPrimitiveTask CurrentTask => Tasks[Index];
|
||||
|
||||
public HTNOperator CurrentOperator => CurrentTask.Operator;
|
||||
|
||||
/// <summary>
|
||||
/// Where we are up to in the <see cref="Tasks"/>
|
||||
/// </summary>
|
||||
public int Index = 0;
|
||||
|
||||
public HTNPlan(List<HTNPrimitiveTask> tasks, List<int> branchTraversalRecord, List<Dictionary<string, object>?> effects)
|
||||
{
|
||||
Tasks = tasks;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.CPUJob.JobQueues;
|
||||
using Content.Server.NPC.HTN.PrimitiveTasks;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
@@ -11,10 +12,11 @@ namespace Content.Server.NPC.HTN;
|
||||
/// </summary>
|
||||
public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
{
|
||||
private readonly HTNSystem _htn;
|
||||
private readonly HTNCompoundTask _rootTask;
|
||||
private readonly HTNTask _rootTask;
|
||||
private NPCBlackboard _blackboard;
|
||||
|
||||
private IPrototypeManager _protoManager;
|
||||
|
||||
/// <summary>
|
||||
/// Branch traversal of an existing plan (if applicable).
|
||||
/// </summary>
|
||||
@@ -22,13 +24,13 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
|
||||
public HTNPlanJob(
|
||||
double maxTime,
|
||||
HTNSystem htn,
|
||||
HTNCompoundTask rootTask,
|
||||
IPrototypeManager protoManager,
|
||||
HTNTask rootTask,
|
||||
NPCBlackboard blackboard,
|
||||
List<int>? branchTraversal,
|
||||
CancellationToken cancellationToken = default) : base(maxTime, cancellationToken)
|
||||
{
|
||||
_htn = htn;
|
||||
_protoManager = protoManager;
|
||||
_rootTask = rootTask;
|
||||
_blackboard = blackboard;
|
||||
_branchTraversal = branchTraversal;
|
||||
@@ -47,7 +49,6 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
|
||||
// branch traversal record. Whenever we find a new compound task this updates.
|
||||
var btrIndex = 0;
|
||||
var btr = new List<int>();
|
||||
|
||||
// For some tasks we may do something expensive or want to re-use the planning result.
|
||||
// e.g. pathfind to a target before deciding to attack it.
|
||||
@@ -83,8 +84,6 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
PrimitiveCount = primitiveCount,
|
||||
});
|
||||
|
||||
btr.Add(btrIndex);
|
||||
|
||||
// TODO: Early out if existing plan is better and save lots of time.
|
||||
// my brain is not working rn AAA
|
||||
|
||||
@@ -94,7 +93,7 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
}
|
||||
else
|
||||
{
|
||||
RestoreTolastDecomposedTask(decompHistory, tasksToProcess, appliedStates, finalPlan, ref primitiveCount, ref _blackboard, ref btrIndex, ref btr);
|
||||
RestoreTolastDecomposedTask(decompHistory, tasksToProcess, appliedStates, finalPlan, ref primitiveCount, ref _blackboard, ref btrIndex);
|
||||
}
|
||||
break;
|
||||
case HTNPrimitiveTask primitive:
|
||||
@@ -105,7 +104,7 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
}
|
||||
else
|
||||
{
|
||||
RestoreTolastDecomposedTask(decompHistory, tasksToProcess, appliedStates, finalPlan, ref primitiveCount, ref _blackboard, ref btrIndex, ref btr);
|
||||
RestoreTolastDecomposedTask(decompHistory, tasksToProcess, appliedStates, finalPlan, ref primitiveCount, ref _blackboard, ref btrIndex);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -157,9 +156,9 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
/// <summary>
|
||||
/// Goes through each compound task branch and tries to find an appropriate one.
|
||||
/// </summary>
|
||||
private bool TryFindSatisfiedMethod(HTNCompoundTask compound, Queue<HTNTask> tasksToProcess, NPCBlackboard blackboard, ref int mtrIndex)
|
||||
private bool TryFindSatisfiedMethod(HTNCompoundTask compoundId, Queue<HTNTask> tasksToProcess, NPCBlackboard blackboard, ref int mtrIndex)
|
||||
{
|
||||
var compBranches = _htn.CompoundBranches[compound];
|
||||
var compound = _protoManager.Index<HTNCompoundPrototype>(compoundId.Task);
|
||||
|
||||
for (var i = mtrIndex; i < compound.Branches.Count; i++)
|
||||
{
|
||||
@@ -178,9 +177,7 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
if (!isValid)
|
||||
continue;
|
||||
|
||||
var branchTasks = compBranches[i];
|
||||
|
||||
foreach (var task in branchTasks)
|
||||
foreach (var task in branch.Tasks)
|
||||
{
|
||||
tasksToProcess.Enqueue(task);
|
||||
}
|
||||
@@ -201,8 +198,7 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
List<HTNPrimitiveTask> finalPlan,
|
||||
ref int primitiveCount,
|
||||
ref NPCBlackboard blackboard,
|
||||
ref int mtrIndex,
|
||||
ref List<int> btr)
|
||||
ref int mtrIndex)
|
||||
{
|
||||
tasksToProcess.Clear();
|
||||
|
||||
@@ -214,11 +210,11 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
mtrIndex = lastDecomp.BranchTraversal + 1;
|
||||
|
||||
var count = finalPlan.Count;
|
||||
var reduction = count - primitiveCount;
|
||||
|
||||
// Final plan only has primitive tasks added to it so we can just remove the count we've tracked since the last decomp.
|
||||
finalPlan.RemoveRange(count - primitiveCount, primitiveCount);
|
||||
appliedStates.RemoveRange(count - primitiveCount, primitiveCount);
|
||||
btr.RemoveRange(count - primitiveCount, primitiveCount);
|
||||
finalPlan.RemoveRange(reduction, primitiveCount);
|
||||
appliedStates.RemoveRange(reduction, primitiveCount);
|
||||
|
||||
primitiveCount = lastDecomp.PrimitiveCount;
|
||||
blackboard = lastDecomp.Blackboard;
|
||||
@@ -241,7 +237,7 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
public int PrimitiveCount;
|
||||
|
||||
/// <summary>
|
||||
/// The compound task that owns this decomposition.
|
||||
/// The task that owns this decomposition.
|
||||
/// </summary>
|
||||
public HTNCompoundTask CompoundTask = default!;
|
||||
|
||||
|
||||
9
Content.Server/NPC/HTN/HTNPlanState.cs
Normal file
9
Content.Server/NPC/HTN/HTNPlanState.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
[Flags]
|
||||
public enum HTNPlanState : byte
|
||||
{
|
||||
TaskFinished = 1 << 0,
|
||||
|
||||
PlanFinished = 1 << 1,
|
||||
}
|
||||
@@ -13,8 +13,7 @@ using JetBrains.Annotations;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
@@ -25,20 +24,14 @@ public sealed class HTNSystem : EntitySystem
|
||||
[Dependency] private readonly NPCSystem _npc = default!;
|
||||
[Dependency] private readonly NPCUtilitySystem _utility = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
private readonly JobQueue _planQueue = new(0.004);
|
||||
|
||||
private readonly HashSet<ICommonSession> _subscribers = new();
|
||||
|
||||
// hngngghghgh
|
||||
public IReadOnlyDictionary<HTNCompoundTask, List<HTNTask>[]> CompoundBranches => _compoundBranches;
|
||||
private Dictionary<HTNCompoundTask, List<HTNTask>[]> _compoundBranches = new();
|
||||
|
||||
// Hierarchical Task Network
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_sawmill = Logger.GetSawmill("npc.htn");
|
||||
SubscribeLocalEvent<HTNComponent, ComponentShutdown>(OnHTNShutdown);
|
||||
SubscribeNetworkEvent<RequestHTNMessage>(OnHTNMessage);
|
||||
|
||||
@@ -69,7 +62,9 @@ public sealed class HTNSystem : EntitySystem
|
||||
private void OnLoad()
|
||||
{
|
||||
// Clear all NPCs in case they're hanging onto stale tasks
|
||||
foreach (var comp in EntityQuery<HTNComponent>(true))
|
||||
var query = AllEntityQuery<HTNComponent>();
|
||||
|
||||
while (query.MoveNext(out var comp))
|
||||
{
|
||||
comp.PlanningToken?.Cancel();
|
||||
comp.PlanningToken = null;
|
||||
@@ -77,73 +72,64 @@ public sealed class HTNSystem : EntitySystem
|
||||
if (comp.Plan != null)
|
||||
{
|
||||
var currentOperator = comp.Plan.CurrentOperator;
|
||||
currentOperator.Shutdown(comp.Blackboard, HTNOperatorStatus.Failed);
|
||||
ShutdownTask(currentOperator, comp.Blackboard, HTNOperatorStatus.Failed);
|
||||
ShutdownPlan(comp);
|
||||
comp.Plan = null;
|
||||
RequestPlan(comp);
|
||||
}
|
||||
}
|
||||
|
||||
_compoundBranches.Clear();
|
||||
|
||||
// Add dependencies for all operators.
|
||||
// We put code on operators as I couldn't think of a clean way to put it on systems.
|
||||
foreach (var compound in _prototypeManager.EnumeratePrototypes<HTNCompoundTask>())
|
||||
foreach (var compound in _prototypeManager.EnumeratePrototypes<HTNCompoundPrototype>())
|
||||
{
|
||||
UpdateCompound(compound);
|
||||
}
|
||||
|
||||
foreach (var primitive in _prototypeManager.EnumeratePrototypes<HTNPrimitiveTask>())
|
||||
{
|
||||
UpdatePrimitive(primitive);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPrototypeLoad(PrototypesReloadedEventArgs obj)
|
||||
{
|
||||
if (!obj.ByType.ContainsKey(typeof(HTNCompoundPrototype)))
|
||||
return;
|
||||
|
||||
OnLoad();
|
||||
}
|
||||
|
||||
private void UpdatePrimitive(HTNPrimitiveTask primitive)
|
||||
private void UpdateCompound(HTNCompoundPrototype compound)
|
||||
{
|
||||
foreach (var precon in primitive.Preconditions)
|
||||
{
|
||||
precon.Initialize(EntityManager.EntitySysManager);
|
||||
}
|
||||
|
||||
primitive.Operator.Initialize(EntityManager.EntitySysManager);
|
||||
}
|
||||
|
||||
private void UpdateCompound(HTNCompoundTask compound)
|
||||
{
|
||||
var branchies = new List<HTNTask>[compound.Branches.Count];
|
||||
_compoundBranches.Add(compound, branchies);
|
||||
|
||||
for (var i = 0; i < compound.Branches.Count; i++)
|
||||
{
|
||||
var branch = compound.Branches[i];
|
||||
var brancho = new List<HTNTask>(branch.TaskPrototypes.Count);
|
||||
branchies[i] = brancho;
|
||||
|
||||
// Didn't do this in a typeserializer because we can't recursively grab our own prototype during it, woohoo!
|
||||
foreach (var proto in branch.TaskPrototypes)
|
||||
{
|
||||
if (_prototypeManager.TryIndex<HTNCompoundTask>(proto, out var compTask))
|
||||
{
|
||||
brancho.Add(compTask);
|
||||
}
|
||||
else if (_prototypeManager.TryIndex<HTNPrimitiveTask>(proto, out var primTask))
|
||||
{
|
||||
brancho.Add(primTask);
|
||||
}
|
||||
else
|
||||
{
|
||||
_sawmill.Error($"Unable to find HTNTask for {proto} on {compound.ID}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var precon in branch.Preconditions)
|
||||
{
|
||||
precon.Initialize(EntityManager.EntitySysManager);
|
||||
}
|
||||
|
||||
foreach (var task in branch.Tasks)
|
||||
{
|
||||
UpdateTask(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTask(HTNTask task)
|
||||
{
|
||||
switch (task)
|
||||
{
|
||||
case HTNCompoundTask:
|
||||
// NOOP, handled elsewhere
|
||||
break;
|
||||
case HTNPrimitiveTask primitive:
|
||||
foreach (var precon in primitive.Preconditions)
|
||||
{
|
||||
precon.Initialize(EntityManager.EntitySysManager);
|
||||
}
|
||||
|
||||
primitive.Operator.Initialize(EntityManager.EntitySysManager);
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +163,7 @@ public sealed class HTNSystem : EntitySystem
|
||||
{
|
||||
if (comp.PlanningJob.Exception != null)
|
||||
{
|
||||
_sawmill.Fatal($"Received exception on planning job for {uid}!");
|
||||
Log.Fatal($"Received exception on planning job for {uid}!");
|
||||
_npc.SleepNPC(uid);
|
||||
var exc = comp.PlanningJob.Exception;
|
||||
RemComp<HTNComponent>(uid);
|
||||
@@ -209,7 +195,13 @@ public sealed class HTNSystem : EntitySystem
|
||||
if (comp.Plan == null || newPlanBetter)
|
||||
{
|
||||
comp.CheckServices = false;
|
||||
comp.Plan?.CurrentTask.Operator.Shutdown(comp.Blackboard, HTNOperatorStatus.BetterPlan);
|
||||
|
||||
if (comp.Plan != null)
|
||||
{
|
||||
ShutdownTask(comp.Plan.CurrentOperator, comp.Blackboard, HTNOperatorStatus.BetterPlan);
|
||||
ShutdownPlan(comp);
|
||||
}
|
||||
|
||||
comp.Plan = comp.PlanningJob.Result;
|
||||
|
||||
// Startup the first task and anything else we need to do.
|
||||
@@ -227,7 +219,7 @@ public sealed class HTNSystem : EntitySystem
|
||||
{
|
||||
text.AppendLine($"BTR: {string.Join(", ", comp.Plan.BranchTraversalRecord)}");
|
||||
text.AppendLine($"tasks:");
|
||||
var root = _prototypeManager.Index<HTNCompoundTask>(comp.RootTask);
|
||||
var root = comp.RootTask;
|
||||
var btr = new List<int>();
|
||||
var level = -1;
|
||||
AppendDebugText(root, text, comp.Plan.BranchTraversalRecord, btr, ref level);
|
||||
@@ -267,23 +259,24 @@ public sealed class HTNSystem : EntitySystem
|
||||
|
||||
if (task is HTNPrimitiveTask primitive)
|
||||
{
|
||||
text.AppendLine(primitive.ID);
|
||||
text.AppendLine(primitive.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (task is HTNCompoundTask compound)
|
||||
if (task is HTNCompoundTask compTask)
|
||||
{
|
||||
var compound = _prototypeManager.Index<HTNCompoundPrototype>(compTask.Task);
|
||||
level++;
|
||||
text.AppendLine(compound.ID);
|
||||
var branches = _compoundBranches[compound];
|
||||
var branches = compound.Branches;
|
||||
|
||||
for (var i = 0; i < branches.Length; i++)
|
||||
for (var i = 0; i < branches.Count; i++)
|
||||
{
|
||||
var branch = branches[i];
|
||||
btr.Add(i);
|
||||
text.AppendLine($" branch {string.Join(", ", btr)}:");
|
||||
|
||||
foreach (var sub in branch)
|
||||
foreach (var sub in branch.Tasks)
|
||||
{
|
||||
AppendDebugText(sub, text, planBtr, btr, ref level);
|
||||
}
|
||||
@@ -344,21 +337,22 @@ public sealed class HTNSystem : EntitySystem
|
||||
case HTNOperatorStatus.Continuing:
|
||||
break;
|
||||
case HTNOperatorStatus.Failed:
|
||||
currentOperator.Shutdown(blackboard, status);
|
||||
component.Plan = null;
|
||||
ShutdownTask(currentOperator, blackboard, status);
|
||||
ShutdownPlan(component);
|
||||
break;
|
||||
// Operator completed so go to the next one.
|
||||
case HTNOperatorStatus.Finished:
|
||||
currentOperator.Shutdown(blackboard, status);
|
||||
ShutdownTask(currentOperator, blackboard, status);
|
||||
component.Plan.Index++;
|
||||
|
||||
// Plan finished!
|
||||
if (component.Plan.Tasks.Count <= component.Plan.Index)
|
||||
{
|
||||
component.Plan = null;
|
||||
ShutdownPlan(component);
|
||||
break;
|
||||
}
|
||||
|
||||
ConditionalShutdown(component.Plan, currentOperator, blackboard, HTNPlanState.TaskFinished);
|
||||
StartupTask(component.Plan.Tasks[component.Plan.Index], component.Blackboard, component.Plan.Effects[component.Plan.Index]);
|
||||
break;
|
||||
default:
|
||||
@@ -367,6 +361,50 @@ public sealed class HTNSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
public void ShutdownTask(HTNOperator currentOperator, NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
{
|
||||
if (currentOperator is IHtnConditionalShutdown conditional &&
|
||||
(conditional.ShutdownState & HTNPlanState.TaskFinished) != 0x0)
|
||||
{
|
||||
conditional.ConditionalShutdown(blackboard);
|
||||
}
|
||||
|
||||
currentOperator.TaskShutdown(blackboard, status);
|
||||
}
|
||||
|
||||
public void ShutdownPlan(HTNComponent component)
|
||||
{
|
||||
DebugTools.Assert(component.Plan != null);
|
||||
var blackboard = component.Blackboard;
|
||||
|
||||
foreach (var task in component.Plan.Tasks)
|
||||
{
|
||||
if (task.Operator is IHtnConditionalShutdown conditional &&
|
||||
(conditional.ShutdownState & HTNPlanState.PlanFinished) != 0x0)
|
||||
{
|
||||
conditional.ConditionalShutdown(blackboard);
|
||||
}
|
||||
|
||||
task.Operator.PlanShutdown(component.Blackboard);
|
||||
}
|
||||
|
||||
component.Plan = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shuts down the current operator conditionally.
|
||||
/// </summary>
|
||||
private void ConditionalShutdown(HTNPlan plan, HTNOperator currentOperator, NPCBlackboard blackboard, HTNPlanState state)
|
||||
{
|
||||
if (currentOperator is not IHtnConditionalShutdown conditional)
|
||||
return;
|
||||
|
||||
if ((conditional.ShutdownState & state) == 0x0)
|
||||
return;
|
||||
|
||||
conditional.ConditionalShutdown(blackboard);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a new primitive task. Will apply effects from planning if applicable.
|
||||
/// </summary>
|
||||
@@ -400,8 +438,8 @@ public sealed class HTNSystem : EntitySystem
|
||||
|
||||
var job = new HTNPlanJob(
|
||||
0.02,
|
||||
this,
|
||||
_prototypeManager.Index<HTNCompoundTask>(component.RootTask),
|
||||
_prototypeManager,
|
||||
component.RootTask,
|
||||
component.Blackboard.ShallowClone(), branchTraversal, cancelToken.Token);
|
||||
|
||||
_planQueue.EnqueueJob(job);
|
||||
@@ -425,13 +463,13 @@ public sealed class HTNSystem : EntitySystem
|
||||
|
||||
if (task is HTNPrimitiveTask primitive)
|
||||
{
|
||||
builder.AppendLine(buffer + $"Primitive: {task.ID}");
|
||||
builder.AppendLine(buffer + $"Primitive: {task}");
|
||||
builder.AppendLine(buffer + $" operator: {primitive.Operator.GetType().Name}");
|
||||
}
|
||||
else if (task is HTNCompoundTask compound)
|
||||
else if (task is HTNCompoundTask compTask)
|
||||
{
|
||||
builder.AppendLine(buffer + $"Compound: {task.ID}");
|
||||
var compoundBranches = CompoundBranches[compound];
|
||||
var compound = _prototypeManager.Index<HTNCompoundPrototype>(compTask.Task);
|
||||
builder.AppendLine(buffer + $"Compound: {task}");
|
||||
|
||||
for (var i = 0; i < compound.Branches.Count; i++)
|
||||
{
|
||||
@@ -439,9 +477,8 @@ public sealed class HTNSystem : EntitySystem
|
||||
|
||||
builder.AppendLine(buffer + " branch:");
|
||||
indent++;
|
||||
var branchTasks = compoundBranches[i];
|
||||
|
||||
foreach (var branchTask in branchTasks)
|
||||
foreach (var branchTask in branch.Tasks)
|
||||
{
|
||||
AppendDomain(builder, branchTask, ref indent);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
public abstract class HTNTask : IPrototype
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
public abstract class HTNTask
|
||||
{
|
||||
[IdDataField] public string ID { get; } = default!;
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
using Content.Server.NPC.HTN.PrimitiveTasks;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Serialization.Markdown.Sequence;
|
||||
using Robust.Shared.Serialization.Markdown.Validation;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
public sealed class HTNTaskListSerializer : ITypeSerializer<List<string>, SequenceDataNode>
|
||||
{
|
||||
public ValidationNode Validate(ISerializationManager serializationManager, SequenceDataNode node,
|
||||
IDependencyCollection dependencies, ISerializationContext? context = null)
|
||||
{
|
||||
var list = new List<ValidationNode>();
|
||||
var protoManager = dependencies.Resolve<IPrototypeManager>();
|
||||
|
||||
foreach (var data in node.Sequence)
|
||||
{
|
||||
if (data is not MappingDataNode mapping)
|
||||
{
|
||||
list.Add(new ErrorNode(data, $"Found invalid mapping node on {data}"));
|
||||
continue;
|
||||
}
|
||||
|
||||
var id = ((ValueDataNode) mapping["id"]).Value;
|
||||
|
||||
var isCompound = protoManager.HasIndex<HTNCompoundTask>(id);
|
||||
var isPrimitive = protoManager.HasIndex<HTNPrimitiveTask>(id);
|
||||
|
||||
list.Add(isCompound ^ isPrimitive
|
||||
? new ValidatedValueNode(node)
|
||||
: new ErrorNode(node, $"Found duplicated HTN compound and primitive tasks for {id}"));
|
||||
}
|
||||
|
||||
return new ValidatedSequenceNode(list);
|
||||
}
|
||||
|
||||
public List<string> Read(ISerializationManager serializationManager, SequenceDataNode node,
|
||||
IDependencyCollection dependencies,
|
||||
SerializationHookContext hookCtx, ISerializationContext? context = null,
|
||||
ISerializationManager.InstantiationDelegate<List<string>>? instanceProvider = null)
|
||||
{
|
||||
var value = instanceProvider != null ? instanceProvider() : new List<string>();
|
||||
foreach (var data in node.Sequence)
|
||||
{
|
||||
var mapping = (MappingDataNode) data;
|
||||
var id = ((ValueDataNode) mapping["id"]).Value;
|
||||
// Can't check prototypes here because we're still loading them so yay!
|
||||
value.Add(id);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public DataNode Write(ISerializationManager serializationManager, List<string> value,
|
||||
IDependencyCollection dependencies, bool alwaysWrite = false,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
var sequence = new SequenceDataNode();
|
||||
|
||||
foreach (var task in value)
|
||||
{
|
||||
var mapping = new MappingDataNode
|
||||
{
|
||||
["id"] = new ValueDataNode(task)
|
||||
};
|
||||
|
||||
sequence.Add(mapping);
|
||||
}
|
||||
|
||||
return sequence;
|
||||
}
|
||||
}
|
||||
8
Content.Server/NPC/HTN/IHTNCompound.cs
Normal file
8
Content.Server/NPC/HTN/IHTNCompound.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a HTN task that can be decomposed into primitive tasks.
|
||||
/// </summary>
|
||||
public interface IHTNCompound
|
||||
{
|
||||
}
|
||||
17
Content.Server/NPC/HTN/IHtnConditionalShutdown.cs
Normal file
17
Content.Server/NPC/HTN/IHtnConditionalShutdown.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
/// <summary>
|
||||
/// Helper interface to run the appropriate shutdown for a particular task.
|
||||
/// </summary>
|
||||
public interface IHtnConditionalShutdown
|
||||
{
|
||||
/// <summary>
|
||||
/// When to shut the task down.
|
||||
/// </summary>
|
||||
HTNPlanState ShutdownState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Run whenever the <see cref="ShutdownState"/> specifies.
|
||||
/// </summary>
|
||||
void ConditionalShutdown(NPCBlackboard blackboard);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Content.Shared.Hands.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.NPC.HTN.Preconditions;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the active hand entity has the specified components.
|
||||
/// </summary>
|
||||
public sealed class ActiveHandComponentPrecondition : HTNPrecondition
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField("invert")]
|
||||
public bool Invert;
|
||||
|
||||
[DataField("components", required: true)]
|
||||
public ComponentRegistry Components = new();
|
||||
|
||||
public override bool IsMet(NPCBlackboard blackboard)
|
||||
{
|
||||
if (!blackboard.TryGetValue<Hand>(NPCBlackboard.ActiveHand, out var hand, _entManager) || hand.HeldEntity == null)
|
||||
{
|
||||
return Invert;
|
||||
}
|
||||
|
||||
foreach (var comp in Components)
|
||||
{
|
||||
var hasComp = _entManager.HasComponent(hand.HeldEntity, comp.Value.Component.GetType());
|
||||
|
||||
if (!hasComp ||
|
||||
Invert && hasComp)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Content.Shared.Hands.Components;
|
||||
|
||||
namespace Content.Server.NPC.HTN.Preconditions;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if an entity is held in the active hand.
|
||||
/// </summary>
|
||||
public sealed class ActiveHandEntityPrecondition : HTNPrecondition
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public override bool IsMet(NPCBlackboard blackboard)
|
||||
{
|
||||
if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out Hand? activeHand, _entManager))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return activeHand.HeldEntity != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Content.Shared.Hands.Components;
|
||||
|
||||
namespace Content.Server.NPC.HTN.Preconditions;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the active hand is unoccupied.
|
||||
/// </summary>
|
||||
public sealed class ActiveHandFreePrecondition : HTNPrecondition
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public override bool IsMet(NPCBlackboard blackboard)
|
||||
{
|
||||
return blackboard.TryGetValue<bool>(NPCBlackboard.ActiveHandFree, out var handFree, _entManager) && handFree;
|
||||
}
|
||||
}
|
||||
48
Content.Server/NPC/HTN/Preconditions/GunAmmoPrecondition.cs
Normal file
48
Content.Server/NPC/HTN/Preconditions/GunAmmoPrecondition.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Content.Server.Weapons.Ranged.Systems;
|
||||
using Content.Shared.Weapons.Ranged.Events;
|
||||
|
||||
namespace Content.Server.NPC.HTN.Preconditions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets ammo for this NPC's selected gun; either active hand or itself.
|
||||
/// </summary>
|
||||
public sealed class GunAmmoPrecondition : HTNPrecondition
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField("minPercent")]
|
||||
public float MinPercent = 0f;
|
||||
|
||||
[DataField("maxPercent")]
|
||||
public float MaxPercent = 1f;
|
||||
|
||||
public override bool IsMet(NPCBlackboard blackboard)
|
||||
{
|
||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
var gunSystem = _entManager.System<GunSystem>();
|
||||
|
||||
if (!gunSystem.TryGetGun(owner, out var gunUid, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ammoEv = new GetAmmoCountEvent();
|
||||
_entManager.EventBus.RaiseLocalEvent(gunUid, ref ammoEv);
|
||||
float percent;
|
||||
|
||||
if (ammoEv.Capacity == 0)
|
||||
percent = 0f;
|
||||
else
|
||||
percent = ammoEv.Count / (float) ammoEv.Capacity;
|
||||
|
||||
percent = Math.Clamp(percent, 0f, 1f);
|
||||
|
||||
if (MaxPercent < percent)
|
||||
return false;
|
||||
|
||||
if (MinPercent > percent)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ public sealed class TargetInLOSPrecondition : HTNPrecondition
|
||||
private InteractionSystem _interaction = default!;
|
||||
|
||||
[DataField("targetKey")]
|
||||
public string TargetKey = "CombatTarget";
|
||||
public string TargetKey = "Target";
|
||||
|
||||
[DataField("rangeKey")]
|
||||
public string RangeKey = "RangeKey";
|
||||
|
||||
@@ -40,6 +40,14 @@ public abstract class HTNOperator
|
||||
return HTNOperatorStatus.Finished;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the plan has finished running.
|
||||
/// </summary>
|
||||
public virtual void PlanShutdown(NPCBlackboard blackboard)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called the first time an operator runs.
|
||||
/// </summary>
|
||||
@@ -48,5 +56,5 @@ public abstract class HTNOperator
|
||||
/// <summary>
|
||||
/// Called whenever the operator stops running.
|
||||
/// </summary>
|
||||
public virtual void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status) {}
|
||||
public virtual void TaskShutdown(NPCBlackboard blackboard, HTNOperatorStatus status) {}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks;
|
||||
|
||||
[Prototype("htnPrimitive")]
|
||||
public sealed class HTNPrimitiveTask : HTNTask
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using Content.Server.NPC.Components;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
|
||||
|
||||
public sealed class JukeOperator : HTNOperator, IHtnConditionalShutdown
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField("jukeType")]
|
||||
public JukeType JukeType = JukeType.AdjacentTile;
|
||||
|
||||
[DataField("shutdownState")]
|
||||
public HTNPlanState ShutdownState { get; } = HTNPlanState.PlanFinished;
|
||||
|
||||
public override void Startup(NPCBlackboard blackboard)
|
||||
{
|
||||
base.Startup(blackboard);
|
||||
var juke = _entManager.EnsureComponent<NPCJukeComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
|
||||
juke.JukeType = JukeType;
|
||||
}
|
||||
|
||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
||||
{
|
||||
return HTNOperatorStatus.Finished;
|
||||
}
|
||||
|
||||
public void ConditionalShutdown(NPCBlackboard blackboard)
|
||||
{
|
||||
_entManager.RemoveComponent<NPCJukeComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,25 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.NPC.Components;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Melee;
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat.Melee;
|
||||
|
||||
/// <summary>
|
||||
/// Attacks the specified key in melee combat.
|
||||
/// </summary>
|
||||
public sealed class MeleeOperator : HTNOperator
|
||||
public sealed class MeleeOperator : HTNOperator, IHtnConditionalShutdown
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// When to shut the task down.
|
||||
/// </summary>
|
||||
[DataField("shutdownState")]
|
||||
public HTNPlanState ShutdownState { get; } = HTNPlanState.TaskFinished;
|
||||
|
||||
/// <summary>
|
||||
/// Key that contains the target entity.
|
||||
/// </summary>
|
||||
@@ -53,10 +60,11 @@ public sealed class MeleeOperator : HTNOperator
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
public void ConditionalShutdown(NPCBlackboard blackboard)
|
||||
{
|
||||
base.Shutdown(blackboard, status);
|
||||
_entManager.RemoveComponent<NPCMeleeCombatComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
|
||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
_entManager.System<SharedCombatModeSystem>().SetInCombatMode(owner, false);
|
||||
_entManager.RemoveComponent<NPCMeleeCombatComponent>(owner);
|
||||
blackboard.Remove<EntityUid>(TargetKey);
|
||||
}
|
||||
|
||||
@@ -96,9 +104,10 @@ public sealed class MeleeOperator : HTNOperator
|
||||
status = HTNOperatorStatus.Failed;
|
||||
}
|
||||
|
||||
if (status != HTNOperatorStatus.Continuing)
|
||||
// Mark it as finished to continue the plan.
|
||||
if (status == HTNOperatorStatus.Continuing && ShutdownState == HTNPlanState.PlanFinished)
|
||||
{
|
||||
_entManager.RemoveComponent<NPCMeleeCombatComponent>(owner);
|
||||
status = HTNOperatorStatus.Finished;
|
||||
}
|
||||
|
||||
return status;
|
||||
@@ -1,16 +1,20 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.NPC.Components;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Ranged;
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat.Ranged;
|
||||
|
||||
public sealed class RangedOperator : HTNOperator
|
||||
public sealed class GunOperator : HTNOperator, IHtnConditionalShutdown
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField("shutdownState")]
|
||||
public HTNPlanState ShutdownState { get; } = HTNPlanState.TaskFinished;
|
||||
|
||||
/// <summary>
|
||||
/// Key that contains the target entity.
|
||||
/// </summary>
|
||||
@@ -23,6 +27,12 @@ public sealed class RangedOperator : HTNOperator
|
||||
[DataField("targetState")]
|
||||
public MobState TargetState = MobState.Alive;
|
||||
|
||||
/// <summary>
|
||||
/// Do we require line of sight of the target before failing.
|
||||
/// </summary>
|
||||
[DataField("requireLOS")]
|
||||
public bool RequireLOS = false;
|
||||
|
||||
// Like movement we add a component and pass it off to the dedicated system.
|
||||
|
||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||
@@ -60,10 +70,11 @@ public sealed class RangedOperator : HTNOperator
|
||||
}
|
||||
}
|
||||
|
||||
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
public void ConditionalShutdown(NPCBlackboard blackboard)
|
||||
{
|
||||
base.Shutdown(blackboard, status);
|
||||
_entManager.RemoveComponent<NPCRangedCombatComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
|
||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
_entManager.System<SharedCombatModeSystem>().SetInCombatMode(owner, false);
|
||||
_entManager.RemoveComponent<NPCRangedCombatComponent>(owner);
|
||||
blackboard.Remove<EntityUid>(TargetKey);
|
||||
}
|
||||
|
||||
@@ -89,9 +100,14 @@ public sealed class RangedOperator : HTNOperator
|
||||
switch (combat.Status)
|
||||
{
|
||||
case CombatStatus.TargetUnreachable:
|
||||
case CombatStatus.NotInSight:
|
||||
status = HTNOperatorStatus.Failed;
|
||||
break;
|
||||
case CombatStatus.NotInSight:
|
||||
if (RequireLOS)
|
||||
status = HTNOperatorStatus.Failed;
|
||||
else
|
||||
status = HTNOperatorStatus.Continuing;
|
||||
break;
|
||||
case CombatStatus.Normal:
|
||||
status = HTNOperatorStatus.Continuing;
|
||||
break;
|
||||
@@ -106,9 +122,10 @@ public sealed class RangedOperator : HTNOperator
|
||||
status = HTNOperatorStatus.Failed;
|
||||
}
|
||||
|
||||
if (status != HTNOperatorStatus.Continuing)
|
||||
// Mark it as finished to continue the plan.
|
||||
if (status == HTNOperatorStatus.Continuing && ShutdownState == HTNPlanState.PlanFinished)
|
||||
{
|
||||
_entManager.RemoveComponent<NPCRangedCombatComponent>(owner);
|
||||
status = HTNOperatorStatus.Finished;
|
||||
}
|
||||
|
||||
return status;
|
||||
@@ -4,14 +4,14 @@ using System.Threading.Tasks;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Interaction;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Interactions;
|
||||
|
||||
public sealed class AltInteractOperator : HTNOperator
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField("targetKey")]
|
||||
public string Key = "CombatTarget";
|
||||
public string Key = "Target";
|
||||
|
||||
/// <summary>
|
||||
/// If this alt-interaction started a do_after where does the key get stored.
|
||||
@@ -0,0 +1,31 @@
|
||||
using Content.Server.Hands.Systems;
|
||||
using Content.Shared.Hands.Components;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Interactions;
|
||||
|
||||
/// <summary>
|
||||
/// Drops the active hand entity underneath us.
|
||||
/// </summary>
|
||||
public sealed class DropOperator : HTNOperator
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
||||
{
|
||||
if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out Hand? activeHand, _entManager))
|
||||
{
|
||||
return HTNOperatorStatus.Finished;
|
||||
}
|
||||
|
||||
var owner = blackboard.GetValueOrDefault<EntityUid>(NPCBlackboard.Owner, _entManager);
|
||||
// TODO: Need some sort of interaction cooldown probably.
|
||||
var handsSystem = _entManager.System<HandsSystem>();
|
||||
|
||||
if (handsSystem.TryDrop(owner))
|
||||
{
|
||||
return HTNOperatorStatus.Finished;
|
||||
}
|
||||
|
||||
return HTNOperatorStatus.Failed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Content.Server.Hands.Systems;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Interactions;
|
||||
|
||||
public sealed class EquipOperator : HTNOperator
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField("target")]
|
||||
public string Target = "Target";
|
||||
|
||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
||||
{
|
||||
if (!blackboard.TryGetValue<EntityUid>(Target, out var target, _entManager))
|
||||
{
|
||||
return HTNOperatorStatus.Failed;
|
||||
}
|
||||
|
||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
var handsSystem = _entManager.System<HandsSystem>();
|
||||
|
||||
// TODO: As elsewhere need some generic interaction cooldown system
|
||||
if (handsSystem.TryPickup(owner, target))
|
||||
{
|
||||
return HTNOperatorStatus.Finished;
|
||||
}
|
||||
|
||||
return HTNOperatorStatus.Failed;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
using Content.Server.Interaction;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Timing;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Interactions;
|
||||
|
||||
public sealed class InteractWithOperator : HTNOperator
|
||||
{
|
||||
@@ -24,6 +25,7 @@ public sealed class InteractWithOperator : HTNOperator
|
||||
return HTNOperatorStatus.Continuing;
|
||||
}
|
||||
|
||||
_entManager.System<SharedCombatModeSystem>().SetInCombatMode(owner, false);
|
||||
_entManager.System<InteractionSystem>().UserInteraction(owner, targetXform.Coordinates, moveTarget);
|
||||
|
||||
return HTNOperatorStatus.Finished;
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Hands.Systems;
|
||||
using Content.Shared.Hands.Components;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Interactions;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Swaps to any free hand.
|
||||
/// </summary>
|
||||
public sealed class SwapToFreeHandOperator : HTNOperator
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard, CancellationToken cancelToken)
|
||||
{
|
||||
if (!blackboard.TryGetValue<List<string>>(NPCBlackboard.FreeHands, out var hands, _entManager) ||
|
||||
!_entManager.TryGetComponent<HandsComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner), out var handsComp))
|
||||
{
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
foreach (var hand in hands)
|
||||
{
|
||||
return (true, new Dictionary<string, object>()
|
||||
{
|
||||
{
|
||||
NPCBlackboard.ActiveHand, handsComp.Hands[hand]
|
||||
},
|
||||
{
|
||||
NPCBlackboard.ActiveHandFree, true
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
||||
{
|
||||
// TODO: Need interaction cooldown
|
||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
var handSystem = _entManager.System<HandsSystem>();
|
||||
|
||||
if (!handSystem.TrySelectEmptyHand(owner))
|
||||
{
|
||||
return HTNOperatorStatus.Failed;
|
||||
}
|
||||
|
||||
return HTNOperatorStatus.Finished;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
||||
/// <summary>
|
||||
/// Moves an NPC to the specified target key. Hands the actual steering off to NPCSystem.Steering
|
||||
/// </summary>
|
||||
public sealed class MoveToOperator : HTNOperator
|
||||
public sealed class MoveToOperator : HTNOperator, IHtnConditionalShutdown
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
@@ -19,6 +19,12 @@ public sealed class MoveToOperator : HTNOperator
|
||||
private PathfindingSystem _pathfind = default!;
|
||||
private SharedTransformSystem _transform = default!;
|
||||
|
||||
/// <summary>
|
||||
/// When to shut the task down.
|
||||
/// </summary>
|
||||
[DataField("shutdownState")]
|
||||
public HTNPlanState ShutdownState { get; } = HTNPlanState.TaskFinished;
|
||||
|
||||
/// <summary>
|
||||
/// Should we assume the MovementTarget is reachable during planning or should we pathfind to it?
|
||||
/// </summary>
|
||||
@@ -35,7 +41,7 @@ public sealed class MoveToOperator : HTNOperator
|
||||
/// Target Coordinates to move to. This gets removed after execution.
|
||||
/// </summary>
|
||||
[DataField("targetKey")]
|
||||
public string TargetKey = "MovementTarget";
|
||||
public string TargetKey = "TargetCoordinates";
|
||||
|
||||
/// <summary>
|
||||
/// Where the pathfinding result will be stored (if applicable). This gets removed after execution.
|
||||
@@ -49,6 +55,12 @@ public sealed class MoveToOperator : HTNOperator
|
||||
[DataField("rangeKey")]
|
||||
public string RangeKey = "MovementRange";
|
||||
|
||||
/// <summary>
|
||||
/// Do we only need to move into line of sight.
|
||||
/// </summary>
|
||||
[DataField("stopOnLineOfSight")]
|
||||
public bool StopOnLineOfSight;
|
||||
|
||||
private const string MovementCancelToken = "MovementCancelToken";
|
||||
|
||||
public override void Initialize(IEntitySystemManager sysManager)
|
||||
@@ -132,6 +144,7 @@ public sealed class MoveToOperator : HTNOperator
|
||||
|
||||
// Re-use the path we may have if applicable.
|
||||
var comp = _steering.Register(uid, targetCoordinates);
|
||||
comp.ArriveOnLineOfSight = StopOnLineOfSight;
|
||||
|
||||
if (blackboard.TryGetValue<float>(RangeKey, out var range, _entManager))
|
||||
{
|
||||
@@ -150,10 +163,30 @@ public sealed class MoveToOperator : HTNOperator
|
||||
}
|
||||
}
|
||||
|
||||
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
||||
{
|
||||
base.Shutdown(blackboard, status);
|
||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
|
||||
if (!_entManager.TryGetComponent<NPCSteeringComponent>(owner, out var steering))
|
||||
return HTNOperatorStatus.Failed;
|
||||
|
||||
// Just keep moving in the background and let the other tasks handle it.
|
||||
if (ShutdownState == HTNPlanState.PlanFinished && steering.Status == SteeringStatus.Moving)
|
||||
{
|
||||
return HTNOperatorStatus.Finished;
|
||||
}
|
||||
|
||||
return steering.Status switch
|
||||
{
|
||||
SteeringStatus.InRange => HTNOperatorStatus.Finished,
|
||||
SteeringStatus.NoPath => HTNOperatorStatus.Failed,
|
||||
SteeringStatus.Moving => HTNOperatorStatus.Continuing,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
public void ConditionalShutdown(NPCBlackboard blackboard)
|
||||
{
|
||||
// Cleanup the blackboard and remove steering.
|
||||
if (blackboard.TryGetValue<CancellationTokenSource>(MovementCancelToken, out var cancelToken, _entManager))
|
||||
{
|
||||
@@ -171,20 +204,4 @@ public sealed class MoveToOperator : HTNOperator
|
||||
|
||||
_steering.Unregister(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
|
||||
}
|
||||
|
||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
||||
{
|
||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
|
||||
if (!_entManager.TryGetComponent<NPCSteeringComponent>(owner, out var steering))
|
||||
return HTNOperatorStatus.Failed;
|
||||
|
||||
return steering.Status switch
|
||||
{
|
||||
SteeringStatus.InRange => HTNOperatorStatus.Finished,
|
||||
SteeringStatus.NoPath => HTNOperatorStatus.Failed,
|
||||
SteeringStatus.Moving => HTNOperatorStatus.Continuing,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
||||
|
||||
/// <summary>
|
||||
/// What it sounds like.
|
||||
/// </summary>
|
||||
public sealed class NoOperator : HTNOperator
|
||||
{
|
||||
|
||||
}
|
||||
@@ -16,8 +16,8 @@ public sealed class PickAccessibleOperator : HTNOperator
|
||||
[DataField("rangeKey", required: true)]
|
||||
public string RangeKey = string.Empty;
|
||||
|
||||
[DataField("targetKey", required: true)]
|
||||
public string TargetKey = string.Empty;
|
||||
[DataField("targetCoordinates")]
|
||||
public string TargetCoordinates = "TargetCoordinates";
|
||||
|
||||
/// <summary>
|
||||
/// Where the pathfinding result will be stored (if applicable). This gets removed after execution.
|
||||
@@ -58,7 +58,7 @@ public sealed class PickAccessibleOperator : HTNOperator
|
||||
|
||||
return (true, new Dictionary<string, object>()
|
||||
{
|
||||
{ TargetKey, target },
|
||||
{ TargetCoordinates, target },
|
||||
{ PathfindKey, path}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ public sealed class RotateToTargetOperator : HTNOperator
|
||||
_rotate = sysManager.GetEntitySystem<RotateToFaceSystem>();
|
||||
}
|
||||
|
||||
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
public override void TaskShutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
{
|
||||
base.Shutdown(blackboard, status);
|
||||
base.TaskShutdown(blackboard, status);
|
||||
blackboard.Remove<Angle>(TargetKey);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,9 +36,9 @@ public sealed class MedibotInjectOperator : HTNOperator
|
||||
_solution = sysManager.GetEntitySystem<SolutionContainerSystem>();
|
||||
}
|
||||
|
||||
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
public override void TaskShutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
{
|
||||
base.Shutdown(blackboard, status);
|
||||
base.TaskShutdown(blackboard, status);
|
||||
blackboard.Remove<EntityUid>(TargetKey);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@ public sealed class UtilityOperator : HTNOperator
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField("key")] public string Key = "CombatTarget";
|
||||
[DataField("key")] public string Key = "Target";
|
||||
|
||||
/// <summary>
|
||||
/// The EntityCoordinates of the specified target.
|
||||
/// </summary>
|
||||
[DataField("keyCoordinates")]
|
||||
public string KeyCoordinates = "CombatTargetCoordinates";
|
||||
public string KeyCoordinates = "TargetCoordinates";
|
||||
|
||||
[DataField("proto", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<UtilityQueryPrototype>))]
|
||||
public string Prototype = string.Empty;
|
||||
|
||||
@@ -25,9 +25,9 @@ public sealed class WaitOperator : HTNOperator
|
||||
return timer <= 0f ? HTNOperatorStatus.Finished : HTNOperatorStatus.Continuing;
|
||||
}
|
||||
|
||||
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
public override void TaskShutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
{
|
||||
base.Shutdown(blackboard, status);
|
||||
base.TaskShutdown(blackboard, status);
|
||||
|
||||
// The replacement plan may want this value so only dump it if we're successful.
|
||||
if (status != HTNOperatorStatus.BetterPlan)
|
||||
|
||||
Reference in New Issue
Block a user