using System.Threading; using System.Threading.Tasks; using Content.Server.NPC.Components; using Content.Server.NPC.Pathfinding; using Content.Server.NPC.Systems; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Physics.Components; 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 partial class MoveToOperator : HTNOperator, IHtnConditionalShutdown { [Dependency] private readonly IEntityManager _entManager = default!; private NPCSteeringSystem _steering = default!; private PathfindingSystem _pathfind = default!; private SharedTransformSystem _transform = default!; /// /// When to shut the task down. /// [DataField("shutdownState")] public HTNPlanState ShutdownState { get; private set; } = HTNPlanState.TaskFinished; /// /// Should we assume the MovementTarget is reachable during planning or should we pathfind to it? /// [DataField("pathfindInPlanning")] public bool PathfindInPlanning = true; /// /// When we're finished moving to the target should we remove its key? /// [DataField("removeKeyOnFinish")] public bool RemoveKeyOnFinish = true; /// /// Target Coordinates to move to. This gets removed after execution. /// [DataField("targetKey")] public string TargetKey = "TargetCoordinates"; /// /// Where the pathfinding result will be stored (if applicable). This gets removed after execution. /// [DataField("pathfindKey")] public string PathfindKey = NPCBlackboard.PathfindKey; /// /// How close we need to get before considering movement finished. /// [DataField("rangeKey")] public string RangeKey = "MovementRange"; /// /// Do we only need to move into line of sight. /// [DataField("stopOnLineOfSight")] public bool StopOnLineOfSight; private const string MovementCancelToken = "MovementCancelToken"; public override void Initialize(IEntitySystemManager sysManager) { base.Initialize(sysManager); _pathfind = sysManager.GetEntitySystem(); _steering = sysManager.GetEntitySystem(); _transform = sysManager.GetEntitySystem(); } public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard, CancellationToken cancelToken) { if (!blackboard.TryGetValue(TargetKey, out var targetCoordinates, _entManager)) { 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 (!_entManager.TryGetComponent(xform.GridUid, out var ownerGrid) || !_entManager.TryGetComponent(targetCoordinates.GetGridUid(_entManager), out var targetGrid)) { return (false, null); } var range = blackboard.GetValueOrDefault(RangeKey, _entManager); if (xform.Coordinates.TryDistance(_entManager, targetCoordinates, out var distance) && distance <= range) { // In range return (true, new Dictionary() { {NPCBlackboard.OwnerCoordinates, blackboard.GetValueOrDefault(NPCBlackboard.OwnerCoordinates, _entManager)} }); } if (!PathfindInPlanning) { return (true, new Dictionary() { {NPCBlackboard.OwnerCoordinates, targetCoordinates} }); } var path = await _pathfind.GetPath( blackboard.GetValue(NPCBlackboard.Owner), xform.Coordinates, targetCoordinates, range, cancelToken, _pathfind.GetFlags(blackboard)); if (path.Result != PathResult.Path) { return (false, null); } return (true, new Dictionary() { {NPCBlackboard.OwnerCoordinates, targetCoordinates}, {PathfindKey, path} }); } // 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); var targetCoordinates = blackboard.GetValue(TargetKey); var uid = blackboard.GetValue(NPCBlackboard.Owner); // Re-use the path we may have if applicable. var comp = _steering.Register(uid, targetCoordinates); comp.ArriveOnLineOfSight = StopOnLineOfSight; if (blackboard.TryGetValue(RangeKey, out var range, _entManager)) { comp.Range = range; } if (blackboard.TryGetValue(PathfindKey, out var result, _entManager)) { if (blackboard.TryGetValue(NPCBlackboard.OwnerCoordinates, out var coordinates, _entManager)) { var mapCoords = coordinates.ToMap(_entManager, _transform); _steering.PrunePath(uid, mapCoords, targetCoordinates.ToMapPos(_entManager, _transform) - mapCoords.Position, result.Path); } comp.CurrentPath = new Queue(result.Path); } } public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) { var owner = blackboard.GetValue(NPCBlackboard.Owner); if (!_entManager.TryGetComponent(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(MovementCancelToken, out var cancelToken, _entManager)) { 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)); } }