OMRBotController.Performance.cs
using Sandbox;
using System.Collections.Generic;
using Sandbox.Navigation;
using System;
using System.Linq;
/// <summary>
/// BOT 9: separate strategic sampling from per-frame aim/input execution, and
/// keep all expensive paths behind one counted API boundary. Caches are owned
/// by this bot and cleared on spawn, layout changes, disable and destruction.
/// </summary>
public sealed partial class OMRBotController
{
[Property, Group( "BOT 9 / Performance" ), Range( 0.05f, 0.25f )]
public float StrategicSampleInterval { get; set; } = 0.10f;
[Property, Group( "BOT 9 / Performance" ), Range( 0.1f, 0.5f )]
public float TacticalQueryBudgetInterval { get; set; } = 0.20f;
[ConVar( "omr_bot_perf" )]
public static bool BotPerformanceLogging { get; set; }
public int PathQueriesLastSecond { get; private set; }
public int TraversalProbesLastSecond { get; private set; }
public string PerformanceStatus { get; private set; } = "READY";
private OMRBotManager _navigationManager;
private OneMoreRoundWeapon _strategyWeapon;
private int _strategyWeaponRevision = -2;
private RoundModifier _strategyModifier = (RoundModifier)(-1);
private bool _strategyHadSight;
private float _strategyHealth = -1f;
private float _nextStrategicSampleAt;
private float _nextTacticalBudgetAt;
private float _nextMovementPathAt;
private float _nextPerformanceReportAt;
private int _pathQueries;
private int _traversalProbes;
private int _runtimeMatchRound = -1;
private RoundPhase _runtimeMatchPhase = (RoundPhase)(-1);
private SessionMode _runtimeSessionMode = (SessionMode)(-1);
private readonly List<Vector3> _refreshedPathPoints = new();
private readonly Dictionary<(int X, int Y, int Height), TraversalProbeSample> _traversalProbeCache = new();
private readonly struct TraversalProbeSample
{
public readonly Vector3 Origin;
public readonly Vector3 Direction;
public readonly float ExpiresAt;
public readonly bool Blocked;
public TraversalProbeSample( Vector3 origin, Vector3 direction, bool blocked )
{
Origin = origin; Direction = direction; Blocked = blocked;
ExpiresAt = Time.Now + 0.075f;
}
}
private NavMeshPath CalculateCountedBotPath( Vector3 start, Vector3 target )
{
_pathQueries++;
return Scene.NavMesh.CalculatePath( new CalculatePathRequest { Start = start, Target = target } );
}
private void RefreshTacticalQueryBudget()
{
if ( Time.Now < _nextTacticalBudgetAt ) return;
_nextTacticalBudgetAt = Time.Now + MathF.Max( 0.10f, TacticalQueryBudgetInterval );
_tacticalPathQueriesRemaining = Math.Clamp( TacticalPathQueryBudgetPerUpdate, 2, 8 );
}
private bool ShouldSampleStrategy( OneMoreRoundWeapon weapon )
{
float health = GetOwnHealthFraction();
int revision = weapon?.StatRevision ?? -1;
bool changed = _strategyHadSight != _hasConfirmedSight ||
_strategyWeapon != weapon || MathF.Abs( _strategyHealth - health ) > 0.01f ||
_strategyWeaponRevision != revision || _strategyModifier != _activeModifier;
if ( !changed && Time.Now < _nextStrategicSampleAt ) return false;
_strategyHadSight = _hasConfirmedSight;
_strategyWeapon = weapon;
_strategyHealth = health;
_strategyWeaponRevision = revision;
_strategyModifier = _activeModifier;
_nextStrategicSampleAt = Time.Now + MathF.Max( 0.05f, StrategicSampleInterval );
return true;
}
private bool PrepareBotSessionLifecycle()
{
if ( _roundManager?.IsValid != true )
_roundManager = Scene?.GetAllComponents<RoundManager>().FirstOrDefault();
if ( _navigationManager?.IsValid != true )
_navigationManager = Scene?.GetAllComponents<OMRBotManager>().FirstOrDefault();
if ( _roundManager?.IsValid != true ) return true;
SessionMode mode = _roundManager.Mode;
RoundPhase phase = _roundManager.Phase;
int round = _roundManager.RoundNumber;
bool changed = mode != _runtimeSessionMode;
bool enteringFight = mode == SessionMode.Match && phase == RoundPhase.Fighting &&
( _runtimeMatchPhase != RoundPhase.Fighting || round != _runtimeMatchRound );
_runtimeSessionMode = mode;
_runtimeMatchPhase = phase;
_runtimeMatchRound = round;
// A survivor may remain IsAlive across resets. Phase/round transitions must
// also reset perception, paths and cover; death->alive alone misses that case.
if ( ( changed || enteringFight ) && _playerState.IsAlive ) BeginNewBotLife();
if ( mode == SessionMode.Menu || ( mode == SessionMode.Match && phase != RoundPhase.Fighting ) )
{
StopMovementOnly();
SafeReleaseLastCombatWeapon();
ReleaseBotWeaponIntent();
CombatStatus = "ROUND LOCKED";
CurrentIntent = "WAITING FOR ROUND";
return false;
}
return true;
}
private void UpdatePerformanceCounters()
{
if ( Time.Now < _nextPerformanceReportAt ) return;
PathQueriesLastSecond = _pathQueries;
TraversalProbesLastSecond = _traversalProbes;
_pathQueries = 0; _traversalProbes = 0;
_nextPerformanceReportAt = Time.Now + 1f;
PerformanceStatus = $"PATH {PathQueriesLastSecond}/s · PROBE {TraversalProbesLastSecond}/s";
if ( BotPerformanceLogging )
Log.Info( $"[OMR BOT PERF] {PerformanceStatus} | Round:{_runtimeMatchRound} | {CombatStatus}" );
}
private void ResetPerformanceCaches()
{
_strategyWeapon = null;
_strategyWeaponRevision = -2;
_strategyHealth = -1f;
_nextStrategicSampleAt = 0f;
_nextTacticalBudgetAt = 0f;
_nextMovementPathAt = 0f;
_refreshedPathPoints.Clear();
_traversalProbeCache.Clear();
_nextCoverShortlistAt = 0f;
_coverShortlist.Clear();
_tacticalRouteCache.Clear();
_tacticalPathQueriesRemaining = 0;
_navigationProjectionValid = false;
_nextNavigationProjectionAt = 0f;
}
private bool _navigationProjectionValid;
private Vector3 _navigationProjectionOrigin;
private Vector3 _navigationProjectionTarget;
private Vector3 _navigationStartOnMesh;
private Vector3 _navigationTargetOnMesh;
private float _nextNavigationProjectionAt;
private bool TryProjectNavigationEndpoints( Vector3 target )
{
if ( Time.Now < _nextNavigationProjectionAt &&
( WorldPosition - _navigationProjectionOrigin ).Length <= 48f &&
( target - _navigationProjectionTarget ).Length <= 24f ) return _navigationProjectionValid;
Vector3? start = Scene.NavMesh.GetClosestPoint( WorldPosition, 192f );
Vector3? end = Scene.NavMesh.GetClosestPoint( target, 192f );
_navigationProjectionValid = start.HasValue && end.HasValue;
_navigationProjectionOrigin = WorldPosition;
_navigationProjectionTarget = target;
_nextNavigationProjectionAt = Time.Now + 0.15f;
if ( !_navigationProjectionValid ) return false;
_navigationStartOnMesh = start.Value;
_navigationTargetOnMesh = end.Value;
return true;
}
protected override void OnDisabled()
{
if ( Networking.IsHost && !IsProxy )
{
StopMovementOnly();
SafeReleaseLastCombatWeapon();
}
ResetPerformanceCaches();
base.OnDisabled();
}
protected override void OnDestroy()
{
SafeReleaseLastCombatWeapon();
ResetPerformanceCaches();
base.OnDestroy();
}
}