HTNSystem.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. using System.Linq;
  2. using System.Text;
  3. using System.Threading;
  4. using Content.Server.Administration.Managers;
  5. using Robust.Shared.CPUJob.JobQueues;
  6. using Robust.Shared.CPUJob.JobQueues.Queues;
  7. using Content.Server.NPC.HTN.PrimitiveTasks;
  8. using Content.Server.NPC.Systems;
  9. using Content.Shared.Administration;
  10. using Content.Shared.Mobs;
  11. using Content.Shared.NPC;
  12. using JetBrains.Annotations;
  13. using Robust.Shared.Player;
  14. using Robust.Shared.Prototypes;
  15. using Robust.Shared.Utility;
  16. namespace Content.Server.NPC.HTN;
  17. public sealed class HTNSystem : EntitySystem
  18. {
  19. [Dependency] private readonly IAdminManager _admin = default!;
  20. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  21. [Dependency] private readonly NPCSystem _npc = default!;
  22. [Dependency] private readonly NPCUtilitySystem _utility = default!;
  23. private readonly JobQueue _planQueue = new(0.004);
  24. private readonly HashSet<ICommonSession> _subscribers = new();
  25. // Hierarchical Task Network
  26. public override void Initialize()
  27. {
  28. base.Initialize();
  29. SubscribeLocalEvent<HTNComponent, MobStateChangedEvent>(_npc.OnMobStateChange);
  30. SubscribeLocalEvent<HTNComponent, MapInitEvent>(_npc.OnNPCMapInit);
  31. SubscribeLocalEvent<HTNComponent, PlayerAttachedEvent>(_npc.OnPlayerNPCAttach);
  32. SubscribeLocalEvent<HTNComponent, PlayerDetachedEvent>(_npc.OnPlayerNPCDetach);
  33. SubscribeLocalEvent<HTNComponent, ComponentShutdown>(OnHTNShutdown);
  34. SubscribeNetworkEvent<RequestHTNMessage>(OnHTNMessage);
  35. SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypeLoad);
  36. OnLoad();
  37. }
  38. private void OnHTNMessage(RequestHTNMessage msg, EntitySessionEventArgs args)
  39. {
  40. if (!_admin.HasAdminFlag(args.SenderSession, AdminFlags.Debug))
  41. {
  42. _subscribers.Remove(args.SenderSession);
  43. return;
  44. }
  45. if (_subscribers.Add(args.SenderSession))
  46. return;
  47. _subscribers.Remove(args.SenderSession);
  48. }
  49. private void OnLoad()
  50. {
  51. // Clear all NPCs in case they're hanging onto stale tasks
  52. var query = AllEntityQuery<HTNComponent>();
  53. while (query.MoveNext(out var comp))
  54. {
  55. comp.PlanningToken?.Cancel();
  56. comp.PlanningToken = null;
  57. if (comp.Plan != null)
  58. {
  59. var currentOperator = comp.Plan.CurrentOperator;
  60. ShutdownTask(currentOperator, comp.Blackboard, HTNOperatorStatus.Failed);
  61. ShutdownPlan(comp);
  62. comp.Plan = null;
  63. RequestPlan(comp);
  64. }
  65. }
  66. // Add dependencies for all operators.
  67. // We put code on operators as I couldn't think of a clean way to put it on systems.
  68. foreach (var compound in _prototypeManager.EnumeratePrototypes<HTNCompoundPrototype>())
  69. {
  70. UpdateCompound(compound);
  71. }
  72. }
  73. private void OnPrototypeLoad(PrototypesReloadedEventArgs obj)
  74. {
  75. OnLoad();
  76. }
  77. private void UpdateCompound(HTNCompoundPrototype compound)
  78. {
  79. for (var i = 0; i < compound.Branches.Count; i++)
  80. {
  81. var branch = compound.Branches[i];
  82. foreach (var precon in branch.Preconditions)
  83. {
  84. precon.Initialize(EntityManager.EntitySysManager);
  85. }
  86. foreach (var task in branch.Tasks)
  87. {
  88. UpdateTask(task);
  89. }
  90. }
  91. }
  92. private void UpdateTask(HTNTask task)
  93. {
  94. switch (task)
  95. {
  96. case HTNCompoundTask:
  97. // NOOP, handled elsewhere
  98. break;
  99. case HTNPrimitiveTask primitive:
  100. foreach (var precon in primitive.Preconditions)
  101. {
  102. precon.Initialize(EntityManager.EntitySysManager);
  103. }
  104. primitive.Operator.Initialize(EntityManager.EntitySysManager);
  105. break;
  106. default:
  107. throw new NotImplementedException();
  108. }
  109. }
  110. private void OnHTNShutdown(EntityUid uid, HTNComponent component, ComponentShutdown args)
  111. {
  112. _npc.OnNPCShutdown(uid, component, args);
  113. component.PlanningToken?.Cancel();
  114. component.PlanningJob = null;
  115. }
  116. /// <summary>
  117. /// Enable / disable the hierarchical task network of an entity
  118. /// </summary>
  119. /// <param name="ent">The entity and its <see cref="HTNComponent"/></param>
  120. /// <param name="state">Set 'true' to enable, or 'false' to disable, the HTN</param>
  121. /// <param name="planCooldown">Specifies a time in seconds before the entity can start planning a new action (only takes effect when the HTN is enabled)</param>
  122. // ReSharper disable once InconsistentNaming
  123. [PublicAPI]
  124. public void SetHTNEnabled(Entity<HTNComponent> ent, bool state, float planCooldown = 0f)
  125. {
  126. if (ent.Comp.Enabled == state)
  127. return;
  128. ent.Comp.Enabled = state;
  129. ent.Comp.PlanAccumulator = planCooldown;
  130. ent.Comp.PlanningToken?.Cancel();
  131. ent.Comp.PlanningToken = null;
  132. if (ent.Comp.Plan != null)
  133. {
  134. var currentOperator = ent.Comp.Plan.CurrentOperator;
  135. ShutdownTask(currentOperator, ent.Comp.Blackboard, HTNOperatorStatus.Failed);
  136. ShutdownPlan(ent.Comp);
  137. ent.Comp.Plan = null;
  138. }
  139. if (ent.Comp.Enabled && ent.Comp.PlanAccumulator <= 0)
  140. RequestPlan(ent.Comp);
  141. }
  142. /// <summary>
  143. /// Forces the NPC to replan.
  144. /// </summary>
  145. [PublicAPI]
  146. public void Replan(HTNComponent component)
  147. {
  148. component.PlanAccumulator = 0f;
  149. }
  150. public void UpdateNPC(ref int count, int maxUpdates, float frameTime)
  151. {
  152. _planQueue.Process();
  153. var query = EntityQueryEnumerator<ActiveNPCComponent, HTNComponent>();
  154. while (query.MoveNext(out var uid, out _, out var comp))
  155. {
  156. // If we're over our max count or it's not MapInit then ignore the NPC.
  157. if (count >= maxUpdates)
  158. break;
  159. if (!comp.Enabled)
  160. continue;
  161. if (comp.PlanningJob != null)
  162. {
  163. if (comp.PlanningJob.Exception != null)
  164. {
  165. Log.Fatal($"Received exception on planning job for {uid}!");
  166. _npc.SleepNPC(uid);
  167. var exc = comp.PlanningJob.Exception;
  168. RemComp<HTNComponent>(uid);
  169. throw exc;
  170. }
  171. // If a new planning job has finished then handle it.
  172. if (comp.PlanningJob.Status != JobStatus.Finished)
  173. continue;
  174. var newPlanBetter = false;
  175. // If old traversal is better than new traversal then ignore the new plan
  176. if (comp.Plan != null && comp.PlanningJob.Result != null)
  177. {
  178. var oldMtr = comp.Plan.BranchTraversalRecord;
  179. var mtr = comp.PlanningJob.Result.BranchTraversalRecord;
  180. for (var i = 0; i < oldMtr.Count; i++)
  181. {
  182. if (i < mtr.Count && oldMtr[i] > mtr[i])
  183. {
  184. newPlanBetter = true;
  185. break;
  186. }
  187. }
  188. }
  189. if (comp.Plan == null || newPlanBetter)
  190. {
  191. comp.CheckServices = false;
  192. if (comp.Plan != null)
  193. {
  194. ShutdownTask(comp.Plan.CurrentOperator, comp.Blackboard, HTNOperatorStatus.BetterPlan);
  195. ShutdownPlan(comp);
  196. }
  197. comp.Plan = comp.PlanningJob.Result;
  198. // Startup the first task and anything else we need to do.
  199. if (comp.Plan != null)
  200. {
  201. StartupTask(comp.Plan.Tasks[comp.Plan.Index], comp.Blackboard, comp.Plan.Effects[comp.Plan.Index]);
  202. }
  203. // Send debug info
  204. foreach (var session in _subscribers)
  205. {
  206. var text = new StringBuilder();
  207. if (comp.Plan != null)
  208. {
  209. text.AppendLine($"BTR: {string.Join(", ", comp.Plan.BranchTraversalRecord)}");
  210. text.AppendLine($"tasks:");
  211. var root = comp.RootTask;
  212. var btr = new List<int>();
  213. var level = -1;
  214. AppendDebugText(root, text, comp.Plan.BranchTraversalRecord, btr, ref level);
  215. }
  216. RaiseNetworkEvent(new HTNMessage()
  217. {
  218. Uid = GetNetEntity(uid),
  219. Text = text.ToString(),
  220. }, session.Channel);
  221. }
  222. }
  223. // Keeping old plan
  224. else
  225. {
  226. comp.CheckServices = true;
  227. }
  228. comp.PlanningJob = null;
  229. comp.PlanningToken = null;
  230. }
  231. Update(comp, frameTime);
  232. count++;
  233. }
  234. }
  235. private void AppendDebugText(HTNTask task, StringBuilder text, List<int> planBtr, List<int> btr, ref int level)
  236. {
  237. // If it's the selected BTR then highlight.
  238. for (var i = 0; i < btr.Count; i++)
  239. {
  240. text.Append("--");
  241. }
  242. text.Append(' ');
  243. if (task is HTNPrimitiveTask primitive)
  244. {
  245. text.AppendLine(primitive.ToString());
  246. return;
  247. }
  248. if (task is HTNCompoundTask compTask)
  249. {
  250. var compound = _prototypeManager.Index<HTNCompoundPrototype>(compTask.Task);
  251. level++;
  252. text.AppendLine(compound.ID);
  253. var branches = compound.Branches;
  254. for (var i = 0; i < branches.Count; i++)
  255. {
  256. var branch = branches[i];
  257. btr.Add(i);
  258. text.AppendLine($" branch {string.Join(", ", btr)}:");
  259. foreach (var sub in branch.Tasks)
  260. {
  261. AppendDebugText(sub, text, planBtr, btr, ref level);
  262. }
  263. btr.RemoveAt(btr.Count - 1);
  264. }
  265. level--;
  266. return;
  267. }
  268. throw new NotImplementedException();
  269. }
  270. private void Update(HTNComponent component, float frameTime)
  271. {
  272. // If we're not planning then countdown to next one.
  273. if (component.PlanningJob == null)
  274. component.PlanAccumulator -= frameTime;
  275. // We'll still try re-planning occasionally even when we're updating in case new data comes in.
  276. if (component.PlanAccumulator <= 0f)
  277. {
  278. RequestPlan(component);
  279. }
  280. // Getting a new plan so do nothing.
  281. if (component.Plan == null)
  282. return;
  283. // Run the existing plan still
  284. var status = HTNOperatorStatus.Finished;
  285. // Continuously run operators until we can't anymore.
  286. while (status != HTNOperatorStatus.Continuing && component.Plan != null)
  287. {
  288. // Run the existing operator
  289. var currentOperator = component.Plan.CurrentOperator;
  290. var currentTask = component.Plan.CurrentTask;
  291. var blackboard = component.Blackboard;
  292. // Service still on cooldown.
  293. if (component.CheckServices)
  294. {
  295. foreach (var service in currentTask.Services)
  296. {
  297. var serviceResult = _utility.GetEntities(blackboard, service.Prototype);
  298. blackboard.SetValue(service.Key, serviceResult.GetHighest());
  299. }
  300. component.CheckServices = false;
  301. }
  302. status = currentOperator.Update(blackboard, frameTime);
  303. switch (status)
  304. {
  305. case HTNOperatorStatus.Continuing:
  306. break;
  307. case HTNOperatorStatus.Failed:
  308. ShutdownTask(currentOperator, blackboard, status);
  309. ShutdownPlan(component);
  310. break;
  311. // Operator completed so go to the next one.
  312. case HTNOperatorStatus.Finished:
  313. ShutdownTask(currentOperator, blackboard, status);
  314. component.Plan.Index++;
  315. // Plan finished!
  316. if (component.Plan.Tasks.Count <= component.Plan.Index)
  317. {
  318. ShutdownPlan(component);
  319. break;
  320. }
  321. ConditionalShutdown(component.Plan, currentOperator, blackboard, HTNPlanState.TaskFinished);
  322. StartupTask(component.Plan.Tasks[component.Plan.Index], component.Blackboard, component.Plan.Effects[component.Plan.Index]);
  323. break;
  324. default:
  325. throw new InvalidOperationException();
  326. }
  327. }
  328. }
  329. public void ShutdownTask(HTNOperator currentOperator, NPCBlackboard blackboard, HTNOperatorStatus status)
  330. {
  331. if (currentOperator is IHtnConditionalShutdown conditional &&
  332. (conditional.ShutdownState & HTNPlanState.TaskFinished) != 0x0)
  333. {
  334. conditional.ConditionalShutdown(blackboard);
  335. }
  336. currentOperator.TaskShutdown(blackboard, status);
  337. }
  338. public void ShutdownPlan(HTNComponent component)
  339. {
  340. DebugTools.Assert(component.Plan != null);
  341. var blackboard = component.Blackboard;
  342. foreach (var task in component.Plan.Tasks)
  343. {
  344. if (task.Operator is IHtnConditionalShutdown conditional &&
  345. (conditional.ShutdownState & HTNPlanState.PlanFinished) != 0x0)
  346. {
  347. conditional.ConditionalShutdown(blackboard);
  348. }
  349. task.Operator.PlanShutdown(component.Blackboard);
  350. }
  351. component.Plan = null;
  352. }
  353. /// <summary>
  354. /// Shuts down the current operator conditionally.
  355. /// </summary>
  356. private void ConditionalShutdown(HTNPlan plan, HTNOperator currentOperator, NPCBlackboard blackboard, HTNPlanState state)
  357. {
  358. if (currentOperator is not IHtnConditionalShutdown conditional)
  359. return;
  360. if ((conditional.ShutdownState & state) == 0x0)
  361. return;
  362. conditional.ConditionalShutdown(blackboard);
  363. }
  364. /// <summary>
  365. /// Starts a new primitive task. Will apply effects from planning if applicable.
  366. /// </summary>
  367. private void StartupTask(HTNPrimitiveTask primitive, NPCBlackboard blackboard, Dictionary<string, object>? effects)
  368. {
  369. // We may have planner only tasks where we want to reuse their data during update
  370. // e.g. if we pathfind to an enemy to know if we can attack it, we don't want to do another pathfind immediately
  371. if (effects != null && primitive.ApplyEffectsOnStartup)
  372. {
  373. foreach (var (key, value) in effects)
  374. {
  375. blackboard.SetValue(key, value);
  376. }
  377. }
  378. primitive.Operator.Startup(blackboard);
  379. }
  380. /// <summary>
  381. /// Request a new plan for this component, even if running an existing plan.
  382. /// </summary>
  383. /// <param name="component"></param>
  384. private void RequestPlan(HTNComponent component)
  385. {
  386. if (component.PlanningJob != null)
  387. return;
  388. component.PlanAccumulator += component.PlanCooldown;
  389. var cancelToken = new CancellationTokenSource();
  390. var branchTraversal = component.Plan?.BranchTraversalRecord;
  391. var job = new HTNPlanJob(
  392. 0.02,
  393. _prototypeManager,
  394. component.RootTask,
  395. component.Blackboard.ShallowClone(), branchTraversal, cancelToken.Token);
  396. _planQueue.EnqueueJob(job);
  397. component.PlanningJob = job;
  398. component.PlanningToken = cancelToken;
  399. }
  400. public string GetDomain(HTNCompoundTask compound)
  401. {
  402. // TODO: Recursively add each one
  403. var indent = 0;
  404. var builder = new StringBuilder();
  405. AppendDomain(builder, compound, ref indent);
  406. return builder.ToString();
  407. }
  408. private void AppendDomain(StringBuilder builder, HTNTask task, ref int indent)
  409. {
  410. var buffer = string.Concat(Enumerable.Repeat(" ", indent));
  411. if (task is HTNPrimitiveTask primitive)
  412. {
  413. builder.AppendLine(buffer + $"Primitive: {task}");
  414. builder.AppendLine(buffer + $" operator: {primitive.Operator.GetType().Name}");
  415. }
  416. else if (task is HTNCompoundTask compTask)
  417. {
  418. var compound = _prototypeManager.Index<HTNCompoundPrototype>(compTask.Task);
  419. builder.AppendLine(buffer + $"Compound: {task}");
  420. for (var i = 0; i < compound.Branches.Count; i++)
  421. {
  422. var branch = compound.Branches[i];
  423. builder.AppendLine(buffer + " branch:");
  424. indent++;
  425. foreach (var branchTask in branch.Tasks)
  426. {
  427. AppendDomain(builder, branchTask, ref indent);
  428. }
  429. indent--;
  430. }
  431. }
  432. }
  433. }
  434. /// <summary>
  435. /// The outcome of the current operator during update.
  436. /// </summary>
  437. public enum HTNOperatorStatus : byte
  438. {
  439. Continuing,
  440. Failed,
  441. Finished,
  442. /// <summary>
  443. /// Was a better plan than this found?
  444. /// </summary>
  445. BetterPlan,
  446. }