virtual ActionResult<CTFBot> Update(CTFBot *actor, float dt) override
		{
			TRACE("[this: %08x] [actor: #%d]", (uintptr_t)this, ENTINDEX(actor));
			
			if (this->m_hHint == nullptr) {
				return ActionResult<CTFBot>::Done("No hint entity");
			}
			
			INextBot *nextbot = rtti_cast<INextBot *>(actor);
			
			if (nextbot->IsRangeGreaterThan(this->m_hHint->GetAbsOrigin(), 25.0f)) {
				TRACE_MSG("range_to_hint > 25: pathing\n");
				
				if (this->m_ctRecomputePath.IsElapsed()) {
					TRACE_MSG("recomputing path\n");
					
					this->m_ctRecomputePath.Start(RandomFloat(1.0f, 2.0f));
					
					CTFBotPathCost cost_func(actor, FASTEST_ROUTE);
					this->m_PathFollower.Compute(nextbot, this->m_hHint->GetAbsOrigin(), cost_func, 0.0f, true);
				}
				
				this->m_PathFollower.Update(nextbot);
				if (!this->m_PathFollower.IsValid()) {
					return ActionResult<CTFBot>::Done("Path failed");
				}
				
				return ActionResult<CTFBot>::Continue();
			}
			
			TRACE_MSG("at hint: creating dispenser entity\n");
			
			CBaseEntity *ent = CreateEntityByName("obj_dispenser");
			if (ent == nullptr) {
				return ActionResult<CTFBot>::Done("Couldn't create entity");
			}
			
			auto dispenser = rtti_cast<CObjectDispenser *>(ent);
			dispenser->SetName(this->m_hHint->GetEntityName());
			dispenser->m_nDefaultUpgradeLevel = 2;
			dispenser->SetAbsOrigin(this->m_hHint->GetAbsOrigin());
			dispenser->SetAbsAngles(this->m_hHint->GetAbsAngles());
			dispenser->Spawn();
			
			dispenser->StartPlacement(actor);
			
			suppress_speak = true;
			dispenser->StartBuilding(actor);
			suppress_speak = false;
			
			this->m_hHint->SetOwnerEntity(dispenser);
			
			actor->SpeakConceptIfAllowed(MP_CONCEPT_BUILDING_OBJECT, "objtype:dispenser");
			
			return ActionResult<CTFBot>::Done("Built a dispenser");
		}
//-----------------------------------------------------------------------------
// Purpose: Start placing or building the currently selected object
//-----------------------------------------------------------------------------
void CTFWeaponBuilder::PrimaryAttack( void )
{
	CTFPlayer *pOwner = ToTFPlayer( GetOwner() );
	if ( !pOwner )
		return;

	if ( !CanAttack() )
		return;

	// Necessary so that we get the latest building position for the test, otherwise
	// we are one frame behind.
	UpdatePlacementState();

	// What state should we move to?
	switch( m_iBuildState )
	{
	case BS_IDLE:
		{
			// Idle state starts selection
			SetCurrentState( BS_SELECTING );
		}
		break;

	case BS_SELECTING:
		{
			// Do nothing, client handles selection
			return;
		}
		break;

	case BS_PLACING:
		{
			if ( m_hObjectBeingBuilt )
			{
				int iFlags = m_hObjectBeingBuilt->GetObjectFlags();

				// Tricky, because this can re-calc the object position and change whether its a valid 
				// pos or not. Best not to do this only in debug, but we can be pretty sure that this
				// will give the same result as was calculated in UpdatePlacementState() above.
				Assert( IsValidPlacement() );

				// If we're placing an attachment, like a sapper, play a placement animation on the owner
				if ( m_hObjectBeingBuilt->MustBeBuiltOnAttachmentPoint() )
				{
					pOwner->DoAnimationEvent( PLAYERANIMEVENT_ATTACK_GRENADE );
				}

				StartBuilding();

				// Should we switch away?
				if ( iFlags & OF_ALLOW_REPEAT_PLACEMENT )
				{
					// Start placing another
					SetCurrentState( BS_PLACING );
					StartPlacement(); 
				}
				else
				{
					SwitchOwnersWeaponToLast();
				}
			}
		}
		break;

	case BS_PLACING_INVALID:
		{
			if ( m_flNextDenySound < gpGlobals->curtime )
			{
				CSingleUserRecipientFilter filter( pOwner );
				EmitSound( filter, entindex(), "Player.DenyWeaponSelection" );

				m_flNextDenySound = gpGlobals->curtime + 0.5;
			}
		}
		break;
	}

	m_flNextPrimaryAttack = gpGlobals->curtime + 0.2f;
}
//-----------------------------------------------------------------------------
// Purpose: Start placing or building the currently selected object
//-----------------------------------------------------------------------------
void CWeaponBuilder::PrimaryAttack( void )
{
	CBaseTFPlayer *pOwner = ToBaseTFPlayer( GetOwner() );
	if ( !pOwner )
		return;

	// What state should we move to?
	switch( m_iBuildState )
	{
	case BS_IDLE:
		{
			// Idle state starts selection
			SetCurrentState( BS_SELECTING );
		}
		break;

	case BS_SELECTING:
		{
			// Do nothing, client handles selection
			return;
		}
		break;

	case BS_PLACING:
		{
			if ( m_hObjectBeingBuilt )
			{
				// Give the object a chance to veto the "start building" command. Objects like barbed wire
				// may want to change their properties instead of actually building yet.
				if ( m_hObjectBeingBuilt->PreStartBuilding() )
				{
					int iFlags = m_hObjectBeingBuilt->GetObjectFlags();

					// Can't build if the game hasn't started
					if ( !tf_fastbuild.GetInt() && CurrentActIsAWaitingAct() )
					{
						ClientPrint( pOwner, HUD_PRINTCENTER, "Can't build until the game's started.\n" );
						return;
					}

					StartBuilding();

					// Should we switch away?
					if ( iFlags & OF_ALLOW_REPEAT_PLACEMENT )
					{
						// Start placing another
						SetCurrentState( BS_PLACING );
						StartPlacement(); 
					}
					else
					{
						pOwner->SwitchToNextBestWeapon( NULL );
					}
				}
			}
		}
		break;

	case BS_PLACING_INVALID:
		{
			WeaponSound( SINGLE_NPC );

			// If there is any associated error text when placing the object, display it
			if( m_hObjectBeingBuilt != NULL )
			{
				if (m_hObjectBeingBuilt->MustBeBuiltInResourceZone())
				{
					ClientPrint( pOwner, HUD_PRINTCENTER, "Only placeable in an empty resource zone.\n" );
				}
				else if (m_hObjectBeingBuilt->MustBeBuiltInConstructionYard())
				{
					ClientPrint( pOwner, HUD_PRINTCENTER, "Only placeable in a construction yard.\n" );
				}
			}
		}
		break;
	}

	m_flNextPrimaryAttack = gpGlobals->curtime + 0.2f;
}
		virtual ActionResult<CTFBot> Update(CTFBot *actor, float dt) override
		{
			TRACE("[this: %08x] [actor: #%d]", (uintptr_t)this, ENTINDEX(actor));
			
			if (this->m_hHint == nullptr) {
				return ActionResult<CTFBot>::Done("No hint entity");
			}
			
			INextBot *nextbot = rtti_cast<INextBot *>(actor);
			
			float range_to_hint = nextbot->GetRangeTo(this->m_hHint->GetAbsOrigin());
			
			if (range_to_hint < 200.0f) {
				TRACE_MSG("range_to_hint < 200: crouching/aiming\n");
				actor->PressCrouchButton();
				actor->GetBodyInterface()->AimHeadTowards(this->m_hHint->GetAbsOrigin(),
					IBody::LookAtPriorityType::OVERRIDE_ALL, 0.1f, nullptr, "Placing dispenser");
				
				if (!this->m_bNearHint) {
					this->m_bNearHint = true;
					//TFGameRules()->VoiceCommand(actor, 1, 4);
					//actor->SpeakConceptIfAllowed(MP_CONCEPT_PLAYER_DISPENSERHERE);
				}
			} else {
				this->m_bNearHint = false;
			}
			
			if (range_to_hint > 25.0f) {
				TRACE_MSG("range_to_hint > 25: pathing\n");
				
				if (this->m_ctRecomputePath.IsElapsed()) {
					TRACE_MSG("recomputing path\n");
					
					this->m_ctRecomputePath.Start(RandomFloat(1.0f, 2.0f));
					
					CTFBotPathCost cost_func(actor, SAFEST_ROUTE);
					this->m_PathFollower.Compute(nextbot, this->m_hHint->GetAbsOrigin(), cost_func, 0.0f, true);
				}
				
				this->m_PathFollower.Update(nextbot);
				if (!this->m_PathFollower.IsValid()) {
					return ActionResult<CTFBot>::Done("Path failed");
				}
				
				return ActionResult<CTFBot>::Continue();
			}
			
			TRACE_MSG("at hint: creating dispenser entity\n");
			
			CBaseEntity *ent = CreateEntityByName("obj_dispenser");
			if (ent == nullptr) {
				return ActionResult<CTFBot>::Done("Couldn't create entity");
			}
			
			// TODO: increment hint dword 0x370 (not important for mvm)
			
			auto dispenser = rtti_cast<CObjectDispenser *>(ent);
			dispenser->SetName(this->m_hHint->GetEntityName());
			dispenser->m_nDefaultUpgradeLevel = 2;
			dispenser->SetAbsOrigin(this->m_hHint->GetAbsOrigin());
			dispenser->SetAbsAngles(this->m_hHint->GetAbsAngles());
			dispenser->Spawn();
			
			dispenser->StartPlacement(actor);
			
			suppress_speak = true;
			dispenser->StartBuilding(actor);
			suppress_speak = false;
			
			this->m_hHint->SetOwnerEntity(dispenser);
			
			actor->SpeakConceptIfAllowed(MP_CONCEPT_BUILDING_OBJECT, "objtype:dispenser");
			
			return ActionResult<CTFBot>::Done("Built a dispenser");
		}
//-----------------------------------------------------------------------------
// Purpose: Start placing or building the currently selected object
//-----------------------------------------------------------------------------
void CTFWeaponBuilder::PrimaryAttack( void )
{
	CTFPlayer *pOwner = ToTFPlayer( GetOwner() );
	if ( !pOwner )
		return;

	if ( !CanAttack() )
		return;

	// Necessary so that we get the latest building position for the test, otherwise
	// we are one frame behind.
	UpdatePlacementState();

	// What state should we move to?
	switch( m_iBuildState )
	{
	case BS_IDLE:
		{
			// Idle state starts selection
			SetCurrentState( BS_SELECTING );
		}
		break;

	case BS_SELECTING:
		{
			// Do nothing, client handles selection
			return;
		}
		break;

	case BS_PLACING:
		{
			if ( m_hObjectBeingBuilt )
			{
				int iFlags = m_hObjectBeingBuilt->GetObjectFlags();

				// Tricky, because this can re-calc the object position and change whether its a valid 
				// pos or not. Best not to do this only in debug, but we can be pretty sure that this
				// will give the same result as was calculated in UpdatePlacementState() above.
				Assert( IsValidPlacement() );

				// If we're placing an attachment, like a sapper, play a placement animation on the owner
				if ( m_hObjectBeingBuilt->MustBeBuiltOnAttachmentPoint() )
				{
					pOwner->DoAnimationEvent( PLAYERANIMEVENT_ATTACK_GRENADE );
				}

				// Need to save this for later since StartBuilding will clear m_hObjectBeingBuilt.
				CBaseObject *pParentObject = m_hObjectBeingBuilt->GetParentObject();

				StartBuilding();

				// Attaching a sapper to a teleporter automatically saps another end.
				if ( GetType() == OBJ_ATTACHMENT_SAPPER )
				{
					CObjectTeleporter *pTeleporter = dynamic_cast<CObjectTeleporter *>( pParentObject );

					if ( pTeleporter )
					{
						CObjectTeleporter *pMatch = pTeleporter->GetMatchingTeleporter();

						// If the other end is not already sapped then place a sapper on it.
						if ( pMatch && !pMatch->IsPlacing() && !pMatch->HasSapper() )
						{
							SetCurrentState( BS_PLACING );
							StartPlacement();
							if ( m_hObjectBeingBuilt.Get() )
							{
								m_hObjectBeingBuilt->UpdateAttachmentPlacement( pMatch );
								StartBuilding();
							}
						}
					}
				}

				// Should we switch away?
				if ( iFlags & OF_ALLOW_REPEAT_PLACEMENT )
				{
					// Start placing another
					SetCurrentState( BS_PLACING );
					StartPlacement(); 
				}
				else
				{
					SwitchOwnersWeaponToLast();
				}
			}
		}
		break;

	case BS_PLACING_INVALID:
		{
			if ( m_flNextDenySound < gpGlobals->curtime )
			{
				CSingleUserRecipientFilter filter( pOwner );
				EmitSound( filter, entindex(), "Player.DenyWeaponSelection" );

				m_flNextDenySound = gpGlobals->curtime + 0.5;
			}
		}
		break;
	}

	m_flNextPrimaryAttack = gpGlobals->curtime + 0.2f;
}