HTNPlanJob.cs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. using System.Linq;
  2. using System.Threading;
  3. using System.Threading.Tasks;
  4. using Robust.Shared.CPUJob.JobQueues;
  5. using Content.Server.NPC.HTN.PrimitiveTasks;
  6. using Robust.Shared.Prototypes;
  7. namespace Content.Server.NPC.HTN;
  8. /// <summary>
  9. /// A time-sliced job that will retrieve an HTN plan eventually.
  10. /// </summary>
  11. public sealed class HTNPlanJob : Job<HTNPlan>
  12. {
  13. private readonly HTNTask _rootTask;
  14. private NPCBlackboard _blackboard;
  15. private IPrototypeManager _protoManager;
  16. /// <summary>
  17. /// Branch traversal of an existing plan (if applicable).
  18. /// </summary>
  19. private List<int>? _branchTraversal;
  20. public HTNPlanJob(
  21. double maxTime,
  22. IPrototypeManager protoManager,
  23. HTNTask rootTask,
  24. NPCBlackboard blackboard,
  25. List<int>? branchTraversal,
  26. CancellationToken cancellationToken = default) : base(maxTime, cancellationToken)
  27. {
  28. _protoManager = protoManager;
  29. _rootTask = rootTask;
  30. _blackboard = blackboard;
  31. _branchTraversal = branchTraversal;
  32. }
  33. protected override async Task<HTNPlan?> Process()
  34. {
  35. /*
  36. * Really the best reference for what a HTN looks like is http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter12_Exploring_HTN_Planners_through_Example.pdf
  37. * It's kinda like a behaviour tree but also can consider multiple actions in sequence.
  38. *
  39. * Methods have been renamed to branches
  40. */
  41. var decompHistory = new Stack<DecompositionState>();
  42. // branch traversal record. Whenever we find a new compound task this updates.
  43. var btrIndex = 0;
  44. // For some tasks we may do something expensive or want to re-use the planning result.
  45. // e.g. pathfind to a target before deciding to attack it.
  46. // Given all of the primitive tasks are singletons we need to store the data somewhere
  47. // hence we'll store it here.
  48. var appliedStates = new List<Dictionary<string, object>?>();
  49. var tasksToProcess = new Queue<HTNTask>();
  50. var finalPlan = new List<HTNPrimitiveTask>();
  51. tasksToProcess.Enqueue(_rootTask);
  52. // How many primitive tasks we've added since last record.
  53. var primitiveCount = 0;
  54. int tasksProcessed = 0;
  55. while (tasksToProcess.TryDequeue(out var currentTask))
  56. {
  57. if (tasksProcessed++ > _rootTask.MaximumTasks)
  58. throw new Exception("HTN Planner exceeded maximum tasks");
  59. switch (currentTask)
  60. {
  61. case HTNCompoundTask compound:
  62. await SuspendIfOutOfTime();
  63. if (TryFindSatisfiedMethod(compound, tasksToProcess, _blackboard, ref btrIndex))
  64. {
  65. // Need to copy worldstate to roll it back
  66. // Don't need to copy taskstoprocess as we can just clear it and set it to the compound task we roll back to.
  67. // Don't need to copy finalplan as we can just count how many primitives we've added since last record
  68. decompHistory.Push(new DecompositionState()
  69. {
  70. Blackboard = _blackboard.ShallowClone(),
  71. CompoundTask = compound,
  72. BranchTraversal = btrIndex,
  73. PrimitiveCount = primitiveCount,
  74. });
  75. // TODO: Early out if existing plan is better and save lots of time.
  76. // my brain is not working rn AAA
  77. primitiveCount = 0;
  78. // Reset method traversal
  79. btrIndex = 0;
  80. }
  81. else
  82. {
  83. RestoreTolastDecomposedTask(decompHistory, tasksToProcess, appliedStates, finalPlan, ref primitiveCount, ref _blackboard, ref btrIndex);
  84. }
  85. break;
  86. case HTNPrimitiveTask primitive:
  87. if (await WaitAsyncTask(PrimitiveConditionMet(primitive, _blackboard, appliedStates)))
  88. {
  89. primitiveCount++;
  90. finalPlan.Add(primitive);
  91. }
  92. else
  93. {
  94. RestoreTolastDecomposedTask(decompHistory, tasksToProcess, appliedStates, finalPlan, ref primitiveCount, ref _blackboard, ref btrIndex);
  95. }
  96. break;
  97. }
  98. }
  99. if (finalPlan.Count == 0)
  100. {
  101. return null;
  102. }
  103. var branchTraversalRecord = decompHistory.Reverse().Select(o => o.BranchTraversal).ToList();
  104. return new HTNPlan(finalPlan, branchTraversalRecord, appliedStates);
  105. }
  106. private async Task<bool> PrimitiveConditionMet(HTNPrimitiveTask primitive, NPCBlackboard blackboard, List<Dictionary<string, object>?> appliedStates)
  107. {
  108. blackboard.ReadOnly = true;
  109. foreach (var con in primitive.Preconditions)
  110. {
  111. if (con.IsMet(blackboard))
  112. continue;
  113. return false;
  114. }
  115. var (valid, effects) = await primitive.Operator.Plan(blackboard, Cancellation);
  116. if (!valid)
  117. return false;
  118. blackboard.ReadOnly = false;
  119. if (effects != null)
  120. {
  121. foreach (var (key, value) in effects)
  122. {
  123. blackboard.SetValue(key, value);
  124. }
  125. }
  126. appliedStates.Add(effects);
  127. return true;
  128. }
  129. /// <summary>
  130. /// Goes through each compound task branch and tries to find an appropriate one.
  131. /// </summary>
  132. private bool TryFindSatisfiedMethod(HTNCompoundTask compoundId, Queue<HTNTask> tasksToProcess, NPCBlackboard blackboard, ref int mtrIndex)
  133. {
  134. var compound = _protoManager.Index<HTNCompoundPrototype>(compoundId.Task);
  135. for (; mtrIndex < compound.Branches.Count; mtrIndex++)
  136. {
  137. var branch = compound.Branches[mtrIndex];
  138. var isValid = true;
  139. foreach (var con in branch.Preconditions)
  140. {
  141. if (con.IsMet(blackboard))
  142. continue;
  143. isValid = false;
  144. break;
  145. }
  146. if (!isValid)
  147. continue;
  148. foreach (var task in branch.Tasks)
  149. {
  150. tasksToProcess.Enqueue(task);
  151. }
  152. return true;
  153. }
  154. return false;
  155. }
  156. /// <summary>
  157. /// Restores the planner state.
  158. /// </summary>
  159. private void RestoreTolastDecomposedTask(
  160. Stack<DecompositionState> decompHistory,
  161. Queue<HTNTask> tasksToProcess,
  162. List<Dictionary<string, object>?> appliedStates,
  163. List<HTNPrimitiveTask> finalPlan,
  164. ref int primitiveCount,
  165. ref NPCBlackboard blackboard,
  166. ref int mtrIndex)
  167. {
  168. tasksToProcess.Clear();
  169. // No plan found so this will just break normally.
  170. if (!decompHistory.TryPop(out var lastDecomp))
  171. return;
  172. // Increment MTR so next time we try the next method on the compound task.
  173. mtrIndex = lastDecomp.BranchTraversal + 1;
  174. var count = finalPlan.Count;
  175. var reduction = count - primitiveCount;
  176. // Final plan only has primitive tasks added to it so we can just remove the count we've tracked since the last decomp.
  177. finalPlan.RemoveRange(reduction, primitiveCount);
  178. appliedStates.RemoveRange(reduction, primitiveCount);
  179. primitiveCount = lastDecomp.PrimitiveCount;
  180. blackboard = lastDecomp.Blackboard;
  181. tasksToProcess.Enqueue(lastDecomp.CompoundTask);
  182. }
  183. /// <summary>
  184. /// Stores the state of an HTN Plan while planning it. This is so we can rollback if a particular branch is unsuitable.
  185. /// </summary>
  186. private sealed class DecompositionState
  187. {
  188. /// <summary>
  189. /// Blackboard as at decomposition.
  190. /// </summary>
  191. public NPCBlackboard Blackboard = default!;
  192. /// <summary>
  193. /// How many primitive tasks we've added since last decompositionstate.
  194. /// </summary>
  195. public int PrimitiveCount;
  196. /// <summary>
  197. /// The task that owns this decomposition.
  198. /// </summary>
  199. public HTNCompoundTask CompoundTask = default!;
  200. // This may not be necessary for planning but may be useful for debugging so I didn't remove it.
  201. /// <summary>
  202. /// Which branch (AKA method) we took of the compound task. Whenever we rollback the decomposition state
  203. /// this gets incremented by 1 so we check the next method.
  204. /// </summary>
  205. public int BranchTraversal;
  206. }
  207. }