MatchPerformanceProbe.cs
using Sandbox;
using System;
namespace PaintballBaddies;

/// <summary>Opt-in editor baseline. Reports pacing, never asserts shipping performance.</summary>
public sealed class MatchPerformanceProbe : Component
{
    [Property] public float SampleSeconds { get; set; }
    [Property] public bool StressImpacts { get; set; }
    [Property] public bool MovingCamera { get; set; }
    [Property] public bool YardCamera { get; set; }
    [Property] public bool SixCharacters { get; set; }
    [Property] public bool CitizenCharacters { get; set; }
    [Property] public bool ActiveFirefight { get; set; }
    [Property] public bool MovingTarget { get; set; }
    [Property] public bool MobileOpponents { get; set; }
    private readonly HashSet<ArenaOpponent> stagedBots=new();
    private CitizenPlayerPresentation citizenPlayer;
    private RosterSelection citizenRoster;
    private bool previousCitizen;
    private readonly List<CitizenOpponentPresentation> citizenBots=new();
    private readonly List<GameObject> extraOpponents = new();
    private PlayerController player;
    private bool restoreCamera;
    private Vector3 originalPosition;
    private Rotation originalRotation;
    private double nextImpact;
    private int injectedImpacts,peakDroplets;
    private readonly List<double> frames=new();
    private readonly List<(double Elapsed, double Milliseconds)> hitches=new();
    private double start,previous;
    private bool started;
    private double firstShotAt=-1;
    protected override void OnUpdate()
    {
        var match=Scene.GetAllComponents<ArenaMatch>().FirstOrDefault();
        if(!match.IsValid() || !match.PlayerState.IsValid())return;
        var now=RealTime.NowDouble;
        if(!started)
        {
            player=Scene.GetAllComponents<PlayerController>().FirstOrDefault();
            if(MovingCamera && player.IsValid())
            {
                restoreCamera=player.UseCameraControls;player.UseCameraControls=false;
                originalPosition=WorldPosition;originalRotation=WorldRotation;
            }
            if(CitizenCharacters)
            {
                citizenRoster=player.Components.Get<RosterSelection>();
                previousCitizen=citizenRoster.UseCitizenCharacters;
                citizenRoster.UseCitizenCharacters=true;
            }
            match.StartRound();match.PlayerState.HitLimit=10000;
            if(SixCharacters && Scene.GetAllComponents<ArenaOpponent>().Count() < 5)
            {
                for(int i=0;i<2;i++)
                {
                    var go=new GameObject(true,"Six-character performance opponent");
                    go.WorldPosition=new Vector3(i==0 ? -650 : 650,1250,1);
                    var bot=go.Components.Create<ArenaOpponent>();bot.Character=i==0 ? "roxie" : "mei";bot.CombatEnabled=true;
                    extraOpponents.Add(go);
                }
            }
            start=previous=now;started=true;return;
        }
        var elapsed=now-start;
        if(firstShotAt<0 && Scene.GetAllComponents<ArenaOpponent>().Any(x=>x.ShotsFired>0))firstShotAt=elapsed;
        if(ActiveFirefight)
        {
            player.WorldPosition=new Vector3(-950,-500+(MovingTarget ? MathF.Sin((float)elapsed*1.2f)*90 : 0),2);
            foreach(var bot in Scene.GetAllComponents<ArenaOpponent>())
            {
                if(MobileOpponents || !bot.Body.IsValid())continue;
                if(stagedBots.Add(bot))
                {
                    bot.WorldPosition=new Vector3(-700,-620+(stagedBots.Count-1)*60,2);
                    bot.WorldRotation=Rotation.FromYaw(180);
                }
                if(bot.Components.Get<NavMeshAgent>() is {} agent)
                {
                    agent.MaxSpeed=0;agent.UpdatePosition=false;
                    bot.WorldPosition=new Vector3(-700,-620+stagedBots.ToList().IndexOf(bot)*60,2);
                }
            }
        }
        if(CitizenCharacters)
        {
            citizenPlayer=player.Components.Get<CitizenPlayerPresentation>();
            foreach(var bot in Scene.GetAllComponents<ArenaOpponent>())
                if(bot.Components.Get<CitizenOpponentPresentation>() is {} presentation && !citizenBots.Contains(presentation))
                    citizenBots.Add(presentation);
        }
        var end=SampleSeconds > 0 ? Math.Clamp(SampleSeconds, 10, 160)+5 : MovingCamera ? 45 : 25;
        if(MovingCamera)
        {
            var angle=(float)(elapsed*Math.PI*2/40);
            WorldPosition=YardCamera ? new Vector3(MathF.Cos(angle)*850,1180+MathF.Sin(angle)*230,110+MathF.Sin(angle*2)*30)
                : new Vector3(MathF.Cos(angle)*850,MathF.Sin(angle)*620,110+MathF.Sin(angle*2)*30);
            WorldRotation=Rotation.LookAt(new Vector3(0,YardCamera ? 1180 : 0,40)-WorldPosition);
        }
        var impacts=PaintImpactSystem.Find(Scene);
        if(StressImpacts && elapsed>=5 && now>=nextImpact)
        {
            nextImpact=now+.2;
            // Keep annex stress effects in the camera's tested area, rather
            // than behind the divider at the stationary player's spawn.
            var point=YardCamera
                ? new Vector3(MathF.Cos((float)elapsed)*180,1180+MathF.Sin((float)elapsed)*140,1)
                : match.PlayerState.WorldPosition+Vector3.Forward*100;
            var hit=Scene.Trace.Ray(point+Vector3.Up*100,point-Vector3.Up*10).Run();
            if(hit.Hit && impacts.IsValid())
                for(int i=0;i<3;i++){impacts.Spawn(hit,i%2==0 ? Color.Cyan : new Color(1,.12f,.6f));injectedImpacts++;}
        }
        if(impacts.IsValid())peakDroplets=Math.Max(peakDroplets,impacts.ActiveDroplets);
        if(elapsed>=5 && elapsed<=end)
        {
            var milliseconds=(now-previous)*1000;
            frames.Add(milliseconds);
            // Buffer observations; logging in a long frame can itself disturb pacing.
            if(milliseconds>33.333 && hitches.Count<16)hitches.Add((elapsed,milliseconds));
        }
        previous=now;
        if(elapsed<end)return;
        if(frames.Count<2){Log.Info("PB_PERF insufficient samples");Destroy();return;}
        var sorted=frames.OrderBy(x=>x).ToArray();
        double Percentile(double p)=>sorted[Math.Min(sorted.Length-1,(int)Math.Ceiling(p*sorted.Length)-1)];
        var mean=frames.Average();
        Log.Info($"PB_PERF samples={frames.Count} mean_ms={mean:F3} mean_fps={1000/mean:F1} p50_ms={Percentile(.5):F3} p95_ms={Percentile(.95):F3} p99_ms={Percentile(.99):F3} max_ms={sorted[^1]:F3} over33ms={frames.Count(x=>x>33.333)} bots={Scene.GetAllComponents<ArenaOpponent>().Count()} shots={Scene.GetAllComponents<ArenaOpponent>().Sum(x=>x.ShotsFired)} decals={PaintImpactSystem.Find(Scene)?.ActiveCount}");
        Log.Info($"PB_PERF stress={StressImpacts} injected_impacts={injectedImpacts} peak_droplets={peakDroplets} yard_camera={YardCamera} six_characters={SixCharacters}");
        Log.Info($"PB_PERF CONTACT first_shot_seconds={firstShotAt:F3} player_hits={match.PlayerState.PaintHits} shooting_bots={Scene.GetAllComponents<ArenaOpponent>().Count(x=>x.ShotsFired>0)}");
        Log.Info($"PB_PERF SPACING moves={Scene.GetAllComponents<ArenaOpponent>().Sum(x=>x.SpacingMoves)}");
        if(ActiveFirefight)Log.Info($"PB_PERF FIREFIGHT shooting_bots={Scene.GetAllComponents<ArenaOpponent>().Count(x=>x.ShotsFired>0)} reloads={Scene.GetAllComponents<ArenaOpponent>().Sum(x=>x.Magazine.Reloads)} player_hits={match.PlayerState.PaintHits}");
        if(ActiveFirefight)foreach(var bot in Scene.GetAllComponents<ArenaOpponent>())Log.Info($"PB_PERF AI position={bot.WorldPosition} sees={bot.SeesPlayer} combat={bot.CombatEnabled} cover={bot.UsingCover} player={player.WorldPosition} round={match.InRound}");
        foreach(var bot in Scene.GetAllComponents<ArenaOpponent>())Log.Info($"PB_PERF NAV character={bot.Character} distance={bot.DistanceTravelled:F1} position={bot.WorldPosition} navigating={bot.Components.Get<NavMeshAgent>()?.IsNavigating} generating={Scene.NavMesh.IsGenerating}");
        if(CitizenCharacters && !ActiveFirefight)foreach(var bot in Scene.GetAllComponents<ArenaOpponent>())
        {
            var sight=Scene.Trace.Ray(bot.WorldPosition+Vector3.Up*60,player.EyePosition).IgnoreGameObjectHierarchy(bot.GameObject).Run();
            var anchor=bot.PresentationAnchor;
            var muzzle=anchor.WorldPosition+anchor.WorldRotation.Forward*17.1f+anchor.WorldRotation.Up*4;
            var obstruction=Scene.Trace.Sphere(.35f,bot.WorldPosition+Vector3.Up*60,muzzle).IgnoreGameObjectHierarchy(bot.GameObject).Run();
            Log.Info($"PB_PERF SIGHT character={bot.Character} sees={bot.SeesPlayer} sight_hit={sight.GameObject?.Name} eye={player.EyePosition} muzzle_hit={obstruction.GameObject?.Name} cover={bot.UsingCover} plan={bot.HasCoverPlan}");
        }
        if(CitizenCharacters)Log.Info($"PB_PERF CITIZEN player_ready={citizenPlayer.IsReady} bots_ready={citizenBots.Count(x=>x.IsValid() && x.IsReady)} suits={string.Join(",",citizenBots.Where(x=>x.IsValid()).Select(x=>x.EquippedSuit))}");
        if(CitizenCharacters)
        {
            var motions=Scene.GetAllComponents<CitizenSuitFollowThrough>().ToArray();
            Log.Info($"PB_PERF SUIT_MOTION ready={motions.Count(x=>x.MorphsReady)} total={motions.Length} peak={motions.Select(x=>x.PeakWeight).DefaultIfEmpty(0).Max():F4}");
        }
        foreach(var hitch in hitches)Log.Info($"PB_PERF HITCH elapsed_seconds={hitch.Elapsed:F3} frame_ms={hitch.Milliseconds:F3}");
        Log.Info($"PB_PERF COMPLETE editor match, moving_target={MovingTarget}, mobile_opponents={MobileOpponents}, moving_camera={MovingCamera}, five-second warmup, sample_seconds={end-5}");
        Destroy();
    }
    protected override void OnDestroy()
    {
        if(citizenRoster.IsValid())citizenRoster.UseCitizenCharacters=previousCitizen;
        foreach(var opponent in extraOpponents)if(opponent.IsValid())opponent.Destroy();
        if(MovingCamera && player.IsValid())
        {
            player.UseCameraControls=restoreCamera;
            WorldPosition=originalPosition;WorldRotation=originalRotation;
        }
    }
}