2022-09-06 00:28:23 +10:00
using System.Linq ;
using System.Text ;
using System.Threading ;
using Content.Server.Administration.Managers ;
2023-05-15 14:47:12 +12:00
using Robust.Shared.CPUJob.JobQueues ;
using Robust.Shared.CPUJob.JobQueues.Queues ;
2022-09-06 00:28:23 +10:00
using Content.Server.NPC.HTN.PrimitiveTasks ;
using Content.Server.NPC.Systems ;
using Content.Shared.Administration ;
2023-08-25 17:05:21 +10:00
using Content.Shared.Mobs ;
2022-09-06 00:28:23 +10:00
using Content.Shared.NPC ;
using JetBrains.Annotations ;
2023-10-28 09:59:53 +11:00
using Robust.Shared.Player ;
2022-09-06 00:28:23 +10:00
using Robust.Shared.Prototypes ;
2023-08-02 10:48:56 +10:00
using Robust.Shared.Utility ;
2022-09-06 00:28:23 +10:00
namespace Content.Server.NPC.HTN ;
public sealed class HTNSystem : EntitySystem
{
[Dependency] private readonly IAdminManager _admin = default ! ;
[Dependency] private readonly IPrototypeManager _prototypeManager = default ! ;
[Dependency] private readonly NPCSystem _npc = default ! ;
2023-05-02 04:57:11 +10:00
[Dependency] private readonly NPCUtilitySystem _utility = default ! ;
2022-09-06 00:28:23 +10:00
2023-05-02 04:57:11 +10:00
private readonly JobQueue _planQueue = new ( 0.004 ) ;
2022-09-06 00:28:23 +10:00
private readonly HashSet < ICommonSession > _subscribers = new ( ) ;
// Hierarchical Task Network
public override void Initialize ( )
{
base . Initialize ( ) ;
2023-08-25 17:05:21 +10:00
SubscribeLocalEvent < HTNComponent , MobStateChangedEvent > ( _npc . OnMobStateChange ) ;
SubscribeLocalEvent < HTNComponent , MapInitEvent > ( _npc . OnNPCMapInit ) ;
SubscribeLocalEvent < HTNComponent , PlayerAttachedEvent > ( _npc . OnPlayerNPCAttach ) ;
SubscribeLocalEvent < HTNComponent , PlayerDetachedEvent > ( _npc . OnPlayerNPCDetach ) ;
2022-09-06 00:28:23 +10:00
SubscribeLocalEvent < HTNComponent , ComponentShutdown > ( OnHTNShutdown ) ;
SubscribeNetworkEvent < RequestHTNMessage > ( OnHTNMessage ) ;
2023-12-22 09:13:45 -05:00
SubscribeLocalEvent < PrototypesReloadedEventArgs > ( OnPrototypeLoad ) ;
2022-09-06 00:28:23 +10:00
OnLoad ( ) ;
}
private void OnHTNMessage ( RequestHTNMessage msg , EntitySessionEventArgs args )
{
2023-10-28 09:59:53 +11:00
if ( ! _admin . HasAdminFlag ( args . SenderSession , AdminFlags . Debug ) )
2022-09-06 00:28:23 +10:00
{
_subscribers . Remove ( args . SenderSession ) ;
return ;
}
if ( _subscribers . Add ( args . SenderSession ) )
return ;
_subscribers . Remove ( args . SenderSession ) ;
}
private void OnLoad ( )
{
2022-12-24 12:37:58 +11:00
// Clear all NPCs in case they're hanging onto stale tasks
2023-08-02 10:48:56 +10:00
var query = AllEntityQuery < HTNComponent > ( ) ;
while ( query . MoveNext ( out var comp ) )
2022-12-24 12:37:58 +11:00
{
comp . PlanningToken ? . Cancel ( ) ;
comp . PlanningToken = null ;
if ( comp . Plan ! = null )
{
var currentOperator = comp . Plan . CurrentOperator ;
2023-08-02 10:48:56 +10:00
ShutdownTask ( currentOperator , comp . Blackboard , HTNOperatorStatus . Failed ) ;
ShutdownPlan ( comp ) ;
2022-12-24 12:37:58 +11:00
comp . Plan = null ;
2023-08-02 10:48:56 +10:00
RequestPlan ( comp ) ;
2022-12-24 12:37:58 +11:00
}
}
2022-09-06 00:28:23 +10:00
// Add dependencies for all operators.
// We put code on operators as I couldn't think of a clean way to put it on systems.
2023-08-02 10:48:56 +10:00
foreach ( var compound in _prototypeManager . EnumeratePrototypes < HTNCompoundPrototype > ( ) )
2022-09-06 00:28:23 +10:00
{
UpdateCompound ( compound ) ;
}
}
private void OnPrototypeLoad ( PrototypesReloadedEventArgs obj )
{
2023-08-02 10:48:56 +10:00
OnLoad ( ) ;
2022-09-06 00:28:23 +10:00
}
2023-08-02 10:48:56 +10:00
private void UpdateCompound ( HTNCompoundPrototype compound )
2022-09-06 00:28:23 +10:00
{
2023-02-08 08:27:34 +11:00
for ( var i = 0 ; i < compound . Branches . Count ; i + + )
2022-09-06 00:28:23 +10:00
{
2023-02-08 08:27:34 +11:00
var branch = compound . Branches [ i ] ;
2022-09-06 00:28:23 +10:00
2023-08-02 10:48:56 +10:00
foreach ( var precon in branch . Preconditions )
2022-09-06 00:28:23 +10:00
{
2023-08-02 10:48:56 +10:00
precon . Initialize ( EntityManager . EntitySysManager ) ;
2022-09-06 00:28:23 +10:00
}
2023-08-02 10:48:56 +10:00
foreach ( var task in branch . Tasks )
2022-09-06 00:28:23 +10:00
{
2023-08-02 10:48:56 +10:00
UpdateTask ( task ) ;
2022-09-06 00:28:23 +10:00
}
}
}
2023-08-02 10:48:56 +10:00
private void UpdateTask ( HTNTask task )
{
switch ( task )
{
case HTNCompoundTask :
// NOOP, handled elsewhere
break ;
case HTNPrimitiveTask primitive :
foreach ( var precon in primitive . Preconditions )
{
precon . Initialize ( EntityManager . EntitySysManager ) ;
}
primitive . Operator . Initialize ( EntityManager . EntitySysManager ) ;
break ;
default :
throw new NotImplementedException ( ) ;
}
}
2022-09-06 00:28:23 +10:00
private void OnHTNShutdown ( EntityUid uid , HTNComponent component , ComponentShutdown args )
{
2023-08-25 17:05:21 +10:00
_npc . OnNPCShutdown ( uid , component , args ) ;
2022-09-06 00:28:23 +10:00
component . PlanningToken ? . Cancel ( ) ;
component . PlanningJob = null ;
}
2025-03-01 11:42:33 -06:00
/// <summary>
/// Enable / disable the hierarchical task network of an entity
/// </summary>
/// <param name="ent">The entity and its <see cref="HTNComponent"/></param>
/// <param name="state">Set 'true' to enable, or 'false' to disable, the HTN</param>
/// <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>
// ReSharper disable once InconsistentNaming
[PublicAPI]
public void SetHTNEnabled ( Entity < HTNComponent > ent , bool state , float planCooldown = 0f )
{
if ( ent . Comp . Enabled = = state )
return ;
ent . Comp . Enabled = state ;
ent . Comp . PlanAccumulator = planCooldown ;
ent . Comp . PlanningToken ? . Cancel ( ) ;
ent . Comp . PlanningToken = null ;
if ( ent . Comp . Plan ! = null )
{
var currentOperator = ent . Comp . Plan . CurrentOperator ;
ShutdownTask ( currentOperator , ent . Comp . Blackboard , HTNOperatorStatus . Failed ) ;
ShutdownPlan ( ent . Comp ) ;
ent . Comp . Plan = null ;
}
if ( ent . Comp . Enabled & & ent . Comp . PlanAccumulator < = 0 )
RequestPlan ( ent . Comp ) ;
}
2022-09-06 00:28:23 +10:00
/// <summary>
/// Forces the NPC to replan.
/// </summary>
[PublicAPI]
public void Replan ( HTNComponent component )
{
component . PlanAccumulator = 0f ;
}
public void UpdateNPC ( ref int count , int maxUpdates , float frameTime )
{
_planQueue . Process ( ) ;
2023-04-20 10:43:13 +10:00
var query = EntityQueryEnumerator < ActiveNPCComponent , HTNComponent > ( ) ;
2022-09-06 00:28:23 +10:00
2025-04-29 20:51:41 -04:00
// Move ahead "count" entries in the query.
// This is to ensure that if we didn't process all the npcs the first time,
// we get to the remaining ones instead of iterating over the beginning again.
for ( var i = 0 ; i < count ; i + + )
{
query . MoveNext ( out _ , out _ ) ;
}
// the amount of updates we've processed during this iteration.
var updates = 0 ;
2025-03-01 11:42:33 -06:00
while ( query . MoveNext ( out var uid , out _ , out var comp ) )
2022-09-06 00:28:23 +10:00
{
// If we're over our max count or it's not MapInit then ignore the NPC.
2025-04-29 20:51:41 -04:00
if ( updates > = maxUpdates )
{
// Intentional return. We don't want to go to the end logic and reset count.
return ;
}
2022-09-06 00:28:23 +10:00
2025-03-01 11:42:33 -06:00
if ( ! comp . Enabled )
continue ;
2022-09-06 00:28:23 +10:00
if ( comp . PlanningJob ! = null )
{
if ( comp . PlanningJob . Exception ! = null )
{
2023-08-02 10:48:56 +10:00
Log . Fatal ( $"Received exception on planning job for {uid}!" ) ;
2023-04-20 10:43:13 +10:00
_npc . SleepNPC ( uid ) ;
2022-09-30 14:39:48 +10:00
var exc = comp . PlanningJob . Exception ;
2023-04-20 10:43:13 +10:00
RemComp < HTNComponent > ( uid ) ;
2022-09-30 14:39:48 +10:00
throw exc ;
2022-09-06 00:28:23 +10:00
}
// If a new planning job has finished then handle it.
if ( comp . PlanningJob . Status ! = JobStatus . Finished )
continue ;
var newPlanBetter = false ;
// If old traversal is better than new traversal then ignore the new plan
if ( comp . Plan ! = null & & comp . PlanningJob . Result ! = null )
{
var oldMtr = comp . Plan . BranchTraversalRecord ;
var mtr = comp . PlanningJob . Result . BranchTraversalRecord ;
for ( var i = 0 ; i < oldMtr . Count ; i + + )
{
if ( i < mtr . Count & & oldMtr [ i ] > mtr [ i ] )
{
newPlanBetter = true ;
break ;
}
}
}
if ( comp . Plan = = null | | newPlanBetter )
{
2023-05-15 16:18:18 +10:00
comp . CheckServices = false ;
2023-08-02 10:48:56 +10:00
if ( comp . Plan ! = null )
{
ShutdownTask ( comp . Plan . CurrentOperator , comp . Blackboard , HTNOperatorStatus . BetterPlan ) ;
ShutdownPlan ( comp ) ;
}
2022-09-06 00:28:23 +10:00
comp . Plan = comp . PlanningJob . Result ;
// Startup the first task and anything else we need to do.
if ( comp . Plan ! = null )
{
StartupTask ( comp . Plan . Tasks [ comp . Plan . Index ] , comp . Blackboard , comp . Plan . Effects [ comp . Plan . Index ] ) ;
}
// Send debug info
foreach ( var session in _subscribers )
{
var text = new StringBuilder ( ) ;
if ( comp . Plan ! = null )
{
text . AppendLine ( $"BTR: {string.Join(" , ", comp.Plan.BranchTraversalRecord)}" ) ;
text . AppendLine ( $"tasks:" ) ;
2023-08-02 10:48:56 +10:00
var root = comp . RootTask ;
2023-04-29 16:47:10 +10:00
var btr = new List < int > ( ) ;
var level = - 1 ;
AppendDebugText ( root , text , comp . Plan . BranchTraversalRecord , btr , ref level ) ;
2022-09-06 00:28:23 +10:00
}
RaiseNetworkEvent ( new HTNMessage ( )
{
2023-09-11 09:42:41 +10:00
Uid = GetNetEntity ( uid ) ,
2022-09-06 00:28:23 +10:00
Text = text . ToString ( ) ,
2024-01-22 23:14:13 +01:00
} , session . Channel ) ;
2022-09-06 00:28:23 +10:00
}
}
2023-05-15 16:18:18 +10:00
// Keeping old plan
else
{
comp . CheckServices = true ;
}
2022-09-06 00:28:23 +10:00
comp . PlanningJob = null ;
comp . PlanningToken = null ;
}
Update ( comp , frameTime ) ;
count + + ;
2025-04-29 20:51:41 -04:00
updates + + ;
2022-09-06 00:28:23 +10:00
}
2025-04-29 20:51:41 -04:00
// only reset our counter back to 0 if we finish iterating.
// otherwise it lets us know where we left off.
count = 0 ;
2022-09-06 00:28:23 +10:00
}
2023-04-29 16:47:10 +10:00
private void AppendDebugText ( HTNTask task , StringBuilder text , List < int > planBtr , List < int > btr , ref int level )
{
// If it's the selected BTR then highlight.
for ( var i = 0 ; i < btr . Count ; i + + )
{
2023-05-02 04:57:11 +10:00
text . Append ( "--" ) ;
2023-04-29 16:47:10 +10:00
}
text . Append ( ' ' ) ;
if ( task is HTNPrimitiveTask primitive )
{
2023-08-02 10:48:56 +10:00
text . AppendLine ( primitive . ToString ( ) ) ;
2023-04-29 16:47:10 +10:00
return ;
}
2023-08-02 10:48:56 +10:00
if ( task is HTNCompoundTask compTask )
2023-04-29 16:47:10 +10:00
{
2023-08-02 10:48:56 +10:00
var compound = _prototypeManager . Index < HTNCompoundPrototype > ( compTask . Task ) ;
2023-04-29 16:47:10 +10:00
level + + ;
text . AppendLine ( compound . ID ) ;
2023-08-02 10:48:56 +10:00
var branches = compound . Branches ;
2023-04-29 16:47:10 +10:00
2023-08-02 10:48:56 +10:00
for ( var i = 0 ; i < branches . Count ; i + + )
2023-04-29 16:47:10 +10:00
{
var branch = branches [ i ] ;
btr . Add ( i ) ;
2023-05-02 04:57:11 +10:00
text . AppendLine ( $" branch {string.Join(" , ", btr)}:" ) ;
2023-04-29 16:47:10 +10:00
2023-08-02 10:48:56 +10:00
foreach ( var sub in branch . Tasks )
2023-04-29 16:47:10 +10:00
{
AppendDebugText ( sub , text , planBtr , btr , ref level ) ;
}
btr . RemoveAt ( btr . Count - 1 ) ;
}
level - - ;
return ;
}
throw new NotImplementedException ( ) ;
}
2022-09-06 00:28:23 +10:00
private void Update ( HTNComponent component , float frameTime )
{
// If we're not planning then countdown to next one.
if ( component . PlanningJob = = null )
component . PlanAccumulator - = frameTime ;
// We'll still try re-planning occasionally even when we're updating in case new data comes in.
2025-04-18 11:16:26 +03:00
if ( ( component . ConstantlyReplan | | component . Plan is null ) & & component . PlanAccumulator < = 0f )
2022-09-06 00:28:23 +10:00
{
RequestPlan ( component ) ;
}
// Getting a new plan so do nothing.
if ( component . Plan = = null )
return ;
// Run the existing plan still
var status = HTNOperatorStatus . Finished ;
// Continuously run operators until we can't anymore.
while ( status ! = HTNOperatorStatus . Continuing & & component . Plan ! = null )
{
// Run the existing operator
var currentOperator = component . Plan . CurrentOperator ;
2023-05-02 04:57:11 +10:00
var currentTask = component . Plan . CurrentTask ;
2022-09-06 00:28:23 +10:00
var blackboard = component . Blackboard ;
2023-05-02 04:57:11 +10:00
2023-05-15 16:18:18 +10:00
// Service still on cooldown.
if ( component . CheckServices )
2023-05-02 04:57:11 +10:00
{
2023-05-15 16:18:18 +10:00
foreach ( var service in currentTask . Services )
2023-05-02 04:57:11 +10:00
{
2023-05-15 16:18:18 +10:00
var serviceResult = _utility . GetEntities ( blackboard , service . Prototype ) ;
blackboard . SetValue ( service . Key , serviceResult . GetHighest ( ) ) ;
2023-05-02 04:57:11 +10:00
}
2023-05-15 16:18:18 +10:00
component . CheckServices = false ;
2023-05-02 04:57:11 +10:00
}
2022-09-06 00:28:23 +10:00
status = currentOperator . Update ( blackboard , frameTime ) ;
switch ( status )
{
case HTNOperatorStatus . Continuing :
break ;
case HTNOperatorStatus . Failed :
2023-08-02 10:48:56 +10:00
ShutdownTask ( currentOperator , blackboard , status ) ;
ShutdownPlan ( component ) ;
2022-09-06 00:28:23 +10:00
break ;
// Operator completed so go to the next one.
case HTNOperatorStatus . Finished :
2023-08-02 10:48:56 +10:00
ShutdownTask ( currentOperator , blackboard , status ) ;
2022-09-06 00:28:23 +10:00
component . Plan . Index + + ;
// Plan finished!
if ( component . Plan . Tasks . Count < = component . Plan . Index )
{
2023-08-02 10:48:56 +10:00
ShutdownPlan ( component ) ;
2022-09-06 00:28:23 +10:00
break ;
}
2023-08-02 10:48:56 +10:00
ConditionalShutdown ( component . Plan , currentOperator , blackboard , HTNPlanState . TaskFinished ) ;
2022-09-06 00:28:23 +10:00
StartupTask ( component . Plan . Tasks [ component . Plan . Index ] , component . Blackboard , component . Plan . Effects [ component . Plan . Index ] ) ;
break ;
default :
throw new InvalidOperationException ( ) ;
}
}
}
2023-08-02 10:48:56 +10:00
public void ShutdownTask ( HTNOperator currentOperator , NPCBlackboard blackboard , HTNOperatorStatus status )
{
if ( currentOperator is IHtnConditionalShutdown conditional & &
( conditional . ShutdownState & HTNPlanState . TaskFinished ) ! = 0x0 )
{
conditional . ConditionalShutdown ( blackboard ) ;
}
currentOperator . TaskShutdown ( blackboard , status ) ;
}
public void ShutdownPlan ( HTNComponent component )
{
DebugTools . Assert ( component . Plan ! = null ) ;
var blackboard = component . Blackboard ;
foreach ( var task in component . Plan . Tasks )
{
if ( task . Operator is IHtnConditionalShutdown conditional & &
( conditional . ShutdownState & HTNPlanState . PlanFinished ) ! = 0x0 )
{
conditional . ConditionalShutdown ( blackboard ) ;
}
task . Operator . PlanShutdown ( component . Blackboard ) ;
}
component . Plan = null ;
}
/// <summary>
/// Shuts down the current operator conditionally.
/// </summary>
private void ConditionalShutdown ( HTNPlan plan , HTNOperator currentOperator , NPCBlackboard blackboard , HTNPlanState state )
{
if ( currentOperator is not IHtnConditionalShutdown conditional )
return ;
if ( ( conditional . ShutdownState & state ) = = 0x0 )
return ;
conditional . ConditionalShutdown ( blackboard ) ;
}
2022-09-06 00:28:23 +10:00
/// <summary>
/// Starts a new primitive task. Will apply effects from planning if applicable.
/// </summary>
private void StartupTask ( HTNPrimitiveTask primitive , NPCBlackboard blackboard , Dictionary < string , object > ? effects )
{
// We may have planner only tasks where we want to reuse their data during update
// e.g. if we pathfind to an enemy to know if we can attack it, we don't want to do another pathfind immediately
if ( effects ! = null & & primitive . ApplyEffectsOnStartup )
{
foreach ( var ( key , value ) in effects )
{
blackboard . SetValue ( key , value ) ;
}
}
primitive . Operator . Startup ( blackboard ) ;
}
/// <summary>
/// Request a new plan for this component, even if running an existing plan.
/// </summary>
/// <param name="component"></param>
private void RequestPlan ( HTNComponent component )
{
if ( component . PlanningJob ! = null )
return ;
2025-04-18 11:16:26 +03:00
component . PlanAccumulator = component . PlanCooldown ;
2022-09-06 00:28:23 +10:00
var cancelToken = new CancellationTokenSource ( ) ;
var branchTraversal = component . Plan ? . BranchTraversalRecord ;
var job = new HTNPlanJob (
0.02 ,
2023-08-02 10:48:56 +10:00
_prototypeManager ,
component . RootTask ,
2022-09-06 00:28:23 +10:00
component . Blackboard . ShallowClone ( ) , branchTraversal , cancelToken . Token ) ;
_planQueue . EnqueueJob ( job ) ;
component . PlanningJob = job ;
component . PlanningToken = cancelToken ;
}
public string GetDomain ( HTNCompoundTask compound )
{
// TODO: Recursively add each one
var indent = 0 ;
var builder = new StringBuilder ( ) ;
AppendDomain ( builder , compound , ref indent ) ;
return builder . ToString ( ) ;
}
private void AppendDomain ( StringBuilder builder , HTNTask task , ref int indent )
{
var buffer = string . Concat ( Enumerable . Repeat ( " " , indent ) ) ;
if ( task is HTNPrimitiveTask primitive )
{
2023-08-02 10:48:56 +10:00
builder . AppendLine ( buffer + $"Primitive: {task}" ) ;
2022-09-06 00:28:23 +10:00
builder . AppendLine ( buffer + $" operator: {primitive.Operator.GetType().Name}" ) ;
}
2023-08-02 10:48:56 +10:00
else if ( task is HTNCompoundTask compTask )
2022-09-06 00:28:23 +10:00
{
2023-08-02 10:48:56 +10:00
var compound = _prototypeManager . Index < HTNCompoundPrototype > ( compTask . Task ) ;
builder . AppendLine ( buffer + $"Compound: {task}" ) ;
2022-09-06 00:28:23 +10:00
2023-02-08 08:27:34 +11:00
for ( var i = 0 ; i < compound . Branches . Count ; i + + )
2022-09-06 00:28:23 +10:00
{
2023-02-08 08:27:34 +11:00
var branch = compound . Branches [ i ] ;
2022-09-06 00:28:23 +10:00
builder . AppendLine ( buffer + " branch:" ) ;
indent + + ;
2023-08-02 10:48:56 +10:00
foreach ( var branchTask in branch . Tasks )
2022-09-06 00:28:23 +10:00
{
AppendDomain ( builder , branchTask , ref indent ) ;
}
indent - - ;
}
}
}
}
/// <summary>
/// The outcome of the current operator during update.
/// </summary>
public enum HTNOperatorStatus : byte
{
Continuing ,
Failed ,
Finished ,
/// <summary>
/// Was a better plan than this found?
/// </summary>
BetterPlan ,
}