MoveToOperator.cs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. using System.Threading;
  2. using System.Threading.Tasks;
  3. using Content.Server.NPC.Components;
  4. using Content.Server.NPC.Pathfinding;
  5. using Content.Server.NPC.Systems;
  6. using Robust.Shared.Map;
  7. using Robust.Shared.Map.Components;
  8. using Robust.Shared.Physics.Components;
  9. namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
  10. /// <summary>
  11. /// Moves an NPC to the specified target key. Hands the actual steering off to NPCSystem.Steering
  12. /// </summary>
  13. public sealed partial class MoveToOperator : HTNOperator, IHtnConditionalShutdown
  14. {
  15. [Dependency] private readonly IEntityManager _entManager = default!;
  16. private NPCSteeringSystem _steering = default!;
  17. private PathfindingSystem _pathfind = default!;
  18. private SharedTransformSystem _transform = default!;
  19. /// <summary>
  20. /// When to shut the task down.
  21. /// </summary>
  22. [DataField("shutdownState")]
  23. public HTNPlanState ShutdownState { get; private set; } = HTNPlanState.TaskFinished;
  24. /// <summary>
  25. /// Should we assume the MovementTarget is reachable during planning or should we pathfind to it?
  26. /// </summary>
  27. [DataField("pathfindInPlanning")]
  28. public bool PathfindInPlanning = true;
  29. /// <summary>
  30. /// When we're finished moving to the target should we remove its key?
  31. /// </summary>
  32. [DataField("removeKeyOnFinish")]
  33. public bool RemoveKeyOnFinish = true;
  34. /// <summary>
  35. /// Target Coordinates to move to. This gets removed after execution.
  36. /// </summary>
  37. [DataField("targetKey")]
  38. public string TargetKey = "TargetCoordinates";
  39. /// <summary>
  40. /// Where the pathfinding result will be stored (if applicable). This gets removed after execution.
  41. /// </summary>
  42. [DataField("pathfindKey")]
  43. public string PathfindKey = NPCBlackboard.PathfindKey;
  44. /// <summary>
  45. /// How close we need to get before considering movement finished.
  46. /// </summary>
  47. [DataField("rangeKey")]
  48. public string RangeKey = "MovementRange";
  49. /// <summary>
  50. /// Do we only need to move into line of sight.
  51. /// </summary>
  52. [DataField("stopOnLineOfSight")]
  53. public bool StopOnLineOfSight;
  54. private const string MovementCancelToken = "MovementCancelToken";
  55. public override void Initialize(IEntitySystemManager sysManager)
  56. {
  57. base.Initialize(sysManager);
  58. _pathfind = sysManager.GetEntitySystem<PathfindingSystem>();
  59. _steering = sysManager.GetEntitySystem<NPCSteeringSystem>();
  60. _transform = sysManager.GetEntitySystem<SharedTransformSystem>();
  61. }
  62. public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
  63. CancellationToken cancelToken)
  64. {
  65. if (!blackboard.TryGetValue<EntityCoordinates>(TargetKey, out var targetCoordinates, _entManager))
  66. {
  67. return (false, null);
  68. }
  69. var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
  70. if (!_entManager.TryGetComponent<TransformComponent>(owner, out var xform) ||
  71. !_entManager.TryGetComponent<PhysicsComponent>(owner, out var body))
  72. return (false, null);
  73. if (!_entManager.TryGetComponent<MapGridComponent>(xform.GridUid, out var ownerGrid) ||
  74. !_entManager.TryGetComponent<MapGridComponent>(targetCoordinates.GetGridUid(_entManager), out var targetGrid))
  75. {
  76. return (false, null);
  77. }
  78. var range = blackboard.GetValueOrDefault<float>(RangeKey, _entManager);
  79. if (xform.Coordinates.TryDistance(_entManager, targetCoordinates, out var distance) && distance <= range)
  80. {
  81. // In range
  82. return (true, new Dictionary<string, object>()
  83. {
  84. {NPCBlackboard.OwnerCoordinates, blackboard.GetValueOrDefault<EntityCoordinates>(NPCBlackboard.OwnerCoordinates, _entManager)}
  85. });
  86. }
  87. if (!PathfindInPlanning)
  88. {
  89. return (true, new Dictionary<string, object>()
  90. {
  91. {NPCBlackboard.OwnerCoordinates, targetCoordinates}
  92. });
  93. }
  94. var path = await _pathfind.GetPath(
  95. blackboard.GetValue<EntityUid>(NPCBlackboard.Owner),
  96. xform.Coordinates,
  97. targetCoordinates,
  98. range,
  99. cancelToken,
  100. _pathfind.GetFlags(blackboard));
  101. if (path.Result != PathResult.Path)
  102. {
  103. return (false, null);
  104. }
  105. return (true, new Dictionary<string, object>()
  106. {
  107. {NPCBlackboard.OwnerCoordinates, targetCoordinates},
  108. {PathfindKey, path}
  109. });
  110. }
  111. // Given steering is complicated we'll hand it off to a dedicated system rather than this singleton operator.
  112. public override void Startup(NPCBlackboard blackboard)
  113. {
  114. base.Startup(blackboard);
  115. // Need to remove the planning value for execution.
  116. blackboard.Remove<EntityCoordinates>(NPCBlackboard.OwnerCoordinates);
  117. var targetCoordinates = blackboard.GetValue<EntityCoordinates>(TargetKey);
  118. var uid = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
  119. // Re-use the path we may have if applicable.
  120. var comp = _steering.Register(uid, targetCoordinates);
  121. comp.ArriveOnLineOfSight = StopOnLineOfSight;
  122. if (blackboard.TryGetValue<float>(RangeKey, out var range, _entManager))
  123. {
  124. comp.Range = range;
  125. }
  126. if (blackboard.TryGetValue<PathResultEvent>(PathfindKey, out var result, _entManager))
  127. {
  128. if (blackboard.TryGetValue<EntityCoordinates>(NPCBlackboard.OwnerCoordinates, out var coordinates, _entManager))
  129. {
  130. var mapCoords = coordinates.ToMap(_entManager, _transform);
  131. _steering.PrunePath(uid, mapCoords, targetCoordinates.ToMapPos(_entManager, _transform) - mapCoords.Position, result.Path);
  132. }
  133. comp.CurrentPath = new Queue<PathPoly>(result.Path);
  134. }
  135. }
  136. public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
  137. {
  138. var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
  139. if (!_entManager.TryGetComponent<NPCSteeringComponent>(owner, out var steering))
  140. return HTNOperatorStatus.Failed;
  141. // Just keep moving in the background and let the other tasks handle it.
  142. if (ShutdownState == HTNPlanState.PlanFinished && steering.Status == SteeringStatus.Moving)
  143. {
  144. return HTNOperatorStatus.Finished;
  145. }
  146. return steering.Status switch
  147. {
  148. SteeringStatus.InRange => HTNOperatorStatus.Finished,
  149. SteeringStatus.NoPath => HTNOperatorStatus.Failed,
  150. SteeringStatus.Moving => HTNOperatorStatus.Continuing,
  151. _ => throw new ArgumentOutOfRangeException()
  152. };
  153. }
  154. public void ConditionalShutdown(NPCBlackboard blackboard)
  155. {
  156. // Cleanup the blackboard and remove steering.
  157. if (blackboard.TryGetValue<CancellationTokenSource>(MovementCancelToken, out var cancelToken, _entManager))
  158. {
  159. cancelToken.Cancel();
  160. blackboard.Remove<CancellationTokenSource>(MovementCancelToken);
  161. }
  162. // OwnerCoordinates is only used in planning so dump it.
  163. blackboard.Remove<PathResultEvent>(PathfindKey);
  164. if (RemoveKeyOnFinish)
  165. {
  166. blackboard.Remove<EntityCoordinates>(TargetKey);
  167. }
  168. _steering.Unregister(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
  169. }
  170. }