Player/Bot/PlayerBotController.cs
using System.Threading;

/// <summary>
/// Takes control over a player.
/// </summary>
public partial class PlayerBotController : Component
{
	[ConVar( "sbdm.bots.notarget" )]
	public static bool NoTarget { get; set; } = false;

	[RequireComponent]
	public NavMeshAgent MeshAgent { get; set; }

	[RequireComponent]
	public PlayerController Controller { get; set; }

	[RequireComponent]
	public Player Player { get; set; }

	[RequireComponent]
	public PlayerInventory Inventory { get; set; }

	protected override void OnEnabled()
	{
		base.OnEnabled();

		// We handle our GOs position manually/via physics controller
		MeshAgent.UpdatePosition = false;
		MeshAgent.UpdateRotation = false;

		MeshAgent.MaxSpeed = Controller.RunSpeed;

		RunBehaviour();
	}

	private async void RunBehaviour()
	{
		if ( MeshAgent == null || IsProxy )
			return;

		while ( Scene.NavMesh.IsGenerating )
		{
			await Task.DelaySeconds( 1 );
		}

		while ( this.IsValid() && Enabled )
		{
			await RunSelector(
				GameObject.EnabledToken,
				Combat,
				Roam,
				Idle
			);
		}
	}

	private async Task<bool> Idle( CancellationToken token )
	{
		Log.Trace( $"Bot {GameObject} failed to perform any behaviour, idling for a bit" );
		await Task.DelaySeconds( 2, token );
		return true;
	}

	protected override void OnFixedUpdate()
	{
		if ( MeshAgent == null || IsProxy )
			return;

		SyncNavAgentWithPhysics();

		FindEnemies();
	}

	private void SyncNavAgentWithPhysics()
	{
		if ( NoTarget )
		{
			Controller.WishVelocity = Vector3.Zero;
			return;
		}

		Controller.WishVelocity = MeshAgent.WishVelocity;

		// We may have some desync between agent and player position
		// this can happen because the physics simulation takes precedence over the navmesh navigation
		if ( WorldPosition.WithZ( 0 ).DistanceSquared( MeshAgent.AgentPosition.WithZ( 0 ) ) > MeshAgent.Radius * MeshAgent.Radius )
		{
			MeshAgent.SetAgentPosition( WorldPosition );
		}
		// Check z independently, we may are more lenient with desync in z
		if ( MathF.Abs( WorldPosition.z - MeshAgent.AgentPosition.z ) > MeshAgent.Height * MeshAgent.Height )
		{
			MeshAgent.SetAgentPosition( WorldPosition );
		}
	}

	private async Task<bool> TryDivertForConsumable(
		CancellationToken token,
		Vector3? mainTarget = null,
		Predicate<BasePickup> pickupFilter = null )
	{
		// find nearby coffins first
		var veryCloseCoffins = FindCoffinsInRadius( 200f );
		var coffinTarget = veryCloseCoffins.FirstOrDefault();
		while ( coffinTarget.IsValid() && !token.IsCancellationRequested )
		{
			MeshAgent.MoveTo( coffinTarget.WorldPosition );
			await Task.FixedUpdate();
		}

		if ( token.IsCancellationRequested )
			return false;

		bool IsConsumableAndInLOS( BasePickup pickup )
		{
			if ( pickup is not AmmoPickup && pickup is not HealthPickup && pickup is not ArmourPickup )
				return false;

			if ( pickupFilter != null && !pickupFilter( pickup ) )
			{
				return false;
			}

			var trace = Scene.Trace.Ray( WorldPosition, pickup.WorldPosition + Vector3.Up * 5f )
				.IgnoreGameObjectHierarchy( GameObject )
				.Run();

			return !trace.Hit || trace.GameObject == pickup.GameObject;
		}

		var veryCloseConsumables = FindPickupsInRadius<BasePickup>( 400f, IsConsumableAndInLOS );
		var consumablePickupTarget = veryCloseConsumables.FirstOrDefault();

		if ( consumablePickupTarget == null )
			return false;

		var consumablePickupPosition = GetClosestPointOnPickupToNavMesh( consumablePickupTarget, MeshAgent.Radius );
		var mainTargetCloser = mainTarget.HasValue && consumablePickupTarget.WorldPosition.DistanceSquared( WorldPosition ) > mainTarget.Value.DistanceSquared( WorldPosition );

		while ( IsPickupValid( consumablePickupTarget ) && !token.IsCancellationRequested && !mainTargetCloser )
		{
			mainTargetCloser = mainTarget.HasValue && consumablePickupTarget.WorldPosition.DistanceSquared( WorldPosition ) > mainTarget.Value.DistanceSquared( WorldPosition );
			MeshAgent.MoveTo( consumablePickupPosition );
			await Task.FixedUpdate();
		}

		if ( token.IsCancellationRequested )
			return false;

		// Recursively attempt to pick up other nearby consumables
		if ( !mainTargetCloser )
		{
			await TryDivertForConsumable( token, mainTarget );
		}

		return true;
	}

	private Player _currentTarget;

	private TimeSince _timeSinceTargetSwitch = 0;

	private Player FindAndUpdateTarget()
	{
		// select target
		var shouldTrySwitchTarget = _timeSinceTargetSwitch > 5.0f || !IsCurrentTargetValid();
		if ( shouldTrySwitchTarget )
		{
			if ( _relevantEnemies.Count > 0 )
			{
				_timeSinceTargetSwitch = 0;
				// pick last seen enemy
				_currentTarget = _relevantEnemies.OrderBy( x => _lastSeenEnemies[x].Relative ).First();
			}
		}

		if ( !IsCurrentTargetValid() )
		{
			_currentTarget = null;
		}

		return _currentTarget;
	}
	bool IsCurrentTargetValid()
	{
		if ( !_currentTarget.IsValid() ) return false;

		if ( !_relevantEnemies.Contains( _currentTarget ) ) return false;

		if ( !_lastSeenEnemies.TryGetValue( _currentTarget, out TimeSince value ) || value > 5f ) return false;

		if ( _currentTarget.Health <= 0 ) return false;

		return true;
	}

	private HashSet<Player> _relevantEnemies = new();

	private Dictionary<Player, TimeSince> _lastSeenEnemies = new();

	private void FindEnemies()
	{
		var allPlayers = Scene.GetAllComponents<Player>();
		// Filter out players not in LOS
		var playersInLOS = allPlayers.Where( potentialEnemy =>
		{
			var basicFilter = potentialEnemy != Player && potentialEnemy.Health >= 0 && potentialEnemy.WorldPosition.DistanceSquared( WorldPosition ) < 750f * 750f;
			if ( !basicFilter ) return false;

			var trace = Scene.Trace.Ray( Controller.EyePosition, potentialEnemy.Controller.EyePosition )
				.IgnoreGameObjectHierarchy( GameObject )
				.Run();

			return trace.Hit && trace.GameObject == potentialEnemy.GameObject;

		} ).ToHashSet();

		// Update relevant enemies and last seen times
		foreach ( var enemy in playersInLOS )
		{
			_lastSeenEnemies[enemy] = 0;
			_relevantEnemies.Add( enemy );
		}

		// Remove enemies not seen for over 5 seconds
		foreach ( var enemy in _lastSeenEnemies )
		{
			if ( enemy.Value > 5 )
			{
				_relevantEnemies.Remove( enemy.Key );
			}
		}
	}

	private bool IsPickupValid( BasePickup pickup )
	{
		return pickup.IsValid() && pickup.IsPickupEnabled && pickup.CanPickup( Player, Inventory );
	}

	/// <summary>
	/// This looks more complicated that it is.
	/// Basically we just want to find the point of the pickup collider that is closest to the navmesh.
	/// </summary>
	Vector3 GetClosestPointOnPickupToNavMesh( BasePickup pickup, float checkRadius )
	{
		// Check if the pickup's position is directly on the navmesh
		var navMeshPoint = Scene.NavMesh.GetClosestPoint( pickup.WorldPosition, checkRadius );
		if ( navMeshPoint.HasValue && (navMeshPoint.Value - pickup.WorldPosition).LengthSquared < checkRadius * checkRadius )
		{
			// Fast path: Return the pickup's position if it's close to the navmesh
			return pickup.WorldPosition;
		}

		var localBounds = pickup.Collider.LocalBounds;
		var localCorners = localBounds.Corners;

		Vector3 closestPoint = pickup.WorldPosition;
		float minDistanceSquared = float.MaxValue;

		foreach ( var localPoint in localCorners )
		{
			var worldPoint = pickup.WorldTransform.PointToLocal( localPoint );
			navMeshPoint = Scene.NavMesh.GetClosestPoint( worldPoint, checkRadius );

			if ( navMeshPoint.HasValue )
			{
				float distanceSquared = (worldPoint - navMeshPoint.Value).LengthSquared;
				if ( distanceSquared < minDistanceSquared )
				{
					minDistanceSquared = distanceSquared;
					closestPoint = worldPoint;
				}
			}
		}

		return closestPoint;
	}

	bool IsPickupReachable( BasePickup pickup )
	{
		// Avoid targeting pickups that are not on the navmesh or are unreachable
		float checkRadius = MeshAgent.Radius;

		// Find the point on the pickup closest to the navmesh
		Vector3 closestPointOnPickup = GetClosestPointOnPickupToNavMesh( pickup, checkRadius );

		return IsPositionReachable( closestPointOnPickup );
	}

	bool IsPositionReachable( Vector3 Position )
	{
		float checkRadius = MeshAgent.Radius;

		var navMeshPoint = Scene.NavMesh.GetClosestPoint( Position, checkRadius );

		// If there's no valid navmesh point near the pickup, it's unreachable
		if ( !navMeshPoint.HasValue || (navMeshPoint.Value - Position).LengthSquared > checkRadius * checkRadius )
		{
			return false;
		}

		var path = Scene.NavMesh.CalculatePath( new Sandbox.Navigation.CalculatePathRequest()
		{
			Agent = MeshAgent,
			Start = WorldPosition,
			Target = navMeshPoint.Value
		} );

		if ( path.Status != Sandbox.Navigation.NavMeshPathStatus.Complete )
		{
			return false;
		}

		// Check if the end of the path is close enough to the navmesh point near the pickup
		if ( (path.Points.Last().Position - navMeshPoint.Value).LengthSquared > checkRadius * checkRadius )
		{
			return false;
		}

		return true;
	}

	IEnumerable<T> FindPickupsInRadius<T>( float radius, Predicate<T> pred ) where T : BasePickup
	{
		return Scene.GetAll<T>()
				.Where(
					x => x.IsValid() &&
					x.IsPickupEnabled && x.CanPickup( Player, Inventory ) &&
					x.WorldPosition.DistanceSquared( WorldPosition ) < radius * radius &&
					pred( x ) &&
					IsPickupReachable( x )
				)
				.OrderBy( x => x.WorldPosition.DistanceSquared( WorldPosition ) );
	}

	IEnumerable<Coffin> FindCoffinsInRadius( float radius )
	{
		return
			Scene.GetAll<Coffin>()
				.Where(
					x => x.IsValid() &&
					x.WorldPosition.DistanceSquared( WorldPosition ) < radius * radius &&
					IsPositionReachable( x.WorldPosition )
				)
				.OrderBy( x => x.WorldPosition.DistanceSquared( WorldPosition ) );
	}

	/// <summary>
	/// Executes tasks in sequence until one returns true.
	/// </summary>
	private async Task<bool> RunSelector( CancellationToken token, params Func<CancellationToken, Task<bool>>[] tasks )
	{
		foreach ( var task in tasks )
		{
			if ( token.IsCancellationRequested )
				return false;

			if ( await task( token ) )
			{
				return true;
			}
		}
		return false;
	}

	/// <summary>
	/// Executes tasks in parallel with different cancellation/success modes.
	/// </summary>
	private async Task<bool> RunParallel( CancellationToken externalToken, params Func<CancellationToken, Task<bool>>[] tasks )
	{
		using var cts = CancellationTokenSource.CreateLinkedTokenSource( externalToken );
		var token = cts.Token;

		var taskList = tasks.Select( task => task( token ) ).ToList();

		// Await the first task to complete
		var firstCompletedTask = await Task.WhenAny( taskList );

		// Cancel the other tasks
		cts.Cancel();

		try
		{
			// Get the result of the first completed task
			bool result = await firstCompletedTask;

			// This ensures that all task exceptions are observed
			await Task.WhenAll( taskList );

			return result;
		}
		catch ( OperationCanceledException )
		{
			// Handle task cancellation
			return firstCompletedTask.Result;
		}
		catch ( Exception )
		{
			// Handle any exceptions from the tasks
			return firstCompletedTask.Result;
		}
	}
}