using System.Threading; using System.Threading.Tasks; using Content.Server.NPC.Components; using Content.Server.NPC.Pathfinding; 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; /// /// 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, CancellationToken cancelToken) { 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)) { 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 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); // Re-use the path we may have if applicable. var comp = _steering.Register(blackboard.GetValue(NPCBlackboard.Owner), targetCoordinates); if (blackboard.TryGetValue(RangeKey, out var range)) { comp.Range = range; } if (blackboard.TryGetValue(PathfindKey, out var result)) { if (blackboard.TryGetValue(NPCBlackboard.OwnerCoordinates, out var coordinates)) { var mapCoords = coordinates.ToMap(_entManager); _steering.PrunePath(mapCoords, targetCoordinates.ToMapPos(_entManager) - mapCoords.Position, result.Path); } comp.CurrentPath = result.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() }; } }