Pathfinder rework (#11452)

This commit is contained in:
metalgearsloth
2022-09-30 14:39:48 +10:00
committed by GitHub
parent fd3b29fb03
commit f456ad911e
80 changed files with 3606 additions and 4374 deletions

View File

@@ -1,11 +1,13 @@
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Content.Server.NPC.HTN.PrimitiveTasks;
/// <summary>
/// Concrete code that gets run for an NPC task.
/// </summary>
[ImplicitDataDefinitionForInheritors]
[ImplicitDataDefinitionForInheritors, MeansImplicitUse]
public abstract class HTNOperator
{
/// <summary>
@@ -20,9 +22,11 @@ public abstract class HTNOperator
/// Called during planning.
/// </summary>
/// <param name="blackboard">The blackboard for the NPC.</param>
/// <param name="cancelToken"></param>
/// <returns>Whether the plan is still valid and the effects to apply to the blackboard.
/// These get re-applied during execution and are up to the operator to use or discard.</returns>
public virtual async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
public virtual async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
return (true, null);
}

View File

@@ -1,3 +1,4 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.MobState;
using Content.Server.NPC.Components;
@@ -34,7 +35,8 @@ public sealed class MeleeOperator : HTNOperator
melee.Target = blackboard.GetValue<EntityUid>(TargetKey);
}
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
// Don't attack if they're already as wounded as we want them.
if (!blackboard.TryGetValue<EntityUid>(TargetKey, out var target))
@@ -62,7 +64,6 @@ public sealed class MeleeOperator : HTNOperator
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
{
base.Update(blackboard, frameTime);
// TODO:
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
var status = HTNOperatorStatus.Continuing;
@@ -79,6 +80,7 @@ public sealed class MeleeOperator : HTNOperator
{
switch (combat.Status)
{
case CombatStatus.TargetOutOfRange:
case CombatStatus.Normal:
status = HTNOperatorStatus.Continuing;
break;

View File

@@ -1,32 +1,24 @@
using Robust.Shared.Map;
using JetBrains.Annotations;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Melee;
/// <summary>
/// Selects a target for melee.
/// </summary>
[MeansImplicitUse]
public sealed class PickMeleeTargetOperator : NPCCombatOperator
{
protected override float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, bool canMove, EntityQuery<TransformComponent> xformQuery)
protected override float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, float distance, bool canMove, EntityQuery<TransformComponent> xformQuery)
{
var ourCoordinates = blackboard.GetValueOrDefault<EntityCoordinates>(NPCBlackboard.OwnerCoordinates);
if (!xformQuery.TryGetComponent(uid, out var targetXform))
return -1f;
var targetCoordinates = targetXform.Coordinates;
if (!ourCoordinates.TryDistance(EntManager, targetCoordinates, out var distance))
return -1f;
var rating = 0f;
if (existingTarget == uid)
{
rating += 3f;
rating += 2f;
}
rating += 1f / distance * 4f;
if (distance > 0f)
rating += 50f / distance;
return rating;
}

View File

@@ -2,10 +2,11 @@ using System.Threading;
using System.Threading.Tasks;
using Content.Server.NPC.Components;
using Content.Server.NPC.Pathfinding;
using Content.Server.NPC.Pathfinding.Pathfinders;
using Content.Server.NPC.Systems;
using Content.Shared.NPC;
using Robust.Shared.Map;
using Robust.Shared.Physics.Components;
using YamlDotNet.Core.Tokens;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
@@ -58,7 +59,8 @@ public sealed class MoveToOperator : HTNOperator
_steering = sysManager.GetEntitySystem<NPCSteeringSystem>();
}
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
if (!blackboard.TryGetValue<EntityCoordinates>(TargetKey, out var targetCoordinates))
{
@@ -72,8 +74,7 @@ public sealed class MoveToOperator : HTNOperator
return (false, null);
if (!_mapManager.TryGetGrid(xform.GridUid, out var ownerGrid) ||
!_mapManager.TryGetGrid(targetCoordinates.GetGridUid(_entManager), out var targetGrid) ||
ownerGrid != targetGrid)
!_mapManager.TryGetGrid(targetCoordinates.GetGridUid(_entManager), out var targetGrid))
{
return (false, null);
}
@@ -97,30 +98,25 @@ public sealed class MoveToOperator : HTNOperator
});
}
var cancelToken = new CancellationTokenSource();
var access = blackboard.GetValueOrDefault<ICollection<string>>(NPCBlackboard.Access) ?? new List<string>();
var path = await _pathfind.GetPath(
blackboard.GetValue<EntityUid>(NPCBlackboard.Owner),
xform.Coordinates,
targetCoordinates,
range,
cancelToken,
_pathfind.GetFlags(blackboard));
var job = _pathfind.RequestPath(
new PathfindingArgs(
blackboard.GetValue<EntityUid>(NPCBlackboard.Owner),
access,
body.CollisionMask,
ownerGrid.GetTileRef(xform.Coordinates),
ownerGrid.GetTileRef(targetCoordinates),
range), cancelToken.Token);
job.Run();
await job.AsTask.WaitAsync(cancelToken.Token);
if (job.Result == null)
if (path.Result != PathResult.Path)
{
return (false, null);
}
return (true, new Dictionary<string, object>()
{
{NPCBlackboard.OwnerCoordinates, targetCoordinates},
{PathfindKey, job.Result}
{PathfindKey, path}
});
}
// Given steering is complicated we'll hand it off to a dedicated system rather than this singleton operator.
@@ -131,23 +127,25 @@ public sealed class MoveToOperator : HTNOperator
// Need to remove the planning value for execution.
blackboard.Remove<EntityCoordinates>(NPCBlackboard.OwnerCoordinates);
var targetCoordinates = blackboard.GetValue<EntityCoordinates>(TargetKey);
// Re-use the path we may have if applicable.
var comp = _steering.Register(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner), blackboard.GetValue<EntityCoordinates>(TargetKey));
var comp = _steering.Register(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner), targetCoordinates);
if (blackboard.TryGetValue<float>(RangeKey, out var range))
{
comp.Range = range;
}
if (blackboard.TryGetValue<Queue<TileRef>>(PathfindKey, out var path))
if (blackboard.TryGetValue<PathResultEvent>(PathfindKey, out var result))
{
if (blackboard.TryGetValue<EntityCoordinates>(NPCBlackboard.OwnerCoordinates, out var coordinates))
{
_steering.PrunePath(coordinates, path);
var mapCoords = coordinates.ToMap(_entManager);
_steering.PrunePath(mapCoords, targetCoordinates.ToMapPos(_entManager) - mapCoords.Position, result.Path);
}
comp.CurrentPath = path;
comp.CurrentPath = result.Path;
}
}
@@ -163,7 +161,7 @@ public sealed class MoveToOperator : HTNOperator
}
// OwnerCoordinates is only used in planning so dump it.
blackboard.Remove<Queue<TileRef>>(PathfindKey);
blackboard.Remove<PathResultEvent>(PathfindKey);
if (RemoveKeyOnFinish)
{

View File

@@ -1,6 +1,10 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Interaction;
using Content.Server.NPC.Pathfinding;
using Content.Server.NPC.Systems;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.MobState;
using Content.Shared.MobState.Components;
using Robust.Shared.Map;
@@ -10,8 +14,9 @@ namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
public abstract class NPCCombatOperator : HTNOperator
{
[Dependency] protected readonly IEntityManager EntManager = default!;
private FactionSystem _tags = default!;
private FactionSystem _factions = default!;
protected InteractionSystem Interaction = default!;
private PathfindingSystem _pathfinding = default!;
[ViewVariables, DataField("key")] public string Key = "CombatTarget";
@@ -21,16 +26,25 @@ public abstract class NPCCombatOperator : HTNOperator
[ViewVariables, DataField("keyCoordinates")]
public string KeyCoordinates = "CombatTargetCoordinates";
/// <summary>
/// Regardless of pathfinding or LOS these are the max we'll check
/// </summary>
private const int MaxConsideredTargets = 10;
private const int MaxTargetCount = 5;
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
_tags = sysManager.GetEntitySystem<FactionSystem>();
sysManager.GetEntitySystem<ExamineSystemShared>();
_factions = sysManager.GetEntitySystem<FactionSystem>();
Interaction = sysManager.GetEntitySystem<InteractionSystem>();
_pathfinding = sysManager.GetEntitySystem<PathfindingSystem>();
}
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
var targets = GetTargets(blackboard);
var targets = await GetTargets(blackboard);
if (targets.Count == 0)
{
@@ -49,34 +63,68 @@ public abstract class NPCCombatOperator : HTNOperator
return (true, effects);
}
private List<(EntityUid Entity, float Rating)> GetTargets(NPCBlackboard blackboard)
private async Task<List<(EntityUid Entity, float Rating, float Distance)>> GetTargets(NPCBlackboard blackboard)
{
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntManager);
var targets = new List<(EntityUid Entity, float Rating)>();
var targets = new List<(EntityUid Entity, float Rating, float Distance)>();
blackboard.TryGetValue<EntityUid>(Key, out var existingTarget);
var xformQuery = EntManager.GetEntityQuery<TransformComponent>();
var mobQuery = EntManager.GetEntityQuery<MobStateComponent>();
var canMove = blackboard.GetValueOrDefault<bool>(NPCBlackboard.CanMove, EntManager);
var cancelToken = new CancellationTokenSource();
var count = 0;
if (xformQuery.TryGetComponent(existingTarget, out var targetXform))
{
var distance = await _pathfinding.GetPathDistance(owner, targetXform.Coordinates,
SharedInteractionSystem.InteractionRange, cancelToken.Token, _pathfinding.GetFlags(blackboard));
if (distance != null)
{
targets.Add((existingTarget, GetRating(blackboard, existingTarget, existingTarget, distance.Value, canMove, xformQuery), distance.Value));
}
}
// TODO: Need a perception system instead
foreach (var target in _tags
// TODO: This will be expensive so will be good to optimise and cut corners.
foreach (var target in _factions
.GetNearbyHostiles(owner, radius))
{
if (mobQuery.TryGetComponent(target, out var mobState) &&
mobState.CurrentState > DamageState.Alive)
mobState.CurrentState > DamageState.Alive ||
!xformQuery.TryGetComponent(target, out targetXform))
{
continue;
}
targets.Add((target, GetRating(blackboard, target, existingTarget, canMove, xformQuery)));
count++;
if (count >= MaxConsideredTargets)
break;
if (!ExamineSystemShared.InRangeUnOccluded(owner, target, radius, null))
{
continue;
}
var distance = await _pathfinding.GetPathDistance(owner, targetXform.Coordinates,
SharedInteractionSystem.InteractionRange, cancelToken.Token, _pathfinding.GetFlags(blackboard));
if (distance == null)
continue;
targets.Add((target, GetRating(blackboard, target, existingTarget, distance.Value, canMove, xformQuery), distance.Value));
if (targets.Count >= MaxTargetCount)
break;
}
targets.Sort((x, y) => y.Rating.CompareTo(x.Rating));
return targets;
}
protected abstract float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, bool canMove,
protected abstract float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, float distance, bool canMove,
EntityQuery<TransformComponent> xformQuery);
}

View File

@@ -1,3 +1,4 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.NPC.Pathfinding;
@@ -13,8 +14,7 @@ public sealed class PickAccessibleComponentOperator : HTNOperator
{
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private PathfindingSystem _path = default!;
private PathfindingSystem _pathfinding = default!;
private EntityLookupSystem _lookup = default!;
[DataField("rangeKey", required: true)]
@@ -26,15 +26,22 @@ public sealed class PickAccessibleComponentOperator : HTNOperator
[ViewVariables, DataField("component", required: true)]
public string Component = string.Empty;
/// <summary>
/// Where the pathfinding result will be stored (if applicable). This gets removed after execution.
/// </summary>
[ViewVariables, DataField("pathfindKey")]
public string PathfindKey = "MovementPathfind";
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
_path = sysManager.GetEntitySystem<PathfindingSystem>();
_lookup = sysManager.GetEntitySystem<EntityLookupSystem>();
_pathfinding = sysManager.GetEntitySystem<PathfindingSystem>();
}
/// <inheritdoc/>
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
// Check if the component exists
if (!_factory.TryGetRegistration(Component, out var registration))
@@ -56,6 +63,7 @@ public sealed class PickAccessibleComponentOperator : HTNOperator
// TODO: Need to get ones that are accessible.
// TODO: Look at unreal HTN to see repeatable ones maybe?
// TODO: Need type
foreach (var entity in _lookup.GetEntitiesInRange(coordinates, range))
{
if (entity == owner || !query.TryGetComponent(entity, out var comp))
@@ -69,27 +77,31 @@ public sealed class PickAccessibleComponentOperator : HTNOperator
return (false, null);
}
blackboard.TryGetValue<float>(RangeKey, out var maxRange);
if (maxRange == 0f)
maxRange = 7f;
while (targets.Count > 0)
{
// TODO: Get nearest at some stage
var target = _random.PickAndTake(targets);
var path = await _pathfinding.GetRandomPath(
owner,
1.4f,
maxRange,
cancelToken,
flags: _pathfinding.GetFlags(blackboard));
// TODO: God the path api sucks PLUS I need some fast way to get this.
var job = _path.RequestPath(owner, target.Owner, CancellationToken.None);
if (job == null)
continue;
await job.AsTask;
if (job.Result == null || !_entManager.TryGetComponent<TransformComponent>(target.Owner, out var targetXform))
if (path.Result != PathResult.Path)
{
continue;
return (false, null);
}
var target = path.Path.Last().Coordinates;
return (true, new Dictionary<string, object>()
{
{ TargetKey, targetXform.Coordinates },
{ TargetKey, target },
{ PathfindKey, path}
});
}

View File

@@ -1,6 +1,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.NPC.Pathfinding;
using Content.Server.NPC.Pathfinding.Accessible;
using Robust.Shared.Random;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
@@ -10,9 +11,7 @@ namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
/// </summary>
public sealed class PickAccessibleOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private AiReachableSystem _reachable = default!;
private PathfindingSystem _pathfinding = default!;
[DataField("rangeKey", required: true)]
public string RangeKey = string.Empty;
@@ -20,44 +19,48 @@ public sealed class PickAccessibleOperator : HTNOperator
[ViewVariables, DataField("targetKey", required: true)]
public string TargetKey = string.Empty;
/// <summary>
/// Where the pathfinding result will be stored (if applicable). This gets removed after execution.
/// </summary>
[ViewVariables, DataField("pathfindKey")]
public string PathfindKey = "MovementPathfind";
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
_reachable = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AiReachableSystem>();
_pathfinding = sysManager.GetEntitySystem<PathfindingSystem>();
}
/// <inheritdoc/>
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
// Very inefficient (should weight each region by its node count) but better than the old system
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
if (!_entManager.TryGetComponent(_entManager.GetComponent<TransformComponent>(owner).GridUid, out IMapGridComponent? grid))
return (false, null);
blackboard.TryGetValue<float>(RangeKey, out var maxRange);
var reachableArgs = ReachableArgs.GetArgs(owner, blackboard.GetValueOrDefault<float>(RangeKey));
var entityRegion = _reachable.GetRegion(owner);
var reachableRegions = _reachable.GetReachableRegions(reachableArgs, entityRegion);
if (maxRange == 0f)
maxRange = 7f;
if (reachableRegions.Count == 0)
return (false, null);
var path = await _pathfinding.GetRandomPath(
owner,
1.4f,
maxRange,
cancelToken,
flags: _pathfinding.GetFlags(blackboard));
var reachableNodes = new List<PathfindingNode>();
foreach (var region in reachableRegions)
if (path.Result != PathResult.Path)
{
foreach (var node in region.Nodes)
{
reachableNodes.Add(node);
}
return (false, null);
}
var targetNode = _random.Pick(reachableNodes);
var target = path.Path.Last().Coordinates;
var target = grid.Grid.GridTileToLocal(targetNode.TileRef.GridIndices);
return (true, new Dictionary<string, object>()
{
{ TargetKey, target },
{ PathfindKey, path}
});
}
}

View File

@@ -1,3 +1,4 @@
using System.Threading;
using System.Threading.Tasks;
using Robust.Shared.Random;
@@ -10,7 +11,8 @@ public sealed class PickRandomRotationOperator : HTNOperator
[ViewVariables, DataField("targetKey")]
public string TargetKey = "RotateTarget";
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
var rotation = _random.NextAngle();
return (true, new Dictionary<string, object>()

View File

@@ -1,3 +1,4 @@
using System.Threading;
using System.Threading.Tasks;
using Robust.Shared.Random;
@@ -22,7 +23,8 @@ public sealed class RandomOperator : HTNOperator
/// </summary>
[DataField("maxKey", required: true)] public string MaxKey = string.Empty;
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
return (true, new Dictionary<string, object>()
{

View File

@@ -1,38 +1,19 @@
using Robust.Shared.Map;
using JetBrains.Annotations;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Ranged;
/// <summary>
/// Selects a target for ranged combat.
/// </summary>
[UsedImplicitly]
public sealed class PickRangedTargetOperator : NPCCombatOperator
{
protected override float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, bool canMove, EntityQuery<TransformComponent> xformQuery)
protected override float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, float distance, bool canMove, EntityQuery<TransformComponent> xformQuery)
{
var ourCoordinates = blackboard.GetValueOrDefault<EntityCoordinates>(NPCBlackboard.OwnerCoordinates);
if (!xformQuery.TryGetComponent(uid, out var targetXform))
return -1f;
var targetCoordinates = targetXform.Coordinates;
if (!ourCoordinates.TryDistance(EntManager, targetCoordinates, out var distance))
return -1f;
// TODO: Uhh make this better with penetration or something.
var inLOS = Interaction.InRangeUnobstructed(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner),
uid, distance + 0.1f);
if (!canMove && !inLOS)
return -1f;
// Yeah look I just came up with values that seemed okay but they will need a lot of tweaking.
// Having a debug overlay just to project these would be very useful when finetuning in future.
var rating = 0f;
if (inLOS)
rating += 4f;
if (existingTarget == uid)
{
rating += 2f;

View File

@@ -1,3 +1,4 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.NPC.Components;
using Content.Shared.MobState;
@@ -24,7 +25,8 @@ public sealed class RangedOperator : HTNOperator
// 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)
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
// Don't attack if they're already as wounded as we want them.
if (!blackboard.TryGetValue<EntityUid>(TargetKey, out var target))

View File

@@ -1,3 +1,4 @@
using System.Threading;
using System.Threading.Tasks;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
@@ -12,7 +13,8 @@ public sealed class SetFloatOperator : HTNOperator
[ViewVariables(VVAccess.ReadWrite), DataField("amount")]
public float Amount;
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
return (true, new Dictionary<string, object>()
{

View File

@@ -1,3 +1,4 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.NPC.Components;
@@ -31,7 +32,8 @@ public sealed class PickNearbyInjectableOperator : HTNOperator
_lookup = sysManager.GetEntitySystem<EntityLookupSystem>();
}
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);

View File

@@ -1,4 +1,5 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.NPC.Components;
using Robust.Shared.Random;
@@ -10,7 +11,8 @@ public sealed class PickPathfindPointOperator : HTNOperator
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);