NPC refactor (#10122)
Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
This commit is contained in:
248
Content.Server/NPC/HTN/HTNPlanJob.cs
Normal file
248
Content.Server/NPC/HTN/HTNPlanJob.cs
Normal file
@@ -0,0 +1,248 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.CPUJob.JobQueues;
|
||||
using Content.Server.NPC.HTN.PrimitiveTasks;
|
||||
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
/// <summary>
|
||||
/// A time-sliced job that will retrieve an HTN plan eventually.
|
||||
/// </summary>
|
||||
public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
{
|
||||
private readonly HTNCompoundTask _rootTask;
|
||||
private NPCBlackboard _blackboard;
|
||||
|
||||
/// <summary>
|
||||
/// Branch traversal of an existing plan (if applicable).
|
||||
/// </summary>
|
||||
private List<int>? _branchTraversal;
|
||||
|
||||
public HTNPlanJob(
|
||||
double maxTime,
|
||||
HTNCompoundTask rootTask,
|
||||
NPCBlackboard blackboard,
|
||||
List<int>? branchTraversal,
|
||||
CancellationToken cancellationToken = default) : base(maxTime, cancellationToken)
|
||||
{
|
||||
_rootTask = rootTask;
|
||||
_blackboard = blackboard;
|
||||
_branchTraversal = branchTraversal;
|
||||
}
|
||||
|
||||
protected override async Task<HTNPlan?> Process()
|
||||
{
|
||||
/*
|
||||
* Really the best reference for what a HTN looks like is http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter12_Exploring_HTN_Planners_through_Example.pdf
|
||||
* It's kinda like a behaviour tree but also can consider multiple actions in sequence.
|
||||
*
|
||||
* Methods have been renamed to branches
|
||||
*/
|
||||
|
||||
var decompHistory = new Stack<DecompositionState>();
|
||||
|
||||
// branch traversal record. Whenever we find a new compound task this updates.
|
||||
var btrIndex = 0;
|
||||
var btr = new List<int>();
|
||||
|
||||
// For some tasks we may do something expensive or want to re-use the planning result.
|
||||
// e.g. pathfind to a target before deciding to attack it.
|
||||
// Given all of the primitive tasks are singletons we need to store the data somewhere
|
||||
// hence we'll store it here.
|
||||
var appliedStates = new List<Dictionary<string, object>?>();
|
||||
|
||||
var tasksToProcess = new Queue<HTNTask>();
|
||||
var finalPlan = new List<HTNPrimitiveTask>();
|
||||
tasksToProcess.Enqueue(_rootTask);
|
||||
|
||||
// How many primitive tasks we've added since last record.
|
||||
var primitiveCount = 0;
|
||||
|
||||
while (tasksToProcess.TryDequeue(out var currentTask))
|
||||
{
|
||||
switch (currentTask)
|
||||
{
|
||||
case HTNCompoundTask compound:
|
||||
await SuspendIfOutOfTime();
|
||||
|
||||
if (TryFindSatisfiedMethod(compound, tasksToProcess, _blackboard, ref btrIndex))
|
||||
{
|
||||
// Need to copy worldstate to roll it back
|
||||
// Don't need to copy taskstoprocess as we can just clear it and set it to the compound task we roll back to.
|
||||
// Don't need to copy finalplan as we can just count how many primitives we've added since last record
|
||||
|
||||
decompHistory.Push(new DecompositionState()
|
||||
{
|
||||
Blackboard = _blackboard.ShallowClone(),
|
||||
CompoundTask = compound,
|
||||
BranchTraversal = btrIndex,
|
||||
PrimitiveCount = primitiveCount,
|
||||
});
|
||||
|
||||
btr.Add(btrIndex);
|
||||
|
||||
// TODO: Early out if existing plan is better and save lots of time.
|
||||
// my brain is not working rn AAA
|
||||
|
||||
primitiveCount = 0;
|
||||
// Reset method traversal
|
||||
btrIndex = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
RestoreTolastDecomposedTask(decompHistory, tasksToProcess, appliedStates, finalPlan, ref primitiveCount, ref _blackboard, ref btrIndex, ref btr);
|
||||
}
|
||||
break;
|
||||
case HTNPrimitiveTask primitive:
|
||||
if (await WaitAsyncTask(PrimitiveConditionMet(primitive, _blackboard, appliedStates)))
|
||||
{
|
||||
primitiveCount++;
|
||||
finalPlan.Add(primitive);
|
||||
}
|
||||
else
|
||||
{
|
||||
RestoreTolastDecomposedTask(decompHistory, tasksToProcess, appliedStates, finalPlan, ref primitiveCount, ref _blackboard, ref btrIndex, ref btr);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (finalPlan.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var branchTraversalRecord = decompHistory.Reverse().Select(o => o.BranchTraversal).ToList();
|
||||
|
||||
return new HTNPlan(finalPlan, branchTraversalRecord, appliedStates);
|
||||
}
|
||||
|
||||
private async Task<bool> PrimitiveConditionMet(HTNPrimitiveTask primitive, NPCBlackboard blackboard, List<Dictionary<string, object>?> appliedStates)
|
||||
{
|
||||
blackboard.ReadOnly = true;
|
||||
|
||||
foreach (var con in primitive.Preconditions)
|
||||
{
|
||||
if (con.IsMet(blackboard))
|
||||
continue;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var (valid, effects) = await primitive.Operator.Plan(blackboard);
|
||||
|
||||
if (!valid)
|
||||
return false;
|
||||
|
||||
blackboard.ReadOnly = false;
|
||||
|
||||
if (effects != null)
|
||||
{
|
||||
foreach (var (key, value) in effects)
|
||||
{
|
||||
blackboard.SetValue(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
appliedStates.Add(effects);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Goes through each compound task branch and tries to find an appropriate one.
|
||||
/// </summary>
|
||||
private bool TryFindSatisfiedMethod(HTNCompoundTask compound, Queue<HTNTask> tasksToProcess, NPCBlackboard blackboard, ref int mtrIndex)
|
||||
{
|
||||
for (var i = mtrIndex; i < compound.Branches.Count; i++)
|
||||
{
|
||||
var branch = compound.Branches[i];
|
||||
var isValid = true;
|
||||
|
||||
foreach (var con in branch.Preconditions)
|
||||
{
|
||||
if (con.IsMet(blackboard))
|
||||
continue;
|
||||
|
||||
isValid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isValid)
|
||||
continue;
|
||||
|
||||
foreach (var task in branch.Tasks)
|
||||
{
|
||||
tasksToProcess.Enqueue(task);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the planner state.
|
||||
/// </summary>
|
||||
private void RestoreTolastDecomposedTask(
|
||||
Stack<DecompositionState> decompHistory,
|
||||
Queue<HTNTask> tasksToProcess,
|
||||
List<Dictionary<string, object>?> appliedStates,
|
||||
List<HTNPrimitiveTask> finalPlan,
|
||||
ref int primitiveCount,
|
||||
ref NPCBlackboard blackboard,
|
||||
ref int mtrIndex,
|
||||
ref List<int> btr)
|
||||
{
|
||||
tasksToProcess.Clear();
|
||||
|
||||
// No plan found so this will just break normally.
|
||||
if (!decompHistory.TryPop(out var lastDecomp))
|
||||
return;
|
||||
|
||||
// Increment MTR so next time we try the next method on the compound task.
|
||||
mtrIndex = lastDecomp.BranchTraversal + 1;
|
||||
|
||||
var count = finalPlan.Count;
|
||||
|
||||
// Final plan only has primitive tasks added to it so we can just remove the count we've tracked since the last decomp.
|
||||
finalPlan.RemoveRange(count - primitiveCount, primitiveCount);
|
||||
appliedStates.RemoveRange(count - primitiveCount, primitiveCount);
|
||||
btr.RemoveRange(count - primitiveCount, primitiveCount);
|
||||
|
||||
primitiveCount = lastDecomp.PrimitiveCount;
|
||||
blackboard = lastDecomp.Blackboard;
|
||||
tasksToProcess.Enqueue(lastDecomp.CompoundTask);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores the state of an HTN Plan while planning it. This is so we can rollback if a particular branch is unsuitable.
|
||||
/// </summary>
|
||||
private sealed class DecompositionState
|
||||
{
|
||||
/// <summary>
|
||||
/// Blackboard as at decomposition.
|
||||
/// </summary>
|
||||
public NPCBlackboard Blackboard = default!;
|
||||
|
||||
/// <summary>
|
||||
/// How many primitive tasks we've added since last decompositionstate.
|
||||
/// </summary>
|
||||
public int PrimitiveCount;
|
||||
|
||||
/// <summary>
|
||||
/// The compound task that owns this decomposition.
|
||||
/// </summary>
|
||||
public HTNCompoundTask CompoundTask = default!;
|
||||
|
||||
// This may not be necessary for planning but may be useful for debugging so I didn't remove it.
|
||||
/// <summary>
|
||||
/// Which branch (AKA method) we took of the compound task. Whenever we rollback the decomposition state
|
||||
/// this gets incremented by 1 so we check the next method.
|
||||
/// </summary>
|
||||
public int BranchTraversal;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user