Editor/DebugVizHandlers.cs

Two editor IBridgeHandler implementations. SetTimeScaleHandler sets Game.ActiveScene.TimeScale during play mode from a JSON 'scale' number (clamped 0–100). GetProfilerStatsHandler returns Sandbox.Diagnostics.PerformanceStats values and timing averages over an optional 'frames' window.

File Access
using Editor;
using Sandbox;
using System;
using System.Text.Json;
using System.Threading.Tasks;

// ═══════════════════════════════════════════════════════════════════════════
// set_time_scale — set the running play scene's TimeScale.
//
// Ported from the Unity bridge's playtest_set_time_scale. 0 = pause,
// 1 = normal, 0.1 = slow-mo, 2+ = fast-forward.
//
// API is corpus-confirmed: Scene.TimeScale is a public settable float
// (sdoomresurrection Code/ui/Pause.cs: `Scene.TimeScale = 0f` / `= 1f`;
// ss1 Code/Manager.cs: animated ramps). Game.ActiveScene is the running play
// scene (CLAUDE.md "Verified s&box APIs").
//
// Targets Game.ActiveScene and REQUIRES play mode — the edit scene does not
// tick, so scaling it is meaningless. This is therefore NOT a scene-mutating
// command: it must be allowed WHILE Game.IsPlaying (adding it to
// _sceneMutatingCommands would get it refused exactly when it is needed).
//
// Registration (in MyEditorMenu.cs RegisterHandlers, play-mode group):
//   Register( "set_time_scale", () => new SetTimeScaleHandler() );
// Do NOT add to _sceneMutatingCommands.
//
// Lives in the same assembly as MyEditorMenu.cs (no namespace) so it sees the
// global IBridgeHandler contract. Unsandboxed editor code — System.* is fine.
//
// ⚠ Compile-verify live before shipping (editor was offline at authoring time):
//   sync to <project>/Libraries/claudebridge/Editor/, restart_editor (or
//   trigger_hotload), get_compile_errors, then start_play + set_time_scale 0.1
//   + capture_view to confirm slow-mo.
// ═══════════════════════════════════════════════════════════════════════════

/// <summary>
/// set_time_scale — set Game.ActiveScene.TimeScale during play mode. Params:
///   scale : float multiplier (required; 0 = pause, 1 = normal). Clamped 0–100.
/// </summary>
public class SetTimeScaleHandler : IBridgeHandler
{
	public Task<object> Execute( JsonElement p )
	{
		try
		{
			if ( !p.TryGetProperty( "scale", out var sEl ) || sEl.ValueKind != JsonValueKind.Number )
				return Task.FromResult<object>( new { error = "scale is required (a number; 0 = pause, 1 = normal, 0.1 = slow-mo)" } );

			float scale = (float)sEl.GetDouble();
			if ( scale < 0f ) scale = 0f;
			if ( scale > 100f ) scale = 100f;

			if ( !Game.IsPlaying )
				return Task.FromResult<object>( new { error = "set_time_scale only applies in play mode — call start_play first (the edit scene does not tick)" } );

			var scene = Game.ActiveScene;
			if ( scene == null )
				return Task.FromResult<object>( new { error = "No active play scene" } );

			float previous = scene.TimeScale;
			scene.TimeScale = scale;

			return Task.FromResult<object>( new
			{
				set = true,
				timeScale = scene.TimeScale,
				previous,
				paused = scale == 0f
			} );
		}
		catch ( Exception ex )
		{
			return Task.FromResult<object>( new { error = $"set_time_scale failed: {ex.Message}" } );
		}
	}
}

// ═══════════════════════════════════════════════════════════════════════════
// get_profiler_stats — read live engine performance counters.
//
// Ported from the Unity bridge's get_profiler_stats. Reads
// Sandbox.Diagnostics.PerformanceStats. All members are corpus-confirmed
// (darkrpog Code/Diagnostics/RoleplayPerfDiagnostics.cs + RoleplayPerfFrameBurst.cs):
//   FrameTime (s), GpuFrametime (ms), GpuFrameNumber, BytesAllocated,
//   ApproximateProcessMemoryUsage, Exceptions, and Timings.{Update,Physics,Ui,
//   Render,Network,GcPause}.AverageMs(frames).
//
// Read-only → NOT scene-mutating. Most meaningful during play, but the editor
// renders frames too so the counters are populated in edit mode. Unsandboxed
// editor code, so System.Math is fine.
//
// Registration: Register( "get_profiler_stats", () => new GetProfilerStatsHandler() );
//
// ⚠ Compile-verify live before shipping (editor was offline at authoring time):
//   the exact PerformanceStats / Timings member names are corpus-grounded but
//   not reflected against this SDK — describe_type PerformanceStats if it fails.
// ═══════════════════════════════════════════════════════════════════════════

/// <summary>
/// get_profiler_stats — dump Sandbox.Diagnostics.PerformanceStats. Params:
///   frames : averaging window for per-category timings (optional, default 60).
/// </summary>
public class GetProfilerStatsHandler : IBridgeHandler
{
	public Task<object> Execute( JsonElement p )
	{
		try
		{
			int frames = 60;
			if ( p.TryGetProperty( "frames", out var fEl ) && fEl.ValueKind == JsonValueKind.Number )
				frames = Math.Clamp( fEl.GetInt32(), 1, 1000 );

			double frameTimeSec = Sandbox.Diagnostics.PerformanceStats.FrameTime;
			double frameMs = frameTimeSec * 1000.0;
			double fps = frameTimeSec > 0 ? 1.0 / frameTimeSec : 0;

			return Task.FromResult<object>( new
			{
				fps = Math.Round( fps, 1 ),
				frameMs = Math.Round( frameMs, 3 ),
				gpuMs = Math.Round( Sandbox.Diagnostics.PerformanceStats.GpuFrametime, 3 ),
				gpuFrameNumber = Sandbox.Diagnostics.PerformanceStats.GpuFrameNumber,
				bytesAllocated = Sandbox.Diagnostics.PerformanceStats.BytesAllocated,
				processMemoryBytes = (long)Sandbox.Diagnostics.PerformanceStats.ApproximateProcessMemoryUsage,
				exceptions = Sandbox.Diagnostics.PerformanceStats.Exceptions,
				avgFrames = frames,
				timingsMs = new
				{
					update  = Math.Round( Sandbox.Diagnostics.PerformanceStats.Timings.Update.AverageMs( frames ), 3 ),
					physics = Math.Round( Sandbox.Diagnostics.PerformanceStats.Timings.Physics.AverageMs( frames ), 3 ),
					ui      = Math.Round( Sandbox.Diagnostics.PerformanceStats.Timings.Ui.AverageMs( frames ), 3 ),
					render  = Math.Round( Sandbox.Diagnostics.PerformanceStats.Timings.Render.AverageMs( frames ), 3 ),
					network = Math.Round( Sandbox.Diagnostics.PerformanceStats.Timings.Network.AverageMs( frames ), 3 ),
					gcPause = Math.Round( Sandbox.Diagnostics.PerformanceStats.Timings.GcPause.AverageMs( frames ), 3 )
				}
			} );
		}
		catch ( Exception ex )
		{
			return Task.FromResult<object>( new { error = $"get_profiler_stats failed: {ex.Message}" } );
		}
	}
}