CatStation/Content.Server/NPC/HTN/HTNPlanJob.cs
Dmitry 25c5e59248
Upstream (#3172)
Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: Samuka-C <47865393+Samuka-C@users.noreply.github.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Co-authored-by: Partmedia <kevinz5000@gmail.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: themias <89101928+themias@users.noreply.github.com>
Co-authored-by: Victor Shen <71985089+Vexerot@users.noreply.github.com>
Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com>
Co-authored-by: Milon <milonpl.git@proton.me>
Co-authored-by: Kirus59 <145689588+Kirus59@users.noreply.github.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: Stomf <5dorkydorks@gmail.com>
Co-authored-by: drakewill-CRL <46307022+drakewill-CRL@users.noreply.github.com>
Co-authored-by: PraxisMapper <praxismapper@gmail.com>
Co-authored-by: EmoGarbage404 <retron404@gmail.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: IProduceWidgets <107586145+IProduceWidgets@users.noreply.github.com>
Co-authored-by: TytosB <54259736+TytosB@users.noreply.github.com>
Co-authored-by: abadaba695 <spacestation13thingy@gmail.com>
Co-authored-by: kosticia <kosticia46@gmail.com>
Co-authored-by: Thinbug <101073555+Thinbug0@users.noreply.github.com>
Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com>
Co-authored-by: ActiveMammmoth <140334666+ActiveMammmoth@users.noreply.github.com>
Co-authored-by: Myra <vasilis@pikachu.systems>
Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: K-Dynamic <20566341+K-Dynamic@users.noreply.github.com>
Co-authored-by: Gentleman-Bird <dcgreen406@gmail.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: BIGZi0348 <svalker0348@gmail.com>
Co-authored-by: LaCumbiaDelCoronavirus <90893484+LaCumbiaDelCoronavirus@users.noreply.github.com>
Co-authored-by: imatsoup <93290208+imatsoup@users.noreply.github.com>
Co-authored-by: Matthew Herber <32679887+happyrobot33@users.noreply.github.com>
Co-authored-by: Ertanic <36124833+Ertanic@users.noreply.github.com>
Co-authored-by: MissKay1994 <15877268+MissKay1994@users.noreply.github.com>
Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com>
Co-authored-by: eoineoineoin <helloworld@eoinrul.es>
Co-authored-by: Tiniest Shark <head.rebel@yahoo.com>
Co-authored-by: nikitosych <boriszyn@gmail.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: Perry Fraser <perryprog@users.noreply.github.com>
Co-authored-by: YoungThug <ramialanbagy@gmail.com>
Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com>
Co-authored-by: Vladislav Suchkov <20380250+murolem@users.noreply.github.com>
Co-authored-by: Prole <172158352+Prole0@users.noreply.github.com>
Co-authored-by: Unkn0wn_Gh0st <shadowstalkermll@gmail.com>
Co-authored-by: 3nderall <101940324+3nderall@users.noreply.github.com>
Co-authored-by: Radezolid <snappednexus@gmail.com>
Co-authored-by: J <billsmith116@gmail.com>
Co-authored-by: Ghagliiarghii <68826635+Ghagliiarghii@users.noreply.github.com>
Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>
Co-authored-by: youtissoum <51883137+youtissoum@users.noreply.github.com>
Co-authored-by: Minemoder5000 <minemoder50000@gmail.com>
Co-authored-by: Spanky <scott@wearejacob.com>
Co-authored-by: Spessmann <156740760+Spessmann@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: brainfood1183 <113240905+brainfood1183@users.noreply.github.com>
Co-authored-by: Deerstop <edainturner@gmail.com>
Co-authored-by: B_Kirill <153602297+B-Kirill@users.noreply.github.com>
Co-authored-by: archee1 <archee3@hotmail.co.uk>
Co-authored-by: Cojoke <83733158+Cojoke-dot@users.noreply.github.com>
Co-authored-by: Quantum-cross <7065792+Quantum-cross@users.noreply.github.com>
Co-authored-by: poklj <compgeek223@gmail.com>
Co-authored-by: Krunklehorn <42424291+Krunklehorn@users.noreply.github.com>
Co-authored-by: OnyxTheBrave <131422822+OnyxTheBrave@users.noreply.github.com>
Co-authored-by: UpAndLeaves <92269094+Alpha-Two@users.noreply.github.com>
Co-authored-by: Flareguy <78941145+Flareguy@users.noreply.github.com>
Co-authored-by: Zalycon <84675130+Zalycon@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: Verm <32827189+Vermidia@users.noreply.github.com>
Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com>
Co-authored-by: ScarKy0 <scarky0@onet.eu>
Co-authored-by: Dmitry <57028746+dimm00n@users.noreply.github.com>
2025-05-24 11:00:17 +03:00

256 lines
8.7 KiB
C#

using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Robust.Shared.CPUJob.JobQueues;
using Content.Server.NPC.HTN.PrimitiveTasks;
using Robust.Shared.Prototypes;
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 HTNTask _rootTask;
private NPCBlackboard _blackboard;
private IPrototypeManager _protoManager;
/// <summary>
/// Branch traversal of an existing plan (if applicable).
/// </summary>
private List<int>? _branchTraversal;
public HTNPlanJob(
double maxTime,
IPrototypeManager protoManager,
HTNTask rootTask,
NPCBlackboard blackboard,
List<int>? branchTraversal,
CancellationToken cancellationToken = default) : base(maxTime, cancellationToken)
{
_protoManager = protoManager;
_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;
// 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 Stack<HTNTask>();
var finalPlan = new List<HTNPrimitiveTask>();
tasksToProcess.Push(_rootTask);
// How many primitive tasks we've added since last record.
var primitiveCount = 0;
int tasksProcessed = 0;
while (tasksToProcess.TryPop(out var currentTask))
{
if (tasksProcessed++ > _rootTask.MaximumTasks)
throw new Exception("HTN Planner exceeded maximum tasks");
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,
});
// 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);
}
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);
}
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, Cancellation);
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 compoundId, Stack<HTNTask> tasksToProcess, NPCBlackboard blackboard, ref int mtrIndex)
{
var compound = _protoManager.Index<HTNCompoundPrototype>(compoundId.Task);
for (; mtrIndex < compound.Branches.Count; mtrIndex++)
{
var branch = compound.Branches[mtrIndex];
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.AsEnumerable().Reverse())
{
tasksToProcess.Push(task);
}
return true;
}
return false;
}
/// <summary>
/// Restores the planner state.
/// </summary>
private void RestoreTolastDecomposedTask(
Stack<DecompositionState> decompHistory,
Stack<HTNTask> tasksToProcess,
List<Dictionary<string, object>?> appliedStates,
List<HTNPrimitiveTask> finalPlan,
ref int primitiveCount,
ref NPCBlackboard blackboard,
ref int mtrIndex)
{
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;
var reduction = count - primitiveCount;
// 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(reduction, primitiveCount);
appliedStates.RemoveRange(reduction, primitiveCount);
primitiveCount = lastDecomp.PrimitiveCount;
blackboard = lastDecomp.Blackboard;
tasksToProcess.Push(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 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;
}
}