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 Robust.Shared.Map; namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators; /// /// Moves an NPC to the specified target key. Hands the actual steering off to NPCSystem.Steering /// public sealed class MoveToOperator : HTNOperator { [Dependency] private readonly IEntityManager _entManager = default!; [Dependency] private readonly IMapManager _mapManager = default!; private NPCSteeringSystem _steering = default!; private PathfindingSystem _pathfind = default!; /// /// Should we assume the MovementTarget is reachable during planning or should we pathfind to it? /// [ViewVariables, DataField("pathfindInPlanning")] public bool PathfindInPlanning = true; /// /// When we're finished moving to the target should we remove its key? /// [ViewVariables, DataField("removeKeyOnFinish")] public bool RemoveKeyOnFinish = true; /// /// Target Coordinates to move to. This gets removed after execution. /// [ViewVariables, DataField("targetKey")] public string TargetKey = "MovementTarget"; /// /// Where the pathfinding result will be stored (if applicable). This gets removed after execution. /// [ViewVariables, DataField("pathfindKey")] public string PathfindKey = "MovementPathfind"; /// /// How close we need to get before considering movement finished. /// [ViewVariables, DataField("rangeKey")] public string RangeKey = "MovementRange"; private const string MovementCancelToken = "MovementCancelToken"; public override void Initialize(IEntitySystemManager sysManager) { base.Initialize(sysManager); _pathfind = sysManager.GetEntitySystem(); _steering = sysManager.GetEntitySystem(); } public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard) { if (!blackboard.TryGetValue(TargetKey, out var targetCoordinates)) { return (false, null); } var owner = blackboard.GetValue(NPCBlackboard.Owner); if (!_entManager.TryGetComponent(owner, out var xform) || !_entManager.TryGetComponent(owner, out var body)) return (false, null); if (!_mapManager.TryGetGrid(xform.GridUid, out var ownerGrid) || !_mapManager.TryGetGrid(targetCoordinates.GetGridUid(_entManager), out var targetGrid) || ownerGrid != targetGrid) { return (false, null); } var range = blackboard.GetValueOrDefault(RangeKey); if (xform.Coordinates.TryDistance(_entManager, targetCoordinates, out var distance) && distance <= range) { // In range return (true, new Dictionary() { {NPCBlackboard.OwnerCoordinates, blackboard.GetValueOrDefault(NPCBlackboard.OwnerCoordinates)} }); } if (!PathfindInPlanning) { return (true, new Dictionary() { {NPCBlackboard.OwnerCoordinates, targetCoordinates} }); } var cancelToken = new CancellationTokenSource(); var access = blackboard.GetValueOrDefault>(NPCBlackboard.Access) ?? new List(); var job = _pathfind.RequestPath( new PathfindingArgs( blackboard.GetValue(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) return (false, null); return (true, new Dictionary() { {NPCBlackboard.OwnerCoordinates, targetCoordinates}, {PathfindKey, job.Result} }); } // Given steering is complicated we'll hand it off to a dedicated system rather than this singleton operator. public override void Startup(NPCBlackboard blackboard) { base.Startup(blackboard); // Need to remove the planning value for execution. blackboard.Remove(NPCBlackboard.OwnerCoordinates); // Re-use the path we may have if applicable. var comp = _steering.Register(blackboard.GetValue(NPCBlackboard.Owner), blackboard.GetValue(TargetKey)); if (blackboard.TryGetValue(RangeKey, out var range)) { comp.Range = range; } if (blackboard.TryGetValue>(PathfindKey, out var path)) { if (blackboard.TryGetValue(NPCBlackboard.OwnerCoordinates, out var coordinates)) { _steering.PrunePath(coordinates, path); } comp.CurrentPath = path; } } public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status) { base.Shutdown(blackboard, status); // Cleanup the blackboard and remove steering. if (blackboard.TryGetValue(MovementCancelToken, out var cancelToken)) { cancelToken.Cancel(); blackboard.Remove(MovementCancelToken); } // OwnerCoordinates is only used in planning so dump it. blackboard.Remove>(PathfindKey); if (RemoveKeyOnFinish) { blackboard.Remove(TargetKey); } _steering.Unregister(blackboard.GetValue(NPCBlackboard.Owner)); } public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) { var owner = blackboard.GetValue(NPCBlackboard.Owner); if (!_entManager.TryGetComponent(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() }; } }