{"TotalCount":74,"Files":[{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/DebugDrawHandlers.cs","FileName":"DebugDrawHandlers.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"using Editor;\r\nusing Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing System.Text.Json;\r\nusing System.Threading.Tasks;\r\n\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n// debug_draw_* / debug_clear \u2014 visualize debug primitives in the scene.\r\n//\r\n// Ported from the Claude Bridge for Unity\u0027s debug_draw_* family. s\u0026box has no\r\n// bridge debug-viz; this fills the gap so a raycast hit / physics_overlap\r\n// volume / trigger_zone bounds / NPC sight cone / patrol path can be SEEN\r\n// (and screenshot-verified) instead of reasoned about blind.\r\n//\r\n// ONE component, dual render path:\r\n//   \u2022 EDIT scene \u2192 Gizmo.Draw.* inside ClaudeDebugDraw.DrawGizmos()\r\n//   \u2022 PLAY scene \u2192 Game.ActiveScene.DebugOverlay.* re-emitted each OnUpdate()\r\n// A single NotSaved holder GameObject (\u0022__ClaudeDebugDraw\u0022) stores the prim\r\n// list; the draw handlers append, debug_clear destroys it.\r\n//\r\n// APIs reflected live on this SDK (describe_type, 2026-06-18):\r\n//   Gizmo.Draw: Line(a,b) \u00B7 Arrow(from,to,len,width) \u00B7 LineBBox(bbox) \u00B7\r\n//               LineSphere(Sphere,rings) \u00B7 Color/LineThickness/IgnoreDepth\r\n//   Scene.DebugOverlay (DebugOverlaySystem):\r\n//               Line(from,to,color,dur,tx,overlay) \u00B7 Box(BBox,color,dur,tx,overlay) \u00B7\r\n//               Sphere(Sphere,color,dur,tx,overlay)\r\n//\r\n// Must work WHILE playing \u2192 these are NOT added to _sceneMutatingCommands.\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n\r\npublic enum DebugDrawKind { Line, Ray, Box, Sphere }\r\n\r\npublic sealed class DebugDrawPrim\r\n{\r\n\tpublic DebugDrawKind Kind;\r\n\tpublic Vector3 A;            // line/ray start \u00B7 box/sphere center\r\n\tpublic Vector3 B;            // line/ray end\r\n\tpublic Vector3 Size;         // box full extents\r\n\tpublic float Radius;         // sphere\r\n\tpublic Color Color = Color.Yellow;\r\n\tpublic float Thickness = 2f;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Holds bridge-issued debug primitives and renders them in both the editor\r\n/// (DrawGizmos) and play mode (DebugOverlay). One per scene, NotSaved.\r\n/// \u003C/summary\u003E\r\npublic sealed class ClaudeDebugDraw : Component\r\n{\r\n\tpublic List\u003CDebugDrawPrim\u003E Prims { get; set; } = new();\r\n\r\n\tprotected override void DrawGizmos()\r\n\t{\r\n\t\tif ( Prims == null ) return;\r\n\t\tforeach ( var p in Prims )\r\n\t\t{\r\n\t\t\tGizmo.Draw.Color = p.Color;\r\n\t\t\tGizmo.Draw.LineThickness = p.Thickness;\r\n\t\t\tGizmo.Draw.IgnoreDepth = true;\r\n\t\t\tswitch ( p.Kind )\r\n\t\t\t{\r\n\t\t\t\tcase DebugDrawKind.Line:   Gizmo.Draw.Line( p.A, p.B ); break;\r\n\t\t\t\tcase DebugDrawKind.Ray:    Gizmo.Draw.Arrow( p.A, p.B, 8f, 3f ); break;\r\n\t\t\t\tcase DebugDrawKind.Box:    Gizmo.Draw.LineBBox( new BBox( p.A - p.Size * 0.5f, p.A \u002B p.Size * 0.5f ) ); break;\r\n\t\t\t\tcase DebugDrawKind.Sphere: Gizmo.Draw.LineSphere( new Sphere( p.A, p.Radius ), 16 ); break;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( !Game.IsPlaying || Prims == null ) return;\r\n\t\tvar ov = Scene?.DebugOverlay;\r\n\t\tif ( ov == null ) return;\r\n\t\tconst float dur = 0.1f;                       // refreshed every frame while in the list\r\n\t\tvar tx = global::Transform.Zero;               // identity \u2192 world-space coords (Transform is global-namespace, not Sandbox.*)\r\n\t\tforeach ( var p in Prims )\r\n\t\t{\r\n\t\t\tswitch ( p.Kind )\r\n\t\t\t{\r\n\t\t\t\tcase DebugDrawKind.Line:\r\n\t\t\t\tcase DebugDrawKind.Ray:\r\n\t\t\t\t\tov.Line( p.A, p.B, p.Color, dur, tx, true );\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase DebugDrawKind.Box:\r\n\t\t\t\t\tov.Box( new BBox( p.A - p.Size * 0.5f, p.A \u002B p.Size * 0.5f ), p.Color, dur, tx, true );\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase DebugDrawKind.Sphere:\r\n\t\t\t\t\tov.Sphere( new Sphere( p.A, p.Radius ), p.Color, dur, tx, true );\r\n\t\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n\r\ninternal static class DebugDrawHelpers\r\n{\r\n\tstatic readonly CultureInfo Inv = CultureInfo.InvariantCulture;\r\n\r\n\t// ponytail: one global holder per session, recreated if invalidated by a\r\n\t// scene change / hotload. Debug viz is inherently global, so a single\r\n\t// instance is correct \u2014 no per-call scene scan needed.\r\n\tstatic ClaudeDebugDraw _holder;\r\n\r\n\tpublic static Scene CurrentScene()\r\n\t\t=\u003E Game.IsPlaying ? Game.ActiveScene : SceneEditorSession.Active?.Scene;\r\n\r\n\tpublic static ClaudeDebugDraw EnsureHolder()\r\n\t{\r\n\t\tvar scene = CurrentScene();\r\n\t\tif ( scene == null ) return null;\r\n\t\tif ( _holder.IsValid() \u0026\u0026 _holder.Scene == scene ) return _holder;\r\n\t\tvar go = scene.CreateObject( true );\r\n\t\tgo.Name = \u0022__ClaudeDebugDraw\u0022;\r\n\t\tgo.Flags = GameObjectFlags.NotSaved;\r\n\t\t_holder = go.AddComponent\u003CClaudeDebugDraw\u003E();\r\n\t\treturn _holder;\r\n\t}\r\n\r\n\tpublic static int ClearHolder()\r\n\t{\r\n\t\tint n = 0;\r\n\t\t// cached holder \u2014 reliable for the common same-scene case\r\n\t\tif ( _holder.IsValid() )\r\n\t\t{\r\n\t\t\tn \u002B= _holder.Prims?.Count ?? 0;\r\n\t\t\t_holder.GameObject?.Destroy();\r\n\t\t}\r\n\t\t// plus any holders orphaned by an edit\u2194play scene switch (the static ref\r\n\t\t// only tracks the most recent scene\u0027s holder)\r\n\t\tvar scene = CurrentScene();\r\n\t\tif ( scene != null )\r\n\t\t{\r\n\t\t\tforeach ( var c in scene.GetAllComponents\u003CClaudeDebugDraw\u003E().ToList() )\r\n\t\t\t{\r\n\t\t\t\tif ( c == _holder ) continue;\r\n\t\t\t\tn \u002B= c.Prims?.Count ?? 0;\r\n\t\t\t\tc.GameObject?.Destroy();\r\n\t\t\t}\r\n\t\t}\r\n\t\t_holder = null;\r\n\t\treturn n;\r\n\t}\r\n\r\n\tpublic static bool TryVec( JsonElement p, string key, out Vector3 v )\r\n\t{\r\n\t\tv = Vector3.Zero;\r\n\t\tif ( !p.TryGetProperty( key, out var e ) ) return false;\r\n\t\tswitch ( e.ValueKind )\r\n\t\t{\r\n\t\t\tcase JsonValueKind.String:\r\n\t\t\t\tvar s = e.GetString().Split( \u0027,\u0027 );\r\n\t\t\t\tif ( s.Length \u003C 3 ) return false;\r\n\t\t\t\tv = new Vector3( F( s[0] ), F( s[1] ), F( s[2] ) );\r\n\t\t\t\treturn true;\r\n\t\t\tcase JsonValueKind.Array:\r\n\t\t\t\tif ( e.GetArrayLength() \u003C 3 ) return false;\r\n\t\t\t\tv = new Vector3( (float)e[0].GetDouble(), (float)e[1].GetDouble(), (float)e[2].GetDouble() );\r\n\t\t\t\treturn true;\r\n\t\t\tcase JsonValueKind.Object:\r\n\t\t\t\tv = new Vector3(\r\n\t\t\t\t\t(float)e.GetProperty( \u0022x\u0022 ).GetDouble(),\r\n\t\t\t\t\t(float)e.GetProperty( \u0022y\u0022 ).GetDouble(),\r\n\t\t\t\t\t(float)e.GetProperty( \u0022z\u0022 ).GetDouble() );\r\n\t\t\t\treturn true;\r\n\t\t\tdefault:\r\n\t\t\t\treturn false;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic static Color Col( JsonElement p, string key, Color def )\r\n\t{\r\n\t\tif ( !p.TryGetProperty( key, out var e ) || e.ValueKind != JsonValueKind.String ) return def;\r\n\t\tvar s = e.GetString().Split( \u0027,\u0027 );\r\n\t\tif ( s.Length \u003C 3 ) return def;\r\n\t\tfloat a = s.Length \u003E= 4 ? F( s[3] ) : 1f;\r\n\t\treturn new Color( F( s[0] ), F( s[1] ), F( s[2] ), a );\r\n\t}\r\n\r\n\tpublic static float Flt( JsonElement p, string key, float def )\r\n\t\t=\u003E p.TryGetProperty( key, out var e ) \u0026\u0026 e.ValueKind == JsonValueKind.Number ? (float)e.GetDouble() : def;\r\n\r\n\tstatic float F( string s ) =\u003E float.Parse( s.Trim(), Inv );\r\n}\r\n\r\n// \u2500\u2500 handlers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\npublic class DebugDrawLineHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !DebugDrawHelpers.TryVec( p, \u0022from\u0022, out var a ) || !DebugDrawHelpers.TryVec( p, \u0022to\u0022, out var b ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022from and to are required (\\\u0022x,y,z\\\u0022)\u0022 } );\r\n\t\t\tvar h = DebugDrawHelpers.EnsureHolder();\r\n\t\t\tif ( h == null ) return Task.FromResult\u003Cobject\u003E( new { error = \u0022no active scene\u0022 } );\r\n\t\t\th.Prims.Add( new DebugDrawPrim\r\n\t\t\t{\r\n\t\t\t\tKind = DebugDrawKind.Line, A = a, B = b,\r\n\t\t\t\tColor = DebugDrawHelpers.Col( p, \u0022color\u0022, Color.Yellow ),\r\n\t\t\t\tThickness = DebugDrawHelpers.Flt( p, \u0022thickness\u0022, 2f )\r\n\t\t\t} );\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { drawn = \u0022line\u0022, count = h.Prims.Count, mode = Game.IsPlaying ? \u0022play\u0022 : \u0022edit\u0022 } );\r\n\t\t}\r\n\t\tcatch ( Exception ex ) { return Task.FromResult\u003Cobject\u003E( new { error = $\u0022debug_draw_line failed: {ex.Message}\u0022 } ); }\r\n\t}\r\n}\r\n\r\npublic class DebugDrawRayHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !DebugDrawHelpers.TryVec( p, \u0022origin\u0022, out var o ) || !DebugDrawHelpers.TryVec( p, \u0022direction\u0022, out var d ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022origin and direction are required (\\\u0022x,y,z\\\u0022)\u0022 } );\r\n\t\t\tfloat len = DebugDrawHelpers.Flt( p, \u0022length\u0022, 64f );\r\n\t\t\tvar h = DebugDrawHelpers.EnsureHolder();\r\n\t\t\tif ( h == null ) return Task.FromResult\u003Cobject\u003E( new { error = \u0022no active scene\u0022 } );\r\n\t\t\th.Prims.Add( new DebugDrawPrim\r\n\t\t\t{\r\n\t\t\t\tKind = DebugDrawKind.Ray, A = o, B = o \u002B d.Normal * len,\r\n\t\t\t\tColor = DebugDrawHelpers.Col( p, \u0022color\u0022, Color.Yellow ),\r\n\t\t\t\tThickness = DebugDrawHelpers.Flt( p, \u0022thickness\u0022, 2f )\r\n\t\t\t} );\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { drawn = \u0022ray\u0022, count = h.Prims.Count, mode = Game.IsPlaying ? \u0022play\u0022 : \u0022edit\u0022 } );\r\n\t\t}\r\n\t\tcatch ( Exception ex ) { return Task.FromResult\u003Cobject\u003E( new { error = $\u0022debug_draw_ray failed: {ex.Message}\u0022 } ); }\r\n\t}\r\n}\r\n\r\npublic class DebugDrawBoxHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !DebugDrawHelpers.TryVec( p, \u0022center\u0022, out var c ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022center is required (\\\u0022x,y,z\\\u0022)\u0022 } );\r\n\t\t\tVector3 size = DebugDrawHelpers.TryVec( p, \u0022size\u0022, out var sz ) ? sz : new Vector3( 32f, 32f, 32f );\r\n\t\t\tvar h = DebugDrawHelpers.EnsureHolder();\r\n\t\t\tif ( h == null ) return Task.FromResult\u003Cobject\u003E( new { error = \u0022no active scene\u0022 } );\r\n\t\t\th.Prims.Add( new DebugDrawPrim\r\n\t\t\t{\r\n\t\t\t\tKind = DebugDrawKind.Box, A = c, Size = size,\r\n\t\t\t\tColor = DebugDrawHelpers.Col( p, \u0022color\u0022, Color.Green ),\r\n\t\t\t\tThickness = DebugDrawHelpers.Flt( p, \u0022thickness\u0022, 2f )\r\n\t\t\t} );\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { drawn = \u0022box\u0022, count = h.Prims.Count, mode = Game.IsPlaying ? \u0022play\u0022 : \u0022edit\u0022 } );\r\n\t\t}\r\n\t\tcatch ( Exception ex ) { return Task.FromResult\u003Cobject\u003E( new { error = $\u0022debug_draw_box failed: {ex.Message}\u0022 } ); }\r\n\t}\r\n}\r\n\r\npublic class DebugDrawSphereHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !DebugDrawHelpers.TryVec( p, \u0022center\u0022, out var c ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022center is required (\\\u0022x,y,z\\\u0022)\u0022 } );\r\n\t\t\tfloat r = DebugDrawHelpers.Flt( p, \u0022radius\u0022, 32f );\r\n\t\t\tvar h = DebugDrawHelpers.EnsureHolder();\r\n\t\t\tif ( h == null ) return Task.FromResult\u003Cobject\u003E( new { error = \u0022no active scene\u0022 } );\r\n\t\t\th.Prims.Add( new DebugDrawPrim\r\n\t\t\t{\r\n\t\t\t\tKind = DebugDrawKind.Sphere, A = c, Radius = r,\r\n\t\t\t\tColor = DebugDrawHelpers.Col( p, \u0022color\u0022, Color.Red ),\r\n\t\t\t\tThickness = DebugDrawHelpers.Flt( p, \u0022thickness\u0022, 2f )\r\n\t\t\t} );\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { drawn = \u0022sphere\u0022, count = h.Prims.Count, mode = Game.IsPlaying ? \u0022play\u0022 : \u0022edit\u0022 } );\r\n\t\t}\r\n\t\tcatch ( Exception ex ) { return Task.FromResult\u003Cobject\u003E( new { error = $\u0022debug_draw_sphere failed: {ex.Message}\u0022 } ); }\r\n\t}\r\n}\r\n\r\npublic class DebugClearHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tint removed = DebugDrawHelpers.ClearHolder();\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { cleared = true, removed } );\r\n\t\t}\r\n\t\tcatch ( Exception ex ) { return Task.FromResult\u003Cobject\u003E( new { error = $\u0022debug_clear failed: {ex.Message}\u0022 } ); }\r\n\t}\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/EconomySaveHandlers.cs","FileName":"EconomySaveHandlers.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"using Editor;\r\nusing Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Text;\r\nusing System.Text.Json;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\n\r\n// =============================================================================\r\n//  Economy \u0026 Save family (Track E) -- six Tier-2 scaffolds (code-gen; scene-mutating):\r\n//\r\n//    create_currency_account    audited host-authoritative ledger: [Sync(FromHost)]\r\n//                               balance \u002B Deposit/Withdraw/TryTransfer \u002B fixed-size\r\n//                               transaction ring buffer (Time.Now, reason, amount)\r\n//    create_idle_economy        geometric bulk-buy: BaseCost * Growth^Owned, closed-form\r\n//                               Buy 1 / Buy N / Buy Max, income tick auto-wired to a\r\n//                               sibling wallet via TypeLibrary reflection\r\n//    create_signed_save         tamper-evident save: FNV-1a signature over payload\u002Bsalt,\r\n//                               verify-on-load, clamp Sanitize() hook, forced reset on\r\n//                               mismatch, versioned\r\n//    create_meta_progression    between-runs roguelite meta: persistent meta-currency \u002B\r\n//                               unlock flags, Grant/TrySpend/Unlock/IsUnlocked,\r\n//                               OnUnlocked static event, BankRun(int) run-end seam\r\n//    add_steam_stat_currency    currency persisted over Sandbox.Services.Stats\r\n//                               (SetValue/Flush; read-back via GetLocalPlayerStats)\r\n//    create_loot_table_resource GameResource-based loot tables ([AssetType], .loot files)\r\n//                               with nested-table entries \u002B depth-capped resolver component\r\n//\r\n//  Compiles into the SAME editor assembly as MyEditorMenu.cs / ScaffoldHandlers.cs,\r\n//  so it reuses the shared statics on ClaudeBridge (TryResolveProjectPath,\r\n//  SanitizeIdentifier, SerializeGo) and ScaffoldHelpers (PrepareCodeFile /\r\n//  WriteCode / Utf8NoBom). Handler code here is UNSANDBOXED editor code (System.* fine).\r\n//\r\n//  The C# *strings these handlers WRITE TO DISK* are SANDBOXED game code:\r\n//    - sealed Component classes, no virtual members.\r\n//    - [Sync(SyncFlags.FromHost)] for host-auth state (create_economy_wallet-verified);\r\n//      IsProxy guards on every mutation.\r\n//    - System.Math/MathF compile on this SDK; Array.Clone() is blocked (not used).\r\n//    - FileSystem.Data.ReadJsonOrDefault\u003CT\u003E/WriteJson \u002B ReadAllText/WriteAllText/\r\n//      FileExists/DeleteFile all verified live via describe_type BaseFileSystem.\r\n//    - Sandbox.Json.Serialize(object)/Deserialize\u003CT\u003E(string) verified live.\r\n//    - Sandbox.Services.Stats: Increment(string,double), SetValue(string,double,string,object),\r\n//      Flush(), GetLocalPlayerStats(string packageIdent) -\u003E Stats.PlayerStats (NESTED type;\r\n//      .Get(name) returns Stats.PlayerStat with .Value) -- all verified live. There is NO\r\n//      Stats.LocalPlayer on this SDK.\r\n//    - GameResourceAttribute is [Obsolete] on this SDK -- generated resources use\r\n//      [AssetType( Name=..., Extension=..., Category=... )] (the modern corpus pattern).\r\n//    - TypeLibrary wallet wiring copies the compile-verified create_idle_income shape:\r\n//      Game.TypeLibrary.GetType(comp.GetType()) -\u003E Methods.FirstOrDefault(...) -\u003E\r\n//      Invoke / InvokeWithReturn\u003Cbool\u003E (both verified on MethodDescription);\r\n//      PropertyDescription.GetValue(object) verified live.\r\n//\r\n//  Register(...) lines \u002B the _sceneMutatingCommands additions live in MyEditorMenu.cs\r\n//  (orchestrator integration) to keep the files decoupled -- see the handoff summary.\r\n// =============================================================================\r\n\r\n// -----------------------------------------------------------------------------\r\n// create_currency_account -- the audited sibling of create_economy_wallet.\r\n// Wallet = simple money (AddMoney/TrySpend). Account = money \u002B a fixed-size\r\n// transaction ring buffer (timestamp, reason, amount, balance-after) with\r\n// GetRecentTransactions() for ledger UIs / audit trails, plus TryTransfer\r\n// between accounts. Folds the corpus asks create_economy_ledger / create_currency.\r\n// -----------------------------------------------------------------------------\r\npublic class CreateCurrencyAccountHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022CurrencyAccount\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tlong start = p.TryGetProperty( \u0022startingBalance\u0022, out var sv ) \u0026\u0026 sv.TryGetInt64( out var sl ) ? sl : 0L;\r\n\t\t\tint history = p.TryGetProperty( \u0022historySize\u0022, out var hv ) \u0026\u0026 hv.TryGetInt32( out var hi ) ? hi : 32;\r\n\t\t\tif ( history \u003C 1 ) history = 1;\r\n\t\t\tif ( history \u003E 4096 ) history = 4096;\r\n\r\n\t\t\tvar code = BuildCode( className, start, history );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\tobject placedOn = null; string note = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022targetId\u0022, out var tid ) \u0026\u0026 tid.ValueKind == JsonValueKind.String )\r\n\t\t\t\tplacedOn = EconomySaveHelpers.PlaceOnTarget( tid.GetString(), className, out note );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tstartingBalance = start,\r\n\t\t\t\thistorySize = history,\r\n\t\t\t\tplacedOn,\r\n\t\t\t\tnote,\r\n\t\t\t\tnextSteps = new[]\r\n\t\t\t\t{\r\n\t\t\t\t\t$\u0022trigger_hotload to compile {className} into the game assembly.\u0022,\r\n\t\t\t\t\tplacedOn != null\r\n\t\t\t\t\t\t? $\u0022{className} was attached to the target GameObject.\u0022\r\n\t\t\t\t\t\t: $\u0022Place it on a per-player or bank GameObject: add_component_to_new_object (component=\\\u0022{className}\\\u0022) after the hotload, or re-run with targetId.\u0022,\r\n\t\t\t\t\t$\u0022Move money host-side: GetComponent\u003C{className}\u003E()?.Deposit( 100, \\\u0022quest reward\\\u0022 ); .Withdraw( 50, \\\u0022shop\\\u0022 ); .TryTransfer( other, 25, \\\u0022trade\\\u0022 );\u0022,\r\n\t\t\t\t\t$\u0022Read the ledger (host-side, newest first): foreach ( var t in GetComponent\u003C{className}\u003E().GetRecentTransactions() ) Log.Info( $\\\u0022{{t.Time}} {{t.Amount}} {{t.Reason}} -\u003E {{t.BalanceAfter}}\\\u0022 );\u0022,\r\n\t\t\t\t\t$\u0022Bind a HUD: GetComponent\u003C{className}\u003E().OnBalanceChanged = bal =\u003E {{ /* update label */ }}; Balance is [Sync(FromHost)] so clients can read it directly.\u0022,\r\n\t\t\t\t\t$\u0022History keeps the last {history} transactions (HistorySize, fixed once the first transaction is recorded); older entries are overwritten silently. The ledger itself is host-side only -- it does not replicate.\u0022\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_currency_account failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string BuildCode( string className, long start, int history )\r\n\t{\r\n\t\tvar ci = System.Globalization.CultureInfo.InvariantCulture;\r\n\t\tstring st = start.ToString( ci );\r\n\t\tstring hs = history.ToString( ci );\r\n\r\n\t\treturn $@\u0022using Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} -- a host-authoritative currency ACCOUNT: an audited ledger.\r\n///\r\n/// Use create_economy_wallet\u0027s Wallet when you just need money; use this when you need\r\n/// money PLUS an audit trail. Balance is [Sync(SyncFlags.FromHost)] so only the host\r\n/// writes it (clients can\u0027t author their own balance); every Deposit / Withdraw /\r\n/// TryTransfer records a Transaction (Time.Now, reason, signed amount, balance-after)\r\n/// into a fixed-size ring buffer, newest overwriting oldest past HistorySize.\r\n///\r\n/// The ledger is HOST-SIDE ONLY -- it does not replicate. Balance replicates; feed a\r\n/// client-side ledger UI over an RPC if you need remote history. Single-player safe\r\n/// (IsProxy is false with no networking active).\r\n///\r\n/// Usage (host-side):\r\n///   GetComponent\u0026lt;{className}\u0026gt;()?.Deposit( 100, \u0022\u0022quest reward\u0022\u0022 );\r\n///   if ( GetComponent\u0026lt;{className}\u0026gt;().Withdraw( 50, \u0022\u0022shop\u0022\u0022 ) ) {{ /* grant the item */ }}\r\n///   GetComponent\u0026lt;{className}\u0026gt;().TryTransfer( otherAccount, 25, \u0022\u0022trade\u0022\u0022 );\r\n///   foreach ( var t in GetComponent\u0026lt;{className}\u0026gt;().GetRecentTransactions() ) {{ /* newest first */ }}\r\n/// \u003C/summary\u003E\r\npublic sealed class {className} : Component\r\n{{\r\n\t/// Balance the account opens with (host seeds it in OnStart).\r\n\t[Property] public long StartingBalance {{ get; set; }} = {st}L;\r\n\r\n\t/// Ring-buffer capacity. Fixed once the first transaction is recorded.\r\n\t[Property] public int HistorySize {{ get; set; }} = {hs};\r\n\r\n\t// Host-authoritative balance -- replicates to clients, only the host writes.\r\n\t[Sync( SyncFlags.FromHost )] public long Balance {{ get; set; }}\r\n\r\n\t/// One ledger line. Amount is signed: positive = deposit, negative = withdrawal.\r\n\tpublic struct Transaction\r\n\t{{\r\n\t\tpublic float Time;          // Time.Now when recorded\r\n\t\tpublic long Amount;         // signed delta\r\n\t\tpublic string Reason;       // free-form audit string\r\n\t\tpublic long BalanceAfter;   // balance after applying the delta\r\n\t}}\r\n\r\n\t/// Fired (on the writing machine) whenever the balance changes -- bind a HUD here.\r\n\tpublic Action\u003Clong\u003E OnBalanceChanged {{ get; set; }}\r\n\r\n\t// Host-side ring buffer. _head = next write slot, _count = filled slots.\r\n\tprivate Transaction[] _history;\r\n\tprivate int _head;\r\n\tprivate int _count;\r\n\r\n\tprotected override void OnStart()\r\n\t{{\r\n\t\tif ( IsProxy ) return;            // only the authority seeds the balance\r\n\t\tBalance = StartingBalance;\r\n\t\tif ( StartingBalance != 0 ) Record( StartingBalance, \u0022\u0022opening balance\u0022\u0022 );\r\n\t\tOnBalanceChanged?.Invoke( Balance );\r\n\t}}\r\n\r\n\tpublic bool CanAfford( long amount ) =\u003E Balance \u003E= amount;\r\n\r\n\t/// \u003Csummary\u003EDeposit (host-authoritative). Non-positive amounts are ignored.\u003C/summary\u003E\r\n\tpublic void Deposit( long amount, string reason = \u0022\u0022deposit\u0022\u0022 )\r\n\t{{\r\n\t\tif ( IsProxy || amount \u003C= 0 ) return;\r\n\t\tBalance \u002B= amount;\r\n\t\tRecord( amount, reason );\r\n\t\tOnBalanceChanged?.Invoke( Balance );\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003EWithdraw if affordable; returns false and changes nothing if not (host-authoritative).\u003C/summary\u003E\r\n\tpublic bool Withdraw( long amount, string reason = \u0022\u0022withdraw\u0022\u0022 )\r\n\t{{\r\n\t\tif ( IsProxy || amount \u003C= 0 ) return false;\r\n\t\tif ( Balance \u003C amount ) return false;\r\n\t\tBalance -= amount;\r\n\t\tRecord( -amount, reason );\r\n\t\tOnBalanceChanged?.Invoke( Balance );\r\n\t\treturn true;\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Atomically move money into another account (host-authoritative). Both legs are\r\n\t/// recorded in their respective ledgers. Returns false (nothing moves) when the\r\n\t/// target is missing/self, the amount is non-positive, or funds are short.\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool TryTransfer( {className} to, long amount, string reason = \u0022\u0022transfer\u0022\u0022 )\r\n\t{{\r\n\t\tif ( IsProxy || to == null || to == this || amount \u003C= 0 ) return false;\r\n\t\tif ( Balance \u003C amount ) return false;\r\n\t\tBalance -= amount;\r\n\t\tRecord( -amount, reason );\r\n\t\tOnBalanceChanged?.Invoke( Balance );\r\n\t\tto.ReceiveTransfer( amount, reason );\r\n\t\treturn true;\r\n\t}}\r\n\r\n\t// The receiving leg of TryTransfer -- runs on the host alongside the sending leg.\r\n\tprivate void ReceiveTransfer( long amount, string reason )\r\n\t{{\r\n\t\tBalance \u002B= amount;\r\n\t\tRecord( amount, reason );\r\n\t\tOnBalanceChanged?.Invoke( Balance );\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The most recent transactions, NEWEST FIRST. max = 0 returns everything retained\r\n\t/// (up to HistorySize). Host-side only -- proxies always get an empty list.\r\n\t/// \u003C/summary\u003E\r\n\tpublic List\u003CTransaction\u003E GetRecentTransactions( int max = 0 )\r\n\t{{\r\n\t\tvar list = new List\u003CTransaction\u003E();\r\n\t\tif ( _history == null || _count == 0 ) return list;\r\n\t\tint take = _count;\r\n\t\tif ( max \u003E 0 \u0026\u0026 max \u003C take ) take = max;\r\n\t\tfor ( int i = 0; i \u003C take; i\u002B\u002B )\r\n\t\t{{\r\n\t\t\tint idx = ( _head - 1 - i \u002B _history.Length * 2 ) % _history.Length;\r\n\t\t\tlist.Add( _history[idx] );\r\n\t\t}}\r\n\t\treturn list;\r\n\t}}\r\n\r\n\tprivate void Record( long amount, string reason )\r\n\t{{\r\n\t\tif ( _history == null )\r\n\t\t\t_history = new Transaction[HistorySize \u003C 1 ? 1 : HistorySize];\r\n\r\n\t\t_history[_head] = new Transaction\r\n\t\t{{\r\n\t\t\tTime = Time.Now,\r\n\t\t\tAmount = amount,\r\n\t\t\tReason = reason ?? \u0022\u0022\u0022\u0022,\r\n\t\t\tBalanceAfter = Balance\r\n\t\t}};\r\n\t\t_head = ( _head \u002B 1 ) % _history.Length;\r\n\t\tif ( _count \u003C _history.Length ) _count\u002B\u002B;\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n// -----------------------------------------------------------------------------\r\n// create_idle_economy -- geometric bulk-buy purchasing. Generators follow the\r\n// classic BaseCost * Growth^Owned curve; Buy 1 / Buy N / Buy Max use the\r\n// closed-form geometric series (no loops). Income ticks grant into a sibling\r\n// wallet\u0027s AddMoney via TypeLibrary reflection (the compile-verified\r\n// create_idle_income pattern); purchases spend via the sibling\u0027s TrySpend and\r\n// Buy Max reads its Money property the same way.\r\n// -----------------------------------------------------------------------------\r\npublic class CreateIdleEconomyHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar ci = System.Globalization.CultureInfo.InvariantCulture;\r\n\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022IdleEconomy\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tfloat tick = p.TryGetProperty( \u0022tickSeconds\u0022, out var tv ) \u0026\u0026 tv.TryGetSingle( out var tf ) ? tf : 1f;\r\n\t\t\tif ( tick \u003C 0.1f ) tick = 0.1f;\r\n\r\n\t\t\tvar gens = ParseGenerators( p );\r\n\r\n\t\t\tvar code = BuildCode( className, gens, tick, ci );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\tobject placedOn = null; string note = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022targetId\u0022, out var tid ) \u0026\u0026 tid.ValueKind == JsonValueKind.String )\r\n\t\t\t\tplacedOn = EconomySaveHelpers.PlaceOnTarget( tid.GetString(), className, out note );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tgenerators = gens.Select( g =\u003E g.Name ).ToArray(),\r\n\t\t\t\ttickSeconds = tick,\r\n\t\t\t\tplacedOn,\r\n\t\t\t\tnote,\r\n\t\t\t\tnextSteps = new[]\r\n\t\t\t\t{\r\n\t\t\t\t\t$\u0022trigger_hotload to compile {className} into the game assembly.\u0022,\r\n\t\t\t\t\tplacedOn != null\r\n\t\t\t\t\t\t? $\u0022{className} was attached to the target GameObject.\u0022\r\n\t\t\t\t\t\t: $\u0022Place it NEXT TO a wallet component (create_economy_wallet / create_currency_account) on the same GameObject: add_component_to_new_object (component=\\\u0022{className}\\\u0022) after the hotload, or re-run with targetId.\u0022,\r\n\t\t\t\t\t\u0022It auto-wires the sibling wallet by reflection: income invokes AddMoney(long|int), purchases invoke TrySpend(long|int), Buy Max reads the Money property. No wallet sibling = purchases refused with a Log.Warning (never silent).\u0022,\r\n\t\t\t\t\t$\u0022Buy from game code: GetComponent\u003C{className}\u003E().TryBuy( 0, 1 ); .TryBuy( 0, 10 ); int n = GetComponent\u003C{className}\u003E().BuyMax( 0 );\u0022,\r\n\t\t\t\t\t$\u0022Show prices: double cost = GetComponent\u003C{className}\u003E().CostOf( 0, 10 ); int max = GetComponent\u003C{className}\u003E().MaxAffordable( 0 ); -- both closed-form geometric series, no loops.\u0022,\r\n\t\t\t\t\t$\u0022React to events: {className}.OnPurchased \u002B= ( index, count, cost ) =\u003E {{ }}; {className}.OnIncomeTick \u002B= ( amount, total ) =\u003E {{ }};\u0022,\r\n\t\t\t\t\t\u0022Tune GeneratorNames / BaseCosts / Growths / IncomesPerSecond (parallel lists) in the inspector or with set_property. Owned counts are host-side state (not replicated); pair with create_offline_progress for away-time earnings.\u0022\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_idle_economy failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tinternal struct GeneratorDef { public string Name; public float BaseCost; public float Growth; public float IncomePerSecond; }\r\n\r\n\tstatic List\u003CGeneratorDef\u003E ParseGenerators( JsonElement p )\r\n\t{\r\n\t\tvar result = new List\u003CGeneratorDef\u003E();\r\n\t\tif ( p.TryGetProperty( \u0022generators\u0022, out var gv ) \u0026\u0026 gv.ValueKind == JsonValueKind.Array )\r\n\t\t{\r\n\t\t\tforeach ( var item in gv.EnumerateArray() )\r\n\t\t\t{\r\n\t\t\t\tvar g = new GeneratorDef\r\n\t\t\t\t{\r\n\t\t\t\t\tName = item.TryGetProperty( \u0022name\u0022, out var nv ) \u0026\u0026 !string.IsNullOrWhiteSpace( nv.GetString() ) ? nv.GetString() : \u0022Generator\u0022,\r\n\t\t\t\t\tBaseCost = item.TryGetProperty( \u0022baseCost\u0022, out var bv ) \u0026\u0026 bv.TryGetSingle( out var bf ) ? bf : 15f,\r\n\t\t\t\t\tGrowth = item.TryGetProperty( \u0022growth\u0022, out var grv ) \u0026\u0026 grv.TryGetSingle( out var grf ) ? grf : 1.15f,\r\n\t\t\t\t\tIncomePerSecond = item.TryGetProperty( \u0022incomePerSecond\u0022, out var iv ) \u0026\u0026 iv.TryGetSingle( out var inf ) ? inf : 0.5f\r\n\t\t\t\t};\r\n\t\t\t\t// Escape-strip: the name is baked into a generated string literal.\r\n\t\t\t\tg.Name = ( g.Name ?? \u0022Generator\u0022 ).Replace( \u0022\\\\\u0022, \u0022\u0022 ).Replace( \u0022\\\u0022\u0022, \u0022\u0022 );\r\n\t\t\t\tif ( g.BaseCost \u003C= 0f ) g.BaseCost = 1f;\r\n\t\t\t\tif ( g.Growth \u003C 1f ) g.Growth = 1f;\r\n\t\t\t\tif ( g.IncomePerSecond \u003C 0f ) g.IncomePerSecond = 0f;\r\n\t\t\t\tresult.Add( g );\r\n\t\t\t}\r\n\t\t}\r\n\t\tif ( result.Count == 0 )\r\n\t\t{\r\n\t\t\tresult.Add( new GeneratorDef { Name = \u0022Cursor\u0022, BaseCost = 15f, Growth = 1.15f, IncomePerSecond = 0.5f } );\r\n\t\t\tresult.Add( new GeneratorDef { Name = \u0022Farm\u0022, BaseCost = 200f, Growth = 1.15f, IncomePerSecond = 4f } );\r\n\t\t\tresult.Add( new GeneratorDef { Name = \u0022Factory\u0022, BaseCost = 3000f, Growth = 1.12f, IncomePerSecond = 30f } );\r\n\t\t}\r\n\t\treturn result;\r\n\t}\r\n\r\n\tstatic string BuildCode( string className, List\u003CGeneratorDef\u003E gens, float tick, System.Globalization.CultureInfo ci )\r\n\t{\r\n\t\tstring nameLits = string.Join( \u0022, \u0022, gens.Select( g =\u003E $\u0022\\\u0022{g.Name}\\\u0022\u0022 ) );\r\n\t\tstring costLits = string.Join( \u0022, \u0022, gens.Select( g =\u003E g.BaseCost.ToString( ci ) \u002B \u0022f\u0022 ) );\r\n\t\tstring growthLits = string.Join( \u0022, \u0022, gens.Select( g =\u003E g.Growth.ToString( ci ) \u002B \u0022f\u0022 ) );\r\n\t\tstring incomeLits = string.Join( \u0022, \u0022, gens.Select( g =\u003E g.IncomePerSecond.ToString( ci ) \u002B \u0022f\u0022 ) );\r\n\t\tstring tk = tick.ToString( ci ) \u002B \u0022f\u0022;\r\n\r\n\t\treturn $@\u0022using Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} -- a geometric idle economy: generators, bulk buying, passive income.\r\n///\r\n/// COST CURVE: buying copy k of generator i costs BaseCosts[i] * Growths[i]^k -- the\r\n/// classic incremental-game curve. CostOf / MaxAffordable / TryBuy all use the CLOSED-FORM\r\n/// geometric series (no per-copy loops), so Buy 1000 is the same math as Buy 1:\r\n///   cost(n)  = c0 * (g^n - 1) / (g - 1)        where c0 = BaseCost * g^Owned\r\n///   buyMax   = floor( log_g( funds*(g-1)/c0 \u002B 1 ) )\r\n///\r\n/// WALLET WIRING (TypeLibrary reflection -- no compile-time wallet dependency): income\r\n/// invokes AddMoney(long|int) on the first sibling component that has one; purchases\r\n/// invoke TrySpend(long|int); Buy Max reads the sibling\u0027s Money property. Works out of\r\n/// the box next to a create_economy_wallet or create_currency_account scaffold. No wallet\r\n/// sibling = purchases are REFUSED with a Log.Warning (never silent).\r\n///\r\n/// HOST-AUTHORITATIVE: all mutation is IsProxy-guarded; owned counts are host-side state\r\n/// (not replicated -- replicate via your own [Sync]/RPC if clients need them). TotalEarned\r\n/// is [Sync(FromHost)]. Single-player safe.\r\n///\r\n/// Usage:\r\n///   GetComponent\u0026lt;{className}\u0026gt;().TryBuy( 0, 1 );          // Buy 1\r\n///   GetComponent\u0026lt;{className}\u0026gt;().TryBuy( 0, 10 );         // Buy N\r\n///   int bought = GetComponent\u0026lt;{className}\u0026gt;().BuyMax( 0 ); // Buy Max\r\n///   {className}.OnPurchased \u002B= ( i, count, cost ) =\u003E {{ /* refresh shop UI */ }};\r\n///   {className}.OnIncomeTick \u002B= ( amount, total ) =\u003E {{ /* \u002BN popup */ }};\r\n/// \u003C/summary\u003E\r\npublic sealed class {className} : Component\r\n{{\r\n\t/// Generator display names -- parallel to BaseCosts / Growths / IncomesPerSecond.\r\n\t[Property] public List\u003Cstring\u003E GeneratorNames {{ get; set; }} = new List\u003Cstring\u003E {{ {nameLits} }};\r\n\r\n\t/// Cost of the FIRST copy of each generator (curve: BaseCost * Growth^Owned).\r\n\t[Property] public List\u003Cfloat\u003E BaseCosts {{ get; set; }} = new List\u003Cfloat\u003E {{ {costLits} }};\r\n\r\n\t/// Per-copy cost multiplier (1.15 = the classic curve). Values below 1 are treated as 1 (flat cost).\r\n\t[Property] public List\u003Cfloat\u003E Growths {{ get; set; }} = new List\u003Cfloat\u003E {{ {growthLits} }};\r\n\r\n\t/// Income each owned copy produces per second.\r\n\t[Property] public List\u003Cfloat\u003E IncomesPerSecond {{ get; set; }} = new List\u003Cfloat\u003E {{ {incomeLits} }};\r\n\r\n\t/// Seconds between income grants.\r\n\t[Property] public float TickSeconds {{ get; set; }} = {tk};\r\n\r\n\t/// Total income ever granted (host-authoritative, replicates to clients).\r\n\t[Sync( SyncFlags.FromHost )] public float TotalEarned {{ get; set; }}\r\n\r\n\t/// Fires host-side after a purchase: (generatorIndex, countBought, totalCost).\r\n\tpublic static Action\u003Cint, int, double\u003E OnPurchased {{ get; set; }}\r\n\r\n\t/// Fires host-side after each income grant: (amount, newTotalEarned).\r\n\tpublic static Action\u003Cfloat, float\u003E OnIncomeTick {{ get; set; }}\r\n\r\n\t// Host-side owned counts, parallel to the property lists.\r\n\tprivate int[] _owned;\r\n\tprivate TimeUntil _nextTick;\r\n\r\n\tprotected override void OnStart()\r\n\t{{\r\n\t\t_nextTick = TickSeconds;\r\n\t}}\r\n\r\n\tprotected override void OnFixedUpdate()\r\n\t{{\r\n\t\tif ( IsProxy ) return;\r\n\t\tif ( !_nextTick ) return;\r\n\t\t_nextTick = TickSeconds;\r\n\r\n\t\tEnsureOwned();\r\n\t\tfloat amount = 0f;\r\n\t\tfor ( int i = 0; i \u003C _owned.Length; i\u002B\u002B )\r\n\t\t\tamount \u002B= _owned[i] * IncomeOf( i ) * TickSeconds;\r\n\t\tif ( amount \u003C= 0f ) return;\r\n\r\n\t\tTotalEarned \u002B= amount;\r\n\t\tGrantIncome( amount );\r\n\t\tOnIncomeTick?.Invoke( amount, TotalEarned );\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003ECopies of a generator owned (host-side state; 0 on proxies).\u003C/summary\u003E\r\n\tpublic int GetOwned( int index )\r\n\t{{\r\n\t\tEnsureOwned();\r\n\t\treturn index \u003E= 0 \u0026\u0026 index \u003C _owned.Length ? _owned[index] : 0;\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Closed-form cost of the next \u0060count\u0060 copies of generator \u0060index\u0060 from the current\r\n\t/// owned count. 0 for an invalid index or non-positive count.\r\n\t/// \u003C/summary\u003E\r\n\tpublic double CostOf( int index, int count )\r\n\t{{\r\n\t\tif ( count \u003C= 0 || !ValidIndex( index ) ) return 0.0;\r\n\t\tdouble g = GrowthOf( index );\r\n\t\tdouble c0 = BaseCosts[index] * Math.Pow( g, GetOwned( index ) );\r\n\t\tif ( Math.Abs( g - 1.0 ) \u003C 0.0001 ) return c0 * count;\r\n\t\treturn c0 * ( Math.Pow( g, count ) - 1.0 ) / ( g - 1.0 );\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Closed-form Buy-Max count against the sibling wallet\u0027s current Money.\r\n\t/// 0 when nothing is affordable or no wallet sibling exposes a Money property.\r\n\t/// \u003C/summary\u003E\r\n\tpublic int MaxAffordable( int index )\r\n\t{{\r\n\t\tif ( !ValidIndex( index ) ) return 0;\r\n\t\tdouble funds = ReadWalletBalance();\r\n\t\tif ( funds \u003C= 0.0 ) return 0;\r\n\t\tdouble g = GrowthOf( index );\r\n\t\tdouble c0 = BaseCosts[index] * Math.Pow( g, GetOwned( index ) );\r\n\t\tif ( c0 \u003C= 0.0 ) return 0;\r\n\t\tif ( Math.Abs( g - 1.0 ) \u003C 0.0001 ) return (int) Math.Floor( funds / c0 );\r\n\t\treturn (int) Math.Floor( Math.Log( funds * ( g - 1.0 ) / c0 \u002B 1.0 ) / Math.Log( g ) );\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Buy \u0060count\u0060 copies if the sibling wallet\u0027s TrySpend accepts the closed-form cost\r\n\t/// (rounded up to whole currency). Host-only; false when unaffordable or no wallet.\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool TryBuy( int index, int count )\r\n\t{{\r\n\t\tif ( IsProxy || count \u003C= 0 || !ValidIndex( index ) ) return false;\r\n\t\tEnsureOwned();\r\n\t\tdouble cost = CostOf( index, count );\r\n\t\tif ( !SpendFromWallet( cost ) ) return false;\r\n\t\t_owned[index] \u002B= count;\r\n\t\tOnPurchased?.Invoke( index, count, cost );\r\n\t\treturn true;\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Buy as many copies as the wallet can afford. Returns the count bought (0 = none).\r\n\t/// Steps down once past a whole-currency rounding edge rather than failing.\r\n\t/// \u003C/summary\u003E\r\n\tpublic int BuyMax( int index )\r\n\t{{\r\n\t\tint n = MaxAffordable( index );\r\n\t\twhile ( n \u003E 0 )\r\n\t\t{{\r\n\t\t\tif ( TryBuy( index, n ) ) return n;\r\n\t\t\tn--;   // ceil-rounding edge: the closed form said n, the wallet said no -- step down\r\n\t\t}}\r\n\t\treturn 0;\r\n\t}}\r\n\r\n\tprivate bool ValidIndex( int index )\r\n\t\t=\u003E BaseCosts != null \u0026\u0026 index \u003E= 0 \u0026\u0026 index \u003C BaseCosts.Count;\r\n\r\n\tprivate double GrowthOf( int index )\r\n\t{{\r\n\t\tfloat g = Growths != null \u0026\u0026 index \u003C Growths.Count ? Growths[index] : 1.15f;\r\n\t\treturn g \u003C 1f ? 1.0 : g;\r\n\t}}\r\n\r\n\tprivate float IncomeOf( int index )\r\n\t\t=\u003E IncomesPerSecond != null \u0026\u0026 index \u003C IncomesPerSecond.Count \u0026\u0026 index \u003E= 0 ? IncomesPerSecond[index] : 0f;\r\n\r\n\tprivate void EnsureOwned()\r\n\t{{\r\n\t\tint size = BaseCosts?.Count ?? 0;\r\n\t\tint names = GeneratorNames?.Count ?? 0;\r\n\t\tif ( names \u003E size ) size = names;\r\n\t\tif ( size \u003C 1 ) size = 1;\r\n\r\n\t\tif ( _owned == null )\r\n\t\t{{\r\n\t\t\t_owned = new int[size];\r\n\t\t}}\r\n\t\telse if ( _owned.Length \u003C size )\r\n\t\t{{\r\n\t\t\tvar grown = new int[size];\r\n\t\t\tfor ( int i = 0; i \u003C _owned.Length; i\u002B\u002B ) grown[i] = _owned[i];\r\n\t\t\t_owned = grown;\r\n\t\t}}\r\n\t}}\r\n\r\n\t// ---- sibling-wallet wiring (TypeLibrary reflection; no hard wallet dependency) ----\r\n\r\n\t// Deliver income: AddMoney(long|int) on the first sibling that has one.\r\n\tprivate void GrantIncome( float amount )\r\n\t{{\r\n\t\tforeach ( var comp in Components.GetAll() )\r\n\t\t{{\r\n\t\t\tif ( comp == this || comp is null ) continue;\r\n\t\t\tvar type = Game.TypeLibrary?.GetType( comp.GetType() );\r\n\t\t\tvar method = type?.Methods?.FirstOrDefault( m =\u003E m.Name == \u0022\u0022AddMoney\u0022\u0022 );\r\n\t\t\tif ( method == null ) continue;\r\n\t\t\ttry {{ method.Invoke( comp, new object[] {{ (long) amount }} ); return; }}\r\n\t\t\tcatch {{ }}\r\n\t\t\ttry {{ method.Invoke( comp, new object[] {{ (int) amount }} ); return; }}\r\n\t\t\tcatch {{ /* wrong signature -- keep looking */ }}\r\n\t\t}}\r\n\t\t// No wallet sibling -- TotalEarned still accumulates; read it directly.\r\n\t}}\r\n\r\n\t// Spend: TrySpend(long|int) on the first sibling that has one. Never silent on failure.\r\n\tprivate bool SpendFromWallet( double cost )\r\n\t{{\r\n\t\tif ( cost \u003C= 0.0 ) return false;\r\n\t\tlong rounded = (long) Math.Ceiling( cost );\r\n\t\tforeach ( var comp in Components.GetAll() )\r\n\t\t{{\r\n\t\t\tif ( comp == this || comp is null ) continue;\r\n\t\t\tvar type = Game.TypeLibrary?.GetType( comp.GetType() );\r\n\t\t\tvar method = type?.Methods?.FirstOrDefault( m =\u003E m.Name == \u0022\u0022TrySpend\u0022\u0022 );\r\n\t\t\tif ( method == null ) continue;\r\n\t\t\ttry {{ return method.InvokeWithReturn\u003Cbool\u003E( comp, new object[] {{ rounded }} ); }}\r\n\t\t\tcatch {{ }}\r\n\t\t\ttry {{ return method.InvokeWithReturn\u003Cbool\u003E( comp, new object[] {{ (int) rounded }} ); }}\r\n\t\t\tcatch {{ /* wrong signature -- keep looking */ }}\r\n\t\t}}\r\n\t\tLog.Warning( $\u0022\u0022[{className}] No sibling wallet with TrySpend found -- add a create_economy_wallet / create_currency_account component next to it. Purchase refused.\u0022\u0022 );\r\n\t\treturn false;\r\n\t}}\r\n\r\n\t// Read funds for Buy Max: the first sibling exposing a numeric Money property.\r\n\tprivate double ReadWalletBalance()\r\n\t{{\r\n\t\tforeach ( var comp in Components.GetAll() )\r\n\t\t{{\r\n\t\t\tif ( comp == this || comp is null ) continue;\r\n\t\t\tvar type = Game.TypeLibrary?.GetType( comp.GetType() );\r\n\t\t\tvar prop = type?.Properties?.FirstOrDefault( pp =\u003E pp.Name == \u0022\u0022Money\u0022\u0022 || pp.Name == \u0022\u0022Balance\u0022\u0022 );\r\n\t\t\tif ( prop == null ) continue;\r\n\t\t\ttry\r\n\t\t\t{{\r\n\t\t\t\tobject v = prop.GetValue( comp );\r\n\t\t\t\tif ( v is long l ) return l;\r\n\t\t\t\tif ( v is int i ) return i;\r\n\t\t\t\tif ( v is float f ) return f;\r\n\t\t\t\tif ( v is double d ) return d;\r\n\t\t\t}}\r\n\t\t\tcatch {{ /* unreadable -- keep looking */ }}\r\n\t\t}}\r\n\t\treturn 0.0;\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n// -----------------------------------------------------------------------------\r\n// create_signed_save -- tamper-evident save file. The payload POCO is serialized\r\n// to JSON (Sandbox.Json), FNV-1a-64 hashed together with a salt \u002B version, and\r\n// written inside a signed envelope via FileSystem.Data. Load verifies the\r\n// signature; a mismatch = forced reset (delete \u002B defaults) \u002B OnTampered event.\r\n// Clamp-on-load Sanitize() hook \u002B versioning copy create_save_system\u0027s shape.\r\n// -----------------------------------------------------------------------------\r\npublic class CreateSignedSaveHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022SignedSave\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tvar ci = System.Globalization.CultureInfo.InvariantCulture;\r\n\t\t\tstring fileName = p.TryGetProperty( \u0022fileName\u0022, out var fn ) \u0026\u0026 !string.IsNullOrWhiteSpace( fn.GetString() ) ? fn.GetString() : \u0022save_signed.json\u0022;\r\n\t\t\tint version = p.TryGetProperty( \u0022version\u0022, out var vv ) \u0026\u0026 vv.TryGetInt32( out var vi ) ? vi : 1;\r\n\t\t\tfloat autosave = p.TryGetProperty( \u0022autosaveSeconds\u0022, out var av ) \u0026\u0026 av.TryGetSingle( out var af ) ? af : 10f;\r\n\t\t\tstring salt = p.TryGetProperty( \u0022salt\u0022, out var sv ) \u0026\u0026 !string.IsNullOrWhiteSpace( sv.GetString() )\r\n\t\t\t\t? sv.GetString()\r\n\t\t\t\t: Guid.NewGuid().ToString( \u0022N\u0022 );   // unique per generated file by default\r\n\r\n\t\t\t// These are baked into generated string literals -- strip escape characters.\r\n\t\t\tfileName = fileName.Replace( \u0022\\\\\u0022, \u0022\u0022 ).Replace( \u0022\\\u0022\u0022, \u0022\u0022 );\r\n\t\t\tsalt = salt.Replace( \u0022\\\\\u0022, \u0022\u0022 ).Replace( \u0022\\\u0022\u0022, \u0022\u0022 );\r\n\r\n\t\t\tvar code = BuildCode( className, fileName, version.ToString( ci ), autosave.ToString( ci ) \u002B \u0022f\u0022, salt );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\tobject placedOn = null; string note = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022targetId\u0022, out var tid ) \u0026\u0026 tid.ValueKind == JsonValueKind.String )\r\n\t\t\t\tplacedOn = EconomySaveHelpers.PlaceOnTarget( tid.GetString(), className, out note );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tfileName,\r\n\t\t\t\tversion,\r\n\t\t\t\tautosaveSeconds = autosave,\r\n\t\t\t\tplacedOn,\r\n\t\t\t\tnote,\r\n\t\t\t\tnextSteps = new[]\r\n\t\t\t\t{\r\n\t\t\t\t\t$\u0022trigger_hotload to compile {className} into the game assembly.\u0022,\r\n\t\t\t\t\tplacedOn != null\r\n\t\t\t\t\t\t? $\u0022{className} was attached to the target GameObject.\u0022\r\n\t\t\t\t\t\t: $\u0022Place it on your save-manager GameObject: add_component_to_new_object (component=\\\u0022{className}\\\u0022) after the hotload, or re-run with targetId.\u0022,\r\n\t\t\t\t\t$\u0022Add your game fields to the SaveData inner class in {className}.cs, extend Sanitize() to clamp them, and bump Version when the shape changes.\u0022,\r\n\t\t\t\t\t$\u0022Use it: GetComponent\u003C{className}\u003E().Data.Money \u002B= 100; GetComponent\u003C{className}\u003E().MarkDirty(); -- the dirty-flag autosave (or OnDestroy) writes and re-signs.\u0022,\r\n\t\t\t\t\t$\u0022React: {className}.OnLoaded \u002B= d =\u003E {{ }}; {className}.OnSaved \u002B= d =\u003E {{ }}; {className}.OnTampered \u002B= reason =\u003E {{ /* tell the player their save was reset */ }};\u0022,\r\n\t\t\t\t\t\u0022TAMPER = FORCED RESET: an edited payload fails the FNV-1a signature check on load, the file is DELETED and defaults are used (OnTampered fires with the reason). This is tamper-EVIDENT, not cryptographically secure -- the salt ships in the game code, so a determined user can re-sign; it stops casual notepad edits, not reverse engineers.\u0022\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_signed_save failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string BuildCode( string className, string fileName, string version, string autosave, string salt )\r\n\t{\r\n\t\treturn $@\u0022using Sandbox;\r\nusing System;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} -- a tamper-evident, versioned save system.\r\n///\r\n/// The SaveData payload is serialized to JSON, hashed with FNV-1a-64 over\r\n/// payload \u002B version \u002B salt, and written inside a signed envelope to\r\n/// FileSystem.Data. Load re-computes the signature: a mismatch (hand-edited or\r\n/// corrupt file) triggers a FORCED RESET -- the file is deleted, defaults are\r\n/// used, and the static OnTampered event fires. A version mismatch starts fresh\r\n/// (add migrations in Load if you need them). Loaded values pass through the\r\n/// Sanitize() clamp hook so even a re-signed save can\u0027t smuggle absurd values.\r\n///\r\n/// NOT cryptography: the salt ships inside the game assembly, so this is\r\n/// tamper-EVIDENT (stops notepad edits), not tamper-PROOF.\r\n///\r\n/// Host/owner-only (IsProxy-guarded). Dirty-flag autosave every AutosaveSeconds\r\n/// plus a final save in OnDestroy.\r\n///\r\n/// Usage:\r\n///   var save = GetComponent\u0026lt;{className}\u0026gt;();\r\n///   save.Data.Money \u002B= 100; save.MarkDirty();\r\n///   {className}.OnTampered \u002B= reason =\u003E Log.Warning( $\u0022\u0022save reset: {{reason}}\u0022\u0022 );\r\n/// \u003C/summary\u003E\r\npublic sealed class {className} : Component\r\n{{\r\n\t/// FileSystem.Data path the signed envelope is written to.\r\n\t[Property] public string FileName {{ get; set; }} = \u0022\u0022{fileName}\u0022\u0022;\r\n\r\n\t/// Autosave cadence in seconds. 0 disables the heartbeat (OnDestroy still saves).\r\n\t[Property] public float AutosaveSeconds {{ get; set; }} = {autosave};\r\n\r\n\t/// Save-shape version -- bump when SaveData changes so old files start fresh.\r\n\tpublic const int Version = {version};\r\n\r\n\t// Baked-in signing salt (unique to this generated file). Changing it invalidates existing saves.\r\n\tprivate const string Salt = \u0022\u0022{salt}\u0022\u0022;\r\n\r\n\t/// The save payload. Add your own fields here; clamp them in Sanitize().\r\n\tpublic class SaveData\r\n\t{{\r\n\t\tpublic int Money {{ get; set; }}\r\n\t\tpublic int Day {{ get; set; }} = 1;\r\n\t\t// Add game fields here.\r\n\t}}\r\n\r\n\t/// The envelope actually written to disk: version \u002B raw payload JSON \u002B signature.\r\n\tpublic class SaveEnvelope\r\n\t{{\r\n\t\tpublic int Version {{ get; set; }}\r\n\t\tpublic string Payload {{ get; set; }}\r\n\t\tpublic ulong Signature {{ get; set; }}\r\n\t}}\r\n\r\n\tpublic SaveData Data {{ get; private set; }} = new SaveData();\r\n\tpublic bool IsDirty {{ get; private set; }}\r\n\r\n\t/// Fires after a successful Load() with the loaded (sanitized) data.\r\n\tpublic static Action\u003CSaveData\u003E OnLoaded {{ get; set; }}\r\n\t/// Fires after every Save().\r\n\tpublic static Action\u003CSaveData\u003E OnSaved {{ get; set; }}\r\n\t/// Fires when the signature check fails and the save is force-reset. Arg = reason.\r\n\tpublic static Action\u003Cstring\u003E OnTampered {{ get; set; }}\r\n\r\n\tprivate TimeUntil _nextAutosave;\r\n\r\n\tprotected override void OnStart()\r\n\t{{\r\n\t\tif ( IsProxy ) return;   // only the owning machine loads\r\n\t\tLoad();\r\n\t\t_nextAutosave = AutosaveSeconds;\r\n\t}}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{{\r\n\t\tif ( IsProxy || AutosaveSeconds \u003C= 0f ) return;\r\n\t\tif ( _nextAutosave )\r\n\t\t{{\r\n\t\t\t_nextAutosave = AutosaveSeconds;\r\n\t\t\tif ( IsDirty ) Save();\r\n\t\t}}\r\n\t}}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{{\r\n\t\tif ( !IsProxy \u0026\u0026 IsDirty ) Save();\r\n\t}}\r\n\r\n\t/// Mark the data changed so the next autosave tick (or OnDestroy) writes \u002B re-signs it.\r\n\tpublic void MarkDirty() =\u003E IsDirty = true;\r\n\r\n\tpublic void Load()\r\n\t{{\r\n\t\tvar envelope = FileSystem.Data.ReadJsonOrDefault\u003CSaveEnvelope\u003E( FileName, null );\r\n\t\tif ( envelope == null )\r\n\t\t{{\r\n\t\t\t// Missing or unreadable envelope: start fresh (not treated as tampering).\r\n\t\t\tData = new SaveData();\r\n\t\t\tIsDirty = true;\r\n\t\t}}\r\n\t\telse if ( envelope.Version != Version )\r\n\t\t{{\r\n\t\t\t// Old save shape: start fresh (add migrations here later).\r\n\t\t\tData = new SaveData();\r\n\t\t\tIsDirty = true;\r\n\t\t}}\r\n\t\telse if ( envelope.Payload == null || ComputeSignature( envelope.Payload ) != envelope.Signature )\r\n\t\t{{\r\n\t\t\tForceReset( \u0022\u0022signature mismatch -- save file was modified outside the game\u0022\u0022 );\r\n\t\t\treturn;   // ForceReset already fired OnLoaded\r\n\t\t}}\r\n\t\telse\r\n\t\t{{\r\n\t\t\tSaveData loaded = null;\r\n\t\t\ttry {{ loaded = Json.Deserialize\u003CSaveData\u003E( envelope.Payload ); }}\r\n\t\t\tcatch {{ }}\r\n\t\t\tif ( loaded == null )\r\n\t\t\t{{\r\n\t\t\t\tForceReset( \u0022\u0022payload failed to parse despite a valid signature\u0022\u0022 );\r\n\t\t\t\treturn;\r\n\t\t\t}}\r\n\t\t\tData = Sanitize( loaded );\r\n\t\t\tIsDirty = false;\r\n\t\t}}\r\n\t\tOnLoaded?.Invoke( Data );\r\n\t}}\r\n\r\n\tpublic void Save()\r\n\t{{\r\n\t\tvar payload = Json.Serialize( Data );\r\n\t\tvar envelope = new SaveEnvelope\r\n\t\t{{\r\n\t\t\tVersion = Version,\r\n\t\t\tPayload = payload,\r\n\t\t\tSignature = ComputeSignature( payload )\r\n\t\t}};\r\n\t\tFileSystem.Data.WriteJson( FileName, envelope );\r\n\t\tIsDirty = false;\r\n\t\tOnSaved?.Invoke( Data );\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003EDelete the save file and reset to defaults. Fires OnTampered then OnLoaded.\u003C/summary\u003E\r\n\tpublic void ForceReset( string reason )\r\n\t{{\r\n\t\ttry\r\n\t\t{{\r\n\t\t\tif ( FileSystem.Data.FileExists( FileName ) )\r\n\t\t\t\tFileSystem.Data.DeleteFile( FileName );\r\n\t\t}}\r\n\t\tcatch {{ }}\r\n\t\tData = new SaveData();\r\n\t\tIsDirty = true;\r\n\t\tOnTampered?.Invoke( reason ?? \u0022\u0022forced reset\u0022\u0022 );\r\n\t\tOnLoaded?.Invoke( Data );\r\n\t}}\r\n\r\n\t/// Clamp-on-load: keep loaded values inside sane ranges so even a re-signed\r\n\t/// save can\u0027t smuggle absurd values. Extend per field you add.\r\n\tprivate SaveData Sanitize( SaveData d )\r\n\t{{\r\n\t\tif ( d.Money \u003C 0 ) d.Money = 0;\r\n\t\tif ( d.Day \u003C 1 ) d.Day = 1;\r\n\t\treturn d;\r\n\t}}\r\n\r\n\t// FNV-1a 64-bit over payload \u002B version \u002B salt. Deterministic, allocation-light.\r\n\tprivate static ulong ComputeSignature( string payload )\r\n\t{{\r\n\t\tconst ulong offsetBasis = 14695981039346656037UL;\r\n\t\tconst ulong prime = 1099511628211UL;\r\n\r\n\t\tulong hash = offsetBasis;\r\n\t\tstring material = payload \u002B \u0022\u0022|\u0022\u0022 \u002B Version \u002B \u0022\u0022|\u0022\u0022 \u002B Salt;\r\n\t\tfor ( int i = 0; i \u003C material.Length; i\u002B\u002B )\r\n\t\t{{\r\n\t\t\thash ^= material[i];\r\n\t\t\thash *= prime;\r\n\t\t}}\r\n\t\treturn hash;\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n// -----------------------------------------------------------------------------\r\n// create_meta_progression -- the between-runs roguelite meta layer: persistent\r\n// meta-currency \u002B unlock-flag dictionary saved to FileSystem.Data JSON.\r\n// Grant/TrySpend/Unlock/IsUnlocked \u002B a BankRun(int) run-end seam \u002B a static\r\n// OnUnlocked event. Persistence copies create_save_system\u0027s dirty-flag shape.\r\n// -----------------------------------------------------------------------------\r\npublic class CreateMetaProgressionHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022MetaProgression\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tvar ci = System.Globalization.CultureInfo.InvariantCulture;\r\n\t\t\tstring fileName = p.TryGetProperty( \u0022fileName\u0022, out var fn ) \u0026\u0026 !string.IsNullOrWhiteSpace( fn.GetString() ) ? fn.GetString() : \u0022meta.json\u0022;\r\n\t\t\tint version = p.TryGetProperty( \u0022version\u0022, out var vv ) \u0026\u0026 vv.TryGetInt32( out var vi ) ? vi : 1;\r\n\t\t\tfloat autosave = p.TryGetProperty( \u0022autosaveSeconds\u0022, out var av ) \u0026\u0026 av.TryGetSingle( out var af ) ? af : 10f;\r\n\r\n\t\t\tfileName = fileName.Replace( \u0022\\\\\u0022, \u0022\u0022 ).Replace( \u0022\\\u0022\u0022, \u0022\u0022 );\r\n\r\n\t\t\tvar code = BuildCode( className, fileName, version.ToString( ci ), autosave.ToString( ci ) \u002B \u0022f\u0022 );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\tobject placedOn = null; string note = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022targetId\u0022, out var tid ) \u0026\u0026 tid.ValueKind == JsonValueKind.String )\r\n\t\t\t\tplacedOn = EconomySaveHelpers.PlaceOnTarget( tid.GetString(), className, out note );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tfileName,\r\n\t\t\t\tversion,\r\n\t\t\t\tplacedOn,\r\n\t\t\t\tnote,\r\n\t\t\t\tnextSteps = new[]\r\n\t\t\t\t{\r\n\t\t\t\t\t$\u0022trigger_hotload to compile {className} into the game assembly.\u0022,\r\n\t\t\t\t\tplacedOn != null\r\n\t\t\t\t\t\t? $\u0022{className} was attached to the target GameObject.\u0022\r\n\t\t\t\t\t\t: $\u0022Place it on a persistent manager GameObject (one that exists in your hub/menu scene): add_component_to_new_object (component=\\\u0022{className}\\\u0022) after the hotload, or re-run with targetId.\u0022,\r\n\t\t\t\t\t$\u0022At run end, bank the earnings: GetComponent\u003C{className}\u003E().BankRun( runCurrencyEarned ); -- it grants and saves immediately.\u0022,\r\n\t\t\t\t\t$\u0022Gate content: if ( GetComponent\u003C{className}\u003E().TrySpend( 50 ) ) GetComponent\u003C{className}\u003E().Unlock( \\\u0022double_jump\\\u0022 ); then check IsUnlocked( \\\u0022double_jump\\\u0022 ) when building the player.\u0022,\r\n\t\t\t\t\t$\u0022React to unlocks anywhere: {className}.OnUnlocked \u002B= key =\u003E {{ /* flash the new item in the meta shop */ }};\u0022,\r\n\t\t\t\t\t\u0022MetaCurrency and the unlock flags persist to FileSystem.Data across sessions (dirty-flag autosave \u002B OnDestroy). IsProxy-guarded: in multiplayer each machine banks only its own meta file.\u0022\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_meta_progression failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string BuildCode( string className, string fileName, string version, string autosave )\r\n\t{\r\n\t\treturn $@\u0022using Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} -- the between-runs roguelite meta layer.\r\n///\r\n/// Persists a meta-currency plus an unlock-flag dictionary to FileSystem.Data JSON\r\n/// (dirty-flag autosave \u002B OnDestroy, create_save_system\u0027s shape). During a run you earn\r\n/// normal run-currency; at run end call BankRun(earned) to convert it into persistent\r\n/// meta-currency. Spend meta-currency on permanent Unlock() flags and gate content with\r\n/// IsUnlocked(). The static OnUnlocked event fires on every new unlock.\r\n///\r\n/// Owner-only (IsProxy-guarded): each machine banks only its own meta file.\r\n///\r\n/// Usage:\r\n///   GetComponent\u0026lt;{className}\u0026gt;().BankRun( 120 );                    // run over\r\n///   if ( GetComponent\u0026lt;{className}\u0026gt;().TrySpend( 50 ) )\r\n///       GetComponent\u0026lt;{className}\u0026gt;().Unlock( \u0022\u0022double_jump\u0022\u0022 );\r\n///   if ( GetComponent\u0026lt;{className}\u0026gt;().IsUnlocked( \u0022\u0022double_jump\u0022\u0022 ) ) {{ /* enable it */ }}\r\n///   {className}.OnUnlocked \u002B= key =\u003E {{ /* celebrate */ }};\r\n/// \u003C/summary\u003E\r\npublic sealed class {className} : Component\r\n{{\r\n\t/// FileSystem.Data path the meta state is written to.\r\n\t[Property] public string FileName {{ get; set; }} = \u0022\u0022{fileName}\u0022\u0022;\r\n\r\n\t/// Autosave cadence in seconds. 0 disables the heartbeat (OnDestroy still saves).\r\n\t[Property] public float AutosaveSeconds {{ get; set; }} = {autosave};\r\n\r\n\t/// The persisted payload. Bump Version when the shape changes so old files start fresh.\r\n\tpublic class MetaData\r\n\t{{\r\n\t\tpublic int Version {{ get; set; }} = {version};\r\n\t\tpublic long MetaCurrency {{ get; set; }}\r\n\t\tpublic int RunsBanked {{ get; set; }}\r\n\t\tpublic Dictionary\u003Cstring, bool\u003E Unlocks {{ get; set; }} = new Dictionary\u003Cstring, bool\u003E();\r\n\t}}\r\n\r\n\tpublic MetaData Data {{ get; private set; }} = new MetaData();\r\n\tpublic bool IsDirty {{ get; private set; }}\r\n\r\n\t/// Fires (on the owning machine) when a key is unlocked for the FIRST time.\r\n\tpublic static Action\u003Cstring\u003E OnUnlocked {{ get; set; }}\r\n\r\n\t/// Fires whenever MetaCurrency changes -- bind the meta-shop balance label here.\r\n\tpublic Action\u003Clong\u003E OnCurrencyChanged {{ get; set; }}\r\n\r\n\tprivate TimeUntil _nextAutosave;\r\n\r\n\tprotected override void OnStart()\r\n\t{{\r\n\t\tif ( IsProxy ) return;   // only the owning machine loads\r\n\t\tLoad();\r\n\t\t_nextAutosave = AutosaveSeconds;\r\n\t}}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{{\r\n\t\tif ( IsProxy || AutosaveSeconds \u003C= 0f ) return;\r\n\t\tif ( _nextAutosave )\r\n\t\t{{\r\n\t\t\t_nextAutosave = AutosaveSeconds;\r\n\t\t\tif ( IsDirty ) Save();\r\n\t\t}}\r\n\t}}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{{\r\n\t\tif ( !IsProxy \u0026\u0026 IsDirty ) Save();\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003EAdd meta-currency. Non-positive amounts are ignored.\u003C/summary\u003E\r\n\tpublic void Grant( long amount )\r\n\t{{\r\n\t\tif ( IsProxy || amount \u003C= 0 ) return;\r\n\t\tData.MetaCurrency \u002B= amount;\r\n\t\tIsDirty = true;\r\n\t\tOnCurrencyChanged?.Invoke( Data.MetaCurrency );\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003ESpend meta-currency if affordable; false and no change otherwise.\u003C/summary\u003E\r\n\tpublic bool TrySpend( long amount )\r\n\t{{\r\n\t\tif ( IsProxy || amount \u003C= 0 ) return false;\r\n\t\tif ( Data.MetaCurrency \u003C amount ) return false;\r\n\t\tData.MetaCurrency -= amount;\r\n\t\tIsDirty = true;\r\n\t\tOnCurrencyChanged?.Invoke( Data.MetaCurrency );\r\n\t\treturn true;\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003ESet a permanent unlock flag. Idempotent; OnUnlocked fires only the first time. Saves immediately.\u003C/summary\u003E\r\n\tpublic void Unlock( string key )\r\n\t{{\r\n\t\tif ( IsProxy || string.IsNullOrEmpty( key ) ) return;\r\n\t\tif ( Data.Unlocks.TryGetValue( key, out var already ) \u0026\u0026 already ) return;\r\n\t\tData.Unlocks[key] = true;\r\n\t\tSave();   // unlocks are precious -- write through immediately\r\n\t\tOnUnlocked?.Invoke( key );\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003ETrue when a key has been permanently unlocked.\u003C/summary\u003E\r\n\tpublic bool IsUnlocked( string key )\r\n\t\t=\u003E !string.IsNullOrEmpty( key ) \u0026\u0026 Data.Unlocks.TryGetValue( key, out var v ) \u0026\u0026 v;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Run-end seam: convert this run\u0027s earnings into persistent meta-currency and\r\n\t/// save immediately. Call it from your round machine\u0027s end-of-run transition.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void BankRun( int earned )\r\n\t{{\r\n\t\tif ( IsProxy ) return;\r\n\t\tif ( earned \u003E 0 ) Data.MetaCurrency \u002B= earned;\r\n\t\tData.RunsBanked \u002B= 1;\r\n\t\tSave();\r\n\t\tOnCurrencyChanged?.Invoke( Data.MetaCurrency );\r\n\t}}\r\n\r\n\t/// Mark the data changed so the next autosave tick (or OnDestroy) writes it.\r\n\tpublic void MarkDirty() =\u003E IsDirty = true;\r\n\r\n\tpublic void Load()\r\n\t{{\r\n\t\tvar loaded = FileSystem.Data.ReadJsonOrDefault\u003CMetaData\u003E( FileName, null );\r\n\t\tif ( loaded == null || loaded.Version != {version} )\r\n\t\t{{\r\n\t\t\tData = new MetaData();\r\n\t\t\tIsDirty = true;\r\n\t\t}}\r\n\t\telse\r\n\t\t{{\r\n\t\t\tif ( loaded.MetaCurrency \u003C 0 ) loaded.MetaCurrency = 0;\r\n\t\t\tif ( loaded.RunsBanked \u003C 0 ) loaded.RunsBanked = 0;\r\n\t\t\tif ( loaded.Unlocks == null ) loaded.Unlocks = new Dictionary\u003Cstring, bool\u003E();\r\n\t\t\tData = loaded;\r\n\t\t\tIsDirty = false;\r\n\t\t}}\r\n\t\tOnCurrencyChanged?.Invoke( Data.MetaCurrency );\r\n\t}}\r\n\r\n\tpublic void Save()\r\n\t{{\r\n\t\tFileSystem.Data.WriteJson( FileName, Data );\r\n\t\tIsDirty = false;\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n// -----------------------------------------------------------------------------\r\n// add_steam_stat_currency -- currency persisted over Sandbox.Services.Stats.\r\n// Verified live on this SDK: static Stats.Increment(string,double),\r\n// Stats.SetValue(string,double,string,object), Stats.Flush(), and\r\n// Stats.GetLocalPlayerStats(string packageIdent) returning the NESTED\r\n// Stats.PlayerStats (Get(name) -\u003E Stats.PlayerStat with .Value). There is\r\n// NO Stats.LocalPlayer property on this SDK.\r\n// -----------------------------------------------------------------------------\r\npublic class AddSteamStatCurrencyHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022SteamStatCurrency\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tstring statName = p.TryGetProperty( \u0022statName\u0022, out var sv ) \u0026\u0026 !string.IsNullOrWhiteSpace( sv.GetString() ) ? sv.GetString() : \u0022currency\u0022;\r\n\t\t\tstring packageIdent = p.TryGetProperty( \u0022packageIdent\u0022, out var pv ) \u0026\u0026 !string.IsNullOrWhiteSpace( pv.GetString() ) ? pv.GetString() : \u0022\u0022;\r\n\t\t\tbool flushEveryChange = p.TryGetProperty( \u0022flushEveryChange\u0022, out var fv ) \u0026\u0026 fv.ValueKind == JsonValueKind.True;\r\n\r\n\t\t\t// Baked into generated string literals -- strip escape characters.\r\n\t\t\tstatName = statName.Replace( \u0022\\\\\u0022, \u0022\u0022 ).Replace( \u0022\\\u0022\u0022, \u0022\u0022 );\r\n\t\t\tpackageIdent = packageIdent.Replace( \u0022\\\\\u0022, \u0022\u0022 ).Replace( \u0022\\\u0022\u0022, \u0022\u0022 );\r\n\r\n\t\t\tvar code = BuildCode( className, statName, packageIdent, flushEveryChange );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\tobject placedOn = null; string note = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022targetId\u0022, out var tid ) \u0026\u0026 tid.ValueKind == JsonValueKind.String )\r\n\t\t\t\tplacedOn = EconomySaveHelpers.PlaceOnTarget( tid.GetString(), className, out note );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tstatName,\r\n\t\t\t\tpackageIdent = string.IsNullOrEmpty( packageIdent ) ? \u0022(Game.Ident -- the running package)\u0022 : packageIdent,\r\n\t\t\t\tflushEveryChange,\r\n\t\t\t\tplacedOn,\r\n\t\t\t\tnote,\r\n\t\t\t\tnextSteps = new[]\r\n\t\t\t\t{\r\n\t\t\t\t\t$\u0022trigger_hotload to compile {className} into the game assembly.\u0022,\r\n\t\t\t\t\tplacedOn != null\r\n\t\t\t\t\t\t? $\u0022{className} was attached to the target GameObject.\u0022\r\n\t\t\t\t\t\t: $\u0022Place it on the LOCAL player\u0027s GameObject (each player writes only their own Steam stat): add_component_to_new_object (component=\\\u0022{className}\\\u0022) after the hotload, or re-run with targetId.\u0022,\r\n\t\t\t\t\t$\u0022Use it: GetComponent\u003C{className}\u003E().Add( 25 ); if ( GetComponent\u003C{className}\u003E().TrySpend( 10 ) ) {{ }} -- Balance is the in-session truth; every change pushes Stats.SetValue.\u0022,\r\n\t\t\t\t\t$\u0022React: {className}.OnBalanceLoaded \u002B= bal =\u003E {{ }}; and instance OnBalanceChanged for HUD labels. Wait for IsLoaded before showing the balance -- the read-back is async.\u0022,\r\n\t\t\t\t\t\u0022CLOUD SEMANTICS: stats writes are buffered by the backend (Flush() pushes; the component flushes on destroy) and only apply to the LOCAL Steam user -- calling it for another player silently does nothing. Read-back is eventually consistent and can lag minutes; the in-session Balance property is authoritative while playing.\u0022,\r\n\t\t\t\t\t\u0022Stats persist per Steam account per package ident -- dev sessions without a real published ident may read back nothing (you\u0027ll get balance 0 \u002B a log line). This is Steam-cloud persistence, not a local save file; pair with create_signed_save if you need offline saves.\u0022\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022add_steam_stat_currency failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string BuildCode( string className, string statName, string packageIdent, bool flushEveryChange )\r\n\t{\r\n\t\tstring flushLit = flushEveryChange ? \u0022true\u0022 : \u0022false\u0022;\r\n\r\n\t\treturn $@\u0022using Sandbox;\r\nusing Sandbox.Services;\r\nusing System;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} -- a currency persisted over Sandbox.Services.Stats (Steam cloud).\r\n///\r\n/// The stat named StatName stores the ABSOLUTE balance (Stats.SetValue on every change);\r\n/// on start the component reads it back asynchronously via\r\n/// Stats.GetLocalPlayerStats(ident).Refresh() -\u003E Get(StatName).Value and fires\r\n/// OnBalanceLoaded. While playing, the in-session Balance property is the authoritative\r\n/// value -- the cloud read-back is eventually consistent and can lag behind writes.\r\n///\r\n/// SCOPE: stats writes apply only to the LOCAL Steam user (writes for other players\r\n/// silently no-op) and persist per package ident. Attach this to the local player\u0027s\r\n/// GameObject; IsProxy guards keep remote copies inert. Dev sessions without a real\r\n/// published ident may read back nothing (balance starts at 0).\r\n///\r\n/// Usage:\r\n///   GetComponent\u0026lt;{className}\u0026gt;().Add( 25 );\r\n///   if ( GetComponent\u0026lt;{className}\u0026gt;().TrySpend( 10 ) ) {{ /* grant the thing */ }}\r\n///   {className}.OnBalanceLoaded \u002B= bal =\u003E {{ /* show the wallet */ }};\r\n/// \u003C/summary\u003E\r\npublic sealed class {className} : Component\r\n{{\r\n\t/// The Sandbox.Services stat that stores the balance.\r\n\t[Property] public string StatName {{ get; set; }} = \u0022\u0022{statName}\u0022\u0022;\r\n\r\n\t/// Package ident to read stats from. Empty = the running package (Game.Ident).\r\n\t[Property] public string PackageIdent {{ get; set; }} = \u0022\u0022{packageIdent}\u0022\u0022;\r\n\r\n\t/// Push Stats.Flush() after every change (rate-limited by the backend) instead of\r\n\t/// relying on the buffered flush \u002B the OnDestroy flush.\r\n\t[Property] public bool FlushEveryChange {{ get; set; }} = {flushLit};\r\n\r\n\t/// In-session balance -- authoritative while playing. Cloud value catches up on flush.\r\n\tpublic double Balance {{ get; private set; }}\r\n\r\n\t/// True once the async cloud read-back has completed (successfully or not).\r\n\tpublic bool IsLoaded {{ get; private set; }}\r\n\r\n\t/// Fires once after the cloud read-back completes, with the loaded balance.\r\n\tpublic static Action\u003Cdouble\u003E OnBalanceLoaded {{ get; set; }}\r\n\r\n\t/// Fires on every balance change (including the initial load) -- bind a HUD here.\r\n\tpublic Action\u003Cdouble\u003E OnBalanceChanged {{ get; set; }}\r\n\r\n\tprotected override void OnStart()\r\n\t{{\r\n\t\tif ( IsProxy ) return;   // only the local player\u0027s machine touches their stats\r\n\t\t_ = LoadAsync();\r\n\t}}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{{\r\n\t\tif ( !IsProxy \u0026\u0026 IsLoaded ) Stats.Flush();\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003ERe-read the balance from the stats backend (async; also runs on start).\u003C/summary\u003E\r\n\tpublic async System.Threading.Tasks.Task LoadAsync()\r\n\t{{\r\n\t\tdouble loaded = 0.0;\r\n\t\ttry\r\n\t\t{{\r\n\t\t\tstring ident = string.IsNullOrWhiteSpace( PackageIdent ) ? Game.Ident : PackageIdent;\r\n\t\t\tvar stats = Stats.GetLocalPlayerStats( ident );\r\n\t\t\tawait stats.Refresh();\r\n\t\t\tloaded = stats.Get( StatName ).Value;\r\n\t\t}}\r\n\t\tcatch ( Exception ex )\r\n\t\t{{\r\n\t\t\tLog.Warning( $\u0022\u0022[{className}] Stat read-back failed ({{ex.Message}}) -- starting at 0. Stats need a valid package ident \u002B Steam session.\u0022\u0022 );\r\n\t\t}}\r\n\t\tBalance = loaded;\r\n\t\tIsLoaded = true;\r\n\t\tOnBalanceLoaded?.Invoke( Balance );\r\n\t\tOnBalanceChanged?.Invoke( Balance );\r\n\t}}\r\n\r\n\tpublic bool CanAfford( double amount ) =\u003E Balance \u003E= amount;\r\n\r\n\t/// \u003Csummary\u003EAdd currency and push the new balance to the stats backend. Non-positive ignored.\u003C/summary\u003E\r\n\tpublic void Add( double amount )\r\n\t{{\r\n\t\tif ( IsProxy || amount \u003C= 0.0 ) return;\r\n\t\tBalance \u002B= amount;\r\n\t\tPush();\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003ESpend if affordable; returns false and changes nothing if not.\u003C/summary\u003E\r\n\tpublic bool TrySpend( double amount )\r\n\t{{\r\n\t\tif ( IsProxy || amount \u003C= 0.0 ) return false;\r\n\t\tif ( Balance \u003C amount ) return false;\r\n\t\tBalance -= amount;\r\n\t\tPush();\r\n\t\treturn true;\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003EForce-push buffered stat writes to the backend now (rate-limited upstream).\u003C/summary\u003E\r\n\tpublic void Flush() =\u003E Stats.Flush();\r\n\r\n\t// Write the absolute balance to the stat and notify listeners.\r\n\tprivate void Push()\r\n\t{{\r\n\t\tStats.SetValue( StatName, Balance, null, null );\r\n\t\tif ( FlushEveryChange ) Stats.Flush();\r\n\t\tOnBalanceChanged?.Invoke( Balance );\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n// -----------------------------------------------------------------------------\r\n// create_loot_table_resource -- the data-asset sibling of create_weighted_loot_table.\r\n// Generates ONE .cs containing: an entry POCO (name, weight, optional nested table\r\n// reference), a GameResource loot-table asset type ([AssetType] -- the modern\r\n// attribute; GameResourceAttribute is [Obsolete] on this SDK), and a resolver\r\n// Component that rolls a table by cumulative weight with a resolve depth cap.\r\n// Designers author .loot files in the asset browser; code rolls them.\r\n// -----------------------------------------------------------------------------\r\npublic class CreateLootTableResourceHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022LootTableResource\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tstring extension = p.TryGetProperty( \u0022extension\u0022, out var ev ) \u0026\u0026 !string.IsNullOrWhiteSpace( ev.GetString() ) ? ev.GetString() : \u0022loot\u0022;\r\n\t\t\tstring title = p.TryGetProperty( \u0022title\u0022, out var tv ) \u0026\u0026 !string.IsNullOrWhiteSpace( tv.GetString() ) ? tv.GetString() : \u0022Loot Table\u0022;\r\n\t\t\tint maxDepth = p.TryGetProperty( \u0022maxDepth\u0022, out var mv ) \u0026\u0026 mv.TryGetInt32( out var mi ) ? mi : 4;\r\n\t\t\tif ( maxDepth \u003C 0 ) maxDepth = 0;\r\n\t\t\tif ( maxDepth \u003E 16 ) maxDepth = 16;\r\n\r\n\t\t\t// Extension: lowercase alphanumerics only.\r\n\t\t\tvar extChars = new StringBuilder();\r\n\t\t\tforeach ( var c in extension.ToLowerInvariant() )\r\n\t\t\t\tif ( ( c \u003E= \u0027a\u0027 \u0026\u0026 c \u003C= \u0027z\u0027 ) || ( c \u003E= \u00270\u0027 \u0026\u0026 c \u003C= \u00279\u0027 ) ) extChars.Append( c );\r\n\t\t\textension = extChars.Length \u003E 0 ? extChars.ToString() : \u0022loot\u0022;\r\n\r\n\t\t\t// Title is baked into an attribute string literal.\r\n\t\t\ttitle = title.Replace( \u0022\\\\\u0022, \u0022\u0022 ).Replace( \u0022\\\u0022\u0022, \u0022\u0022 );\r\n\r\n\t\t\tvar resolverClass = className \u002B \u0022Resolver\u0022;\r\n\t\t\tvar code = BuildCode( className, resolverClass, extension, title, maxDepth );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\t// Placement attaches the RESOLVER component (the resource itself is an asset type, not a component).\r\n\t\t\tobject placedOn = null; string note = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022targetId\u0022, out var tid ) \u0026\u0026 tid.ValueKind == JsonValueKind.String )\r\n\t\t\t\tplacedOn = EconomySaveHelpers.PlaceOnTarget( tid.GetString(), resolverClass, out note );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tresolverClass,\r\n\t\t\t\textension,\r\n\t\t\t\tmaxDepth,\r\n\t\t\t\tplacedOn,\r\n\t\t\t\tnote,\r\n\t\t\t\tnextSteps = new[]\r\n\t\t\t\t{\r\n\t\t\t\t\t$\u0022trigger_hotload to compile {className} \u002B {resolverClass} into the game assembly -- the \u0027.{extension}\u0027 asset type registers on compile.\u0022,\r\n\t\t\t\t\t$\u0022Author tables as ASSETS: in the editor asset browser, New \u003E {title} creates a .{extension} file; fill Entries (Name, Weight, optional NestedTable reference to another .{extension}) in the inspector.\u0022,\r\n\t\t\t\t\tplacedOn != null\r\n\t\t\t\t\t\t? $\u0022{resolverClass} was attached to the target GameObject -- assign its Table property to a .{extension} asset (set_property with the asset path).\u0022\r\n\t\t\t\t\t\t: $\u0022Attach the resolver: add_component_to_new_object (component=\\\u0022{resolverClass}\\\u0022) after the hotload, then set its Table property to a .{extension} asset path.\u0022,\r\n\t\t\t\t\t$\u0022Roll from game code (host-side): string drop = GetComponent\u003C{resolverClass}\u003E().Roll(); {resolverClass}.OnLoot \u002B= ( go, item ) =\u003E {{ }};\u0022,\r\n\t\t\t\t\t$\u0022Nested tables: an entry with a NestedTable rolls INTO that table instead of dropping its Name -- capped at MaxDepth ({maxDepth}) with a self-reference guard, so cycles terminate.\u0022,\r\n\t\t\t\t\t\u0022Use create_weighted_loot_table instead when you want a single inline component with no asset files; use create_gacha_drop_table for pity \u002B duplicate mechanics. Pick an extension that is NOT a suffix of a built-in one (e.g. avoid \u0027cfg\u0027) or ResourceLibrary.GetAll will pick up engine files as phantom instances.\u0022\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_loot_table_resource failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string BuildCode( string className, string resolverClass, string extension, string title, int maxDepth )\r\n\t{\r\n\t\tvar ci = System.Globalization.CultureInfo.InvariantCulture;\r\n\t\tstring md = maxDepth.ToString( ci );\r\n\r\n\t\treturn $@\u0022using Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\n\r\n/// \u003Csummary\u003E\r\n/// One row of a {className} asset. Amount is picked by cumulative weight; when\r\n/// NestedTable is set the roll continues INTO that table instead of dropping Name.\r\n/// \u003C/summary\u003E\r\npublic sealed class {className}Entry\r\n{{\r\n\t/// What drops when this entry wins (ignored when NestedTable is set).\r\n\t[Property] public string Name {{ get; set; }} = \u0022\u0022\u0022\u0022;\r\n\r\n\t/// Relative chance. Bigger = more likely. Entries with weight \u0026lt;= 0 never win.\r\n\t[Property] public float Weight {{ get; set; }} = 1f;\r\n\r\n\t/// Optional: roll this table instead of dropping Name (depth-capped on resolve).\r\n\t[Property] public {className} NestedTable {{ get; set; }}\r\n}}\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} -- a designer-authored loot table ASSET (.{extension} files).\r\n///\r\n/// Each .{extension} file holds weighted entries; entries may reference other\r\n/// .{extension} assets as nested tables (rarity tiers, per-biome sub-tables).\r\n/// Resolve() rolls by cumulative weight and follows nested references up to a\r\n/// depth cap, so cyclic references terminate. Author the files in the editor\r\n/// asset browser; roll them with {resolverClass} or call Resolve() directly.\r\n/// \u003C/summary\u003E\r\n[AssetType( Name = \u0022\u0022{title}\u0022\u0022, Extension = \u0022\u0022{extension}\u0022\u0022, Category = \u0022\u0022Game\u0022\u0022 )]\r\npublic sealed class {className} : GameResource\r\n{{\r\n\t/// The weighted rows of this table.\r\n\t[Property] public List\u003C{className}Entry\u003E Entries {{ get; set; }} = new List\u003C{className}Entry\u003E();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Roll once: pick an entry by cumulative weight; if it references a nested table,\r\n\t/// keep rolling into it until a plain entry wins or maxDepth is exhausted (then the\r\n\t/// deepest entry\u0027s Name is returned). Null when the table is empty. HOST-authoritative:\r\n\t/// roll on the host and replicate the result -- clients rolling their own loot is the\r\n\t/// classic economy exploit.\r\n\t/// \u003C/summary\u003E\r\n\tpublic string Resolve( int maxDepth = {md} )\r\n\t{{\r\n\t\tvar entry = RollEntry();\r\n\t\tif ( entry == null ) return null;\r\n\t\tif ( entry.NestedTable != null \u0026\u0026 entry.NestedTable != this \u0026\u0026 maxDepth \u003E 0 )\r\n\t\t\treturn entry.NestedTable.Resolve( maxDepth - 1 );\r\n\t\treturn entry.Name;\r\n\t}}\r\n\r\n\t// Cumulative-weight pick over Entries. Null when empty; first entry when all weights are zero.\r\n\tprivate {className}Entry RollEntry()\r\n\t{{\r\n\t\tif ( Entries == null || Entries.Count == 0 ) return null;\r\n\r\n\t\tfloat total = 0f;\r\n\t\tforeach ( var e in Entries )\r\n\t\t\tif ( e != null \u0026\u0026 e.Weight \u003E 0f ) total \u002B= e.Weight;\r\n\t\tif ( total \u003C= 0f ) return Entries[0];\r\n\r\n\t\tfloat roll = Game.Random.Float( 0f, total );\r\n\t\tfloat cumulative = 0f;\r\n\t\t{className}Entry winner = null;\r\n\t\tforeach ( var e in Entries )\r\n\t\t{{\r\n\t\t\tif ( e == null || e.Weight \u003C= 0f ) continue;\r\n\t\t\twinner = e;\r\n\t\t\tcumulative \u002B= e.Weight;\r\n\t\t\tif ( roll \u003C cumulative ) break;\r\n\t\t}}\r\n\t\treturn winner;\r\n\t}}\r\n}}\r\n\r\n/// \u003Csummary\u003E\r\n/// {resolverClass} -- rolls a {className} asset from the scene.\r\n///\r\n/// Assign Table to a .{extension} asset in the inspector (or via set_property with the\r\n/// asset path). Roll() resolves through nested tables up to MaxDepth and fires the\r\n/// static OnLoot event with the winning item name. Call it host-side and replicate\r\n/// the result yourself ([Sync] or an [Rpc.Broadcast]).\r\n///\r\n/// Usage:\r\n///   string drop = GetComponent\u0026lt;{resolverClass}\u0026gt;().Roll();\r\n///   {resolverClass}.OnLoot \u002B= ( go, item ) =\u003E Log.Info( $\u0022\u0022{{go.Name}} got {{item}}\u0022\u0022 );\r\n/// \u003C/summary\u003E\r\npublic sealed class {resolverClass} : Component\r\n{{\r\n\t/// The loot table asset this resolver rolls.\r\n\t[Property] public {className} Table {{ get; set; }}\r\n\r\n\t/// How deep nested-table references may chain before the roll settles.\r\n\t[Property] public int MaxDepth {{ get; set; }} = {md};\r\n\r\n\t/// Fires (on the rolling machine) when Roll() picks a winner: (roller, itemName).\r\n\tpublic static Action\u003CGameObject, string\u003E OnLoot {{ get; set; }}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Roll the assigned table once. Null (with a warning) when no Table is assigned or\r\n\t/// the table is empty. HOST-authoritative by convention -- see the class summary.\r\n\t/// \u003C/summary\u003E\r\n\tpublic string Roll()\r\n\t{{\r\n\t\tif ( Table == null )\r\n\t\t{{\r\n\t\t\tLog.Warning( $\u0022\u0022[{resolverClass}] No Table assigned on {{GameObject.Name}} -- assign a .{extension} asset.\u0022\u0022 );\r\n\t\t\treturn null;\r\n\t\t}}\r\n\r\n\t\tvar drop = Table.Resolve( MaxDepth );\r\n\t\tif ( drop != null ) OnLoot?.Invoke( GameObject, drop );\r\n\t\treturn drop;\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Shared placement helper for the economy/save handlers -- mirrors the standard scaffold\r\n/// placement (create_economy_wallet / create_weighted_loot_table / LootEconomyHelpers).\r\n/// \u003C/summary\u003E\r\ninternal static class EconomySaveHelpers\r\n{\r\n\tpublic static object PlaceOnTarget( string targetId, string className, out string note )\r\n\t{\r\n\t\tnote = null;\r\n\t\tvar scene = SceneEditorSession.Active?.Scene;\r\n\t\tif ( scene == null ) { note = \u0022No active scene to place into.\u0022; return null; }\r\n\t\tif ( !Guid.TryParse( targetId, out var guid ) ) { note = \u0022Invalid targetId GUID.\u0022; return null; }\r\n\t\tvar go = scene.Directory.FindByGuid( guid );\r\n\t\tif ( go == null ) { note = $\u0022Target GameObject not found: {targetId}\u0022; return null; }\r\n\t\tvar typeDesc = Game.TypeLibrary.GetType( className );\r\n\t\tif ( typeDesc == null )\r\n\t\t{\r\n\t\t\tnote = $\u0022Generated {className}.cs but it is not in the TypeLibrary yet -- trigger_hotload, then add it with add_component_with_properties.\u0022;\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\ttry { go.Components.Create( typeDesc ); return ClaudeBridge.SerializeGo( go ); }\r\n\t\tcatch ( Exception ex ) { note = $\u0022Placement failed ({ex.Message}).\u0022; return null; }\r\n\t}\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/Mcp/BridgeProjectTools.cs","FileName":"BridgeProjectTools.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"// AUTO-GENERATED by scripts/emit-mcp-wrappers.mjs \u2014 DO NOT EDIT.\r\n// Regenerate: node scripts/extract-manifest.mjs \u0026\u0026 node scripts/emit-mcp-wrappers.mjs\r\n// Source of truth: sbox-mcp-server/src/tools/ (zod schemas) \u2192 scripts/tools-manifest.json\r\n\r\nusing System.Text.Json.Nodes;\r\nusing System.Threading.Tasks;\r\nusing Editor.Mcp;\r\n\r\n/// \u003Csummary\u003E\r\n/// Project info and config (.sbproj), file read/write, C# script create/edit/delete, hotload, input\r\n/// actions, and publishing metadata.\r\n/// \u003C/summary\u003E\r\n[McpToolset( \u0022bridge_project\u0022, \u0022Project info and config (.sbproj), file read/write, C# script create/edit/delete, hotload, input actions, and publishing metadata.\u0022 )]\r\npublic static class BridgeProjectTools\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Create a new C# component script in the project \u2014 a minimal s\u0026amp;box Component class (name is\r\n\t/// sanitized to a valid identifier), or your exact code when content is provided. Errors if the\r\n\t/// file already exists. Returns { path, created, className } \u2014 the new type is NOT live until a\r\n\t/// recompile, so call trigger_hotload, then attach it with add_component_with_properties\r\n\t/// (component=className).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the component (e.g. \u0027PlayerController\u0027). Will also be the filename.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory under code/ to place the script (e.g. \u0027Components\u0027). Defaults to \u0027code/\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022description\u0022\u003EDescription of what this component does \u2014 used to generate appropriate code.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022properties\u0022\u003EList of [Property] fields to include in the component. JSON array.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022content\u0022\u003EFull C# file content. If provided, ignores name/properties and writes this directly.\u003C/param\u003E\r\n\t[McpTool( \u0022create_script\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateScript( string name, string directory = null, string description = null, JsonNode properties = null, string content = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_script\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022description\u0022, description ), ( \u0022properties\u0022, properties ), ( \u0022content\u0022, content ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Permanently delete a file from the project by its project-relative path (built for C# scripts,\r\n\t/// but removes any file; no recycle bin, and editor undo cannot restore it). Errors if the file\r\n\t/// doesn\u0027t exist. Returns a confirmation with the path \u2014 follow with trigger_hotload so the removed\r\n\t/// class actually leaves the compiled assembly.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022path\u0022\u003ERelative path to the script file to delete.\u003C/param\u003E\r\n\t[McpTool( \u0022delete_script\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E DeleteScript( string path )\r\n\t\t=\u003E McpGate.Run( \u0022delete_script\u0022, McpGate.Args( ( \u0022path\u0022, path ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// One-call project orientation: identity (name/ident/org/type), the open scene with object count,\r\n\t/// scene and prefab file lists (capped at 50 each, Libraries/.sbox excluded), code footprint\r\n\t/// (.cs/.razor counts), custom Component types (up to 100, engine types excluded), and installed\r\n\t/// libraries. Returns a structured summary \u2014 orient here first, then get_scene_hierarchy for the\r\n\t/// scene, describe_type for components, find_broken_references for project health. Read-only.\r\n\t/// \u003C/summary\u003E\r\n\t[McpTool.ReadOnly( \u0022describe_project\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E DescribeProject()\r\n\t\t=\u003E McpGate.Run( \u0022describe_project\u0022, McpGate.Args() );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Edit an existing C# script in place via exact-text find/replace or a full-content overwrite.\r\n\t/// Errors if the file or the find text isn\u0027t found (find/replace replaces ALL occurrences). Returns\r\n\t/// { path, edited, operation } where operation is \u0027find_replace\u0027 or \u0027overwrite\u0027 \u2014 follow with\r\n\t/// trigger_hotload so the change compiles, then get_compile_errors if in doubt.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022path\u0022\u003ERelative path to the script file (e.g. \u0027code/PlayerController.cs\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022operations\u0022\u003EList of edit operations to apply in order. JSON array.\u003C/param\u003E\r\n\t[McpTool( \u0022edit_script\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E EditScript( string path, JsonNode operations )\r\n\t\t=\u003E McpGate.Run( \u0022edit_script\u0022, McpGate.Args( ( \u0022path\u0022, path ), ( \u0022operations\u0022, operations ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Register a custom named INPUT ACTION in the project so a generated game\u0027s custom verbs work in\r\n\t/// play mode. Writes to \u0026lt;project\u0026gt;.sbproj \u2192 Metadata.InputSettings.Actions[]. Idempotent: if\r\n\t/// the action already exists it is left alone (pass update=true to rebind its key). If the project\r\n\t/// has no InputSettings yet, the full DEFAULT action set\r\n\t/// (Forward/Back/Left/Right/Jump/Use/attack1/...) is seeded first so player movement/use are\r\n\t/// preserved \u2014 the engine only auto-injects defaults when a game defines NONE. After adding, call\r\n\t/// it from game code with Input.Pressed(\u0022name\u0022) / Input.Down(\u0022name\u0022) / Input.Released(\u0022name\u0022).\r\n\t/// Note: input config is read at project load, so restart_editor (or reload the project) for a new\r\n\t/// action to take effect in play mode.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EThe action verb game code will call, e.g. \u0022interact\u0022, \u0022sprint\u0022, \u0022drop\u0022. Matches Input.Pressed(\u0022interact\u0022).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022keyboardKey\u0022\u003EDefault keyboard binding, e.g. \u0022e\u0022, \u0022f\u0022, \u0022space\u0022, \u0022mouse1\u0022, \u0022shift\u0022. Omit to add the action with no default key (player can bind it).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022group\u0022\u003EUI group the action is listed under in the bindings menu (e.g. \u0022Actions\u0022, \u0022Movement\u0022, \u0022Other\u0022). Defaults to \u0022Actions\u0022.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022update\u0022\u003EIf the action already exists, rebind its keyboardKey to the provided value instead of leaving it untouched. Default false (idempotent no-op when present).\u003C/param\u003E\r\n\t[McpTool( \u0022ensure_input_action\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E EnsureInputAction( string name, string keyboardKey = null, string group = null, bool? update = null )\r\n\t\t=\u003E McpGate.Run( \u0022ensure_input_action\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022keyboardKey\u0022, keyboardKey ), ( \u0022group\u0022, group ), ( \u0022update\u0022, update ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Fetch package information from the s\u0026amp;box package backend (Package.FetchAsync) by ident.\r\n\t/// Returns { fullIdent, title, summary, description, org } \u2014 no download/rating/dependency data is\r\n\t/// included. Use it to confirm a package exists and what it is before install_asset.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022ident\u0022\u003EPackage identifier (e.g. \u0027facepunch.flatgrass\u0027, \u0027myorg.mygame\u0027).\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022get_package_details\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E GetPackageDetails( string ident )\r\n\t\t=\u003E McpGate.Run( \u0022get_package_details\u0022, McpGate.Args( ( \u0022ident\u0022, ident ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Read the full project configuration from the .sbproj file including title, description, version,\r\n\t/// type, package references, metadata, and raw JSON.\r\n\t/// \u003C/summary\u003E\r\n\t[McpTool.ReadOnly( \u0022get_project_config\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E GetProjectConfig()\r\n\t\t=\u003E McpGate.Run( \u0022get_project_config\u0022, McpGate.Args() );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Get information about the current s\u0026amp;box project \u2014 path, name, game type, dependencies, and\r\n\t/// configuration.\r\n\t/// \u003C/summary\u003E\r\n\t[McpTool.ReadOnly( \u0022get_project_info\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E GetProjectInfo()\r\n\t\t=\u003E McpGate.Run( \u0022get_project_info\u0022, McpGate.Args() );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Browse the project file tree. Optionally filter by directory path and/or file extension (e.g.\r\n\t/// \u0027.cs\u0027, \u0027.scene\u0027). Returns { path, count, files } as project-root-relative paths \u2014 CAPPED AT 500\r\n\t/// files (count reflects the truncated list, with no marker that more exist), so on large projects\r\n\t/// narrow with path/extension or use find_in_project. Recursive by default.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022path\u0022\u003ERelative directory path to list (e.g. \u0027code/Components\u0027). Defaults to project root.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022extension\u0022\u003EFilter by file extension, including the dot (e.g. \u0027.cs\u0027, \u0027.scene\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022recursive\u0022\u003EWhether to list files recursively. Defaults to true.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022list_project_files\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E ListProjectFiles( string path = null, string extension = null, bool? recursive = null )\r\n\t\t=\u003E McpGate.Run( \u0022list_project_files\u0022, McpGate.Args( ( \u0022path\u0022, path ), ( \u0022extension\u0022, extension ), ( \u0022recursive\u0022, recursive ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Read the contents of a file in the s\u0026amp;box project (scripts, scenes, configs, etc.).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022path\u0022\u003ERelative path to the file within the project (e.g. \u0027code/PlayerController.cs\u0027).\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022read_file\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E ReadFile( string path )\r\n\t\t=\u003E McpGate.Run( \u0022read_file\u0022, McpGate.Args( ( \u0022path\u0022, path ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Update project configuration fields for publishing: title, description, version, type, package\r\n\t/// ident, summary, visibility. Only provided fields are changed \u2014 edits string values in the\r\n\t/// .sbproj file in place. Returns { updated, path } (the .sbproj path); read the result back with\r\n\t/// get_project_config to confirm what actually changed.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022title\u0022\u003EProject display title.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022description\u0022\u003EProject description for publishing.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022version\u0022\u003EVersion string (e.g. \u00271.0.0\u0027, \u00272.1.3\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022type\u0022\u003EProject type: \u0027game\u0027, \u0027addon\u0027, \u0027library\u0027, or \u0027template\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022packageIdent\u0022\u003EPackage identifier (e.g. \u0027myorg.mygame\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022summary\u0022\u003EShort summary for asset.party listing.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022isPublic\u0022\u003EWhether the project is publicly visible.\u003C/param\u003E\r\n\t[McpTool( \u0022set_project_config\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SetProjectConfig( string title = null, string description = null, string version = null, string type = null, string packageIdent = null, string summary = null, bool? isPublic = null )\r\n\t\t=\u003E McpGate.Run( \u0022set_project_config\u0022, McpGate.Args( ( \u0022title\u0022, title ), ( \u0022description\u0022, description ), ( \u0022version\u0022, version ), ( \u0022type\u0022, type ), ( \u0022packageIdent\u0022, packageIdent ), ( \u0022summary\u0022, summary ), ( \u0022isPublic\u0022, isPublic ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Set or update the project thumbnail image (thumb.png) used for publishing. Provide either a\r\n\t/// source path or base64 image data.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022sourcePath\u0022\u003ERelative path to an image file within the project to use as thumbnail.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022base64\u0022\u003EBase64-encoded image data to write as thumbnail.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022format\u0022\u003EImage format when using base64 mode. Defaults to \u0027png\u0027. One of: png | jpg.\u003C/param\u003E\r\n\t[McpTool( \u0022set_project_thumbnail\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SetProjectThumbnail( string sourcePath = null, string base64 = null, string format = null )\r\n\t\t=\u003E McpGate.Run( \u0022set_project_thumbnail\u0022, McpGate.Args( ( \u0022sourcePath\u0022, sourcePath ), ( \u0022base64\u0022, base64 ), ( \u0022format\u0022, format ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Force s\u0026amp;box to recompile and hotload all C# scripts immediately. Use after creating or\r\n\t/// editing scripts to see changes in real-time.\r\n\t/// \u003C/summary\u003E\r\n\t[McpTool( \u0022trigger_hotload\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E TriggerHotload()\r\n\t\t=\u003E McpGate.Run( \u0022trigger_hotload\u0022, McpGate.Args() );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Write or overwrite a file in the s\u0026amp;box project (SILENTLY replaces existing content \u2014\r\n\t/// read_file first if you need to preserve it). Creates parent directories as needed; paths are\r\n\t/// confined to the project root (traversal outside it is denied). Returns a confirmation with the\r\n\t/// path \u2014 for C# follow with trigger_hotload so it compiles; for assets (.vmat etc.) follow with\r\n\t/// recompile_asset.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022path\u0022\u003ERelative path for the file (e.g. \u0027code/Components/Health.cs\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022content\u0022\u003EThe full file content to write.\u003C/param\u003E\r\n\t[McpTool( \u0022write_file\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E WriteFile( string path, string content )\r\n\t\t=\u003E McpGate.Run( \u0022write_file\u0022, McpGate.Args( ( \u0022path\u0022, path ), ( \u0022content\u0022, content ) ) );\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/Mcp/BridgeValidationTools.cs","FileName":"BridgeValidationTools.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"// AUTO-GENERATED by scripts/emit-mcp-wrappers.mjs \u2014 DO NOT EDIT.\r\n// Regenerate: node scripts/extract-manifest.mjs \u0026\u0026 node scripts/emit-mcp-wrappers.mjs\r\n// Source of truth: sbox-mcp-server/src/tools/ (zod schemas) \u2192 scripts/tools-manifest.json\r\n\r\nusing System.Text.Json.Nodes;\r\nusing System.Threading.Tasks;\r\nusing Editor.Mcp;\r\n\r\n/// \u003Csummary\u003E\r\n/// Lint and validate the project: networking footguns, sandbox whitelist violations, Razor\r\n/// transpiler footguns, scene setup issues, publishing readiness, save-file inspection, and\r\n/// networked-object state dumps.\r\n/// \u003C/summary\u003E\r\n[McpToolset( \u0022bridge_validation\u0022, \u0022Lint and validate the project: networking footguns, sandbox whitelist violations, Razor transpiler footguns, scene setup issues, publishing readiness, save-file inspection, and networked-object state dumps.\u0022 )]\r\npublic static class BridgeValidationTools\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Scan the project for broken references, two layers in one call: (1) every GameObject in the open\r\n\t/// scene \u2014 renderers with no Model (missing_model), component properties pointing at DESTROYED\r\n\t/// GameObjects/Components (dead_gameobject_ref / dead_component_ref), null component entries whose\r\n\t/// type no longer exists (missing_component); (2) every .scene/.prefab FILE \u2014 prefab references to\r\n\t/// deleted/renamed files (missing_prefab_file). Returns { total, showing, truncated,\r\n\t/// objectsScanned, filesScanned, issues } \u2014 each issue has { id, name, component, kind, detail }\r\n\t/// (file-level issues carry the file path in name). Fix missing models with assign_model, dead refs\r\n\t/// with set_property/set_component_reference, missing prefab files by fixing the path or recreating\r\n\t/// via create_prefab. Read-only; safe any time. Results cap at \u0060limit\u0060 (default 100, max 500).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022limit\u0022\u003EMax issues to return (default 100, max 500). total still counts everything.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022scanFiles\u0022\u003EInclude the .scene/.prefab file scan for missing prefab references. Default true.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022find_broken_references\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E FindBrokenReferences( int? limit = null, bool? scanFiles = null )\r\n\t\t=\u003E McpGate.Run( \u0022find_broken_references\u0022, McpGate.Args( ( \u0022limit\u0022, limit ), ( \u0022scanFiles\u0022, scanFiles ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Inspect the live networking contract of a GameObject. Returns {id, name, network: {active,\r\n\t/// isProxy, isOwner, isCreator, ownerId, ownerSteamId, ownerTransfer, orphaned, flags}, components:\r\n\t/// [{component, fields: [{name, type, isSync, syncFlags, value}]}]} \u2014 by default only [Sync]-marked\r\n\t/// fields are listed (components with none are omitted). Unlike get_network_status (session-only),\r\n\t/// this is per-object \u2014 the way to verify a host-authoritative or ownership change actually\r\n\t/// replicated; works in edit or play mode. Follow up with set_ownership to change the owner, or\r\n\t/// networking_lint to find the code-level cause of a bad [Sync] value.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject to inspect.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022allProps\u0022\u003EInclude all component properties, not just [Sync]-marked ones.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022inspect_networked_object\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E InspectNetworkedObject( string id, bool allProps = false )\r\n\t\t=\u003E McpGate.Run( \u0022inspect_networked_object\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022allProps\u0022, allProps ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Static-scan the project\u0027s C# for the highest-frequency networking/authority bugs: a mutator that\r\n\t/// writes a [Sync] field with no IsProxy/Networking.IsHost guard; money/health/score-shaped fields\r\n\t/// marked plain [Sync] (should be SyncFlags.FromHost); List\u0026lt;\u0026gt;/Dictionary\u0026lt;\u0026gt; marked\r\n\t/// [Sync] (should be NetList/NetDictionary); [Sync] fields typed Connection/GameObject (sync a Guid\r\n\t/// instead); [Rpc.Host] methods that mutate without re-checking Rpc.Caller; and component swaps /\r\n\t/// reflection writes missing Network.Refresh(). Returns findings with file:line \u002B the suggested\r\n\t/// fix.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022path\u0022\u003EOptional sub-path under the project (e.g. \u0027Code/Player\u0027) to scope the scan; omit for the whole project.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022networking_lint\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E NetworkingLint( string path = null )\r\n\t\t=\u003E McpGate.Run( \u0022networking_lint\u0022, McpGate.Args( ( \u0022path\u0022, path ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Static-scan .razor and .razor.scss files for the silent footguns that crash the Razor transpiler\r\n\t/// or stylesheet engine with no useful error message: switch expressions inside @code blocks (use\r\n\t/// if/else instead), non-ASCII/emoji inside @code (move to markup or a string constant),\r\n\t/// PanelComponent subclasses missing a BuildHash override (panel never re-renders), and root\r\n\t/// uppercase type-selector rules in .razor.scss (silently skipped -- use a class selector like\r\n\t/// .my-panel). Returns { scanned, findings: [{file, line, match, advice}], clean } matching the\r\n\t/// sandbox_lint shape.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory under the project root to scan (e.g. \u0027UI\u0027, \u0027Code\u0027). Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022razor_lint\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E RazorLint( string directory = null )\r\n\t\t=\u003E McpGate.Run( \u0022razor_lint\u0022, McpGate.Args( ( \u0022directory\u0022, directory ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Static-scan the project\u0027s C# for s\u0026amp;box sandbox whitelist violations BEFORE they cause\r\n\t/// compile errors: System.MathF (use MathX), System.Math (use MathX), Array.Clone() (use\r\n\t/// .ToArray()), System.Net / raw sockets (use Sandbox.Http), System.IO.File (use FileSystem.Data),\r\n\t/// and raw System.Threading.Thread (use async/Task or GameTask). Returns { scanned, findings:\r\n\t/// [{file, line, match, advice}], clean }. Scope to a subdirectory with the directory param.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory under the project root to scan (e.g. \u0027Code\u0027, \u0027Code/Player\u0027). Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022sandbox_lint\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SandboxLint( string directory = null )\r\n\t\t=\u003E McpGate.Run( \u0022sandbox_lint\u0022, McpGate.Args( ( \u0022directory\u0022, directory ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Inspect the game\u0027s FileSystem.Data save files \u2014 the assistant is otherwise blind to persisted\r\n\t/// state. action=\u0027list\u0027 (default) returns \u0060directories\u0060 and \u0060files\u0060 [{name, path, size}] under\r\n\t/// \u0060path\u0060 (omit path for the Data root); action=\u0027read\u0027 returns {path, length, content}, truncating\r\n\t/// content at 60,000 chars; action=\u0027diff\u0027 compares two save files key-by-key, returning \u0060diffCount\u0060\r\n\t/// and up to 200 \u0060diffs\u0060 [{key, change: added|removed|changed}]. Use to verify a save actually\r\n\t/// wrote, debug a load/migration, or confirm a sanitize/clamp ran.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022action\u0022\u003E\u0027list\u0027 (default) enumerates a folder; \u0027read\u0027 dumps one file\u0027s JSON; \u0027diff\u0027 compares \u0060path\u0060 vs \u0060pathB\u0060. One of: list | read | diff. Default: \u0022list\u0022.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022path\u0022\u003EFile or folder path under FileSystem.Data (e.g. \u0027lumber_corp2_progress\u0027 or \u0027\u0026lt;folder\u0026gt;/steam_123.json\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022pathB\u0022\u003ESecond file path for action=\u0027diff\u0027.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022save_inspect\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SaveInspect( string action = \u0022list\u0022, string path = null, string pathB = null )\r\n\t\t=\u003E McpGate.Run( \u0022save_inspect\u0022, McpGate.Args( ( \u0022action\u0022, action ), ( \u0022path\u0022, path ), ( \u0022pathB\u0022, pathB ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Validate the active scene for the silent setup footguns that break controllers/physics/cameras:\r\n\t/// no CameraComponent, no player controller, multiple root Rigidbodies, a Rigidbody with\r\n\t/// MotionEnabled=false fighting a kinematic root, IsTrigger colliders that Scene.Trace will ignore,\r\n\t/// child Rigidbodies breaking collider binding, and missing required child anchors. Returns each\r\n\t/// issue with the GameObject and the exact fix.\r\n\t/// \u003C/summary\u003E\r\n\t[McpTool.ReadOnly( \u0022scene_validate\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SceneValidate()\r\n\t\t=\u003E McpGate.Run( \u0022scene_validate\u0022, McpGate.Args() );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Read from Sandbox.Services \u2014 the cloud stats/leaderboard layer many games use as their real DB.\r\n\t/// action=\u0027stats\u0027 with \u0060name\u0060 returns the local player\u0027s stat {ident, value, sum, min, max,\r\n\t/// lastValue, valueString}; without \u0060name\u0060 it returns only the package ident plus a usage note (it\r\n\t/// does NOT list stat definitions). action=\u0027leaderboard\u0027 (name required) returns {board,\r\n\t/// displayName, totalEntries, count, entries} with at most \u0060limit\u0060 entries (default 10). Read-only;\r\n\t/// use to verify a Stats.Increment/SetValue path or a leaderboard wired correctly.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022action\u0022\u003E\u0027stats\u0027 (default) reads a local-player stat by \u0060name\u0060; \u0027leaderboard\u0027 fetches a board\u0027s top entries. One of: stats | leaderboard. Default: \u0022stats\u0022.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EStat name (action=\u0027stats\u0027) or leaderboard/board name (action=\u0027leaderboard\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022limit\u0022\u003EMax leaderboard entries to return.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022services_query\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E ServicesQuery( string action = \u0022stats\u0022, string name = null, int limit = 10 )\r\n\t\t=\u003E McpGate.Run( \u0022services_query\u0022, McpGate.Args( ( \u0022action\u0022, action ), ( \u0022name\u0022, name ), ( \u0022limit\u0022, limit ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Validate that the project is ready for publishing. Runs four checks: .sbproj exists, at least\r\n\t/// one scene, project Ident set, project Title set. Returns { valid, issueCount, issues, checks } \u2014\r\n\t/// issues are human-readable problems and each checks entry has { check, pass, detail }; fix\r\n\t/// metadata gaps with set_project_config (it does NOT check compile errors \u2014 use get_compile_errors\r\n\t/// for that).\r\n\t/// \u003C/summary\u003E\r\n\t[McpTool.ReadOnly( \u0022validate_project\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E ValidateProject()\r\n\t\t=\u003E McpGate.Run( \u0022validate_project\u0022, McpGate.Args() );\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/NpcBrainHandlers.cs","FileName":"NpcBrainHandlers.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"using Editor;\r\nusing Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Reflection;\r\nusing System.Text;\r\nusing System.Text.Json;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\n\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n//  NPC Brains \u2014 Feature Wave #3 (Phase 1 \u002B simulate_npc_perception)\r\n//\r\n//  Compiles into the SAME editor assembly as MyEditorMenu.cs, so it can use the\r\n//  shared helpers there directly: ClaudeBridge.TryResolveProjectPath /\r\n//  SanitizeIdentifier / ParseVector3, SceneToolHelpers.*, and the IBridgeHandler\r\n//  interface. These handlers run in the UNSANDBOXED editor (System.Math/MathF/IO\r\n//  are all fine here).\r\n//\r\n//  The C# *strings these handlers generate* run in the SANDBOX (the game). That\r\n//  generated code is deliberately restricted to APIs already proven to compile in\r\n//  the sandbox by the existing create_npc_controller / create_networked_player\r\n//  generators: Component, [Property], [Sync], GetOrAddComponent\u003CNavMeshAgent\u003E(),\r\n//  NavMeshAgent.MoveTo(Vector3), IsProxy, TimeSince, Vector3.Dot/.Normal/\r\n//  .DistanceBetween, Scene.GetAllComponents\u003CT\u003E(), scene.Trace.Ray(a,b).Run(),\r\n//  MathX.Clamp. MathX preferred in generated code; System.Math/MathF also compile on the current SDK (verified 2026-06-09). Array.Clone() still blocked.\r\n//\r\n//  Tools in this file:\r\n//    create_npc_brain        (code-gen; scene-mutating)\r\n//    place_patrol_route      (scene-mutating)\r\n//    assign_patrol_route     (scene-mutating)\r\n//    create_npc_spawner      (code-gen; scene-mutating)\r\n//    simulate_npc_perception (READ-ONLY; not scene-mutating)\r\n//\r\n//  Register(...) lines \u002B _sceneMutatingCommands additions are wired by the main\r\n//  agent in MyEditorMenu.cs (see this wave\u0027s summary) to avoid a merge conflict.\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n\r\n/// \u003Csummary\u003E\r\n/// Shared helpers for the NPC-brain generators. Kept internal to this file so it\r\n/// does not collide with anything in MyEditorMenu.cs.\r\n/// \u003C/summary\u003E\r\ninternal static class NpcBrainHelpers\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Read an optional float param, falling back to \u003Cparamref name=\u0022fallback\u0022/\u003E.\r\n\t/// Tolerates the value arriving as a JSON number OR a numeric string.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static float Float( JsonElement p, string key, float fallback )\r\n\t{\r\n\t\tif ( !p.TryGetProperty( key, out var e ) ) return fallback;\r\n\t\tif ( e.ValueKind == JsonValueKind.Number \u0026\u0026 e.TryGetSingle( out var f ) ) return f;\r\n\t\tif ( e.ValueKind == JsonValueKind.String \u0026\u0026 float.TryParse( e.GetString(), out var fs ) ) return fs;\r\n\t\treturn fallback;\r\n\t}\r\n\r\n\tpublic static int Int( JsonElement p, string key, int fallback )\r\n\t{\r\n\t\tif ( !p.TryGetProperty( key, out var e ) ) return fallback;\r\n\t\tif ( e.ValueKind == JsonValueKind.Number \u0026\u0026 e.TryGetInt32( out var i ) ) return i;\r\n\t\tif ( e.ValueKind == JsonValueKind.String \u0026\u0026 int.TryParse( e.GetString(), out var iss ) ) return iss;\r\n\t\treturn fallback;\r\n\t}\r\n\r\n\tpublic static bool Bool( JsonElement p, string key, bool fallback )\r\n\t{\r\n\t\tif ( !p.TryGetProperty( key, out var e ) ) return fallback;\r\n\t\tif ( e.ValueKind == JsonValueKind.True ) return true;\r\n\t\tif ( e.ValueKind == JsonValueKind.False ) return false;\r\n\t\tif ( e.ValueKind == JsonValueKind.String \u0026\u0026 bool.TryParse( e.GetString(), out var b ) ) return b;\r\n\t\treturn fallback;\r\n\t}\r\n\r\n\tpublic static string Str( JsonElement p, string key, string fallback )\r\n\t{\r\n\t\tif ( p.TryGetProperty( key, out var e ) \u0026\u0026 e.ValueKind == JsonValueKind.String )\r\n\t\t{\r\n\t\t\tvar s = e.GetString();\r\n\t\t\tif ( !string.IsNullOrWhiteSpace( s ) ) return s;\r\n\t\t}\r\n\t\treturn fallback;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Format a float as an invariant-culture C# literal with an \u0027f\u0027 suffix, e.g.\r\n\t/// 130 -\u003E \u0022130f\u0022, 0.25 -\u003E \u00220.25f\u0022. Invariant culture matters so a comma-decimal\r\n\t/// locale on the editor machine cannot emit \u00220,25f\u0022 and break compilation.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string F( float v )\r\n\t{\r\n\t\tvar s = v.ToString( \u00220.0###\u0022, System.Globalization.CultureInfo.InvariantCulture );\r\n\t\treturn s \u002B \u0022f\u0022;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Escape a user string for safe embedding inside a C# double-quoted verbatim\r\n\t/// string ( @\u0022\u0022 ), where the only escape needed is doubling the quote char.\r\n\t/// TargetTag is also identifier-ish but tags can legitimately contain symbols,\r\n\t/// so we keep it a string literal rather than sanitizing it to an identifier.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string EscVerbatim( string raw ) =\u003E ( raw ?? \u0022\u0022 ).Replace( \u0022\\\u0022\u0022, \u0022\\\u0022\\\u0022\u0022 );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// cos( fovDegrees / 2 ) computed in the EDITOR (MathF is legal here). Baked as\r\n\t/// the default of the generated CosFovThreshold property so the sandbox brain\r\n\t/// never needs trig. Clamped to a sane FOV range first.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static float CosHalfFov( float fovDegrees )\r\n\t{\r\n\t\tvar fov = Math.Clamp( fovDegrees, 1f, 360f );\r\n\t\tvar halfRad = ( fov * 0.5f ) * ( MathF.PI / 180f );\r\n\t\treturn MathF.Cos( halfRad );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Resolve the component on \u003Cparamref name=\u0022go\u0022/\u003E that exposes a property named\r\n\t/// \u003Cparamref name=\u0022property\u0022/\u003E, and SET that property to \u003Cparamref name=\u0022value\u0022/\u003E.\r\n\t/// Preferred match is a component literally named \u0022NpcBrain\u0022; otherwise the first\r\n\t/// component whose TypeLibrary description has that property. Returns the matched\r\n\t/// component (so the caller can report its name), or null if none matched.\r\n\t///\r\n\t/// We deliberately do the find\u002Bset inside one method so this file never has to\r\n\t/// name the reflection types (TypeDescription / PropertyDescription) \u2014 the rest\r\n\t/// of the addon always uses \u0060var\u0060 for them, which means their namespace is not\r\n\t/// guaranteed to be importable here. Keeping it all behind \u0060var\u0060 mirrors the\r\n\t/// proven SetPrefabRefHandler pattern exactly.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Component SetComponentProperty( GameObject go, string property, object value )\r\n\t{\r\n\t\tComponent fallbackComp = null;\r\n\r\n\t\t// Pass 1: prefer an NpcBrain. Pass 2: any component exposing the property.\r\n\t\tforeach ( var c in go.Components.GetAll() )\r\n\t\t{\r\n\t\t\tvar td = Game.TypeLibrary.GetType( c.GetType().Name );\r\n\t\t\tvar pd = td?.Properties.FirstOrDefault( pp =\u003E pp.Name == property );\r\n\t\t\tif ( pd == null ) continue;\r\n\r\n\t\t\tif ( c.GetType().Name.Equals( \u0022NpcBrain\u0022, StringComparison.OrdinalIgnoreCase ) )\r\n\t\t\t{\r\n\t\t\t\tpd.SetValue( c, value );\r\n\t\t\t\treturn c;\r\n\t\t\t}\r\n\r\n\t\t\tfallbackComp = fallbackComp ?? c;\r\n\t\t}\r\n\r\n\t\tif ( fallbackComp != null )\r\n\t\t{\r\n\t\t\tvar td = Game.TypeLibrary.GetType( fallbackComp.GetType().Name );\r\n\t\t\tvar pd = td?.Properties.FirstOrDefault( pp =\u003E pp.Name == property );\r\n\t\t\tpd?.SetValue( fallbackComp, value );\r\n\t\t}\r\n\r\n\t\treturn fallbackComp;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Find the \u0022perception brain\u0022 component on \u003Cparamref name=\u0022go\u0022/\u003E \u2014 the component\r\n\t/// simulate_npc_perception should read SightRange/FovDegrees/EyeHeight/TargetTag from.\r\n\t///\r\n\t/// Why not just match the type name \u0022NpcBrain\u0022: a custom-named brain (e.g. BigfootBrain,\r\n\t/// generated via create_npc_brain with name=\u0022BigfootBrain\u0022) exposes the same perception\r\n\t/// [Property] surface but a different type name, so a literal name match silently falls\r\n\t/// back to spec defaults. We match by CAPABILITY instead:\r\n\t///   1. a component literally named \u0022NpcBrain\u0022 (the default), else\r\n\t///   2. a component whose TypeLibrary description exposes BOTH SightRange and FovDegrees\r\n\t///      (the perception contract), else\r\n\t///   3. a component whose type name ends with \u0022Brain\u0022.\r\n\t/// Returns null if none match (caller then uses defaults / explicit overrides).\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Component FindPerceptionBrain( GameObject go )\r\n\t{\r\n\t\tif ( go == null ) return null;\r\n\r\n\t\tComponent byProps = null;\r\n\t\tComponent byName  = null;\r\n\r\n\t\tforeach ( var c in go.Components.GetAll() )\r\n\t\t{\r\n\t\t\tvar typeName = c.GetType().Name;\r\n\r\n\t\t\t// 1. Exact \u0022NpcBrain\u0022 wins immediately (the generated default).\r\n\t\t\tif ( typeName.Equals( \u0022NpcBrain\u0022, StringComparison.OrdinalIgnoreCase ) )\r\n\t\t\t\treturn c;\r\n\r\n\t\t\t// 2. Capability match: exposes the perception property contract.\r\n\t\t\tif ( byProps == null )\r\n\t\t\t{\r\n\t\t\t\tvar td = Game.TypeLibrary.GetType( typeName );\r\n\t\t\t\tif ( td != null\r\n\t\t\t\t\t\u0026\u0026 td.Properties.Any( pp =\u003E pp.Name == \u0022SightRange\u0022 )\r\n\t\t\t\t\t\u0026\u0026 td.Properties.Any( pp =\u003E pp.Name == \u0022FovDegrees\u0022 ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tbyProps = c;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\t// 3. Name heuristic: \u0022...Brain\u0022.\r\n\t\t\tif ( byName == null \u0026\u0026 typeName.EndsWith( \u0022Brain\u0022, StringComparison.OrdinalIgnoreCase ) )\r\n\t\t\t\tbyName = c;\r\n\t\t}\r\n\r\n\t\treturn byProps ?? byName;\r\n\t}\r\n}\r\n\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n//  1. create_npc_brain  (code-gen; scene-mutating)\r\n//     Generates an NpcBrain Component: a finite-state machine (Idle/Patrol/\r\n//     Wander/Chase/Search/Flee/Ambush) driven by occlusion-aware perception\r\n//     (FOV cone \u002B range \u002B LOS trace \u002B hearing) with last-known-position memory.\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\npublic class CreateNpcBrainHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar name      = NpcBrainHelpers.Str( p, \u0022name\u0022, \u0022NpcBrain\u0022 );\r\n\t\t\tvar directory = NpcBrainHelpers.Str( p, \u0022directory\u0022, \u0022Code\u0022 );\r\n\r\n\t\t\tvar fileName = name.EndsWith( \u0022.cs\u0022 ) ? name : $\u0022{name}.cs\u0022;\r\n\t\t\tif ( !ClaudeBridge.TryResolveProjectPath( Path.Combine( directory, fileName ), out var fullPath, out var pathErr ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = pathErr } );\r\n\r\n\t\t\tif ( File.Exists( fullPath ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022File already exists: {directory}/{fileName}\u0022 } );\r\n\r\n\t\t\tvar className = ClaudeBridge.SanitizeIdentifier( Path.GetFileNameWithoutExtension( fileName ) );\r\n\r\n\t\t\t// \u2500\u2500 Preset \u2192 defaults. The generated file is identical shape; the preset\r\n\t\t\t//    only changes [Property] defaults (StartState, CanFlee).\r\n\t\t\tvar behavior = NpcBrainHelpers.Str( p, \u0022behavior\u0022, \u0022hunter\u0022 ).ToLowerInvariant();\r\n\t\t\tstring startState;\r\n\t\t\tbool presetCanFlee;\r\n\t\t\tswitch ( behavior )\r\n\t\t\t{\r\n\t\t\t\tcase \u0022patrol\u0022:   startState = \u0022Patrol\u0022; presetCanFlee = false; break;\r\n\t\t\t\tcase \u0022guard\u0022:    startState = \u0022Ambush\u0022; presetCanFlee = false; break;\r\n\t\t\t\tcase \u0022swarm\u0022:    startState = \u0022Wander\u0022; presetCanFlee = false; break;\r\n\t\t\t\tcase \u0022skittish\u0022: startState = \u0022Patrol\u0022; presetCanFlee = true;  break;\r\n\t\t\t\tcase \u0022hunter\u0022:\r\n\t\t\t\tdefault:         behavior = \u0022hunter\u0022; startState = \u0022Patrol\u0022; presetCanFlee = false; break;\r\n\t\t\t}\r\n\r\n\t\t\t// \u2500\u2500 Tunables (params override preset/spec defaults). \u2500\u2500\r\n\t\t\tvar moveSpeed     = NpcBrainHelpers.Float( p, \u0022moveSpeed\u0022,     130f );\r\n\t\t\tvar chaseSpeed    = NpcBrainHelpers.Float( p, \u0022chaseSpeed\u0022,    200f );\r\n\t\t\tvar sightRange    = NpcBrainHelpers.Float( p, \u0022sightRange\u0022,    1500f );\r\n\t\t\tvar fovDegrees    = NpcBrainHelpers.Float( p, \u0022fovDegrees\u0022,    110f );\r\n\t\t\tvar eyeHeight     = NpcBrainHelpers.Float( p, \u0022eyeHeight\u0022,     64f );\r\n\t\t\tvar hearingRadius = NpcBrainHelpers.Float( p, \u0022hearingRadius\u0022, 600f );\r\n\t\t\tvar giveUpTime    = NpcBrainHelpers.Float( p, \u0022giveUpTime\u0022,    6f );\r\n\t\t\tvar searchRadius  = NpcBrainHelpers.Float( p, \u0022searchRadius\u0022,  400f );\r\n\t\t\tvar waypointStop  = NpcBrainHelpers.Float( p, \u0022waypointStopDistance\u0022, 80f );\r\n\t\t\tvar canFlee       = NpcBrainHelpers.Bool(  p, \u0022canFlee\u0022,       presetCanFlee );\r\n\t\t\tvar fleeHealth    = NpcBrainHelpers.Float( p, \u0022fleeHealthFrac\u0022, 0.25f );\r\n\t\t\tvar networked     = NpcBrainHelpers.Bool(  p, \u0022networked\u0022,     true );\r\n\t\t\tvar targetTag     = NpcBrainHelpers.Str(   p, \u0022targetTag\u0022,     \u0022player\u0022 );\r\n\t\t\t// Citizen locomotion animation: when on (default), the generated brain caches a\r\n\t\t\t// SkinnedModelRenderer \u002B CitizenAnimationHelper in OnStart and drives walk/run/idle\r\n\t\t\t// from the NavMeshAgent each frame (so the NPC and every spawner clone animate\r\n\t\t\t// instead of sliding in bind pose). Proven approach ported from BigfootBrain.cs.\r\n\t\t\tvar animate       = NpcBrainHelpers.Bool(  p, \u0022animate\u0022,       true );\r\n\r\n\t\t\tvar cosFov = NpcBrainHelpers.CosHalfFov( fovDegrees );\r\n\r\n\t\t\tvar code = BuildSource(\r\n\t\t\t\tclassName, startState, networked, animate,\r\n\t\t\t\tNpcBrainHelpers.EscVerbatim( targetTag ),\r\n\t\t\t\tmoveSpeed, chaseSpeed, sightRange, fovDegrees, cosFov, eyeHeight,\r\n\t\t\t\thearingRadius, giveUpTime, searchRadius, waypointStop, canFlee, fleeHealth );\r\n\r\n\t\t\tDirectory.CreateDirectory( Path.GetDirectoryName( fullPath ) );\r\n\t\t\tFile.WriteAllText( fullPath, code );\r\n\r\n\t\t\tvar states = new[] { \u0022Idle\u0022, \u0022Patrol\u0022, \u0022Wander\u0022, \u0022Chase\u0022, \u0022Search\u0022, \u0022Flee\u0022, \u0022Ambush\u0022 };\r\n\t\t\tvar props = new[]\r\n\t\t\t{\r\n\t\t\t\t\u0022StartState\u0022,\u0022MoveSpeed\u0022,\u0022ChaseSpeed\u0022,\u0022SightRange\u0022,\u0022FovDegrees\u0022,\u0022CosFovThreshold\u0022,\r\n\t\t\t\t\u0022EyeHeight\u0022,\u0022HearingRadius\u0022,\u0022TargetTag\u0022,\u0022GiveUpTime\u0022,\u0022SearchRadius\u0022,\u0022WaypointStopDistance\u0022,\r\n\t\t\t\t\u0022PingPong\u0022,\u0022CanFlee\u0022,\u0022FleeHealthFrac\u0022,\u0022CurrentHealthFrac\u0022,\u0022Waypoints\u0022,\u0022CurrentState\u0022\r\n\t\t\t};\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated    = true,\r\n\t\t\t\tpath       = $\u0022{directory}/{fileName}\u0022,\r\n\t\t\t\tclassName,\r\n\t\t\t\tbehavior,\r\n\t\t\t\tnetworked,\r\n\t\t\t\tanimate,\r\n\t\t\t\tstatesIncluded = states,\r\n\t\t\t\tpropertyNames  = props,\r\n\t\t\t\tnote = \u0022NavMeshAgent is added automatically via GetOrAddComponent in OnStart. \u0022 \u002B\r\n\t\t\t\t       \u0022Requires bake_navmesh \u002B a navmesh-walkable scene for movement. \u0022 \u002B\r\n\t\t\t\t       \u0022Assign a patrol route with place_patrol_route \u002B assign_patrol_route. \u0022 \u002B\r\n\t\t\t\t       \u0022Verify perception in EDIT mode with simulate_npc_perception; verify chase/search by entering play mode \u0022 \u002B\r\n\t\t\t\t       \u0022(get_runtime_property CurrentState \u002B timed screenshot_from). \u0022 \u002B\r\n\t\t\t\t       ( animate\r\n\t\t\t\t         ? \u0022Locomotion animation ON: caches a SkinnedModelRenderer \u002B CitizenAnimationHelper in OnStart and drives walk/run/idle from the NavMeshAgent each frame \u2014 attach this brain to a GameObject with a Citizen (or any SkinnedModel) renderer (on it or a child) and it animates while moving instead of sliding. Spawner clones inherit it (each runs its own OnStart). Pass animate:false to disable. \u0022\r\n\t\t\t\t         : \u0022Locomotion animation OFF (animate:false): the NPC slides in bind pose; drive a CitizenAnimationHelper yourself if you want walk/run anims. \u0022 ) \u002B\r\n\t\t\t\t       ( networked\r\n\t\t\t\t         ? \u0022Networked: host-authoritative (if(IsProxy)return) \u002B [Sync] CurrentState \u2014 needs a host session; a no-session solo playtest makes everything a proxy so the brain won\u0027t think (use networked:false to iterate solo).\u0022\r\n\t\t\t\t         : \u0022Solo/edit build: no IsProxy guard, so it ticks in a single-machine playtest.\u0022 )\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_npc_brain failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Build the NpcBrain component source. Everything here must be SANDBOX-LEGAL.\r\n\t/// Movement uses only the confirmed NavMeshAgent.MoveTo(Vector3); perception\r\n\t/// uses only Vector3.Dot/.Normal \u002B scene.Trace.Ray(a,b).Run() \u002B Scene.GetAllComponents.\r\n\t/// FOV uses a baked cosine threshold (no trig in the sandbox).\r\n\t/// When \u003Cparamref name=\u0022animate\u0022/\u003E is true the generated brain also caches a\r\n\t/// CitizenAnimationHelper (off a SkinnedModelRenderer) and feeds it the NavMeshAgent\r\n\t/// velocity each frame \u2014 sandbox-legal locomotion ported from BigfootBrain.cs (uses\r\n\t/// Sandbox.Citizen \u002B MathX, never System.Math).\r\n\t/// \u003C/summary\u003E\r\n\tprivate static string BuildSource(\r\n\t\tstring className, string startState, bool networked, bool animate, string targetTagLiteral,\r\n\t\tfloat moveSpeed, float chaseSpeed, float sightRange, float fovDegrees, float cosFov,\r\n\t\tfloat eyeHeight, float hearingRadius, float giveUpTime, float searchRadius,\r\n\t\tfloat waypointStop, bool canFlee, float fleeHealth )\r\n\t{\r\n\t\tstring F( float v ) =\u003E NpcBrainHelpers.F( v );\r\n\r\n\t\t// Host-authority guard line (networked) vs none (solo). The [Sync] on\r\n\t\t// CurrentState lets proxies read the host\u0027s state for client-side animation.\r\n\t\tvar proxyGuard   = networked ? \u0022\\t\\tif ( IsProxy ) return;   // host-authoritative \u2014 only the host thinks\\n\u0022 : \u0022\u0022;\r\n\t\tvar stateAttr    = networked ? \u0022[Sync] \u0022 : \u0022\u0022;\r\n\t\tvar headerNote   = networked\r\n\t\t\t? \u0022// Host-authoritative AI brain. Only the host runs the FSM; CurrentState is [Sync]\u0027d\\n// so proxy clients can animate the NPC. Needs an active network session (a no-session\\n// solo playtest makes everything a proxy \u2014 generate with networked:false to iterate solo).\\n\u0022\r\n\t\t\t: \u0022// Solo / edit-scene AI brain (no networking guard). Ticks in a single-machine playtest.\\n\u0022;\r\n\r\n\t\t// \u2500\u2500 Citizen locomotion animation (ported verbatim from the proven BigfootBrain.cs).\r\n\t\t// Everything here is sandbox-legal: Sandbox.Citizen \u002B GetOrAddComponent \u002B the\r\n\t\t// NavMeshAgent\u0027s own Velocity/WishVelocity, no System.Math. When animate:false these\r\n\t\t// fragments are empty strings, so the generated brain is byte-for-byte the old one.\r\n\t\tvar animUsing  = animate ? \u0022using Sandbox.Citizen;\\n\u0022 : \u0022\u0022;\r\n\t\tvar animFields = animate\r\n\t\t\t? \u0022\\n\\t// Citizen locomotion. Drives the anim helper from the agent\u0027s velocity each frame so the\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t// NPC walks/runs/idles instead of sliding in bind pose. Cached off the SkinnedModelRenderer\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t// in OnStart (works for the source NPC AND its spawner clones \u2014 they each run OnStart).\\n\u0022 \u002B\r\n\t\t\t  \u0022\\tprivate CitizenAnimationHelper _anim;\\n\u0022 \u002B\r\n\t\t\t  \u0022\\tprivate SkinnedModelRenderer _renderer;\\n\u0022\r\n\t\t\t: \u0022\u0022;\r\n\t\t// OnStart wiring. Wiring _anim.Target avoids a WithWishVelocity NRE (see SBOX_KNOWLEDGE.md).\r\n\t\tvar animOnStart = animate\r\n\t\t\t? \u0022\\n\\t\\t// Locomotion animation. Find the SkinnedModelRenderer (this GO or a child), then\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\t// get-or-add a CitizenAnimationHelper and wire its Target \u2014 the helper NREs in\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\t// WithWishVelocity if Target is null. A Citizen .vmdl already has the locomotion\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\t// anim-graph, so once fed velocity it walks/runs/idles on its own.\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\t_renderer = GetComponent\u003CSkinnedModelRenderer\u003E() ?? GetComponentInChildren\u003CSkinnedModelRenderer\u003E();\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\tif ( _renderer.IsValid() )\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\t{\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\t\\t_anim = GetOrAddComponent\u003CCitizenAnimationHelper\u003E();\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\t\\t_anim.Target = _renderer;\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\t}\\n\u0022\r\n\t\t\t: \u0022\u0022;\r\n\t\t// Per-frame drive call (placed at the end of OnUpdate) \u002B the method body.\r\n\t\tvar animUpdateCall = animate ? \u0022\\t\\tDriveAnimation();\\n\u0022 : \u0022\u0022;\r\n\t\tvar animMethod = animate\r\n\t\t\t? \u0022\\n\\t// \u2500\u2500 Locomotion animation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t/// \u003Csummary\u003EFeed the Citizen anim helper from the NavMeshAgent each frame so the NPC\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t/// plays walk/run/idle instead of sliding in bind pose. WithVelocity drives the\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t/// locomotion blend; WithWishVelocity drives lean/start-stop; IsGrounded keeps it out\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t/// of the fall pose. Glance toward the chased target, else toward travel direction.\u003C/summary\u003E\\n\u0022 \u002B\r\n\t\t\t  \u0022\\tprivate void DriveAnimation()\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t{\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\tif ( _anim == null || !_anim.IsValid() ) return;\\n\u0022 \u002B\r\n\t\t\t  \u0022\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\tvar velocity = _agent.Velocity;\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\t_anim.WithVelocity( velocity );\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\t_anim.WithWishVelocity( _agent.WishVelocity );\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\t_anim.IsGrounded = true;\\n\u0022 \u002B\r\n\t\t\t  \u0022\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\tVector3 lookDir;\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\tif ( CurrentState == BrainState.Chase \u0026\u0026 _target.IsValid() )\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\t\\tlookDir = ( _target.WorldPosition - WorldPosition ).WithZ( 0f );\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\telse\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\t\\tlookDir = velocity.WithZ( 0f );\\n\u0022 \u002B\r\n\t\t\t  \u0022\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\tif ( lookDir.Length \u003E 1f )\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t\\t\\t_anim.WithLook( lookDir.Normal, 1f, 0.6f, 0.2f );\\n\u0022 \u002B\r\n\t\t\t  \u0022\\t}\\n\u0022\r\n\t\t\t: \u0022\u0022;\r\n\r\n\t\treturn\r\n$@\u0022using Sandbox;\r\n{animUsing}using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n\r\n{headerNote}public sealed class {className} : Component\r\n{{\r\n\tpublic enum BrainState {{ Idle, Patrol, Wander, Chase, Search, Flee, Ambush }}\r\n\r\n\t// \u2500\u2500 Tunables (all [Property] so the bridge can set_property / tune later) \u2500\u2500\r\n\t[Property] public BrainState StartState {{ get; set; }} = BrainState.{startState};\r\n\t[Property] public float MoveSpeed  {{ get; set; }} = {F( moveSpeed )};\r\n\t[Property] public float ChaseSpeed {{ get; set; }} = {F( chaseSpeed )};\r\n\r\n\t// Perception\r\n\t[Property] public float SightRange    {{ get; set; }} = {F( sightRange )};\r\n\t// FovDegrees is the human-readable full cone angle. The actual gate compares a\r\n\t// dot product against CosFovThreshold = cos(FovDegrees/2), which is baked here so\r\n\t// the sandbox needs no trig. If you change FovDegrees at runtime, also update\r\n\t// CosFovThreshold (tune_npc_perception / set_property), or call SetFov(...) below.\r\n\t[Property] public float FovDegrees      {{ get; set; }} = {F( fovDegrees )};\r\n\t[Property] public float CosFovThreshold {{ get; set; }} = {F( cosFov )};\r\n\t[Property] public float EyeHeight     {{ get; set; }} = {F( eyeHeight )};\r\n\t[Property] public float HearingRadius {{ get; set; }} = {F( hearingRadius )};\r\n\t[Property] public string TargetTag    {{ get; set; }} = @\u0022\u0022{targetTagLiteral}\u0022\u0022;\r\n\r\n\t// Memory / timing\r\n\t[Property] public float GiveUpTime   {{ get; set; }} = {F( giveUpTime )};\r\n\t[Property] public float SearchRadius {{ get; set; }} = {F( searchRadius )};\r\n\t[Property] public float WaypointStopDistance {{ get; set; }} = {F( waypointStop )};\r\n\t[Property] public bool  PingPong     {{ get; set; }} = false;\r\n\r\n\t// Flee (health source is generic: the game sets CurrentHealthFrac 0..1, or\r\n\t// override ShouldFlee() in a partial/subclass \u2014 no hard coupling to any HP comp).\r\n\t[Property] public bool  CanFlee           {{ get; set; }} = {( canFlee ? \u0022true\u0022 : \u0022false\u0022 )};\r\n\t[Property] public float FleeHealthFrac    {{ get; set; }} = {F( fleeHealth )};\r\n\t[Property] public float CurrentHealthFrac {{ get; set; }} = 1f;\r\n\r\n\t// Patrol route (placed \u002B wired by assign_patrol_route, or hand-set in editor).\r\n\t[Property] public List\u003CGameObject\u003E Waypoints {{ get; set; }} = new();\r\n\r\n\t// \u2500\u2500 Runtime state \u2500\u2500\r\n\t{stateAttr}public BrainState CurrentState {{ get; private set; }}\r\n\tprivate GameObject _target;\r\n\tprivate Vector3 _lastKnownPos;\r\n\tprivate TimeSince _timeSinceSeen;\r\n\tprivate Vector3 _wanderTarget;\r\n\tprivate TimeSince _timeSinceWanderPick;\r\n\tprivate int _waypointIndex;\r\n\tprivate int _waypointDir = 1;\r\n\tprivate NavMeshAgent _agent;\r\n{animFields}\r\n\tprotected override void OnStart()\r\n\t{{\r\n\t\t_agent = GetOrAddComponent\u003CNavMeshAgent\u003E();\r\n{animOnStart}\t\tCurrentState = StartState;\r\n\t\t_timeSinceSeen = 999f;\r\n\t\t_lastKnownPos = WorldPosition;\r\n\t\t_wanderTarget = WorldPosition;\r\n\t}}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{{\r\n{proxyGuard}\t\tif ( _agent == null ) return;\r\n\r\n\t\tPerceive();\r\n\t\tThink();\r\n\t\tAct();\r\n{animUpdateCall}\t}}\r\n{animMethod}\r\n\r\n\t/// \u003Csummary\u003ERecompute the FOV cosine from a degree value at runtime (no trig in\r\n\t/// the sandbox: cos(x) via the half-angle identity from a normalized sweep is\r\n\t/// overkill, so we keep it simple \u2014 set both together).\u003C/summary\u003E\r\n\tpublic void SetFov( float degrees, float cosThreshold )\r\n\t{{\r\n\t\tFovDegrees = degrees;\r\n\t\tCosFovThreshold = cosThreshold;\r\n\t}}\r\n\r\n\t// \u2500\u2500 Perception \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\tprivate void Perceive()\r\n\t{{\r\n\t\tvar eye = WorldPosition \u002B Vector3.Up * EyeHeight;\r\n\t\tvar best = FindVisibleTarget( eye, out var sawSomething );\r\n\r\n\t\tif ( best.IsValid() )\r\n\t\t{{\r\n\t\t\t_target = best;\r\n\t\t\t_lastKnownPos = best.WorldPosition;\r\n\t\t\t_timeSinceSeen = 0f;\r\n\t\t\treturn;\r\n\t\t}}\r\n\r\n\t\t// Passive hearing: a candidate within HearingRadius is \u0022\u0022heard\u0022\u0022 (sets a\r\n\t\t// last-known position to investigate) but is NOT treated as seen \u2014 so the\r\n\t\t// NPC investigates rather than instantly aggroing.\r\n\t\tvar heard = FindNearestCandidate( WorldPosition, HearingRadius );\r\n\t\tif ( heard.IsValid() )\r\n\t\t\t_lastKnownPos = heard.WorldPosition;\r\n\r\n\t\t// keep _target ref while it grows stale; _timeSinceSeen advances on its own.\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003EPick the nearest candidate that passes range \u002B FOV cone \u002B LOS.\u003C/summary\u003E\r\n\tprivate GameObject FindVisibleTarget( Vector3 eye, out bool any )\r\n\t{{\r\n\t\tany = false;\r\n\t\tGameObject bestGo = null;\r\n\t\tfloat bestDist = float.MaxValue;\r\n\r\n\t\tforeach ( var cand in Candidates() )\r\n\t\t{{\r\n\t\t\tvar to = cand.WorldPosition - eye;\r\n\t\t\tfloat dist = to.Length;\r\n\t\t\tif ( dist \u003E SightRange ) continue;\r\n\t\t\tif ( dist \u003C 0.01f ) continue;\r\n\r\n\t\t\tvar dir = to.Normal;\r\n\t\t\t// FOV cone gate (cheap): dot \u003E= cos(half-fov). No trig needed.\r\n\t\t\tif ( Vector3.Dot( WorldRotation.Forward, dir ) \u003C CosFovThreshold ) continue;\r\n\r\n\t\t\t// Occlusion trace from the eye to the candidate. IgnoreGameObjectHierarchy\r\n\t\t\t// excludes the NPC\u0027s own colliders so it can\u0027t \u0022\u0022see\u0022\u0022 itself. Clear when the\r\n\t\t\t// ray hits the candidate directly, hits nothing, or the first hit is\r\n\t\t\t// essentially at the candidate (a child collider) \u2014 a distance test that\r\n\t\t\t// needs no extra API. Anything blocking earlier (a tree/wall) fails LOS.\r\n\t\t\tvar tr = Scene.Trace.Ray( eye, cand.WorldPosition ).IgnoreGameObjectHierarchy( GameObject ).Run();\r\n\t\t\tbool clear = !tr.Hit || tr.GameObject == cand || tr.Distance \u003E= dist - 8f;\r\n\t\t\tif ( !clear ) continue;\r\n\r\n\t\t\tany = true;\r\n\t\t\tif ( dist \u003C bestDist ) {{ bestDist = dist; bestGo = cand; }}\r\n\t\t}}\r\n\r\n\t\treturn bestGo;\r\n\t}}\r\n\r\n\tprivate GameObject FindNearestCandidate( Vector3 from, float maxDist )\r\n\t{{\r\n\t\tGameObject best = null;\r\n\t\tfloat bestDist = maxDist;\r\n\t\tforeach ( var cand in Candidates() )\r\n\t\t{{\r\n\t\t\tfloat d = Vector3.DistanceBetween( from, cand.WorldPosition );\r\n\t\t\tif ( d \u003C= bestDist ) {{ bestDist = d; best = cand; }}\r\n\t\t}}\r\n\t\treturn best;\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003ECandidate targets = GameObjects tagged TargetTag, excluding self.\r\n\t/// Uses Scene.GetAllComponents to enumerate, then filters by tag.\u003C/summary\u003E\r\n\tprivate IEnumerable\u003CGameObject\u003E Candidates()\r\n\t{{\r\n\t\tforeach ( var c in Scene.GetAllComponents\u003CCollider\u003E() )\r\n\t\t{{\r\n\t\t\tvar go = c.GameObject;\r\n\t\t\tif ( go == null || go == GameObject ) continue;\r\n\t\t\tif ( !go.Tags.Has( TargetTag ) ) continue;\r\n\t\t\tyield return go;\r\n\t\t}}\r\n\t}}\r\n\r\n\t// \u2500\u2500 Transition table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\tprivate void Think()\r\n\t{{\r\n\t\tbool canSee = _target.IsValid() \u0026\u0026 _timeSinceSeen \u003C 0.1f;\r\n\r\n\t\tif ( CanFlee \u0026\u0026 ShouldFlee() ) {{ CurrentState = BrainState.Flee; return; }}\r\n\r\n\t\tswitch ( CurrentState )\r\n\t\t{{\r\n\t\t\tcase BrainState.Idle:\r\n\t\t\tcase BrainState.Patrol:\r\n\t\t\tcase BrainState.Wander:\r\n\t\t\tcase BrainState.Ambush:\r\n\t\t\t\tif ( canSee ) CurrentState = BrainState.Chase;\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase BrainState.Chase:\r\n\t\t\t\tif ( !canSee \u0026\u0026 _timeSinceSeen \u003E 0.25f ) CurrentState = BrainState.Search;\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase BrainState.Search:\r\n\t\t\t\tif ( canSee ) CurrentState = BrainState.Chase;\r\n\t\t\t\telse if ( _timeSinceSeen \u003E GiveUpTime ) {{ _target = null; CurrentState = StartState; }}\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase BrainState.Flee:\r\n\t\t\t\tif ( !ShouldFlee() ) CurrentState = StartState;\r\n\t\t\t\tbreak;\r\n\t\t}}\r\n\t}}\r\n\r\n\t// \u2500\u2500 Action per state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\tprivate void Act()\r\n\t{{\r\n\t\t// Apply the desired locomotion speed (chase is faster). NavMeshAgent.MaxSpeed\r\n\t\t// is the agent\u0027s speed cap (verified in the navmesh docs).\r\n\t\t_agent.MaxSpeed = ( CurrentState == BrainState.Chase || CurrentState == BrainState.Flee ) ? ChaseSpeed : MoveSpeed;\r\n\r\n\t\tswitch ( CurrentState )\r\n\t\t{{\r\n\t\t\tcase BrainState.Idle:\r\n\t\t\tcase BrainState.Ambush:\r\n\t\t\t\t// Stand still and watch (perception still runs every tick).\r\n\t\t\t\t_agent.Stop();\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase BrainState.Patrol:\r\n\t\t\t\tPatrolStep();\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase BrainState.Wander:\r\n\t\t\t\tWanderStep( WorldPosition, SearchRadius );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase BrainState.Chase:\r\n\t\t\t\tif ( _target.IsValid() )\r\n\t\t\t\t\t_agent.MoveTo( _target.WorldPosition );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase BrainState.Search:\r\n\t\t\t\tif ( Vector3.DistanceBetween( WorldPosition, _lastKnownPos ) \u003E WaypointStopDistance )\r\n\t\t\t\t\t_agent.MoveTo( _lastKnownPos );\r\n\t\t\t\telse\r\n\t\t\t\t\tWanderStep( _lastKnownPos, SearchRadius );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase BrainState.Flee:\r\n\t\t\t\tFleeStep();\r\n\t\t\t\tbreak;\r\n\t\t}}\r\n\t}}\r\n\r\n\tprivate void PatrolStep()\r\n\t{{\r\n\t\tif ( Waypoints == null || Waypoints.Count == 0 ) return;\r\n\t\t_waypointIndex = (int)MathX.Clamp( _waypointIndex, 0, Waypoints.Count - 1 );\r\n\r\n\t\tvar wp = Waypoints[_waypointIndex];\r\n\t\tif ( !wp.IsValid() ) {{ AdvanceWaypoint(); return; }}\r\n\r\n\t\tif ( Vector3.DistanceBetween( WorldPosition, wp.WorldPosition ) \u003C= WaypointStopDistance )\r\n\t\t\tAdvanceWaypoint();\r\n\t\telse\r\n\t\t\t_agent.MoveTo( wp.WorldPosition );\r\n\t}}\r\n\r\n\tprivate void AdvanceWaypoint()\r\n\t{{\r\n\t\tif ( Waypoints == null || Waypoints.Count \u003C= 1 ) return;\r\n\r\n\t\tif ( PingPong )\r\n\t\t{{\r\n\t\t\tif ( _waypointIndex \u002B _waypointDir \u003E= Waypoints.Count || _waypointIndex \u002B _waypointDir \u003C 0 )\r\n\t\t\t\t_waypointDir = -_waypointDir;\r\n\t\t\t_waypointIndex \u002B= _waypointDir;\r\n\t\t}}\r\n\t\telse\r\n\t\t{{\r\n\t\t\t_waypointIndex = ( _waypointIndex \u002B 1 ) % Waypoints.Count;\r\n\t\t}}\r\n\t}}\r\n\r\n\tprivate void WanderStep( Vector3 home, float radius )\r\n\t{{\r\n\t\tbool reached = Vector3.DistanceBetween( WorldPosition, _wanderTarget ) \u003C= WaypointStopDistance;\r\n\t\tif ( reached || _timeSinceWanderPick \u003E 4f )\r\n\t\t{{\r\n\t\t\t// Pick a fresh point near home. Uses only confirmed APIs (Random.Shared\r\n\t\t\t// \u002B Vector3). The agent paths toward the nearest reachable point, so an\r\n\t\t\t// occasional off-mesh pick is harmless. (For strictly-on-mesh wander,\r\n\t\t\t// swap to Scene.NavMesh.GetRandomPoint(home, radius) once its return type\r\n\t\t\t// is confirmed via describe_type.)\r\n\t\t\tvar off = new Vector3(\r\n\t\t\t\tRandom.Shared.Float( -radius, radius ),\r\n\t\t\t\tRandom.Shared.Float( -radius, radius ),\r\n\t\t\t\t0f );\r\n\t\t\t_wanderTarget = home \u002B off;\r\n\t\t\t_timeSinceWanderPick = 0f;\r\n\t\t}}\r\n\t\t_agent.MoveTo( _wanderTarget );\r\n\t}}\r\n\r\n\tprivate void FleeStep()\r\n\t{{\r\n\t\t// Move directly away from the last-known threat position.\r\n\t\tvar away = ( WorldPosition - _lastKnownPos ).Normal;\r\n\t\tif ( away.Length \u003C 0.01f ) away = WorldRotation.Forward;\r\n\t\t_agent.MoveTo( WorldPosition \u002B away * MathX.Clamp( SearchRadius, 100f, 2000f ) );\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003EGeneric flee predicate. Driven by CurrentHealthFrac (the game sets\r\n\t/// it 0..1). Override in a subclass/partial for game-specific logic (e.g. a\r\n\t/// bomb-timer panic in RUN, or a camper-HP check in Sasquatched).\u003C/summary\u003E\r\n\tpublic bool ShouldFlee()\r\n\t{{\r\n\t\treturn CanFlee \u0026\u0026 CurrentHealthFrac \u003C= FleeHealthFrac;\r\n\t}}\r\n\r\n\t// \u2500\u2500 Noise hook (pure C#; the game calls this where a noise happens) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\t// Example: NpcBrain.ReportNoise(flashlightPos, 800f) when a camper clicks a\r\n\t// flashlight, or a gunshot in RUN. NPCs within radius investigate (Search).\r\n\tpublic static void ReportNoise( Scene scene, Vector3 pos, float radius )\r\n\t{{\r\n\t\tif ( scene == null ) return;\r\n\t\tforeach ( var brain in scene.GetAllComponents\u003C{className}\u003E() )\r\n\t\t\tbrain.HearNoise( pos, radius );\r\n\t}}\r\n\r\n\tpublic void HearNoise( Vector3 pos, float radius )\r\n\t{{\r\n\t\tif ( Vector3.DistanceBetween( WorldPosition, pos ) \u003E radius ) return;\r\n\t\t_lastKnownPos = pos;\r\n\t\tif ( CurrentState != BrainState.Chase )\r\n\t\t\tCurrentState = BrainState.Search;\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n//  2. place_patrol_route  (scene-mutating)\r\n//     Create N waypoint empties (tagged), grouped under a parent route object,\r\n//     optionally snapped to the ground so they sit on the navmesh.\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\npublic class PlacePatrolRouteHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\tvar scene = SceneEditorSession.Active?.Scene;\r\n\t\tif ( scene == null )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022No active scene\u0022 } );\r\n\r\n\t\tif ( !p.TryGetProperty( \u0022points\u0022, out var pts ) || pts.ValueKind != JsonValueKind.Array )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022points (Vector3[]) is required\u0022 } );\r\n\r\n\t\tvar rawPoints = new List\u003CVector3\u003E();\r\n\t\tforeach ( var e in pts.EnumerateArray() )\r\n\t\t\trawPoints.Add( ClaudeBridge.ParseVector3( e ) );\r\n\r\n\t\tif ( rawPoints.Count \u003C 2 )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022Provide at least 2 points for a patrol route\u0022 } );\r\n\r\n\t\tvar routeName  = NpcBrainHelpers.Str( p, \u0022name\u0022, \u0022PatrolRoute\u0022 );\r\n\t\tvar tag        = NpcBrainHelpers.Str( p, \u0022tag\u0022, \u0022waypoint\u0022 );\r\n\t\tvar snap       = NpcBrainHelpers.Bool( p, \u0022snapToGround\u0022, true );\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\t// Resolve or create the route parent.\r\n\t\t\tGameObject route = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022parentId\u0022, out var pid ) \u0026\u0026 Guid.TryParse( pid.GetString(), out var parentGuid ) )\r\n\t\t\t\troute = scene.Directory.FindByGuid( parentGuid );\r\n\r\n\t\t\tif ( route == null )\r\n\t\t\t{\r\n\t\t\t\troute = scene.CreateObject( true );\r\n\t\t\t\troute.Name = routeName;\r\n\t\t\t\t// Place the parent at the centroid for a tidy hierarchy \u002B easy framing.\r\n\t\t\t\tvar centroid = Vector3.Zero;\r\n\t\t\t\tforeach ( var pt in rawPoints ) centroid \u002B= pt;\r\n\t\t\t\troute.WorldPosition = centroid / rawPoints.Count;\r\n\t\t\t}\r\n\r\n\t\t\tvar waypointIds = new List\u003Cstring\u003E( rawPoints.Count );\r\n\t\t\tint i = 0;\r\n\t\t\tforeach ( var pt in rawPoints )\r\n\t\t\t{\r\n\t\t\t\tvar pos = pt;\r\n\t\t\t\tif ( snap )\r\n\t\t\t\t{\r\n\t\t\t\t\ttry\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar tr = scene.Trace.Ray( pos \u002B Vector3.Up * 2000f, pos \u002B Vector3.Down * 20000f ).Run();\r\n\t\t\t\t\t\tif ( tr.Hit ) pos = new Vector3( pos.x, pos.y, tr.HitPosition.z );\r\n\t\t\t\t\t}\r\n\t\t\t\t\tcatch { /* keep the raw point on trace failure */ }\r\n\t\t\t\t}\r\n\r\n\t\t\t\tvar wp = scene.CreateObject( true );\r\n\t\t\t\twp.Name = $\u0022{routeName}_WP{i}\u0022;\r\n\t\t\t\twp.WorldPosition = pos;\r\n\t\t\t\twp.Tags.Add( tag );\r\n\t\t\t\twp.SetParent( route, keepWorldPosition: true );\r\n\t\t\t\twaypointIds.Add( wp.Id.ToString() );\r\n\t\t\t\ti\u002B\u002B;\r\n\t\t\t}\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tplaced     = true,\r\n\t\t\t\trouteId    = route.Id.ToString(),\r\n\t\t\t\trouteName  = route.Name,\r\n\t\t\t\twaypointIds,\r\n\t\t\t\tcount      = waypointIds.Count,\r\n\t\t\t\tsnappedToGround = snap,\r\n\t\t\t\tnote = \u0022Wire these into an NpcBrain with assign_patrol_route (pass routeId or waypointIds). \u0022 \u002B\r\n\t\t\t\t       \u0022Validate connectivity with get_navmesh_path between consecutive waypoints (catches a point in a wall).\u0022\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022place_patrol_route failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n//  3. assign_patrol_route  (scene-mutating)\r\n//     Wire a placed route (or an arbitrary GUID list) into a List\u003CGameObject\u003E\r\n//     property (default \u0022Waypoints\u0022) on a target NPC\u0027s component. This is the\r\n//     list-of-GameObject-refs case plain set_property can\u0027t express.\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\npublic class AssignPatrolRouteHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\tvar scene = SceneEditorSession.Active?.Scene;\r\n\t\tif ( scene == null )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022No active scene\u0022 } );\r\n\r\n\t\tif ( !p.TryGetProperty( \u0022npcId\u0022, out var npcEl ) || !Guid.TryParse( npcEl.GetString(), out var npcGuid ) )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022npcId (GameObject GUID holding the NpcBrain) is required\u0022 } );\r\n\r\n\t\tvar npc = scene.Directory.FindByGuid( npcGuid );\r\n\t\tif ( npc == null )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022NPC GameObject not found: {npcEl.GetString()}\u0022 } );\r\n\r\n\t\tvar property = NpcBrainHelpers.Str( p, \u0022property\u0022, \u0022Waypoints\u0022 );\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\t// \u2500\u2500 Gather the ordered waypoint GameObjects: explicit waypointIds win,\r\n\t\t\t//    else the children (hierarchy order) of routeId.\r\n\t\t\tvar waypoints = new List\u003CGameObject\u003E();\r\n\r\n\t\t\tif ( p.TryGetProperty( \u0022waypointIds\u0022, out var wpArr ) \u0026\u0026 wpArr.ValueKind == JsonValueKind.Array )\r\n\t\t\t{\r\n\t\t\t\tforeach ( var e in wpArr.EnumerateArray() )\r\n\t\t\t\t\tif ( Guid.TryParse( e.GetString(), out var g ) )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar go = scene.Directory.FindByGuid( g );\r\n\t\t\t\t\t\tif ( go != null ) waypoints.Add( go );\r\n\t\t\t\t\t}\r\n\t\t\t}\r\n\t\t\telse if ( p.TryGetProperty( \u0022routeId\u0022, out var routeEl ) \u0026\u0026 Guid.TryParse( routeEl.GetString(), out var routeGuid ) )\r\n\t\t\t{\r\n\t\t\t\tvar route = scene.Directory.FindByGuid( routeGuid );\r\n\t\t\t\tif ( route == null )\r\n\t\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022Route GameObject not found: {routeEl.GetString()}\u0022 } );\r\n\t\t\t\tforeach ( var child in route.Children )\r\n\t\t\t\t\twaypoints.Add( child );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022Provide waypointIds (GUID[]) or routeId (route parent GUID)\u0022 } );\r\n\t\t\t}\r\n\r\n\t\t\tif ( waypoints.Count == 0 )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022No valid waypoints resolved from the given ids/route\u0022 } );\r\n\r\n\t\t\t// \u2500\u2500 Resolve the component \u002B property and set the List\u003CGameObject\u003E.\r\n\t\t\t//    SetValue accepts a List\u003CGameObject\u003E; we hand it the concrete list\r\n\t\t\t//    (matches how the editor serializes [Property] lists of refs).\r\n\t\t\tvar comp = NpcBrainHelpers.SetComponentProperty( npc, property, waypoints );\r\n\t\t\tif ( comp == null )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022No component on the NPC exposes a \u0027{property}\u0027 property (expected an NpcBrain with a List\u003CGameObject\u003E {property})\u0022 } );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tassigned  = true,\r\n\t\t\t\tnpcId     = npcEl.GetString(),\r\n\t\t\t\tcomponent = comp.GetType().Name,\r\n\t\t\t\tproperty,\r\n\t\t\t\tcount     = waypoints.Count,\r\n\t\t\t\tnote = \u0022List\u003CGameObject\u003E refs may read back as handles/GUIDs via get_property \u2014 trust this count, or confirm patrol in play mode.\u0022\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022assign_patrol_route failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n//  4. create_npc_spawner  (code-gen; scene-mutating)\r\n//     Generate a spawner Component that clones an NPC prefab over time / in\r\n//     escalating waves at spawn points, capped by maxAlive. Host-authoritative\r\n//     when networked (NetworkSpawn, guarded).\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\npublic class CreateNpcSpawnerHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar name      = NpcBrainHelpers.Str( p, \u0022name\u0022, \u0022NpcSpawner\u0022 );\r\n\t\t\tvar directory = NpcBrainHelpers.Str( p, \u0022directory\u0022, \u0022Code\u0022 );\r\n\r\n\t\t\tvar fileName = name.EndsWith( \u0022.cs\u0022 ) ? name : $\u0022{name}.cs\u0022;\r\n\t\t\tif ( !ClaudeBridge.TryResolveProjectPath( Path.Combine( directory, fileName ), out var fullPath, out var pathErr ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = pathErr } );\r\n\r\n\t\t\tif ( File.Exists( fullPath ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022File already exists: {directory}/{fileName}\u0022 } );\r\n\r\n\t\t\tvar className = ClaudeBridge.SanitizeIdentifier( Path.GetFileNameWithoutExtension( fileName ) );\r\n\r\n\t\t\tvar mode       = NpcBrainHelpers.Str( p, \u0022mode\u0022, \u0022waves\u0022 ).ToLowerInvariant();\r\n\t\t\tif ( mode != \u0022continuous\u0022 \u0026\u0026 mode != \u0022waves\u0022 \u0026\u0026 mode != \u0022burst\u0022 ) mode = \u0022waves\u0022;\r\n\t\t\tvar modeEnum   = mode == \u0022continuous\u0022 ? \u0022Continuous\u0022 : ( mode == \u0022burst\u0022 ? \u0022Burst\u0022 : \u0022Waves\u0022 );\r\n\r\n\t\t\tvar count      = NpcBrainHelpers.Int(   p, \u0022count\u0022, 5 );\r\n\t\t\tvar interval   = NpcBrainHelpers.Float( p, \u0022interval\u0022, 8f );\r\n\t\t\tvar waveCount  = NpcBrainHelpers.Int(   p, \u0022waveCount\u0022, 3 );\r\n\t\t\tvar waveGrowth = NpcBrainHelpers.Float( p, \u0022waveGrowth\u0022, 1f );\r\n\t\t\tvar radius     = NpcBrainHelpers.Float( p, \u0022radius\u0022, 200f );\r\n\t\t\tvar maxAlive   = NpcBrainHelpers.Int(   p, \u0022maxAlive\u0022, 12 );\r\n\t\t\tvar networked  = NpcBrainHelpers.Bool(  p, \u0022networked\u0022, true );\r\n\r\n\t\t\tvar code = BuildSpawnerSource( className, modeEnum, networked,\r\n\t\t\t\tcount, interval, waveCount, waveGrowth, radius, maxAlive );\r\n\r\n\t\t\tDirectory.CreateDirectory( Path.GetDirectoryName( fullPath ) );\r\n\t\t\tFile.WriteAllText( fullPath, code );\r\n\r\n\t\t\tvar props = new[]\r\n\t\t\t{\r\n\t\t\t\t\u0022NpcPrefab\u0022,\u0022SpawnPoints\u0022,\u0022Mode\u0022,\u0022Count\u0022,\u0022Interval\u0022,\u0022WaveCount\u0022,\r\n\t\t\t\t\u0022WaveGrowth\u0022,\u0022Radius\u0022,\u0022MaxAlive\u0022,\u0022AutoStart\u0022\r\n\t\t\t};\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated   = true,\r\n\t\t\t\tpath      = $\u0022{directory}/{fileName}\u0022,\r\n\t\t\t\tclassName,\r\n\t\t\t\tmode,\r\n\t\t\t\tnetworked,\r\n\t\t\t\tpropertyNames = props,\r\n\t\t\t\tnote = \u0022Set NpcPrefab via set_prefab_ref. Add spawn points by reusing place_patrol_route (a route of empties) then \u0022 \u002B\r\n\t\t\t\t       \u0022assign_patrol_route with property=\\\u0022SpawnPoints\\\u0022, or set SpawnPoints by hand. \u0022 \u002B\r\n\t\t\t\t       ( networked\r\n\t\t\t\t         ? \u0022Networked spawns use NetworkSpawn() and are host-only (guarded) \u2014 needs a host session.\u0022\r\n\t\t\t\t         : \u0022Solo build: plain Clone() (no NetworkSpawn).\u0022 ) \u002B\r\n\t\t\t\t       \u0022 Verify by watching GameObject count over time in play mode (get_scene_hierarchy deltas).\u0022\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_npc_spawner failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tprivate static string BuildSpawnerSource(\r\n\t\tstring className, string modeEnum, bool networked,\r\n\t\tint count, float interval, int waveCount, float waveGrowth, float radius, int maxAlive )\r\n\t{\r\n\t\tstring F( float v ) =\u003E NpcBrainHelpers.F( v );\r\n\r\n\t\tvar proxyGuard = networked ? \u0022\\t\\tif ( IsProxy ) return;   // host spawns authoritatively\\n\u0022 : \u0022\u0022;\r\n\t\tvar headerNote = networked\r\n\t\t\t? \u0022// Host-authoritative spawner. Only the host spawns (NetworkSpawn so clients see the\\n// NPCs). Needs an active network session.\\n\u0022\r\n\t\t\t: \u0022// Solo / edit-scene spawner (plain Clone, no networking).\\n\u0022;\r\n\r\n\t\t// Spawn idiom: clone the prefab, place it, and (networked) NetworkSpawn in a\r\n\t\t// try/catch \u2014 the verified solo-safe idiom (NetworkSpawn throws with no session).\r\n\t\tvar spawnBody = networked\r\n\t\t\t?\r\n@\u0022\t\tvar go = NpcPrefab.Clone( pos );\r\n\t\ttry { go.NetworkSpawn(); } catch { /* no session \u2014 fall back to a local object */ }\r\n\t\t_alive.Add( go );\u0022\r\n\t\t\t:\r\n@\u0022\t\tvar go = NpcPrefab.Clone( pos );\r\n\t\t_alive.Add( go );\u0022;\r\n\r\n\t\treturn\r\n$@\u0022using Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n\r\n{headerNote}public sealed class {className} : Component\r\n{{\r\n\tpublic enum SpawnMode {{ Continuous, Waves, Burst }}\r\n\r\n\t[Property] public GameObject NpcPrefab {{ get; set; }}\r\n\t[Property] public List\u003CGameObject\u003E SpawnPoints {{ get; set; }} = new();\r\n\r\n\t[Property] public SpawnMode Mode {{ get; set; }} = SpawnMode.{modeEnum};\r\n\t[Property] public int   Count      {{ get; set; }} = {count};   // per-wave (Waves) or total (Burst/Continuous batch)\r\n\t[Property] public float Interval   {{ get; set; }} = {F( interval )}; // seconds between spawns (Continuous) or waves (Waves)\r\n\t[Property] public int   WaveCount  {{ get; set; }} = {waveCount};\r\n\t[Property] public float WaveGrowth {{ get; set; }} = {F( waveGrowth )}; // multiply Count each wave (\u003E1 = escalating)\r\n\t[Property] public float Radius     {{ get; set; }} = {F( radius )};  // random scatter around a spawn point\r\n\t[Property] public int   MaxAlive   {{ get; set; }} = {maxAlive};   // concurrency cap\r\n\t[Property] public bool  AutoStart  {{ get; set; }} = true;\r\n\r\n\tprivate readonly List\u003CGameObject\u003E _alive = new();\r\n\tprivate TimeSince _timeSinceSpawn;\r\n\tprivate int _wavesDone;\r\n\tprivate float _currentWaveCount;\r\n\tprivate bool _started;\r\n\r\n\tprotected override void OnStart()\r\n\t{{\r\n\t\t_currentWaveCount = Count;\r\n\t\t_timeSinceSpawn = Interval; // fire promptly on the first eligible tick\r\n\t\tif ( AutoStart ) _started = true;\r\n\t}}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{{\r\n{proxyGuard}\t\tif ( !_started || NpcPrefab == null ) return;\r\n\r\n\t\t// Drop dead/destroyed NPCs from the live list so MaxAlive is accurate.\r\n\t\t_alive.RemoveAll( g =\u003E !g.IsValid() );\r\n\r\n\t\tswitch ( Mode )\r\n\t\t{{\r\n\t\t\tcase SpawnMode.Burst:\r\n\t\t\t\tSpawnBatch( (int)_currentWaveCount );\r\n\t\t\t\t_started = false; // one-shot\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase SpawnMode.Continuous:\r\n\t\t\t\tif ( _timeSinceSpawn \u003E= Interval )\r\n\t\t\t\t{{\r\n\t\t\t\t\t_timeSinceSpawn = 0f;\r\n\t\t\t\t\tTrySpawnOne();\r\n\t\t\t\t}}\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase SpawnMode.Waves:\r\n\t\t\t\tif ( _wavesDone \u003E= WaveCount ) {{ _started = false; break; }}\r\n\t\t\t\tif ( _timeSinceSpawn \u003E= Interval )\r\n\t\t\t\t{{\r\n\t\t\t\t\t_timeSinceSpawn = 0f;\r\n\t\t\t\t\tSpawnBatch( (int)_currentWaveCount );\r\n\t\t\t\t\t_wavesDone\u002B\u002B;\r\n\t\t\t\t\t_currentWaveCount = MathX.Clamp( _currentWaveCount * WaveGrowth, 1f, 9999f );\r\n\t\t\t\t}}\r\n\t\t\t\tbreak;\r\n\t\t}}\r\n\t}}\r\n\r\n\tprivate void SpawnBatch( int n )\r\n\t{{\r\n\t\tfor ( int i = 0; i \u003C n; i\u002B\u002B )\r\n\t\t\tif ( !TrySpawnOne() ) break;\r\n\t}}\r\n\r\n\tprivate bool TrySpawnOne()\r\n\t{{\r\n\t\tif ( _alive.Count \u003E= MaxAlive ) return false;\r\n\r\n\t\tvar pos = PickSpawnPos();\r\n{spawnBody}\r\n\t\treturn true;\r\n\t}}\r\n\r\n\tprivate Vector3 PickSpawnPos()\r\n\t{{\r\n\t\tvar basePos = WorldPosition;\r\n\t\tif ( SpawnPoints != null \u0026\u0026 SpawnPoints.Count \u003E 0 )\r\n\t\t{{\r\n\t\t\tvar pick = SpawnPoints[Random.Shared.Next( 0, SpawnPoints.Count )];\r\n\t\t\tif ( pick.IsValid() ) basePos = pick.WorldPosition;\r\n\t\t}}\r\n\r\n\t\tvar off = new Vector3(\r\n\t\t\tRandom.Shared.Float( -Radius, Radius ),\r\n\t\t\tRandom.Shared.Float( -Radius, Radius ),\r\n\t\t\t0f );\r\n\t\treturn basePos \u002B off;\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n//  5. simulate_npc_perception  (READ-ONLY \u2014 NOT scene-mutating)\r\n//     Run the EXACT LOS check an NpcBrain would, in edit mode, without play.\r\n//     FOV cone (dot vs CosFovThreshold) \u002B range \u002B occlusion trace. Reports the\r\n//     result AND why \u2014 the keystone edit-mode verifier for the perception layer.\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\npublic class SimulateNpcPerceptionHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\tvar scene = SceneEditorSession.Active?.Scene;\r\n\t\tif ( scene == null )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022No active scene\u0022 } );\r\n\r\n\t\tif ( !p.TryGetProperty( \u0022npcId\u0022, out var npcEl ) || !Guid.TryParse( npcEl.GetString(), out var npcGuid ) )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022npcId (GameObject GUID with an NpcBrain) is required\u0022 } );\r\n\r\n\t\tvar npc = scene.Directory.FindByGuid( npcGuid );\r\n\t\tif ( npc == null )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022NPC GameObject not found: {npcEl.GetString()}\u0022 } );\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\t// \u2500\u2500 Read perception params from the NPC\u0027s brain if present, else fall back\r\n\t\t\t//    to spec defaults / explicit overrides in the call. Matches the brain by\r\n\t\t\t//    CAPABILITY (exposes SightRange\u002BFovDegrees) or a \u0022...Brain\u0022 type name \u2014 NOT\r\n\t\t\t//    just the literal type name \u0022NpcBrain\u0022 \u2014 so a custom-named brain\r\n\t\t\t//    (e.g. BigfootBrain) is read instead of silently using defaults.\r\n\t\t\tvar brain = NpcBrainHelpers.FindPerceptionBrain( npc );\r\n\t\t\t// \u0060var\u0060 (never name TypeDescription) \u2014 its namespace isn\u0027t guaranteed importable here.\r\n\t\t\tvar brainTd = brain != null ? Game.TypeLibrary.GetType( brain.GetType().Name ) : null;\r\n\r\n\t\t\tfloat ReadBrainFloat( string name, float fallback )\r\n\t\t\t{\r\n\t\t\t\tif ( brain == null || brainTd == null ) return fallback;\r\n\t\t\t\tvar pd = brainTd.Properties.FirstOrDefault( x =\u003E x.Name == name );\r\n\t\t\t\tif ( pd == null ) return fallback;\r\n\t\t\t\ttry\r\n\t\t\t\t{\r\n\t\t\t\t\tvar v = pd.GetValue( brain );\r\n\t\t\t\t\tif ( v is float f ) return f;\r\n\t\t\t\t\tif ( v != null \u0026\u0026 float.TryParse( v.ToString(), out var fp ) ) return fp;\r\n\t\t\t\t}\r\n\t\t\t\tcatch { }\r\n\t\t\t\treturn fallback;\r\n\t\t\t}\r\n\t\t\tstring ReadBrainString( string name, string fallback )\r\n\t\t\t{\r\n\t\t\t\tif ( brain == null || brainTd == null ) return fallback;\r\n\t\t\t\tvar pd = brainTd.Properties.FirstOrDefault( x =\u003E x.Name == name );\r\n\t\t\t\ttry { return pd?.GetValue( brain )?.ToString() ?? fallback; } catch { return fallback; }\r\n\t\t\t}\r\n\r\n\t\t\t// Explicit overrides take precedence over brain-read values.\r\n\t\t\tfloat sightRange = NpcBrainHelpers.Float( p, \u0022sightRange\u0022, ReadBrainFloat( \u0022SightRange\u0022, 1500f ) );\r\n\t\t\tfloat fovDegrees = NpcBrainHelpers.Float( p, \u0022fovDegrees\u0022, ReadBrainFloat( \u0022FovDegrees\u0022, 110f ) );\r\n\t\t\tfloat eyeHeight  = NpcBrainHelpers.Float( p, \u0022eyeHeight\u0022,  ReadBrainFloat( \u0022EyeHeight\u0022, 64f ) );\r\n\t\t\tstring targetTag = NpcBrainHelpers.Str(   p, \u0022targetTag\u0022,  ReadBrainString( \u0022TargetTag\u0022, \u0022player\u0022 ) );\r\n\r\n\t\t\t// Use the brain\u0027s baked CosFovThreshold if available (keeps this query in\r\n\t\t\t// lockstep with the generated component); else compute it here.\r\n\t\t\tfloat cosFov = ReadBrainFloat( \u0022CosFovThreshold\u0022, float.NaN );\r\n\t\t\tif ( float.IsNaN( cosFov ) ) cosFov = NpcBrainHelpers.CosHalfFov( fovDegrees );\r\n\r\n\t\t\t// \u2500\u2500 Resolve the target point: explicit targetId or a raw point.\r\n\t\t\tGameObject targetGo = null;\r\n\t\t\tVector3 targetPos;\r\n\t\t\tif ( p.TryGetProperty( \u0022targetId\u0022, out var tEl ) \u0026\u0026 Guid.TryParse( tEl.GetString(), out var tGuid ) )\r\n\t\t\t{\r\n\t\t\t\ttargetGo = scene.Directory.FindByGuid( tGuid );\r\n\t\t\t\tif ( targetGo == null )\r\n\t\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022Target GameObject not found: {tEl.GetString()}\u0022 } );\r\n\t\t\t\ttargetPos = targetGo.WorldPosition;\r\n\t\t\t}\r\n\t\t\telse if ( p.TryGetProperty( \u0022point\u0022, out var ptEl ) )\r\n\t\t\t{\r\n\t\t\t\ttargetPos = ClaudeBridge.ParseVector3( ptEl );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022Provide targetId (GameObject GUID) or point (Vector3)\u0022 } );\r\n\t\t\t}\r\n\r\n\t\t\tvar eye = npc.WorldPosition \u002B Vector3.Up * eyeHeight;\r\n\t\t\tvar to  = targetPos - eye;\r\n\t\t\tfloat distance = to.Length;\r\n\r\n\t\t\t// Degenerate: target is essentially at the eye.\r\n\t\t\tif ( distance \u003C 0.01f )\r\n\t\t\t{\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t\t{\r\n\t\t\t\t\tcanSee = true, inRange = true, inFov = true, losBlocked = false,\r\n\t\t\t\t\tdistance, angleDeg = 0.0,\r\n\t\t\t\t\teye = new { eye.x, eye.y, eye.z },\r\n\t\t\t\t\tnote = \u0022Target coincides with the NPC eye position.\u0022\r\n\t\t\t\t} );\r\n\t\t\t}\r\n\r\n\t\t\tvar dir = to.Normal;\r\n\t\t\tfloat dot = Vector3.Dot( npc.WorldRotation.Forward, dir );\r\n\r\n\t\t\t// angle (degrees) for human-readable output. MathF is fine here (editor).\r\n\t\t\tfloat angleDeg = MathF.Acos( Math.Clamp( dot, -1f, 1f ) ) * ( 180f / MathF.PI );\r\n\r\n\t\t\tbool inRange = distance \u003C= sightRange;\r\n\t\t\tbool inFov   = dot \u003E= cosFov;\r\n\r\n\t\t\t// Occlusion trace from the eye toward the target. IgnoreGameObjectHierarchy\r\n\t\t\t// drops the NPC\u0027s own colliders (confirmed builder), so any hit is an\r\n\t\t\t// external object. It blocks LOS only if it\u0027s clearly before the target\r\n\t\t\t// (hit on the target itself, or a hit at/after the target distance, is not\r\n\t\t\t// a blocker). Distance test only \u2014 no GameObject.Root needed.\r\n\t\t\tbool losBlocked = false;\r\n\t\t\tobject blockedBy = null;\r\n\t\t\tvar tr = scene.Trace.Ray( eye, targetPos ).IgnoreGameObjectHierarchy( npc ).Run();\r\n\t\t\tif ( tr.Hit )\r\n\t\t\t{\r\n\t\t\t\tbool hitIsTarget = ( targetGo != null \u0026\u0026 tr.GameObject == targetGo )\r\n\t\t\t\t\t|| tr.Distance \u003E= distance - 8f; // a hit at/after the target point isn\u0027t a blocker\r\n\t\t\t\tif ( !hitIsTarget )\r\n\t\t\t\t{\r\n\t\t\t\t\tlosBlocked = true;\r\n\t\t\t\t\tblockedBy = new { id = tr.GameObject?.Id.ToString(), name = tr.GameObject?.Name };\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tbool tagMatch = targetGo == null || targetGo.Tags.Has( targetTag );\r\n\t\t\tbool canSee = inRange \u0026\u0026 inFov \u0026\u0026 !losBlocked \u0026\u0026 tagMatch;\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcanSee,\r\n\t\t\t\tinRange,\r\n\t\t\t\tinFov,\r\n\t\t\t\tlosBlocked,\r\n\t\t\t\tblockedBy,\r\n\t\t\t\ttagMatch,\r\n\t\t\t\tdistance,\r\n\t\t\t\tangleDeg = (double)angleDeg,\r\n\t\t\t\tfovHalfAngleDeg = (double)( fovDegrees * 0.5f ),\r\n\t\t\t\tsightRange,\r\n\t\t\t\ttargetTag,\r\n\t\t\t\teye = new { eye.x, eye.y, eye.z },\r\n\t\t\t\tbrainComponent = brain?.GetType().Name,\r\n\t\t\t\tnote = brain == null\r\n\t\t\t\t\t? \u0022No perception brain found on this GameObject \u2014 used spec defaults / call overrides for the perception params.\u0022\r\n\t\t\t\t\t: $\u0022Read perception params from the \u0027{brain.GetType().Name}\u0027 component\u0027s own SightRange/FovDegrees/EyeHeight/TargetTag (call params override). canSee mirrors what the generated brain computes.\u0022\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022simulate_npc_perception failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/PlaytestHandler.cs","FileName":"PlaytestHandler.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"using Editor;\r\nusing Sandbox;\r\nusing System;\r\nusing System.Collections;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing System.Text.Json;\r\nusing System.Threading.Tasks;\r\n\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n// PLAYTEST HARNESS \u2014 playtest / playtest_status  (the gameplay-verification frontier)\r\n//\r\n// Same assembly as MyEditorMenu.cs (reuses IBridgeHandler \u002B ClaudeBridge helpers).\r\n// Unsandboxed editor code \u2192 System.Math / System.Reflection are fine here.\r\n//\r\n// WHY AN IN-ADDON RUNNER (not TS round-trips):\r\n// Verifying a gameplay LOOP needs input \u002B state-reads \u002B assertions that time-align\r\n// with the game\u0027s frames. Two facts (proven live on the Gravehold player) force this:\r\n//   1. The facepunch PlayerController reads Input.AnalogMove each frame and OVERWRITES\r\n//      a WishVelocity you set \u2014 UNLESS you set \u0060UseInputControls=false\u0060 first. With it\r\n//      off, setting WishVelocity moved the player 0\u2192526u. So a move step must flip that\r\n//      toggle, drive WishVelocity per frame, and ZERO it after (it persists otherwise).\r\n//   2. Transient state (a jump\u0027s z-velocity) is gone by the time a SEPARATE bridge call\r\n//      lands \u2014 so assertions must be evaluated IN-FRAME, inside the editor frame loop.\r\n// =\u003E one async job, ticked by [EditorEvent.Frame], runs a step list and records a\r\n//    pass/fail transcript. TS only starts it (playtest) and polls it (playtest_status).\r\n//\r\n// Step verbs: move \u00B7 look \u00B7 lookDelta \u00B7 action \u00B7 jump \u00B7 set \u00B7 wait \u00B7 capture \u00B7 assert\r\n//   { \u0022move\u0022: {\u0022x\u0022:1}, \u0022frames\u0022:60 }                 analog move (auto UseInputControls=false)\r\n//   { \u0022look\u0022: {\u0022pitch\u0022:0,\u0022yaw\u0022:90,\u0022roll\u0022:0} }        set EyeAngles\r\n//   { \u0022lookDelta\u0022: {\u0022yaw\u0022:2}, \u0022frames\u0022:30 }          sweep EyeAngles\r\n//   { \u0022action\u0022: \u0022use\u0022, \u0022frames\u0022:20 }                 hold a named input action (rising-edge safe)\r\n//   { \u0022jump\u0022: \u00220,0,400\u0022 }                            invoke the controller\u0027s Jump(velocity)\r\n//   { \u0022set\u0022: {\u0022component\u0022:\u0022PlayerController\u0022,\u0022property\u0022:\u0022UseInputControls\u0022,\u0022to\u0022:\u0022false\u0022} }\r\n//   { \u0022wait\u0022: 10 }                                   advance N frames\r\n//   { \u0022capture\u0022: \u0022after-jump\u0022 }                      screenshot the live player POV \u2192 path in transcript\r\n//   { \u0022assert\u0022: {\u0022read\u0022:\u0022Displacement\u0022,\u0022op\u0022:\u0022\u003E\u0022,\u0022value\u0022:50,\u0022desc\u0022:\u0022moved \u003E50u from start\u0022} }\r\n//\r\n// assert.read = \u0022WorldPosition[.x|.y|.z]\u0022 (the controller\u0027s GameObject), \u0022Displacement\u0022\r\n//               (scalar distance moved from job start \u2014 the facing-independent movement proof), OR\r\n//               \u0022\u003CComponent\u003E.\u003CProperty\u003E[.x|.y|.z|.Count]\u0022 (a component on the player).\r\n// assert.op   = \u003E \u003C \u003E= \u003C= == != changed   (changed = differs from the value at job start)\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n\r\ninternal static class PlaytestRunner\r\n{\r\n\tinternal class StepSpec\r\n\t{\r\n\t\tpublic string Kind;\r\n\t\tpublic int Frames = 1;\r\n\t\tpublic Vector2 Move;\r\n\t\tpublic Angles Look; public bool HasLook;\r\n\t\tpublic Angles LookDelta;\r\n\t\tpublic string Action;\r\n\t\tpublic Vector3 JumpVel;\r\n\t\tpublic string SetComponent, SetProperty, SetValue;\r\n\t\tpublic string AssertRead, AssertOp, AssertValue, AssertDesc;\r\n\t\tpublic string CaptureLabel;\r\n\t\tpublic float MoveSpeed = 160f;\r\n\t}\r\n\r\n\tinternal class Job\r\n\t{\r\n\t\tpublic Guid TargetId;\r\n\t\tpublic string ComponentType;\r\n\t\tpublic Component Controller;     // resolved once\r\n\t\tpublic GameObject Anchor;        // controller.GameObject \u2014 the player object\r\n\t\tpublic Vector3 StartPos;         // Anchor.WorldPosition at job start (for the \u0022Displacement\u0022 read)\r\n\t\tpublic List\u003CStepSpec\u003E Steps;\r\n\t\tpublic int Index;\r\n\t\tpublic int FrameInStep;\r\n\t\tpublic List\u003Cobject\u003E Transcript = new();\r\n\t\tpublic int Passed, Failed;\r\n\t\tpublic bool DisabledInput;       // we flipped UseInputControls=false \u2192 restore at teardown\r\n\t\tpublic string HeldAction;        // currently-held action (release at step exit / teardown)\r\n\t\tpublic Dictionary\u003Cstring, string\u003E Baselines = new(); // read-key \u2192 value at job start (for \u0022changed\u0022)\r\n\t\tpublic bool Done;\r\n\t\tpublic string EndReason;\r\n\t\tpublic bool Started;\r\n\t}\r\n\r\n\tprivate static Job _job;\r\n\tprivate static readonly object _lock = new();\r\n\tprivate static object _lastSummary;\r\n\r\n\tinternal static void Start( Job job ) { lock ( _lock ) { _job = job; _lastSummary = null; } }\r\n\tinternal static object ConsumeSummary() { lock ( _lock ) { return _lastSummary; } }\r\n\tinternal static bool IsActive() { lock ( _lock ) { return _job != null; } }\r\n\r\n\t/// \u003Csummary\u003EStop the running job NOW: teardown (restore input state) \u002B summarize as aborted.\u003C/summary\u003E\r\n\tinternal static object Abort()\r\n\t{\r\n\t\tlock ( _lock )\r\n\t\t{\r\n\t\t\tif ( _job == null )\r\n\t\t\t\treturn new { aborted = false, note = \u0022No playtest job is running. playtest_status shows the last summary.\u0022 };\r\n\t\t\tvar j = _job;\r\n\t\t\tTeardown( j );\r\n\t\t\t_lastSummary = Summarize( j, \u0022aborted via playtest_abort\u0022 );\r\n\t\t\t_job = null;\r\n\t\t\treturn new\r\n\t\t\t{\r\n\t\t\t\taborted = true,\r\n\t\t\t\tstepsRun = j.Index,\r\n\t\t\t\tpassed = j.Passed,\r\n\t\t\t\tfailed = j.Failed,\r\n\t\t\t\tnote = \u0022Job stopped, input state restored. The partial transcript is available via playtest_status.\u0022\r\n\t\t\t};\r\n\t\t}\r\n\t}\r\n\r\n\tinternal static object LiveSnapshot()\r\n\t{\r\n\t\tlock ( _lock )\r\n\t\t{\r\n\t\t\tif ( _job == null ) return null;\r\n\t\t\treturn new { active = true, step = _job.Index, totalSteps = _job.Steps.Count, passed = _job.Passed, failed = _job.Failed };\r\n\t\t}\r\n\t}\r\n\r\n\t[EditorEvent.Frame]\r\n\tpublic static void OnFrame()\r\n\t{\r\n\t\tJob j;\r\n\t\tlock ( _lock ) { j = _job; }\r\n\t\tif ( j == null ) return;\r\n\r\n\t\tif ( !Game.IsPlaying )\r\n\t\t{\r\n\t\t\tTeardown( j );\r\n\t\t\tlock ( _lock ) { _lastSummary = Summarize( j, \u0022play mode ended before completion\u0022 ); _job = null; }\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\t// Resolve the controller \u002B anchor once.\r\n\t\t\tif ( !j.Started )\r\n\t\t\t{\r\n\t\t\t\tResolveAnchor( j );\r\n\t\t\t\tCaptureBaselines( j );\r\n\t\t\t\tj.Started = true;\r\n\t\t\t}\r\n\r\n\t\t\tif ( j.Index \u003E= j.Steps.Count )\r\n\t\t\t{\r\n\t\t\t\tTeardown( j );\r\n\t\t\t\tlock ( _lock ) { _lastSummary = Summarize( j, \u0022completed\u0022 ); _job = null; }\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\tvar step = j.Steps[j.Index];\r\n\t\t\tif ( j.FrameInStep == 0 ) StepEnter( j, step );\r\n\t\t\tStepTick( j, step );\r\n\t\t\tj.FrameInStep\u002B\u002B;\r\n\r\n\t\t\tif ( j.FrameInStep \u003E= System.Math.Max( 1, step.Frames ) )\r\n\t\t\t{\r\n\t\t\t\tStepExit( j, step );\r\n\t\t\t\tj.Index\u002B\u002B;\r\n\t\t\t\tj.FrameInStep = 0;\r\n\t\t\t}\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\t// Never let the ticker throw (it\u0027d spam every frame). Record \u002B stop.\r\n\t\t\tj.Transcript.Add( new { step = j.Index, kind = j.Index \u003C j.Steps.Count ? j.Steps[j.Index].Kind : \u0022?\u0022, error = ex.Message } );\r\n\t\t\tTeardown( j );\r\n\t\t\tlock ( _lock ) { _lastSummary = Summarize( j, $\u0022runner error: {ex.Message}\u0022 ); _job = null; }\r\n\t\t}\r\n\t}\r\n\r\n\t// \u2500\u2500 Step lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\tstatic void StepEnter( Job j, StepSpec s )\r\n\t{\r\n\t\tswitch ( s.Kind )\r\n\t\t{\r\n\t\t\tcase \u0022move\u0022:\r\n\t\t\t\tEnsureInputDisabled( j );   // so WishVelocity isn\u0027t overwritten by the controller\r\n\t\t\t\tbreak;\r\n\t\t\tcase \u0022jump\u0022:\r\n\t\t\t\tDoJump( j, s );\r\n\t\t\t\tbreak;\r\n\t\t\tcase \u0022set\u0022:\r\n\t\t\t\tDoSet( j, s );\r\n\t\t\t\tbreak;\r\n\t\t\tcase \u0022assert\u0022:\r\n\t\t\t\tDoAssert( j, s );\r\n\t\t\t\tbreak;\r\n\t\t\tcase \u0022capture\u0022:\r\n\t\t\t\tDoCapture( j, s );\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n\r\n\tstatic void StepTick( Job j, StepSpec s )\r\n\t{\r\n\t\tswitch ( s.Kind )\r\n\t\t{\r\n\t\t\tcase \u0022move\u0022:\r\n\t\t\t{\r\n\t\t\t\tif ( j.Controller == null ) return;\r\n\t\t\t\tvar td = Game.TypeLibrary.GetType( j.Controller.GetType() );\r\n\t\t\t\tvar yaw = ( ReadAngles( j.Controller, td, \u0022EyeAngles\u0022 ) ?? j.Controller.WorldRotation.Angles() ).yaw;\r\n\t\t\t\tvar rot = Rotation.From( 0f, yaw, 0f );\r\n\t\t\t\tvar wish = rot.Forward * s.Move.x \u002B rot.Left * s.Move.y;\r\n\t\t\t\tif ( wish.Length \u003E 1f ) wish = wish.Normal;\r\n\t\t\t\twish *= s.MoveSpeed;\r\n\t\t\t\tTrySetVector3( j.Controller, td, \u0022WishVelocity\u0022, wish );\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t\tcase \u0022look\u0022:\r\n\t\t\t{\r\n\t\t\t\tif ( j.Controller == null ) return;\r\n\t\t\t\tvar td = Game.TypeLibrary.GetType( j.Controller.GetType() );\r\n\t\t\t\tvar a = s.Look; a.pitch = System.Math.Clamp( a.pitch, -89f, 89f );\r\n\t\t\t\tTrySetAngles( j.Controller, td, \u0022EyeAngles\u0022, a );\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t\tcase \u0022lookDelta\u0022:\r\n\t\t\t{\r\n\t\t\t\tif ( j.Controller == null ) return;\r\n\t\t\t\tvar td = Game.TypeLibrary.GetType( j.Controller.GetType() );\r\n\t\t\t\tvar cur = ReadAngles( j.Controller, td, \u0022EyeAngles\u0022 ) ?? new Angles();\r\n\t\t\t\tcur.pitch = System.Math.Clamp( cur.pitch \u002B s.LookDelta.pitch, -89f, 89f );\r\n\t\t\t\tcur.yaw \u002B= s.LookDelta.yaw;\r\n\t\t\t\tcur.roll \u002B= s.LookDelta.roll;\r\n\t\t\t\tTrySetAngles( j.Controller, td, \u0022EyeAngles\u0022, cur );\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t\tcase \u0022action\u0022:\r\n\t\t\t\ttry { Sandbox.Input.SetAction( s.Action, true ); } catch { }\r\n\t\t\t\tj.HeldAction = s.Action;\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n\r\n\tstatic void StepExit( Job j, StepSpec s )\r\n\t{\r\n\t\tswitch ( s.Kind )\r\n\t\t{\r\n\t\t\tcase \u0022move\u0022:\r\n\t\t\t\tif ( j.Controller != null )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar td = Game.TypeLibrary.GetType( j.Controller.GetType() );\r\n\t\t\t\t\tTrySetVector3( j.Controller, td, \u0022WishVelocity\u0022, Vector3.Zero );  // stop \u2014 WishVelocity persists otherwise\r\n\t\t\t\t}\r\n\t\t\t\tj.Transcript.Add( new { step = j.Index, kind = \u0022move\u0022, frames = s.Frames, move = $\u0022{s.Move.x},{s.Move.y}\u0022 } );\r\n\t\t\t\tbreak;\r\n\t\t\tcase \u0022action\u0022:\r\n\t\t\t\ttry { Sandbox.Input.SetAction( s.Action, false ); } catch { }\r\n\t\t\t\tj.HeldAction = null;\r\n\t\t\t\tj.Transcript.Add( new { step = j.Index, kind = \u0022action\u0022, action = s.Action, frames = s.Frames } );\r\n\t\t\t\tbreak;\r\n\t\t\tcase \u0022look\u0022:\r\n\t\t\t\tj.Transcript.Add( new { step = j.Index, kind = \u0022look\u0022, look = $\u0022{s.Look.pitch},{s.Look.yaw},{s.Look.roll}\u0022 } );\r\n\t\t\t\tbreak;\r\n\t\t\tcase \u0022lookDelta\u0022:\r\n\t\t\t\tj.Transcript.Add( new { step = j.Index, kind = \u0022lookDelta\u0022, frames = s.Frames } );\r\n\t\t\t\tbreak;\r\n\t\t\tcase \u0022wait\u0022:\r\n\t\t\t\tj.Transcript.Add( new { step = j.Index, kind = \u0022wait\u0022, frames = s.Frames } );\r\n\t\t\t\tbreak;\r\n\t\t\t// jump/set/assert already recorded their result in StepEnter.\r\n\t\t}\r\n\t}\r\n\r\n\t// \u2500\u2500 Actions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\tstatic void DoJump( Job j, StepSpec s )\r\n\t{\r\n\t\tif ( j.Controller == null )\r\n\t\t{\r\n\t\t\tj.Transcript.Add( new { step = j.Index, kind = \u0022jump\u0022, ok = false, error = \u0022no controller\u0022 } );\r\n\t\t\tj.Failed\u002B\u002B;\r\n\t\t\treturn;\r\n\t\t}\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar m = j.Controller.GetType().GetMethod( \u0022Jump\u0022, new[] { typeof( Vector3 ) } );\r\n\t\t\tif ( m == null )\r\n\t\t\t{\r\n\t\t\t\tj.Transcript.Add( new { step = j.Index, kind = \u0022jump\u0022, ok = false, error = \u0022controller has no Jump(Vector3)\u0022 } );\r\n\t\t\t\tj.Failed\u002B\u002B;\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t\tm.Invoke( j.Controller, new object[] { s.JumpVel } );\r\n\t\t\tj.Transcript.Add( new { step = j.Index, kind = \u0022jump\u0022, ok = true, velocity = $\u0022{s.JumpVel.x},{s.JumpVel.y},{s.JumpVel.z}\u0022 } );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\tj.Transcript.Add( new { step = j.Index, kind = \u0022jump\u0022, ok = false, error = ex.Message } );\r\n\t\t\tj.Failed\u002B\u002B;\r\n\t\t}\r\n\t}\r\n\r\n\tstatic void DoSet( Job j, StepSpec s )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar comp = FindComponent( j, s.SetComponent );\r\n\t\t\tif ( comp == null )\r\n\t\t\t{\r\n\t\t\t\tj.Transcript.Add( new { step = j.Index, kind = \u0022set\u0022, ok = false, error = $\u0022component \u0027{s.SetComponent}\u0027 not found\u0022 } );\r\n\t\t\t\tj.Failed\u002B\u002B; return;\r\n\t\t\t}\r\n\t\t\tvar td = Game.TypeLibrary.GetType( comp.GetType() );\r\n\t\t\tvar pd = td?.Properties.FirstOrDefault( pp =\u003E pp.Name == s.SetProperty );\r\n\t\t\tif ( pd == null )\r\n\t\t\t{\r\n\t\t\t\tj.Transcript.Add( new { step = j.Index, kind = \u0022set\u0022, ok = false, error = $\u0022property \u0027{s.SetProperty}\u0027 not found\u0022 } );\r\n\t\t\t\tj.Failed\u002B\u002B; return;\r\n\t\t\t}\r\n\t\t\tobject typed = CoerceTo( pd.PropertyType, s.SetValue );\r\n\t\t\tpd.SetValue( comp, typed );\r\n\t\t\tj.Transcript.Add( new { step = j.Index, kind = \u0022set\u0022, ok = true, target = $\u0022{s.SetComponent}.{s.SetProperty}\u0022, to = s.SetValue } );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\tj.Transcript.Add( new { step = j.Index, kind = \u0022set\u0022, ok = false, error = ex.Message } );\r\n\t\t\tj.Failed\u002B\u002B;\r\n\t\t}\r\n\t}\r\n\r\n\tstatic void DoAssert( Job j, StepSpec s )\r\n\t{\r\n\t\tstring actual = null;\r\n\t\tbool ok = false;\r\n\t\tstring err = null;\r\n\t\ttry\r\n\t\t{\r\n\t\t\tobject val = ResolveRead( j, s.AssertRead, out err );\r\n\t\t\tif ( err == null )\r\n\t\t\t{\r\n\t\t\t\tactual = ValueToString( val );\r\n\t\t\t\tok = Compare( j, s.AssertRead, val, s.AssertOp, s.AssertValue, out err );\r\n\t\t\t}\r\n\t\t}\r\n\t\tcatch ( Exception ex ) { err = ex.Message; }\r\n\r\n\t\tif ( ok ) j.Passed\u002B\u002B; else j.Failed\u002B\u002B;\r\n\t\tj.Transcript.Add( new\r\n\t\t{\r\n\t\t\tstep = j.Index,\r\n\t\t\tkind = \u0022assert\u0022,\r\n\t\t\tok,\r\n\t\t\tdesc = s.AssertDesc,\r\n\t\t\tread = s.AssertRead,\r\n\t\t\top = s.AssertOp,\r\n\t\t\texpected = s.AssertValue,\r\n\t\t\tactual,\r\n\t\t\terror = err,\r\n\t\t} );\r\n\t}\r\n\r\n\t// \u2500\u2500 Capture: screenshot the live player-POV camera (diagnostic, never pass/fail) \u2500\u2500\r\n\tstatic void DoCapture( Job j, StepSpec s )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar scene = Game.ActiveScene;\r\n\t\t\tvar cam = scene != null ? VisualHelpers.FindMainCamera( scene ) : null;\r\n\t\t\tif ( cam == null )\r\n\t\t\t{\r\n\t\t\t\tj.Transcript.Add( new { step = j.Index, kind = \u0022capture\u0022, ok = false, label = s.CaptureLabel, error = \u0022no main camera in the running scene\u0022 } );\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t\tusing var bmp = new Bitmap( 1280, 720 );\r\n\t\t\tcam.RenderToBitmap( bmp, true );   // renderUI=true \u2192 the running game incl. HUD\r\n\t\t\tstring path = System.IO.Path.Combine( System.IO.Path.GetTempPath(), $\u0022bridge_playtest_{System.Guid.NewGuid():N}.png\u0022 );\r\n\t\t\tSystem.IO.File.WriteAllBytes( path, bmp.ToPng() );\r\n\t\t\tj.Transcript.Add( new { step = j.Index, kind = \u0022capture\u0022, ok = true, label = s.CaptureLabel, path } );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\tj.Transcript.Add( new { step = j.Index, kind = \u0022capture\u0022, ok = false, label = s.CaptureLabel, error = ex.Message } );\r\n\t\t}\r\n\t}\r\n\r\n\t// \u2500\u2500 Read resolution: \u0022WorldPosition.x\u0022 | \u0022\u003CComponent\u003E.\u003CProp\u003E[.sub]\u0022 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\tstatic object ResolveRead( Job j, string read, out string err )\r\n\t{\r\n\t\terr = null;\r\n\t\tif ( string.IsNullOrEmpty( read ) ) { err = \u0022empty read\u0022; return null; }\r\n\t\tvar parts = read.Split( \u0027.\u0027 );\r\n\t\tobject cur;\r\n\t\tint sub;\r\n\r\n\t\tvar head = parts[0];\r\n\t\tif ( head == \u0022Displacement\u0022 )\r\n\t\t{\r\n\t\t\tif ( j.Anchor == null ) { err = \u0022no player object resolved\u0022; return null; }\r\n\t\t\treturn (object) ( j.Anchor.WorldPosition - j.StartPos ).Length;   // scalar \u2014 facing-independent movement proof\r\n\t\t}\r\n\t\tif ( head == \u0022WorldPosition\u0022 || head == \u0022LocalPosition\u0022 || head == \u0022WorldRotation\u0022 || head == \u0022WorldScale\u0022 )\r\n\t\t{\r\n\t\t\tif ( j.Anchor == null ) { err = \u0022no player object resolved\u0022; return null; }\r\n\t\t\tcur = head switch\r\n\t\t\t{\r\n\t\t\t\t\u0022WorldPosition\u0022 =\u003E (object) j.Anchor.WorldPosition,\r\n\t\t\t\t\u0022LocalPosition\u0022 =\u003E j.Anchor.LocalPosition,\r\n\t\t\t\t\u0022WorldRotation\u0022 =\u003E j.Anchor.WorldRotation.Angles(),\r\n\t\t\t\t\u0022WorldScale\u0022    =\u003E j.Anchor.WorldScale,\r\n\t\t\t\t_ =\u003E null,\r\n\t\t\t};\r\n\t\t\tsub = 1;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tif ( parts.Length \u003C 2 ) { err = $\u0022read \u0027{read}\u0027 needs \u003CComponent\u003E.\u003CProperty\u003E\u0022; return null; }\r\n\t\t\tvar comp = FindComponent( j, head );\r\n\t\t\tif ( comp == null ) { err = $\u0022component \u0027{head}\u0027 not found on player\u0022; return null; }\r\n\t\t\tvar td = Game.TypeLibrary.GetType( comp.GetType() );\r\n\t\t\tvar pd = td?.Properties.FirstOrDefault( pp =\u003E pp.Name == parts[1] );\r\n\t\t\tif ( pd == null ) { err = $\u0022property \u0027{head}.{parts[1]}\u0027 not found\u0022; return null; }\r\n\t\t\tcur = pd.GetValue( comp );\r\n\t\t\tsub = 2;\r\n\t\t}\r\n\r\n\t\tfor ( int i = sub; i \u003C parts.Length \u0026\u0026 cur != null; i\u002B\u002B )\r\n\t\t\tcur = SubAccess( cur, parts[i] );\r\n\r\n\t\treturn cur;\r\n\t}\r\n\r\n\tstatic object SubAccess( object v, string sub )\r\n\t{\r\n\t\tif ( v is Vector3 v3 ) return sub switch { \u0022x\u0022 =\u003E v3.x, \u0022y\u0022 =\u003E v3.y, \u0022z\u0022 =\u003E v3.z, _ =\u003E null };\r\n\t\tif ( v is Vector2 v2 ) return sub switch { \u0022x\u0022 =\u003E v2.x, \u0022y\u0022 =\u003E v2.y, _ =\u003E null };\r\n\t\tif ( v is Angles an ) return sub switch { \u0022pitch\u0022 =\u003E an.pitch, \u0022yaw\u0022 =\u003E an.yaw, \u0022roll\u0022 =\u003E an.roll, _ =\u003E null };\r\n\t\tif ( sub == \u0022Count\u0022 )\r\n\t\t{\r\n\t\t\tif ( v is ICollection col ) return col.Count;\r\n\t\t\tif ( v is IEnumerable en ) return en.Cast\u003Cobject\u003E().Count();\r\n\t\t}\r\n\t\t// generic property fallback\r\n\t\ttry { return v.GetType().GetProperty( sub )?.GetValue( v ); } catch { return null; }\r\n\t}\r\n\r\n\tstatic bool Compare( Job j, string readKey, object actual, string op, string expected, out string err )\r\n\t{\r\n\t\terr = null;\r\n\t\tif ( op == \u0022changed\u0022 )\r\n\t\t\treturn j.Baselines.TryGetValue( readKey, out var b ) ? ValueToString( actual ) != b : true;\r\n\r\n\t\t// numeric comparison when both sides are numbers\r\n\t\tif ( TryNum( actual, out var an ) \u0026\u0026 float.TryParse( expected, NumberStyles.Float, CultureInfo.InvariantCulture, out var en ) )\r\n\t\t{\r\n\t\t\treturn op switch\r\n\t\t\t{\r\n\t\t\t\t\u0022\u003E\u0022  =\u003E an \u003E en, \u0022\u003C\u0022 =\u003E an \u003C en, \u0022\u003E=\u0022 =\u003E an \u003E= en, \u0022\u003C=\u0022 =\u003E an \u003C= en,\r\n\t\t\t\t\u0022==\u0022 =\u003E System.Math.Abs( an - en ) \u003C 0.0001f, \u0022!=\u0022 =\u003E System.Math.Abs( an - en ) \u003E= 0.0001f,\r\n\t\t\t\t_ =\u003E SetErr( out err, $\u0022bad numeric op \u0027{op}\u0027\u0022 ),\r\n\t\t\t};\r\n\t\t}\r\n\r\n\t\t// bool / string equality\r\n\t\tvar astr = ValueToString( actual );\r\n\t\treturn op switch\r\n\t\t{\r\n\t\t\t\u0022==\u0022 =\u003E string.Equals( astr, expected, StringComparison.OrdinalIgnoreCase ),\r\n\t\t\t\u0022!=\u0022 =\u003E !string.Equals( astr, expected, StringComparison.OrdinalIgnoreCase ),\r\n\t\t\t_ =\u003E SetErr( out err, $\u0022op \u0027{op}\u0027 needs numeric operands (got \u0027{astr}\u0027 vs \u0027{expected}\u0027)\u0022 ),\r\n\t\t};\r\n\t}\r\n\r\n\tstatic bool SetErr( out string err, string msg ) { err = msg; return false; }\r\n\r\n\tstatic bool TryNum( object v, out float f )\r\n\t{\r\n\t\tf = 0f;\r\n\t\tswitch ( v )\r\n\t\t{\r\n\t\t\tcase float ff: f = ff; return true;\r\n\t\t\tcase double dd: f = (float) dd; return true;\r\n\t\t\tcase int ii: f = ii; return true;\r\n\t\t\tcase long ll: f = ll; return true;\r\n\t\t\tcase short ss: f = ss; return true;\r\n\t\t\tcase byte bb: f = bb; return true;\r\n\t\t\tdefault: return false;\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string ValueToString( object v )\r\n\t{\r\n\t\tif ( v == null ) return \u0022null\u0022;\r\n\t\tif ( v is bool b ) return b ? \u0022True\u0022 : \u0022False\u0022;\r\n\t\tif ( v is Vector3 v3 ) return $\u0022{v3.x},{v3.y},{v3.z}\u0022;\r\n\t\tif ( v is float f ) return f.ToString( CultureInfo.InvariantCulture );\r\n\t\treturn v.ToString();\r\n\t}\r\n\r\n\t// \u2500\u2500 Setup / teardown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\tstatic void ResolveAnchor( Job j )\r\n\t{\r\n\t\tvar scene = Game.ActiveScene;\r\n\t\tif ( scene == null ) return;\r\n\t\tComponent c = null;\r\n\r\n\t\tif ( j.TargetId != Guid.Empty )\r\n\t\t{\r\n\t\t\tvar go = ClaudeBridge.ResolveGameObject( scene, j.TargetId.ToString() );\r\n\t\t\tif ( go != null ) c = FindControllerOn( go, j.ComponentType );\r\n\t\t}\r\n\t\tif ( c == null )\r\n\t\t{\r\n\t\t\tforeach ( var obj in scene.GetAllObjects( true ) )\r\n\t\t\t{\r\n\t\t\t\tc = FindControllerOn( obj, j.ComponentType );\r\n\t\t\t\tif ( c != null ) break;\r\n\t\t\t}\r\n\t\t}\r\n\t\tj.Controller = c;\r\n\t\tj.Anchor = c?.GameObject;\r\n\t}\r\n\r\n\tstatic void CaptureBaselines( Job j )\r\n\t{\r\n\t\t// Anchor position at job start \u2014 the origin for the \u0022Displacement\u0022 read.\r\n\t\tif ( j.Anchor != null ) j.StartPos = j.Anchor.WorldPosition;\r\n\t\t// Record the initial value of every \u0022changed\u0022 read so we can diff later.\r\n\t\tforeach ( var s in j.Steps.Where( x =\u003E x.Kind == \u0022assert\u0022 \u0026\u0026 x.AssertOp == \u0022changed\u0022 ) )\r\n\t\t{\r\n\t\t\tvar v = ResolveRead( j, s.AssertRead, out var e );\r\n\t\t\tif ( e == null ) j.Baselines[s.AssertRead] = ValueToString( v );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic void EnsureInputDisabled( Job j )\r\n\t{\r\n\t\tif ( j.DisabledInput || j.Controller == null ) return;\r\n\t\tvar td = Game.TypeLibrary.GetType( j.Controller.GetType() );\r\n\t\tvar pd = td?.Properties.FirstOrDefault( pp =\u003E pp.Name == \u0022UseInputControls\u0022 );\r\n\t\tif ( pd != null \u0026\u0026 pd.PropertyType == typeof( bool ) )\r\n\t\t{\r\n\t\t\tpd.SetValue( j.Controller, false );\r\n\t\t\tj.DisabledInput = true;\r\n\t\t}\r\n\t}\r\n\r\n\tstatic void Teardown( Job j )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !string.IsNullOrEmpty( j.HeldAction ) )\r\n\t\t\t\ttry { Sandbox.Input.SetAction( j.HeldAction, false ); } catch { }\r\n\r\n\t\t\tif ( j.Controller != null \u0026\u0026 j.Controller.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tvar td = Game.TypeLibrary.GetType( j.Controller.GetType() );\r\n\t\t\t\tTrySetVector3( j.Controller, td, \u0022WishVelocity\u0022, Vector3.Zero );\r\n\t\t\t\tif ( j.DisabledInput )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar pd = td?.Properties.FirstOrDefault( pp =\u003E pp.Name == \u0022UseInputControls\u0022 );\r\n\t\t\t\t\tpd?.SetValue( j.Controller, true );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tcatch { }\r\n\t}\r\n\r\n\tstatic object Summarize( Job j, string reason )\r\n\t{\r\n\t\treturn new\r\n\t\t{\r\n\t\t\tfinished = true,\r\n\t\t\treason,\r\n\t\t\tverdict = j.Failed == 0 ? \u0022PASS\u0022 : \u0022FAIL\u0022,\r\n\t\t\tpassed = j.Passed,\r\n\t\t\tfailed = j.Failed,\r\n\t\t\tstepsRun = j.Index,\r\n\t\t\ttotalSteps = j.Steps.Count,\r\n\t\t\tcontroller = j.Controller?.GetType().Name,\r\n\t\t\tcontrollerResolved = j.Controller != null,\r\n\t\t\ttranscript = j.Transcript,\r\n\t\t};\r\n\t}\r\n\r\n\t// \u2500\u2500 Reflection helpers (self-contained; mirror PlayInputDriver\u0027s idiom) \u2500\u2500\u2500\u2500\u2500\u2500\r\n\tinternal static Component FindControllerOn( GameObject go, string componentType )\r\n\t{\r\n\t\tif ( go == null ) return null;\r\n\t\tvar all = go.Components.GetAll().ToList();\r\n\t\tif ( !string.IsNullOrEmpty( componentType ) )\r\n\t\t\treturn all.FirstOrDefault( c =\u003E c.GetType().Name.Equals( componentType, StringComparison.OrdinalIgnoreCase ) );\r\n\t\tvar exact = all.FirstOrDefault( c =\u003E c.GetType().Name == \u0022PlayerController\u0022 );\r\n\t\tif ( exact != null ) return exact;\r\n\t\treturn all.FirstOrDefault( c =\u003E\r\n\t\t{\r\n\t\t\tvar n = c.GetType().Name;\r\n\t\t\tif ( !n.EndsWith( \u0022Controller\u0022, StringComparison.OrdinalIgnoreCase ) ) return false;\r\n\t\t\tvar td = Game.TypeLibrary.GetType( c.GetType() );\r\n\t\t\treturn td != null \u0026\u0026 td.Properties.Any( pp =\u003E pp.Name == \u0022EyeAngles\u0022 || pp.Name == \u0022WishVelocity\u0022 );\r\n\t\t} );\r\n\t}\r\n\r\n\tstatic Component FindComponent( Job j, string typeName )\r\n\t{\r\n\t\tif ( j.Anchor == null || string.IsNullOrEmpty( typeName ) ) return null;\r\n\t\treturn j.Anchor.Components.GetAll().FirstOrDefault( c =\u003E c.GetType().Name.Equals( typeName, StringComparison.OrdinalIgnoreCase ) );\r\n\t}\r\n\r\n\tstatic Angles? ReadAngles( Component c, TypeDescription td, string member )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar pd = td?.Properties.FirstOrDefault( pp =\u003E pp.Name == member );\r\n\t\t\tif ( pd == null ) return null;\r\n\t\t\tvar v = pd.GetValue( c );\r\n\t\t\tif ( v is Angles a ) return a;\r\n\t\t\tif ( v is Rotation r ) return r.Angles();\r\n\t\t}\r\n\t\tcatch { }\r\n\t\treturn null;\r\n\t}\r\n\r\n\tstatic bool TrySetAngles( Component c, TypeDescription td, string member, Angles value )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar pd = td?.Properties.FirstOrDefault( pp =\u003E pp.Name == member );\r\n\t\t\tif ( pd == null ) return false;\r\n\t\t\tif ( pd.PropertyType == typeof( Angles ) ) { pd.SetValue( c, value ); return true; }\r\n\t\t\tif ( pd.PropertyType == typeof( Rotation ) ) { pd.SetValue( c, Rotation.From( value ) ); return true; }\r\n\t\t}\r\n\t\tcatch { }\r\n\t\treturn false;\r\n\t}\r\n\r\n\tstatic bool TrySetVector3( Component c, TypeDescription td, string member, Vector3 value )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar pd = td?.Properties.FirstOrDefault( pp =\u003E pp.Name == member );\r\n\t\t\tif ( pd == null || pd.PropertyType != typeof( Vector3 ) ) return false;\r\n\t\t\tpd.SetValue( c, value );\r\n\t\t\treturn true;\r\n\t\t}\r\n\t\tcatch { return false; }\r\n\t}\r\n\r\n\tstatic object CoerceTo( Type t, string raw )\r\n\t{\r\n\t\tif ( t == typeof( bool ) ) return raw == \u0022true\u0022 || raw == \u0022True\u0022 || raw == \u00221\u0022;\r\n\t\tif ( t == typeof( float ) ) return float.Parse( raw, NumberStyles.Float, CultureInfo.InvariantCulture );\r\n\t\tif ( t == typeof( int ) ) return (int) float.Parse( raw, NumberStyles.Float, CultureInfo.InvariantCulture );\r\n\t\tif ( t == typeof( Vector3 ) ) return ClaudeBridge.ParseVector3Flexible( ParseElement( raw ) );\r\n\t\treturn raw;\r\n\t}\r\n\r\n\tstatic JsonElement ParseElement( string raw )\r\n\t{\r\n\t\t// Wrap a bare \u0022x,y,z\u0022 or scalar as a JSON string element for ParseVector3Flexible.\r\n\t\tusing var doc = JsonDocument.Parse( JsonSerializer.Serialize( raw ) );\r\n\t\treturn doc.RootElement.Clone();\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// playtest \u2014 run a scripted gameplay-verification sequence in play mode (async, in the\r\n/// editor frame loop) and record a pass/fail transcript. Requires start_play first.\r\n/// \u003C/summary\u003E\r\npublic class PlaytestHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\tif ( !Game.IsPlaying )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022playtest requires play mode \u2014 call start_play first\u0022 } );\r\n\t\tif ( PlaytestRunner.IsActive() )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022a playtest is already running \u2014 poll playtest_status until it finishes\u0022 } );\r\n\t\tif ( !p.TryGetProperty( \u0022steps\u0022, out var stepsEl ) || stepsEl.ValueKind != JsonValueKind.Array )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022steps (an array of step objects) is required\u0022 } );\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar job = new PlaytestRunner.Job { Steps = new List\u003CPlaytestRunner.StepSpec\u003E() };\r\n\r\n\t\t\tif ( p.TryGetProperty( \u0022id\u0022, out var idEl ) \u0026\u0026 idEl.ValueKind == JsonValueKind.String\r\n\t\t\t\t \u0026\u0026 Guid.TryParse( idEl.GetString(), out var gid ) )\r\n\t\t\t\tjob.TargetId = gid;\r\n\t\t\tif ( p.TryGetProperty( \u0022component\u0022, out var compEl ) \u0026\u0026 compEl.ValueKind == JsonValueKind.String )\r\n\t\t\t\tjob.ComponentType = compEl.GetString();\r\n\r\n\t\t\tint idx = 0;\r\n\t\t\tforeach ( var stepEl in stepsEl.EnumerateArray() )\r\n\t\t\t{\r\n\t\t\t\tvar spec = ParseStep( stepEl, idx, out var perr );\r\n\t\t\t\tif ( spec == null )\r\n\t\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022step {idx}: {perr}\u0022 } );\r\n\t\t\t\tjob.Steps.Add( spec );\r\n\t\t\t\tidx\u002B\u002B;\r\n\t\t\t}\r\n\t\t\tif ( job.Steps.Count == 0 )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022steps is empty\u0022 } );\r\n\r\n\t\t\tPlaytestRunner.Start( job );\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tstarted = true,\r\n\t\t\t\tsteps = job.Steps.Count,\r\n\t\t\t\tnote = \u0022Playtest running ASYNC in the editor frame loop. Poll playtest_status until finished:true, then read the transcript (pass/fail per step).\u0022,\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022playtest failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic PlaytestRunner.StepSpec ParseStep( JsonElement e, int idx, out string err )\r\n\t{\r\n\t\terr = null;\r\n\t\tif ( e.ValueKind != JsonValueKind.Object ) { err = \u0022not an object\u0022; return null; }\r\n\t\tvar s = new PlaytestRunner.StepSpec();\r\n\t\tint? framesOverride = ( e.TryGetProperty( \u0022frames\u0022, out var fEl ) \u0026\u0026 fEl.TryGetInt32( out var fi ) )\r\n\t\t\t? System.Math.Clamp( fi, 1, 1800 ) : (int?) null;\r\n\t\tif ( e.TryGetProperty( \u0022moveSpeed\u0022, out var msEl ) \u0026\u0026 msEl.TryGetSingle( out var ms ) ) s.MoveSpeed = ms;\r\n\r\n\t\tif ( e.TryGetProperty( \u0022move\u0022, out var mEl ) )\r\n\t\t{\r\n\t\t\ts.Kind = \u0022move\u0022; s.Move = ParseMove( mEl ); s.Frames = framesOverride ?? 30;\r\n\t\t}\r\n\t\telse if ( e.TryGetProperty( \u0022look\u0022, out var lEl ) )\r\n\t\t{\r\n\t\t\ts.Kind = \u0022look\u0022; s.Look = ParseAngles( lEl ); s.HasLook = true; s.Frames = framesOverride ?? 1;\r\n\t\t}\r\n\t\telse if ( e.TryGetProperty( \u0022lookDelta\u0022, out var ldEl ) )\r\n\t\t{\r\n\t\t\ts.Kind = \u0022lookDelta\u0022; s.LookDelta = ParseAngles( ldEl ); s.Frames = framesOverride ?? 30;\r\n\t\t}\r\n\t\telse if ( e.TryGetProperty( \u0022action\u0022, out var aEl ) \u0026\u0026 aEl.ValueKind == JsonValueKind.String )\r\n\t\t{\r\n\t\t\ts.Kind = \u0022action\u0022; s.Action = aEl.GetString(); s.Frames = framesOverride ?? 20;\r\n\t\t}\r\n\t\telse if ( e.TryGetProperty( \u0022jump\u0022, out var jEl ) )\r\n\t\t{\r\n\t\t\ts.Kind = \u0022jump\u0022; s.JumpVel = ClaudeBridge.ParseVector3Flexible( jEl ); s.Frames = 1;\r\n\t\t}\r\n\t\telse if ( e.TryGetProperty( \u0022set\u0022, out var setEl ) \u0026\u0026 setEl.ValueKind == JsonValueKind.Object )\r\n\t\t{\r\n\t\t\ts.Kind = \u0022set\u0022; s.Frames = 1;\r\n\t\t\ts.SetComponent = GetStr( setEl, \u0022component\u0022 );\r\n\t\t\ts.SetProperty = GetStr( setEl, \u0022property\u0022 );\r\n\t\t\ts.SetValue = GetStr( setEl, \u0022to\u0022 ) ?? GetStr( setEl, \u0022value\u0022 );\r\n\t\t\tif ( s.SetComponent == null || s.SetProperty == null ) { err = \u0022set needs {component, property, to}\u0022; return null; }\r\n\t\t}\r\n\t\telse if ( e.TryGetProperty( \u0022wait\u0022, out var wEl ) \u0026\u0026 wEl.TryGetInt32( out var wf ) )\r\n\t\t{\r\n\t\t\ts.Kind = \u0022wait\u0022; s.Frames = System.Math.Clamp( wf, 1, 1800 );\r\n\t\t}\r\n\t\telse if ( e.TryGetProperty( \u0022capture\u0022, out var capEl ) )\r\n\t\t{\r\n\t\t\ts.Kind = \u0022capture\u0022; s.Frames = 1;\r\n\t\t\ts.CaptureLabel = capEl.ValueKind == JsonValueKind.String ? capEl.GetString() : null;\r\n\t\t}\r\n\t\telse if ( e.TryGetProperty( \u0022assert\u0022, out var asEl ) \u0026\u0026 asEl.ValueKind == JsonValueKind.Object )\r\n\t\t{\r\n\t\t\ts.Kind = \u0022assert\u0022; s.Frames = 1;\r\n\t\t\ts.AssertRead = GetStr( asEl, \u0022read\u0022 );\r\n\t\t\ts.AssertOp = GetStr( asEl, \u0022op\u0022 ) ?? \u0022==\u0022;\r\n\t\t\ts.AssertDesc = GetStr( asEl, \u0022desc\u0022 );\r\n\t\t\tif ( asEl.TryGetProperty( \u0022value\u0022, out var vEl ) )\r\n\t\t\t\ts.AssertValue = vEl.ValueKind == JsonValueKind.String ? vEl.GetString() : vEl.GetRawText();\r\n\t\t\tif ( s.AssertRead == null ) { err = \u0022assert needs {read, op, value}\u0022; return null; }\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\terr = \u0022unknown step (expected one of: move, look, lookDelta, action, jump, set, wait, capture, assert)\u0022;\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\treturn s;\r\n\t}\r\n\r\n\tstatic string GetStr( JsonElement o, string key )\r\n\t\t=\u003E o.TryGetProperty( key, out var v ) \u0026\u0026 v.ValueKind == JsonValueKind.String ? v.GetString() : null;\r\n\r\n\tstatic Vector2 ParseMove( JsonElement el )\r\n\t{\r\n\t\tfloat x = 0f, y = 0f;\r\n\t\tif ( el.ValueKind == JsonValueKind.Object )\r\n\t\t{\r\n\t\t\tif ( el.TryGetProperty( \u0022x\u0022, out var xp ) \u0026\u0026 xp.TryGetSingle( out var xf ) ) x = xf;\r\n\t\t\tif ( el.TryGetProperty( \u0022y\u0022, out var yp ) \u0026\u0026 yp.TryGetSingle( out var yf ) ) y = yf;\r\n\t\t}\r\n\t\telse if ( el.ValueKind == JsonValueKind.String )\r\n\t\t{\r\n\t\t\tvar pr = ( el.GetString() ?? \u0022\u0022 ).Split( \u0027,\u0027 );\r\n\t\t\tif ( pr.Length \u003E 0 ) float.TryParse( pr[0], NumberStyles.Float, CultureInfo.InvariantCulture, out x );\r\n\t\t\tif ( pr.Length \u003E 1 ) float.TryParse( pr[1], NumberStyles.Float, CultureInfo.InvariantCulture, out y );\r\n\t\t}\r\n\t\tvar v = new Vector2( x, y );\r\n\t\tif ( v.Length \u003E 1f ) v = v.Normal;\r\n\t\treturn v;\r\n\t}\r\n\r\n\tstatic Angles ParseAngles( JsonElement el )\r\n\t{\r\n\t\tfloat pitch = 0f, yaw = 0f, roll = 0f;\r\n\t\tif ( el.ValueKind == JsonValueKind.Object )\r\n\t\t{\r\n\t\t\tif ( el.TryGetProperty( \u0022pitch\u0022, out var pp ) \u0026\u0026 pp.TryGetSingle( out var pf ) ) pitch = pf;\r\n\t\t\tif ( el.TryGetProperty( \u0022yaw\u0022, out var yp ) \u0026\u0026 yp.TryGetSingle( out var yf ) ) yaw = yf;\r\n\t\t\tif ( el.TryGetProperty( \u0022roll\u0022, out var rp ) \u0026\u0026 rp.TryGetSingle( out var rf ) ) roll = rf;\r\n\t\t}\r\n\t\telse if ( el.ValueKind == JsonValueKind.String )\r\n\t\t{\r\n\t\t\tvar pr = ( el.GetString() ?? \u0022\u0022 ).Split( \u0027,\u0027 );\r\n\t\t\tif ( pr.Length \u003E 0 ) float.TryParse( pr[0], NumberStyles.Float, CultureInfo.InvariantCulture, out pitch );\r\n\t\t\tif ( pr.Length \u003E 1 ) float.TryParse( pr[1], NumberStyles.Float, CultureInfo.InvariantCulture, out yaw );\r\n\t\t\tif ( pr.Length \u003E 2 ) float.TryParse( pr[2], NumberStyles.Float, CultureInfo.InvariantCulture, out roll );\r\n\t\t}\r\n\t\treturn new Angles( pitch, yaw, roll );\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// playtest_status \u2014 poll the running/finished playtest: live progress while running,\r\n/// or the full pass/fail transcript once finished.\r\n/// \u003C/summary\u003E\r\npublic class PlaytestStatusHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\tvar summary = PlaytestRunner.ConsumeSummary();\r\n\t\tif ( summary != null )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( summary );\r\n\r\n\t\tvar live = PlaytestRunner.LiveSnapshot();\r\n\t\tif ( live != null )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( live );\r\n\r\n\t\treturn Task.FromResult\u003Cobject\u003E( new { active = false, finished = false, note = \u0022No playtest has run yet.\u0022 } );\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// playtest_abort \u2014 stop the running playtest immediately, restoring input state.\r\n/// The partial transcript stays available via playtest_status.\r\n/// \u003C/summary\u003E\r\npublic class PlaytestAbortHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t\t=\u003E Task.FromResult\u003Cobject\u003E( PlaytestRunner.Abort() );\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/VehicleHandlers.cs","FileName":"VehicleHandlers.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"using Editor;\r\nusing Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Text.Json;\r\nusing System.Threading.Tasks;\r\n\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n// Batch 54 \u2014 bridge_vehicle (v2 wave 4): the corpus vehicles theme.\r\n//   create_vehicle_controller \u2014 make any Rigidbody prop drivable (raycast car\r\n//     with suspension, engine, steering, grip \u002B built-in driver seat)\r\n//   create_seat_system       \u2014 standalone generic seat (enter/exit/safe-exit)\r\n//   tune_vehicle             \u2014 apply arcade/drift/offroad/race presets\r\n//   create_physics_grab_tool \u2014 physgun-style spring grab \u002B throw\r\n// Generated code APIs verified live: Rigidbody.ApplyForceAt/GetVelocityAtPoint/\r\n// ApplyTorque/Velocity/Mass (describe_type, 2026-07-09). Driving FEEL needs a\r\n// human playtest \u2014 compiles\u002Bruns \u2260 fun (BRIDGE_GOTCHAS #1).\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n\r\n/// \u003Csummary\u003Ecreate_vehicle_controller \u2014 scaffold a drivable raycast-car component.\u003C/summary\u003E\r\npublic class CreateVehicleControllerHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022VehicleController\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tfloat engine = p.TryGetProperty( \u0022engineForce\u0022, out var ef ) \u0026\u0026 ef.TryGetSingle( out var eff ) ? eff : 900f;\r\n\t\t\tfloat steer  = p.TryGetProperty( \u0022steerStrength\u0022, out var ss ) \u0026\u0026 ss.TryGetSingle( out var ssf ) ? ssf : 2.0f;\r\n\t\t\tfloat grip   = p.TryGetProperty( \u0022grip\u0022, out var g ) \u0026\u0026 g.TryGetSingle( out var gf ) ? gf : 0.85f;\r\n\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, BuildCode( className, engine, steer, grip ) );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tnextSteps = new[]\r\n\t\t\t\t{\r\n\t\t\t\t\t\u0022trigger_hotload, then check compile_status\u0022,\r\n\t\t\t\t\t$\u0022Attach {className} \u002B a Rigidbody \u002B a collider to your vehicle prop (batch_add_component works)\u0022,\r\n\t\t\t\t\t\u0022Enter play mode and press E on the vehicle to drive (WASD; E again to exit)\u0022,\r\n\t\t\t\t\t\u0022tune_vehicle applies arcade/drift/offroad/race presets to the attached component\u0022,\r\n\t\t\t\t\t\u0022HUMAN PLAYTEST REQUIRED for feel \u2014 tune EngineForce/SteerStrength/GripFactor from the inspector while playing\u0022\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_vehicle_controller failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string BuildCode( string className, float engine, float steer, float grip )\r\n\t{\r\n\t\tvar ci = System.Globalization.CultureInfo.InvariantCulture;\r\n\t\treturn $@\u0022using Sandbox;\r\nusing System;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} \u2014 makes a Rigidbody prop drivable: a 4-corner raycast car with\r\n/// spring/damper suspension, engine force, yaw steering, and lateral grip\r\n/// (lower grip = drift). Built-in driver seat: press E (use) to enter \u2014 the\r\n/// driver is hidden while driving (no controller transform fights), the host\r\n/// assigns them vehicle ownership, and a chase camera follows \u2014 E to exit.\r\n/// Requires a Rigidbody \u002B collider on the same GameObject. Tune from the\r\n/// inspector while playing; tune_vehicle applies ready-made presets.\r\n/// \u003C/summary\u003E\r\npublic sealed class {className} : Component, Component.IPressable\r\n{{\r\n\t[Property, Group( \u0022\u0022Engine\u0022\u0022 )] public float EngineForce {{ get; set; }} = {engine.ToString( ci )}f;\r\n\t[Property, Group( \u0022\u0022Engine\u0022\u0022 )] public float MaxSpeed {{ get; set; }} = 800f;\r\n\t[Property, Group( \u0022\u0022Steering\u0022\u0022 )] public float SteerStrength {{ get; set; }} = {steer.ToString( ci )}f;   // yaw rate, rad/s at full speed factor\r\n\t[Property, Group( \u0022\u0022Handling\u0022\u0022 ), Range( 0f, 1f )] public float GripFactor {{ get; set; }} = {grip.ToString( ci )}f;\r\n\t[Property, Group( \u0022\u0022Suspension\u0022\u0022 )] public float SuspensionRest {{ get; set; }} = 24f;\r\n\t[Property, Group( \u0022\u0022Suspension\u0022\u0022 )] public float SuspensionStrength {{ get; set; }} = 90f;\r\n\t[Property, Group( \u0022\u0022Suspension\u0022\u0022 )] public float SuspensionDamping {{ get; set; }} = 8f;\r\n\t[Property, Group( \u0022\u0022Seat\u0022\u0022 )] public Vector3 ExitOffset {{ get; set; }} = new( 0, 80, 20 );\r\n\t[Property, Group( \u0022\u0022Camera\u0022\u0022 )] public float CameraDistance {{ get; set; }} = 260f;\r\n\t[Property, Group( \u0022\u0022Camera\u0022\u0022 )] public float CameraHeight {{ get; set; }} = 110f;\r\n\r\n\t[Sync] public Guid DriverId {{ get; set; }}\r\n\r\n\tpublic bool HasDriver =\u003E DriverId != Guid.Empty;\r\n\tpublic static event Action\u003CGameObject, bool\u003E OnDriverChanged; // (vehicle, entered)\r\n\r\n\tRigidbody _rb;\r\n\tVector3[] _corners;\r\n\tTimeSince _sinceEnter;\r\n\r\n\tprotected override void OnStart()\r\n\t{{\r\n\t\t_rb = GetComponent\u003CRigidbody\u003E();\r\n\t\tif ( _rb == null )\r\n\t\t{{\r\n\t\t\tLog.Warning( $\u0022\u0022{className} needs a Rigidbody on {{GameObject.Name}}\u0022\u0022 );\r\n\t\t\tEnabled = false;\r\n\t\t\treturn;\r\n\t\t}}\r\n\t\tvar bounds = GameObject.GetBounds();\r\n\t\tvar ext = ( bounds.Size * 0.4f ).WithZ( 0 );\r\n\t\t_corners = new[]\r\n\t\t{{\r\n\t\t\tnew Vector3(  ext.x,  ext.y, 0 ), new Vector3(  ext.x, -ext.y, 0 ),\r\n\t\t\tnew Vector3( -ext.x,  ext.y, 0 ), new Vector3( -ext.x, -ext.y, 0 ),\r\n\t\t}};\r\n\t}}\r\n\r\n\t// \u2500\u2500 Seat (IPressable) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\tpublic bool Press( Component.IPressable.Event e )\r\n\t{{\r\n\t\tvar presser = e.Source?.GameObject;\r\n\t\tif ( presser == null ) return false;\r\n\t\tRequestSeat( presser.Id );\r\n\t\treturn true;\r\n\t}}\r\n\r\n\t[Rpc.Host]\r\n\tvoid RequestSeat( Guid pressGuid )\r\n\t{{\r\n\t\tvar presser = Scene.Directory.FindByGuid( pressGuid );\r\n\t\tif ( presser == null ) return;\r\n\t\tif ( HasDriver \u0026\u0026 DriverId != pressGuid ) return;\r\n\r\n\t\tif ( DriverId == pressGuid )\r\n\t\t{{\r\n\t\t\tExit( presser );\r\n\t\t\treturn;\r\n\t\t}}\r\n\r\n\t\t// Enter: hide the player entirely while driving \u2014 parenting a live\r\n\t\t// PlayerController to a moving vehicle makes two systems fight over the\r\n\t\t// transform (the classic seat jitter). Hidden driver \u002B chase camera instead.\r\n\t\tDriverId = pressGuid;\r\n\t\t_sinceEnter = 0;\r\n\t\tvar owner = presser.Network.Owner;\r\n\t\tif ( owner != null ) GameObject.Network.AssignOwnership( owner );\r\n\t\tpresser.Enabled = false;\r\n\t\tOnDriverChanged?.Invoke( GameObject, true );\r\n\t}}\r\n\r\n\tvoid Exit( GameObject driver )\r\n\t{{\r\n\t\tDriverId = Guid.Empty;\r\n\t\tif ( driver != null )\r\n\t\t{{\r\n\t\t\tdriver.WorldPosition = WorldPosition \u002B WorldRotation * ExitOffset;\r\n\t\t\tdriver.Enabled = true;   // their controller re-takes the camera next frame\r\n\t\t}}\r\n\t\tGameObject.Network.DropOwnership();\r\n\t\tOnDriverChanged?.Invoke( GameObject, false );\r\n\t}}\r\n\r\n\t// Chase camera while driving (runs on the driver\u0027s client \u2014 they own the vehicle).\r\n\tprotected override void OnPreRender()\r\n\t{{\r\n\t\tif ( IsProxy || !HasDriver ) return;\r\n\t\tvar cam = Scene.Camera;\r\n\t\tif ( cam == null ) return;\r\n\r\n\t\tvar targetPos = WorldPosition - WorldRotation.Forward.WithZ( 0 ).Normal * CameraDistance \u002B Vector3.Up * CameraHeight;\r\n\t\tcam.WorldPosition = cam.WorldPosition.LerpTo( targetPos, MathX.Clamp( Time.Delta * 6f, 0f, 1f ) );\r\n\t\tcam.WorldRotation = Rotation.LookAt( ( WorldPosition \u002B Vector3.Up * 30f - cam.WorldPosition ).Normal, Vector3.Up );\r\n\t}}\r\n\r\n\t// \u2500\u2500 Driving (vehicle owner only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\tprotected override void OnFixedUpdate()\r\n\t{{\r\n\t\tif ( _rb == null || IsProxy || !HasDriver ) return;\r\n\r\n\t\t// E again to exit (edge-guarded so the entering press cannot instantly exit).\r\n\t\tif ( _sinceEnter \u003E 0.4f \u0026\u0026 Input.Pressed( \u0022\u0022use\u0022\u0022 ) )\r\n\t\t{{\r\n\t\t\tRequestSeat( DriverId );\r\n\t\t\treturn;\r\n\t\t}}\r\n\r\n\t\tvar dt = Time.Delta;\r\n\t\tvar input = Input.AnalogMove;   // x = forward/back, y = left/right\r\n\t\tint grounded = 0;\r\n\r\n\t\t// Suspension: 4 corner rays, spring \u002B damper applied at each corner.\r\n\t\tforeach ( var corner in _corners )\r\n\t\t{{\r\n\t\t\tvar worldCorner = WorldPosition \u002B WorldRotation * corner;\r\n\t\t\tvar tr = Scene.Trace.Ray( worldCorner, worldCorner \u002B Vector3.Down * SuspensionRest * 2f )\r\n\t\t\t\t.IgnoreGameObjectHierarchy( GameObject )\r\n\t\t\t\t.Run();\r\n\t\t\tif ( !tr.Hit ) continue;\r\n\t\t\tgrounded\u002B\u002B;\r\n\t\t\tvar compression = 1f - ( tr.Distance / ( SuspensionRest * 2f ) );\r\n\t\t\tvar pointVel = _rb.GetVelocityAtPoint( worldCorner );\r\n\t\t\tvar force = Vector3.Up * ( compression * SuspensionStrength - pointVel.z * SuspensionDamping ) * _rb.Mass * dt * 50f;\r\n\t\t\t_rb.ApplyForceAt( worldCorner, force );\r\n\t\t}}\r\n\r\n\t\tif ( grounded == 0 ) return;   // airborne \u2014 no engine/steer/grip\r\n\r\n\t\tvar forward = WorldRotation.Forward.WithZ( 0 ).Normal;\r\n\t\tvar speed = _rb.Velocity.WithZ( 0 ).Length;\r\n\r\n\t\t// Engine (mass-scaled so feel survives different props).\r\n\t\tif ( MathF.Abs( input.x ) \u003E 0.01f \u0026\u0026 speed \u003C MaxSpeed )\r\n\t\t\t_rb.ApplyForce( forward * input.x * EngineForce * _rb.Mass );\r\n\r\n\t\t// Steering: set yaw angular velocity directly \u2014 arcade-reliable, immune to the\r\n\t\t// prop\u0027s moment of inertia (torque was far too weak on heavy boxes \u2014 playtested).\r\n\t\tvar steerFactor = MathX.Clamp( speed / 150f, 0.25f, 1f );\r\n\t\tvar direction = _rb.Velocity.Dot( forward ) \u003C -10f ? -1f : 1f;   // reverse steers mirrored\r\n\t\tvar yawRate = MathF.Abs( input.y ) \u003E 0.01f\r\n\t\t\t? input.y * SteerStrength * steerFactor * direction\r\n\t\t\t: 0f;\r\n\t\t_rb.AngularVelocity = _rb.AngularVelocity.WithZ( MathX.Lerp( _rb.AngularVelocity.z, yawRate, MathX.Clamp( dt * 12f, 0f, 1f ) ) );\r\n\r\n\t\t// Lateral grip: kill a fraction of sideways velocity each tick. Low grip = drift.\r\n\t\tvar right = WorldRotation.Right.WithZ( 0 ).Normal;\r\n\t\tvar lateral = right * _rb.Velocity.Dot( right );\r\n\t\t_rb.Velocity -= lateral * GripFactor * MathX.Clamp( dt * 10f, 0f, 1f );\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003Ecreate_seat_system \u2014 scaffold a standalone enter/exit seat component.\u003C/summary\u003E\r\npublic class CreateSeatSystemHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022Seat\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, BuildCode( className ) );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tnextSteps = new[]\r\n\t\t\t\t{\r\n\t\t\t\t\t\u0022trigger_hotload, then check compile_status\u0022,\r\n\t\t\t\t\t$\u0022Attach {className} to any prop (chair, bench, turret mount) \u2014 press E to sit, E to stand\u0022,\r\n\t\t\t\t\t\u0022SeatOffset positions the occupant; exit tries ExitOffsets in order and takes the first clear spot\u0022,\r\n\t\t\t\t\t$\u0022Subscribe to {className}.OnOccupantChanged for camera/UI logic\u0022\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_seat_system failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string BuildCode( string className )\r\n\t{\r\n\t\treturn $@\u0022using Sandbox;\r\nusing System;\r\nusing System.Linq;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} \u2014 a networked one-occupant seat: press E (use) to sit, E again\r\n/// to stand. Claims route through the host so two players can\u0027t share a seat;\r\n/// the occupant is parented to the seat with their controller input disabled\r\n/// (UseInputControls=false, restored on exit). Exit tries each ExitOffsets\r\n/// entry and takes the first spot with clearance. Works for chairs, benches,\r\n/// turret mounts \u2014 anything sittable.\r\n/// \u003C/summary\u003E\r\npublic sealed class {className} : Component, Component.IPressable\r\n{{\r\n\t[Property] public Vector3 SeatOffset {{ get; set; }} = new( 0, 0, 10 );\r\n\t[Property] public System.Collections.Generic.List\u003CVector3\u003E ExitOffsets {{ get; set; }} = new()\r\n\t\t{{ new( 0, 60, 10 ), new( 0, -60, 10 ), new( 60, 0, 10 ), new( -60, 0, 10 ) }};\r\n\r\n\t[Sync] public Guid OccupantId {{ get; set; }}\r\n\r\n\tpublic bool IsOccupied =\u003E OccupantId != Guid.Empty;\r\n\tpublic static event Action\u003CGameObject, GameObject, bool\u003E OnOccupantChanged; // (seat, occupant, seated)\r\n\r\n\tpublic bool Press( Component.IPressable.Event e )\r\n\t{{\r\n\t\tvar presser = e.Source?.GameObject;\r\n\t\tif ( presser == null ) return false;\r\n\t\tRequestSeat( presser.Id );\r\n\t\treturn true;\r\n\t}}\r\n\r\n\t[Rpc.Host]\r\n\tvoid RequestSeat( Guid pressGuid )\r\n\t{{\r\n\t\tvar presser = Scene.Directory.FindByGuid( pressGuid );\r\n\t\tif ( presser == null ) return;\r\n\r\n\t\tif ( OccupantId == pressGuid )\r\n\t\t{{\r\n\t\t\tSetControls( presser, true );\r\n\t\t\tpresser.SetParent( null, true );\r\n\t\t\tpresser.WorldPosition = FindExitSpot( presser );\r\n\t\t\tOccupantId = Guid.Empty;\r\n\t\t\tOnOccupantChanged?.Invoke( GameObject, presser, false );\r\n\t\t\treturn;\r\n\t\t}}\r\n\t\tif ( IsOccupied ) return;\r\n\r\n\t\tOccupantId = pressGuid;\r\n\t\tpresser.SetParent( GameObject, true );\r\n\t\tpresser.LocalPosition = SeatOffset;\r\n\t\tSetControls( presser, false );\r\n\t\tOnOccupantChanged?.Invoke( GameObject, presser, true );\r\n\t}}\r\n\r\n\tVector3 FindExitSpot( GameObject occupant )\r\n\t{{\r\n\t\tforeach ( var offset in ExitOffsets )\r\n\t\t{{\r\n\t\t\tvar spot = WorldPosition \u002B WorldRotation * offset;\r\n\t\t\tvar tr = Scene.Trace.Ray( spot \u002B Vector3.Up * 32f, spot )\r\n\t\t\t\t.IgnoreGameObjectHierarchy( GameObject )\r\n\t\t\t\t.IgnoreGameObjectHierarchy( occupant )\r\n\t\t\t\t.Run();\r\n\t\t\tif ( !tr.Hit ) return spot;\r\n\t\t}}\r\n\t\treturn WorldPosition \u002B Vector3.Up * 48f;   // all blocked \u2014 pop up top\r\n\t}}\r\n\r\n\tstatic void SetControls( GameObject occupant, bool enabled )\r\n\t{{\r\n\t\tforeach ( var comp in occupant.Components.GetAll() )\r\n\t\t{{\r\n\t\t\tif ( comp is null ) continue;\r\n\t\t\tvar type = Game.TypeLibrary?.GetType( comp.GetType() );\r\n\t\t\tvar prop = type?.Properties?.FirstOrDefault( pr =\u003E pr.Name == \u0022\u0022UseInputControls\u0022\u0022 );\r\n\t\t\tprop?.SetValue( comp, enabled );\r\n\t\t}}\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003Etune_vehicle \u2014 apply a handling preset to a vehicle controller component.\u003C/summary\u003E\r\npublic class TuneVehicleHandler : IBridgeHandler\r\n{\r\n\tstatic readonly Dictionary\u003Cstring, Dictionary\u003Cstring, float\u003E\u003E Presets = new( StringComparer.OrdinalIgnoreCase )\r\n\t{\r\n\t\t[\u0022arcade\u0022]  = new() { [\u0022EngineForce\u0022] = 900f,  [\u0022MaxSpeed\u0022] = 800f,  [\u0022SteerStrength\u0022] = 2.0f, [\u0022GripFactor\u0022] = 0.85f, [\u0022SuspensionStrength\u0022] = 90f,  [\u0022SuspensionDamping\u0022] = 8f },\r\n\t\t[\u0022drift\u0022]   = new() { [\u0022EngineForce\u0022] = 1100f, [\u0022MaxSpeed\u0022] = 900f,  [\u0022SteerStrength\u0022] = 2.8f, [\u0022GripFactor\u0022] = 0.35f, [\u0022SuspensionStrength\u0022] = 80f,  [\u0022SuspensionDamping\u0022] = 6f },\r\n\t\t[\u0022offroad\u0022] = new() { [\u0022EngineForce\u0022] = 750f,  [\u0022MaxSpeed\u0022] = 600f,  [\u0022SteerStrength\u0022] = 1.5f, [\u0022GripFactor\u0022] = 0.7f,  [\u0022SuspensionStrength\u0022] = 130f, [\u0022SuspensionDamping\u0022] = 12f },\r\n\t\t[\u0022race\u0022]    = new() { [\u0022EngineForce\u0022] = 1400f, [\u0022MaxSpeed\u0022] = 1400f, [\u0022SteerStrength\u0022] = 1.7f, [\u0022GripFactor\u0022] = 0.95f, [\u0022SuspensionStrength\u0022] = 110f, [\u0022SuspensionDamping\u0022] = 10f },\r\n\t};\r\n\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\tvar scene = SceneEditorSession.Active?.Scene;\r\n\t\tif ( scene == null )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022No active scene\u0022 } );\r\n\r\n\t\tvar id = p.TryGetProperty( \u0022id\u0022, out var idEl ) ? idEl.GetString() : null;\r\n\t\tvar go = ClaudeBridge.ResolveGameObject( scene, id );\r\n\t\tif ( go == null )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022GameObject not found: {id}\u0022 } );\r\n\r\n\t\tvar presetName = p.TryGetProperty( \u0022preset\u0022, out var pr ) ? pr.GetString() : null;\r\n\t\tif ( presetName == null || !Presets.TryGetValue( presetName, out var preset ) )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022preset must be one of: {string.Join( \u0022 | \u0022, Presets.Keys )}\u0022 } );\r\n\r\n\t\tvar compName = p.TryGetProperty( \u0022component\u0022, out var cn ) ? cn.GetString() : null;\r\n\t\tvar component = go.Components.GetAll().FirstOrDefault( c =\u003E c != null \u0026\u0026\r\n\t\t\t( compName != null\r\n\t\t\t\t? c.GetType().Name.Equals( compName, StringComparison.OrdinalIgnoreCase )\r\n\t\t\t\t: c.GetType().Name.Contains( \u0022Vehicle\u0022, StringComparison.OrdinalIgnoreCase ) ) );\r\n\t\tif ( component == null )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = compName != null\r\n\t\t\t\t? $\u0022No \u0027{compName}\u0027 component on the object\u0022\r\n\t\t\t\t: \u0022No component with \u0027Vehicle\u0027 in its type name found \u2014 pass component explicitly\u0022 } );\r\n\r\n\t\tvar typeDesc = Game.TypeLibrary.GetType( component.GetType().Name );\r\n\t\tvar applied = new List\u003Cobject\u003E();\r\n\t\tvar missing = new List\u003Cstring\u003E();\r\n\t\tforeach ( var (propName, value) in preset )\r\n\t\t{\r\n\t\t\tvar propDesc = typeDesc?.Properties.FirstOrDefault( pp =\u003E pp.Name == propName );\r\n\t\t\tif ( propDesc == null ) { missing.Add( propName ); continue; }\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tpropDesc.SetValue( component, value );\r\n\t\t\t\tapplied.Add( new { property = propName, value } );\r\n\t\t\t}\r\n\t\t\tcatch ( Exception ex ) { missing.Add( $\u0022{propName} ({ex.Message})\u0022 ); }\r\n\t\t}\r\n\r\n\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t{\r\n\t\t\ttuned = applied.Count \u003E 0,\r\n\t\t\tpreset = presetName.ToLowerInvariant(),\r\n\t\t\tcomponent = component.GetType().Name,\r\n\t\t\tapplied,\r\n\t\t\tmissing,\r\n\t\t\tnote = missing.Count \u003E 0\r\n\t\t\t\t? \u0022Some preset properties don\u0027t exist on this component \u2014 presets target create_vehicle_controller scaffolds; others tune partially.\u0022\r\n\t\t\t\t: \u0022Preset applied. Enter play mode and drive to feel it; fine-tune the same properties with set_property.\u0022\r\n\t\t} );\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003Ecreate_physics_grab_tool \u2014 scaffold a physgun-style spring grab \u002B throw.\u003C/summary\u003E\r\npublic class CreatePhysicsGrabToolHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022PhysicsGrabTool\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, BuildCode( className ) );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tnextSteps = new[]\r\n\t\t\t\t{\r\n\t\t\t\t\t\u0022trigger_hotload, then check compile_status\u0022,\r\n\t\t\t\t\t$\u0022Attach {className} to the player object (needs a camera child or PlayerController for aim)\u0022,\r\n\t\t\t\t\t\u0022Hold attack2 (right mouse) on a Rigidbody prop to grab; scroll-free: it follows at grab distance; attack1 throws\u0022,\r\n\t\t\t\t\t\u0022ensure_input_action if your project lacks attack1/attack2 bindings\u0022\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_physics_grab_tool failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string BuildCode( string className )\r\n\t{\r\n\t\treturn $@\u0022using Sandbox;\r\nusing System;\r\nusing System.Linq;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} \u2014 a physgun-lite for the player: hold GrabAction (default\r\n/// attack2) while looking at a Rigidbody prop to grab it; it spring-follows a\r\n/// point in front of your view (physics stays LIVE \u2014 it collides and swings,\r\n/// unlike a parented carry); press ThrowAction (default attack1) to launch it.\r\n/// Grab requests route through the host, which assigns the grabber network\r\n/// ownership of the prop. Owner-only logic; attach to the player object.\r\n/// \u003C/summary\u003E\r\npublic sealed class {className} : Component\r\n{{\r\n\t[Property] public float Range {{ get; set; }} = 300f;\r\n\t[Property] public float SpringStrength {{ get; set; }} = 12f;\r\n\t[Property] public float ThrowForce {{ get; set; }} = 600f;\r\n\t[Property] public float MaxMass {{ get; set; }} = 2000f;\r\n\t[Property] public string GrabAction {{ get; set; }} = \u0022\u0022attack2\u0022\u0022;\r\n\t[Property] public string ThrowAction {{ get; set; }} = \u0022\u0022attack1\u0022\u0022;\r\n\r\n\tGameObject _held;\r\n\tfloat _holdDistance;\r\n\r\n\tpublic bool IsHolding =\u003E _held.IsValid();\r\n\tpublic static event Action\u003CGameObject, GameObject, bool\u003E OnGrabChanged; // (player, prop, grabbed)\r\n\r\n\tprotected override void OnFixedUpdate()\r\n\t{{\r\n\t\tif ( IsProxy ) return;\r\n\r\n\t\tvar eye = GetEye( out var dir );\r\n\r\n\t\tif ( IsHolding \u0026\u0026 Input.Pressed( ThrowAction ) )\r\n\t\t{{\r\n\t\t\tvar rb = _held.GetComponent\u003CRigidbody\u003E();\r\n\t\t\trb?.ApplyImpulse( dir * ThrowForce * ( rb.Mass ) );\r\n\t\t\tRelease();\r\n\t\t\treturn;\r\n\t\t}}\r\n\r\n\t\tif ( Input.Down( GrabAction ) )\r\n\t\t{{\r\n\t\t\tif ( !IsHolding ) TryGrab( eye, dir );\r\n\t\t\telse Hold( eye, dir );\r\n\t\t}}\r\n\t\telse if ( IsHolding )\r\n\t\t{{\r\n\t\t\tRelease();\r\n\t\t}}\r\n\t}}\r\n\r\n\tVector3 GetEye( out Vector3 dir )\r\n\t{{\r\n\t\tvar cam = Scene.Camera;\r\n\t\tif ( cam != null )\r\n\t\t{{\r\n\t\t\tdir = cam.WorldRotation.Forward;\r\n\t\t\treturn cam.WorldPosition;\r\n\t\t}}\r\n\t\tdir = WorldRotation.Forward;\r\n\t\treturn WorldPosition \u002B Vector3.Up * 64f;\r\n\t}}\r\n\r\n\tvoid TryGrab( Vector3 eye, Vector3 dir )\r\n\t{{\r\n\t\tvar tr = Scene.Trace.Ray( eye, eye \u002B dir * Range )\r\n\t\t\t.IgnoreGameObjectHierarchy( GameObject )\r\n\t\t\t.Run();\r\n\t\tif ( !tr.Hit || tr.GameObject == null ) return;\r\n\r\n\t\tvar rb = tr.GameObject.GetComponent\u003CRigidbody\u003E();\r\n\t\tif ( rb == null || rb.Mass \u003E MaxMass ) return;\r\n\r\n\t\t_held = tr.GameObject;\r\n\t\t_holdDistance = MathX.Clamp( tr.Distance, 60f, Range );\r\n\t\tRequestGrabOwnership( _held.Id );\r\n\t\tOnGrabChanged?.Invoke( GameObject, _held, true );\r\n\t}}\r\n\r\n\tvoid Hold( Vector3 eye, Vector3 dir )\r\n\t{{\r\n\t\tif ( !_held.IsValid() ) {{ _held = null; return; }}\r\n\t\tvar rb = _held.GetComponent\u003CRigidbody\u003E();\r\n\t\tif ( rb == null ) {{ Release(); return; }}\r\n\r\n\t\tvar target = eye \u002B dir * _holdDistance;\r\n\t\t// Velocity-set spring: stiff, stable, still collides with the world.\r\n\t\trb.Velocity = ( target - _held.WorldPosition ) * SpringStrength;\r\n\t\trb.AngularVelocity = rb.AngularVelocity.LerpTo( Vector3.Zero, Time.Delta * 5f );\r\n\t}}\r\n\r\n\tvoid Release()\r\n\t{{\r\n\t\tif ( _held.IsValid() )\r\n\t\t\tOnGrabChanged?.Invoke( GameObject, _held, false );\r\n\t\t_held = null;\r\n\t}}\r\n\r\n\t[Rpc.Host]\r\n\tvoid RequestGrabOwnership( Guid propId )\r\n\t{{\r\n\t\tvar prop = Scene.Directory.FindByGuid( propId );\r\n\t\tvar caller = Rpc.Caller;\r\n\t\tif ( prop == null || caller is null ) return;\r\n\t\tprop.Network.AssignOwnership( caller );\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/GameFeelHandlers.cs","FileName":"GameFeelHandlers.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"using Editor;\r\nusing Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Text;\r\nusing System.Text.Json;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\n\r\n// =============================================================================\r\n//  Game Feel pack (v1.19.0) -- three \u0022juice\u0022 scaffolds (code-gen; scene-mutating):\r\n//\r\n//    create_camera_shake         trauma-based Perlin camera shake component\r\n//    add_flicker_light           flicker/pulse animator for an existing light\r\n//    create_floating_combat_text rising/fading world-space damage popups\r\n//\r\n//  Compiles into the SAME editor assembly as MyEditorMenu.cs / ScaffoldHandlers.cs,\r\n//  so it reuses the shared statics on ClaudeBridge (TryResolveProjectPath,\r\n//  SanitizeIdentifier, SerializeGo) and ScaffoldHelpers (PrepareCodeFile /\r\n//  WriteCode / Utf8NoBom). Handler code here is UNSANDBOXED editor code.\r\n//\r\n//  The C# *strings these handlers WRITE TO DISK* are SANDBOXED game code and must\r\n//  obey the s\u0026box sandbox rules:\r\n//    - MathX preferred; System.Math/MathF also compile on the current SDK.\r\n//      Array.Clone() is still whitelist-blocked (not used here).\r\n//    - only sandbox-proven APIs: Component, [Property], List\u003CT\u003E, TimeSince,\r\n//      Game.Random.Float (compile-verified in create_weighted_loot_table),\r\n//      Sandbox.Utility.Noise.Perlin (fully qualified to dodge a using),\r\n//      new GameObject(...) for runtime spawns.\r\n//    - all three generated components are LOCAL/visual-only -- no [Sync], no\r\n//      RPCs. Multiplayer note lands in the nextSteps (wrap the calls in an\r\n//      [Rpc.Broadcast] so every client sees the juice).\r\n//\r\n//  Register(...) lines \u002B the _sceneMutatingCommands additions live in\r\n//  MyEditorMenu.cs (Batch 44) to keep the files decoupled.\r\n// =============================================================================\r\n\r\n// -----------------------------------------------------------------------------\r\n// create_camera_shake -- trauma-based camera shake (the corpus-standard model:\r\n// shake magnitude = Trauma^2, Perlin-driven offsets, decays over time).\r\n//\r\n// Applied in OnPreRender AFTER controllers have positioned the camera. The\r\n// un-apply guard (compare against what we last WROTE) makes it correct on both\r\n// a static camera (no accumulation) and a controller-driven one (no fighting).\r\n// -----------------------------------------------------------------------------\r\npublic class CreateCameraShakeHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar ci = System.Globalization.CultureInfo.InvariantCulture;\r\n\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022CameraShake\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tfloat maxOffset = p.TryGetProperty( \u0022maxOffset\u0022,      out var ov ) \u0026\u0026 ov.TryGetSingle( out var of ) ? of : 6f;\r\n\t\t\tfloat maxAngle  = p.TryGetProperty( \u0022maxAngle\u0022,       out var av ) \u0026\u0026 av.TryGetSingle( out var af ) ? af : 4f;\r\n\t\t\tfloat frequency = p.TryGetProperty( \u0022frequency\u0022,      out var fv ) \u0026\u0026 fv.TryGetSingle( out var ff ) ? ff : 10f;\r\n\t\t\tfloat decay     = p.TryGetProperty( \u0022decayPerSecond\u0022, out var dv ) \u0026\u0026 dv.TryGetSingle( out var df ) ? df : 1.5f;\r\n\r\n\t\t\t// Defensive clamps so a silly value can\u0027t emit a nauseating component.\r\n\t\t\tif ( maxOffset \u003C 0f )    maxOffset = 0f;\r\n\t\t\tif ( maxAngle  \u003C 0f )    maxAngle  = 0f;\r\n\t\t\tif ( frequency \u003C 0.1f )  frequency = 0.1f;\r\n\t\t\tif ( decay     \u003C 0.05f ) decay     = 0.05f;\r\n\r\n\t\t\tvar code = BuildCode( className, maxOffset, maxAngle, frequency, decay, ci );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\tobject placedOn = null; string note = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022targetId\u0022, out var tid ) \u0026\u0026 tid.ValueKind == JsonValueKind.String )\r\n\t\t\t\tplacedOn = GameFeelHelpers.PlaceOnTarget( tid.GetString(), className, out note );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tmaxOffset,\r\n\t\t\t\tmaxAngle,\r\n\t\t\t\tfrequency,\r\n\t\t\t\tdecayPerSecond = decay,\r\n\t\t\t\tplacedOn,\r\n\t\t\t\tnote,\r\n\t\t\t\tnextSteps = new[]\r\n\t\t\t\t{\r\n\t\t\t\t\t$\u0022trigger_hotload to compile {className} into the game assembly.\u0022,\r\n\t\t\t\t\tplacedOn != null\r\n\t\t\t\t\t\t? $\u0022{className} was attached to the target GameObject.\u0022\r\n\t\t\t\t\t\t: $\u0022Attach it to the CAMERA GameObject: add_component_with_properties (component=\\\u0022{className}\\\u0022) after the hotload, or re-run with targetId.\u0022,\r\n\t\t\t\t\t$\u0022Fire a shake from any game code: {className}.Shake( 0.4f ) -- explosions ~0.6-1.0, hits ~0.2-0.4, footsteps ~0.05. Trauma stacks and clamps at 1.\u0022,\r\n\t\t\t\t\t\u0022LOCAL-only: call it inside an [Rpc.Broadcast] handler if every client should feel the shake.\u0022,\r\n\t\t\t\t\t\u0022Tune MaxOffset / MaxAngle / Frequency / DecayPerSecond with set_property, then verify in play mode: playtest with a capture step, or set_runtime_property Trauma=1 and take_screenshot.\u0022\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_camera_shake failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string BuildCode( string className, float maxOffset, float maxAngle, float frequency, float decay, System.Globalization.CultureInfo ci )\r\n\t{\r\n\t\tstring mo = maxOffset.ToString( ci ) \u002B \u0022f\u0022;\r\n\t\tstring ma = maxAngle.ToString( ci ) \u002B \u0022f\u0022;\r\n\t\tstring fq = frequency.ToString( ci ) \u002B \u0022f\u0022;\r\n\t\tstring dc = decay.ToString( ci ) \u002B \u0022f\u0022;\r\n\r\n\t\treturn $@\u0022using Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} -- trauma-based camera shake. Attach to the camera GameObject.\r\n///\r\n/// The standard game-feel model: an event adds Trauma (0..1), shake magnitude\r\n/// is Trauma^2 (small hits barely register, big hits slam), offsets are smooth\r\n/// Perlin noise (not white-noise jitter), and Trauma decays every frame.\r\n///\r\n/// Usage from anywhere:  {className}.Shake( 0.5f );\r\n/// LOCAL-only -- wrap the call in an [Rpc.Broadcast] if all clients should shake.\r\n/// \u003C/summary\u003E\r\npublic sealed class {className} : Component\r\n{{\r\n\t/// Current shake energy, 0..1. Add via Shake(); decays by DecayPerSecond.\r\n\t[Property] public float Trauma {{ get; set; }}\r\n\r\n\t/// Positional shake at full trauma, in world units.\r\n\t[Property] public float MaxOffset {{ get; set; }} = {mo};\r\n\r\n\t/// Rotational shake at full trauma, in degrees (pitch/yaw/roll).\r\n\t[Property] public float MaxAngle {{ get; set; }} = {ma};\r\n\r\n\t/// Noise speed -- higher = more violent rattle, lower = drunken sway.\r\n\t[Property] public float Frequency {{ get; set; }} = {fq};\r\n\r\n\t/// How much trauma drains per second.\r\n\t[Property] public float DecayPerSecond {{ get; set; }} = {dc};\r\n\r\n\tprivate static readonly List\u003C{className}\u003E _active = new List\u003C{className}\u003E();\r\n\r\n\tprivate Vector3 _lastWrittenPos;\r\n\tprivate Rotation _lastWrittenRot;\r\n\tprivate Vector3 _appliedOffset;\r\n\tprivate Rotation _appliedRot = Rotation.Identity;\r\n\tprivate bool _hasApplied;\r\n\r\n\t/// \u003Csummary\u003EAdd trauma to every active {className} (usually the one on the local camera).\u003C/summary\u003E\r\n\tpublic static void Shake( float trauma )\r\n\t{{\r\n\t\tforeach ( var s in _active )\r\n\t\t\ts.Trauma = MathX.Clamp( s.Trauma \u002B trauma, 0f, 1f );\r\n\t}}\r\n\r\n\tprotected override void OnEnabled()\r\n\t{{\r\n\t\t_active.Add( this );\r\n\t}}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{{\r\n\t\t_active.Remove( this );\r\n\t\tRemoveAppliedShake();\r\n\t}}\r\n\r\n\tprotected override void OnPreRender()\r\n\t{{\r\n\t\tvar go = GameObject;\r\n\r\n\t\t// Recover the unshaken base. If a controller re-wrote the camera since our\r\n\t\t// last write, ITS value is the new base and our old offset is already gone --\r\n\t\t// only un-apply when the transform still equals exactly what we wrote.\r\n\t\tvar basePos = go.WorldPosition;\r\n\t\tvar baseRot = go.WorldRotation;\r\n\t\tif ( _hasApplied \u0026\u0026 basePos == _lastWrittenPos ) basePos -= _appliedOffset;\r\n\t\tif ( _hasApplied \u0026\u0026 baseRot == _lastWrittenRot ) baseRot = baseRot * _appliedRot.Inverse;\r\n\t\t_hasApplied = false;\r\n\r\n\t\tTrauma = MathX.Clamp( Trauma - DecayPerSecond * Time.Delta, 0f, 1f );\r\n\t\tfloat shake = Trauma * Trauma;\r\n\r\n\t\tif ( shake \u003C 0.0005f )\r\n\t\t{{\r\n\t\t\tgo.WorldPosition = basePos;\r\n\t\t\tgo.WorldRotation = baseRot;\r\n\t\t\treturn;\r\n\t\t}}\r\n\r\n\t\t// Smooth signed noise per axis (-1..1), decorrelated by row offset.\r\n\t\tfloat t = Time.Now * Frequency;\r\n\t\tfloat N( float row ) =\u003E (Sandbox.Utility.Noise.Perlin( t, row ) - 0.5f) * 2f;\r\n\r\n\t\t_appliedOffset = new Vector3( N( 0f ), N( 17f ), N( 31f ) ) * (MaxOffset * shake);\r\n\t\t_appliedRot = Rotation.From( N( 47f ) * MaxAngle * shake, N( 61f ) * MaxAngle * shake, N( 83f ) * MaxAngle * shake );\r\n\r\n\t\tgo.WorldPosition = basePos \u002B _appliedOffset;\r\n\t\tgo.WorldRotation = baseRot * _appliedRot;\r\n\t\t_lastWrittenPos = go.WorldPosition;\r\n\t\t_lastWrittenRot = go.WorldRotation;\r\n\t\t_hasApplied = true;\r\n\t}}\r\n\r\n\tprivate void RemoveAppliedShake()\r\n\t{{\r\n\t\tif ( !_hasApplied ) return;\r\n\t\tvar go = GameObject;\r\n\t\tif ( go.WorldPosition == _lastWrittenPos ) go.WorldPosition -= _appliedOffset;\r\n\t\tif ( go.WorldRotation == _lastWrittenRot ) go.WorldRotation = go.WorldRotation * _appliedRot.Inverse;\r\n\t\t_hasApplied = false;\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n// -----------------------------------------------------------------------------\r\n// add_flicker_light -- generate a light-flicker animator and (optionally) attach\r\n// it to an existing light GameObject. Presets: Candle, Fluorescent, Faulty,\r\n// Pulse, Lightning. Modulates Light.LightColor around a captured base color;\r\n// restores the base on disable.\r\n// -----------------------------------------------------------------------------\r\npublic class AddFlickerLightHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar ci = System.Globalization.CultureInfo.InvariantCulture;\r\n\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022FlickerLight\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tvar style = p.TryGetProperty( \u0022style\u0022, out var sv ) \u0026\u0026 !string.IsNullOrWhiteSpace( sv.GetString() )\r\n\t\t\t\t? sv.GetString() : \u0022Candle\u0022;\r\n\t\t\t// Validate against the generated enum so a typo can\u0027t emit uncompilable code.\r\n\t\t\tvar validStyles = new[] { \u0022Candle\u0022, \u0022Fluorescent\u0022, \u0022Faulty\u0022, \u0022Pulse\u0022, \u0022Lightning\u0022 };\r\n\t\t\tvar matched = validStyles.FirstOrDefault( s =\u003E s.Equals( style, StringComparison.OrdinalIgnoreCase ) );\r\n\t\t\tif ( matched == null )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022Unknown style \u0027{style}\u0027. Valid: {string.Join( \u0022, \u0022, validStyles )}\u0022 } );\r\n\t\t\tstyle = matched;\r\n\r\n\t\t\tfloat intensity = p.TryGetProperty( \u0022intensity\u0022, out var iv ) \u0026\u0026 iv.TryGetSingle( out var iff ) ? iff : 0.5f;\r\n\t\t\tfloat speed     = p.TryGetProperty( \u0022speed\u0022,     out var spv ) \u0026\u0026 spv.TryGetSingle( out var spf ) ? spf : 1f;\r\n\t\t\tintensity = intensity \u003C 0f ? 0f : intensity \u003E 1f ? 1f : intensity;\r\n\t\t\tif ( speed \u003C 0.05f ) speed = 0.05f;\r\n\r\n\t\t\tvar code = BuildCode( className, style, intensity, speed, ci );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\tobject placedOn = null; string note = null;\r\n\t\t\t// \u0060lightId\u0060 is the ergonomic param name; \u0060targetId\u0060 also accepted (sibling convention).\r\n\t\t\tstring target = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022lightId\u0022, out var lid ) \u0026\u0026 lid.ValueKind == JsonValueKind.String ) target = lid.GetString();\r\n\t\t\telse if ( p.TryGetProperty( \u0022targetId\u0022, out var tid ) \u0026\u0026 tid.ValueKind == JsonValueKind.String ) target = tid.GetString();\r\n\t\t\tif ( target != null )\r\n\t\t\t\tplacedOn = GameFeelHelpers.PlaceOnTarget( target, className, out note );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tstyle,\r\n\t\t\t\tintensity,\r\n\t\t\t\tspeed,\r\n\t\t\t\tplacedOn,\r\n\t\t\t\tnote,\r\n\t\t\t\tnextSteps = new[]\r\n\t\t\t\t{\r\n\t\t\t\t\t$\u0022trigger_hotload to compile {className} into the game assembly.\u0022,\r\n\t\t\t\t\tplacedOn != null\r\n\t\t\t\t\t\t? $\u0022{className} was attached to the target GameObject.\u0022\r\n\t\t\t\t\t\t: $\u0022Attach it to a GameObject that has a light component (PointLight / SpotLight / DirectionalLight): add_component_with_properties (component=\\\u0022{className}\\\u0022) after the hotload, or re-run with lightId.\u0022,\r\n\t\t\t\t\t\u0022The animator modulates the light\u0027s LightColor around its starting color and restores it on disable -- tune Style / Intensity / Speed with set_property.\u0022,\r\n\t\t\t\t\t\u0022Verify in play mode: start_play, then take_screenshot twice ~a second apart and compare the light\u0027s brightness (or capture_view for a scene-only frame).\u0022\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022add_flicker_light failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string BuildCode( string className, string style, float intensity, float speed, System.Globalization.CultureInfo ci )\r\n\t{\r\n\t\tstring it = intensity.ToString( ci ) \u002B \u0022f\u0022;\r\n\t\tstring sp = speed.ToString( ci ) \u002B \u0022f\u0022;\r\n\r\n\t\treturn $@\u0022using Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} -- flickers the light on this GameObject. Attach next to a\r\n/// PointLight / SpotLight / DirectionalLight; it modulates LightColor around\r\n/// the color it found on enable and restores it on disable.\r\n///\r\n/// Styles: Candle (soft organic sway), Fluorescent (mostly steady, random\r\n/// dips), Faulty (hard on/off cuts), Pulse (slow sine breathing), Lightning\r\n/// (dim baseline, rare bright flashes).\r\n/// \u003C/summary\u003E\r\npublic sealed class {className} : Component\r\n{{\r\n\tpublic enum FlickerStyle {{ Candle, Fluorescent, Faulty, Pulse, Lightning }}\r\n\r\n\t[Property] public FlickerStyle Style {{ get; set; }} = FlickerStyle.{style};\r\n\r\n\t/// Flicker depth: 0 = steady, 1 = full blackouts / double-bright flashes.\r\n\t[Property] public float Intensity {{ get; set; }} = {it};\r\n\r\n\t/// Speed multiplier for the whole pattern.\r\n\t[Property] public float Speed {{ get; set; }} = {sp};\r\n\r\n\tprivate Light _light;\r\n\tprivate Color _baseColor;\r\n\tprivate float _seed;\r\n\tprivate float _mult = 1f;\r\n\r\n\tprotected override void OnEnabled()\r\n\t{{\r\n\t\t_light = GetComponent\u003CLight\u003E();\r\n\t\tif ( _light == null )\r\n\t\t{{\r\n\t\t\tLog.Warning( $\u0022\u0022{className}: no Light component on {{GameObject.Name}} -- disabling.\u0022\u0022 );\r\n\t\t\tEnabled = false;\r\n\t\t\treturn;\r\n\t\t}}\r\n\t\t_baseColor = _light.LightColor;\r\n\t\t_seed = Game.Random.Float( 0f, 512f );\r\n\t}}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{{\r\n\t\tif ( _light != null ) _light.LightColor = _baseColor;\r\n\t}}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{{\r\n\t\tif ( _light == null ) return;\r\n\r\n\t\tfloat t = (Time.Now \u002B _seed) * Speed;\r\n\t\tfloat n = Sandbox.Utility.Noise.Perlin( t * 6f, _seed ); // smooth 0..1\r\n\r\n\t\tfloat target = Style switch\r\n\t\t{{\r\n\t\t\tFlickerStyle.Candle      =\u003E MathX.Lerp( 1f - Intensity * 0.6f, 1f, n ),\r\n\t\t\tFlickerStyle.Fluorescent =\u003E n \u003E 0.75f ? 1f - Intensity : 1f,\r\n\t\t\tFlickerStyle.Faulty      =\u003E Sandbox.Utility.Noise.Perlin( t * 14f, _seed ) \u003E 0.55f ? 1f : 1f - Intensity,\r\n\t\t\tFlickerStyle.Pulse       =\u003E MathX.Lerp( 1f - Intensity, 1f, 0.5f \u002B 0.5f * MathF.Sin( t * 4f ) ),\r\n\t\t\tFlickerStyle.Lightning   =\u003E n \u003E 0.92f ? 1f \u002B Intensity * 2f : 1f - Intensity * 0.85f,\r\n\t\t\t_ =\u003E 1f\r\n\t\t}};\r\n\r\n\t\t// Smooth toward the target so hard styles read as a light, not strobe noise.\r\n\t\t_mult = MathX.Lerp( _mult, target, MathX.Clamp( Time.Delta * 24f, 0f, 1f ) );\r\n\t\t_light.LightColor = _baseColor * _mult;\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n// -----------------------------------------------------------------------------\r\n// create_floating_combat_text -- rising/fading world-space text popups\r\n// (damage numbers, \u0022\u002B10 gold\u0022, pickup names). TextRenderer-based -- no Razor,\r\n// no WorldPanel, works with zero UI setup. The generated class IS the popup\r\n// behavior and carries a static Spawn() factory; nothing to place in the scene.\r\n// -----------------------------------------------------------------------------\r\npublic class CreateFloatingCombatTextHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar ci = System.Globalization.CultureInfo.InvariantCulture;\r\n\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022FloatingCombatText\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tfloat riseSpeed = p.TryGetProperty( \u0022riseSpeed\u0022, out var rv ) \u0026\u0026 rv.TryGetSingle( out var rf ) ? rf : 48f;\r\n\t\t\tfloat lifetime  = p.TryGetProperty( \u0022lifetime\u0022,  out var lv ) \u0026\u0026 lv.TryGetSingle( out var lf ) ? lf : 1.1f;\r\n\t\t\tfloat fontSize  = p.TryGetProperty( \u0022fontSize\u0022,  out var fv ) \u0026\u0026 fv.TryGetSingle( out var ff ) ? ff : 24f;\r\n\t\t\tif ( riseSpeed \u003C 0f )   riseSpeed = 0f;\r\n\t\t\tif ( lifetime  \u003C 0.1f ) lifetime  = 0.1f;\r\n\t\t\tif ( fontSize  \u003C 1f )   fontSize  = 1f;\r\n\r\n\t\t\tvar code = BuildCode( className, riseSpeed, lifetime, fontSize, ci );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\triseSpeed,\r\n\t\t\t\tlifetime,\r\n\t\t\t\tfontSize,\r\n\t\t\t\tnextSteps = new[]\r\n\t\t\t\t{\r\n\t\t\t\t\t$\u0022trigger_hotload to compile {className} into the game assembly.\u0022,\r\n\t\t\t\t\t$\u0022Nothing to place -- spawn popups from any game code: {className}.Spawn( hitPosition \u002B Vector3.Up * 32f, \\\u0022-25\\\u0022, Color.Red ) (optional 4th arg scales the text).\u0022,\r\n\t\t\t\t\t$\u0022Pairs with create_health_system: call {className}.Spawn from the damage path so every hit prints its number.\u0022,\r\n\t\t\t\t\t\u0022LOCAL-only: spawn inside an [Rpc.Broadcast] handler if every client should see the popup.\u0022,\r\n\t\t\t\t\t\u0022Verify in play mode: execute a spawn (e.g. via invoke_method on a test component), then take_screenshot -- the text rises and fades over Lifetime seconds.\u0022\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_floating_combat_text failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string BuildCode( string className, float riseSpeed, float lifetime, float fontSize, System.Globalization.CultureInfo ci )\r\n\t{\r\n\t\tstring rs = riseSpeed.ToString( ci ) \u002B \u0022f\u0022;\r\n\t\tstring lt = lifetime.ToString( ci ) \u002B \u0022f\u0022;\r\n\t\tstring fs = fontSize.ToString( ci ) \u002B \u0022f\u0022;\r\n\r\n\t\treturn $@\u0022using Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} -- a rising, fading world-space text popup (damage numbers,\r\n/// \u0022\u0022\u002B10 gold\u0022\u0022, pickup names). TextRenderer-based: no Razor, no panels.\r\n///\r\n/// Spawn from anywhere:\r\n///   {className}.Spawn( position, \u0022\u0022-25\u0022\u0022, Color.Red );\r\n///   {className}.Spawn( position, \u0022\u0022\u002B10 gold\u0022\u0022, Color.Yellow, 1.5f );\r\n///\r\n/// The popup billboards to the camera, rises, fades out, and destroys itself.\r\n/// LOCAL-only -- spawn inside an [Rpc.Broadcast] if all clients should see it.\r\n/// \u003C/summary\u003E\r\npublic sealed class {className} : Component\r\n{{\r\n\t/// World units risen per second.\r\n\t[Property] public float RiseSpeed {{ get; set; }} = {rs};\r\n\r\n\t/// Seconds until fully faded and destroyed.\r\n\t[Property] public float Lifetime {{ get; set; }} = {lt};\r\n\r\n\tprivate TextRenderer _text;\r\n\tprivate Color _startColor;\r\n\tprivate TimeSince _age;\r\n\r\n\t/// \u003Csummary\u003ESpawn a popup at a world position. Returns the popup GameObject.\u003C/summary\u003E\r\n\tpublic static GameObject Spawn( Vector3 position, string text, Color color, float size = 1f )\r\n\t{{\r\n\t\tvar go = new GameObject( true, \u0022\u0022FloatingText\u0022\u0022 );\r\n\t\tgo.WorldPosition = position;\r\n\r\n\t\tvar tr = go.AddComponent\u003CTextRenderer\u003E();\r\n\t\ttr.Text = text;\r\n\t\ttr.Color = color;\r\n\t\ttr.FontSize = {fs} * size;\r\n\r\n\t\tgo.AddComponent\u003C{className}\u003E();\r\n\t\treturn go;\r\n\t}}\r\n\r\n\tprotected override void OnStart()\r\n\t{{\r\n\t\t_age = 0f;\r\n\t\t_text = GetComponent\u003CTextRenderer\u003E();\r\n\t\tif ( _text != null ) _startColor = _text.Color;\r\n\t}}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{{\r\n\t\tvar go = GameObject;\r\n\t\tgo.WorldPosition \u002B= Vector3.Up * (RiseSpeed * Time.Delta);\r\n\r\n\t\t// Billboard: face the same way the camera faces, mirrored toward it.\r\n\t\tvar cam = Scene?.Camera;\r\n\t\tif ( cam != null )\r\n\t\t\tgo.WorldRotation = Rotation.LookAt( -cam.WorldRotation.Forward );\r\n\r\n\t\tif ( _text != null )\r\n\t\t\t_text.Color = _startColor.WithAlpha( _startColor.a * MathX.Clamp( 1f - _age / Lifetime, 0f, 1f ) );\r\n\r\n\t\tif ( _age \u003E= Lifetime )\r\n\t\t\tgo.Destroy();\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Shared placement helper for the game-feel handlers -- mirrors the standard\r\n/// scaffold placement (create_weighted_loot_table / create_event_director).\r\n/// \u003C/summary\u003E\r\ninternal static class GameFeelHelpers\r\n{\r\n\tpublic static object PlaceOnTarget( string targetId, string className, out string note )\r\n\t{\r\n\t\tnote = null;\r\n\t\tvar scene = SceneEditorSession.Active?.Scene;\r\n\t\tif ( scene == null ) { note = \u0022No active scene to place into.\u0022; return null; }\r\n\t\tif ( !Guid.TryParse( targetId, out var guid ) ) { note = \u0022Invalid targetId GUID.\u0022; return null; }\r\n\t\tvar go = scene.Directory.FindByGuid( guid );\r\n\t\tif ( go == null ) { note = $\u0022Target GameObject not found: {targetId}\u0022; return null; }\r\n\t\tvar typeDesc = Game.TypeLibrary.GetType( className );\r\n\t\tif ( typeDesc == null )\r\n\t\t{\r\n\t\t\tnote = $\u0022Generated {className}.cs but it is not in the TypeLibrary yet -- trigger_hotload, then add it with add_component_with_properties.\u0022;\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\ttry { go.Components.Create( typeDesc ); return ClaudeBridge.SerializeGo( go ); }\r\n\t\tcatch ( Exception ex ) { note = $\u0022Placement failed ({ex.Message}).\u0022; return null; }\r\n\t}\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/Mcp/BridgeGameObjectTools.cs","FileName":"BridgeGameObjectTools.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"// AUTO-GENERATED by scripts/emit-mcp-wrappers.mjs \u2014 DO NOT EDIT.\r\n// Regenerate: node scripts/extract-manifest.mjs \u0026\u0026 node scripts/emit-mcp-wrappers.mjs\r\n// Source of truth: sbox-mcp-server/src/tools/ (zod schemas) \u2192 scripts/tools-manifest.json\r\n\r\nusing System.Text.Json.Nodes;\r\nusing System.Threading.Tasks;\r\nusing Editor.Mcp;\r\n\r\n/// \u003Csummary\u003E\r\n/// GameObject lifecycle, hierarchy, transforms, tags, selection, and bulk layout (align,\r\n/// distribute, scatter, snap-to-ground, grid duplicate) in the open scene. Objects are referenced\r\n/// by GUID from get_scene_hierarchy or find_objects.\r\n/// \u003C/summary\u003E\r\n[McpToolset( \u0022bridge_gameobject\u0022, \u0022GameObject lifecycle, hierarchy, transforms, tags, selection, and bulk layout (align, distribute, scatter, snap-to-ground, grid duplicate) in the open scene. Objects are referenced by GUID from get_scene_hierarchy or find_objects.\u0022 )]\r\npublic static class BridgeGameObjectTools\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Align several GameObjects on one axis so they share a coordinate. mode = first (match the first\r\n\t/// object), min, max, or average; defaults to first. Returns { aligned, axis, mode, target } \u2014\r\n\t/// aligned is the object count and target the shared coordinate; verify positions with\r\n\t/// get_scene_hierarchy or a screenshot.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022ids\u0022\u003EGUIDs of the GameObjects to align (\u0026gt;= 2).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022axis\u0022\u003EAxis to align on. One of: x | y | z.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022mode\u0022\u003ETarget coordinate to align to (default first). One of: first | min | max | average.\u003C/param\u003E\r\n\t[McpTool( \u0022align_objects\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E AlignObjects( string[] ids, string axis, string mode = null )\r\n\t\t=\u003E McpGate.Run( \u0022align_objects\u0022, McpGate.Args( ( \u0022ids\u0022, ids ), ( \u0022axis\u0022, axis ), ( \u0022mode\u0022, mode ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Commit a dry-run plan returned by place_along_path, grid_duplicate, or scatter_props. Plans are\r\n\t/// scene-scoped, capped, and expire after 10 minutes. Success consumes the plan; a complete\r\n\t/// rollback restores it for retry. The stored transforms are applied without rerolling randomness\r\n\t/// or repeating ground traces. Creation rolls back on failure; grid commits reject a\r\n\t/// changed/missing source before creating anything. Returns slot-to-GUID receipts.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022planId\u0022\u003EPlan id returned by a placement tool with dryRun:true.\u003C/param\u003E\r\n\t[McpTool( \u0022commit_placement_plan\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CommitPlacementPlan( string planId )\r\n\t\t=\u003E McpGate.Run( \u0022commit_placement_plan\u0022, McpGate.Args( ( \u0022planId\u0022, planId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Create a new GameObject in the active scene. Returns its GUID for future reference.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EDisplay name (e.g. \u0027Player\u0027, \u0027Enemy Spawn Point\u0027). Defaults to \u0027New Object\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022position\u0022\u003EWorld position. As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022rotation\u0022\u003EWorld rotation. As \u0022pitch,yaw,roll\u0022 degrees.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022scale\u0022\u003EUniform scale (number) or per-axis scale \u2014 object {x,y,z} or comma string \u0022x,y,z\u0022. As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022parent\u0022\u003EGUID of parent GameObject. Omit for scene root.\u003C/param\u003E\r\n\t[McpTool( \u0022create_gameobject\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateGameobject( string name = null, string position = null, string rotation = null, string scale = null, string parent = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_gameobject\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022position\u0022, position ), ( \u0022rotation\u0022, rotation ), ( \u0022scale\u0022, scale ), ( \u0022parent\u0022, parent ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Delete a GameObject from the active scene by its GUID.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject to delete.\u003C/param\u003E\r\n\t[McpTool( \u0022delete_gameobject\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E DeleteGameobject( string id )\r\n\t\t=\u003E McpGate.Run( \u0022delete_gameobject\u0022, McpGate.Args( ( \u0022id\u0022, id ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Evenly space GameObjects along an axis between the lowest and highest (keeps the two ends fixed,\r\n\t/// spreads the rest evenly). Returns { distributed, axis, from, to } \u2014 the object count and the\r\n\t/// fixed end coordinates the rest were spread between.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022ids\u0022\u003EGUIDs of the GameObjects to distribute (\u0026gt;= 3).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022axis\u0022\u003EAxis to distribute along. One of: x | y | z.\u003C/param\u003E\r\n\t[McpTool( \u0022distribute_objects\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E DistributeObjects( string[] ids, string axis )\r\n\t\t=\u003E McpGate.Run( \u0022distribute_objects\u0022, McpGate.Args( ( \u0022ids\u0022, ids ), ( \u0022axis\u0022, axis ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Clone a GameObject with all its components. Returns { duplicated, original, gameObject } \u2014\r\n\t/// gameObject.id is the clone\u0027s new GUID; pass it to set_transform / add_component_with_properties.\r\n\t/// If offset is omitted the clone lands exactly on top of the original.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject to duplicate.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003ENew name for the clone.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022offset\u0022\u003EPosition offset from original so the clone doesn\u0027t overlap. As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t[McpTool( \u0022duplicate_gameobject\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E DuplicateGameobject( string id, string name = null, string offset = null )\r\n\t\t=\u003E McpGate.Run( \u0022duplicate_gameobject\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022name\u0022, name ), ( \u0022offset\u0022, offset ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Query the scene for GameObjects by name (case-insensitive substring), component type name,\r\n\t/// and/or tag \u2014 combine filters (AND). Returns {id,name} for matches (limit default 50, max 500).\r\n\t/// Read-only; works during play. Use it to get GUIDs to feed into\r\n\t/// align/distribute/set_tint/group/delete/etc.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EName substring (case-insensitive).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022component\u0022\u003EComponent type name, e.g. \u0027PointLight\u0027, \u0027SkinnedModelRenderer\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022tag\u0022\u003ETag the object must have.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022limit\u0022\u003EMax results (default 50, max 500).\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022find_objects\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E FindObjects( string name = null, string component = null, string tag = null, int? limit = null )\r\n\t\t=\u003E McpGate.Run( \u0022find_objects\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022component\u0022, component ), ( \u0022tag\u0022, tag ), ( \u0022limit\u0022, limit ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Find GameObjects within a world-space radius of exactly one explicit position or originId,\r\n\t/// sorted nearest first. Optional name/component/tag filters are applied before the capped result,\r\n\t/// and the Scene root is excluded. Returns pivot-distance results plus\r\n\t/// requestedRadius/radiusClamped and total/showing/truncated/scanned; it deliberately does not\r\n\t/// pretend render or collider overlap is pivot distance. Read-only and play-aware.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022position\u0022\u003EWorld-space search center; use instead of originId. As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022originId\u0022\u003EGameObject GUID whose world position is the center.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022radius\u0022\u003ESearch radius in world units (default 256, max 1,000,000).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022limit\u0022\u003EMaximum results (default 50).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003ECase-insensitive GameObject name substring.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022component\u0022\u003ERequired component type name.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022tag\u0022\u003ERequired GameObject tag.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022includeOrigin\u0022\u003EInclude originId itself (default false).\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022find_objects_near\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E FindObjectsNear( string position = null, string originId = null, double? radius = null, int? limit = null, string name = null, string component = null, string tag = null, bool? includeOrigin = null )\r\n\t\t=\u003E McpGate.Run( \u0022find_objects_near\u0022, McpGate.Args( ( \u0022position\u0022, position ), ( \u0022originId\u0022, originId ), ( \u0022radius\u0022, radius ), ( \u0022limit\u0022, limit ), ( \u0022name\u0022, name ), ( \u0022component\u0022, component ), ( \u0022tag\u0022, tag ), ( \u0022includeOrigin\u0022, includeOrigin ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Highlight a GameObject by selecting it in the editor. NOTE: s\u0026amp;box exposes no dedicated focus\r\n\t/// API, so this only sets the selection \u2014 it does NOT move any camera (returns { focused, id, note\r\n\t/// } saying so). To actually point the viewport at an object use frame_camera; to aim a screenshot\r\n\t/// use screenshot_from.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject to focus.\u003C/param\u003E\r\n\t[McpTool( \u0022focus_object\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E FocusObject( string id )\r\n\t\t=\u003E McpGate.Run( \u0022focus_object\u0022, McpGate.Args( ( \u0022id\u0022, id ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Get provenance-rich world bounds for a GameObject. Preserves legacy top-level\r\n\t/// center/size/extents/mins/maxs/radius/position/empty for compatibility, and adds render plus\r\n\t/// independent physics and solidPhysics aggregates. Collider outputs include trigger policy, capped\r\n\t/// contributor GameObject IDs and component type names, unsupported counts, and the exact API\r\n\t/// source. Read-only and play-aware.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject to measure.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022get_bounds\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E GetBounds( string id )\r\n\t\t=\u003E McpGate.Run( \u0022get_bounds\u0022, McpGate.Args( ( \u0022id\u0022, id ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Get the scene tree \u2014 GameObjects with their names, GUIDs, components, and parent/child\r\n\t/// relationships. Pair maxDepth with rootId to drill into a subtree without paying for the whole\r\n\t/// scene.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022maxDepth\u0022\u003EMaximum recursion depth. Defaults to 10. Use 1 or 2 for cheap top-level overviews.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022rootId\u0022\u003EOptional GUID of a GameObject to start traversal from. Omit to walk from the scene roots.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022get_scene_hierarchy\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E GetSceneHierarchy( int? maxDepth = null, string rootId = null )\r\n\t\t=\u003E McpGate.Run( \u0022get_scene_hierarchy\u0022, McpGate.Args( ( \u0022maxDepth\u0022, maxDepth ), ( \u0022rootId\u0022, rootId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Get the GameObjects currently selected by the user in the s\u0026amp;box editor. Returns { count,\r\n\t/// selected } where each entry is a serialized GameObject (id, name, enabled, position, rotation,\r\n\t/// scale, components, childCount) \u2014 use the ids with set_transform, add_component_with_properties,\r\n\t/// etc. Handy for \u0027do X to what I have selected\u0027 requests.\r\n\t/// \u003C/summary\u003E\r\n\t[McpTool.ReadOnly( \u0022get_selected_objects\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E GetSelectedObjects()\r\n\t\t=\u003E McpGate.Run( \u0022get_selected_objects\u0022, McpGate.Args() );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Read the tags currently on a GameObject. (Pair with set_tags to add/remove/clear, and\r\n\t/// find_objects to query by tag.).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022get_tags\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E GetTags( string id )\r\n\t\t=\u003E McpGate.Run( \u0022get_tags\u0022, McpGate.Args( ( \u0022id\u0022, id ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Clone a GameObject into an X/Y/Z grid. Existing calls mutate immediately and keep the legacy\r\n\t/// result. Use dryRun:true to preview exact capped transforms and receive a planId;\r\n\t/// commit_placement_plan then clones atomically and rejects the commit if the source transform or\r\n\t/// parent changed after preview.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject to clone.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022countX\u0022\u003ECopies along X (default 1).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022countY\u0022\u003ECopies along Y (default 1).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022countZ\u0022\u003ECopies along Z (default 1).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022spacing\u0022\u003ESpacing between copies per axis (default 100,100,100). As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022dryRun\u0022\u003EPreview only: return deterministic transforms and planId without cloning.\u003C/param\u003E\r\n\t[McpTool( \u0022grid_duplicate\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E GridDuplicate( string id, int? countX = null, int? countY = null, int? countZ = null, string spacing = null, bool? dryRun = null )\r\n\t\t=\u003E McpGate.Run( \u0022grid_duplicate\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022countX\u0022, countX ), ( \u0022countY\u0022, countY ), ( \u0022countZ\u0022, countZ ), ( \u0022spacing\u0022, spacing ), ( \u0022dryRun\u0022, dryRun ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Parent a set of GameObjects under a new empty group object (placed at their centroid) \u2014 tidies\r\n\t/// the hierarchy and lets you move/rotate them together.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022ids\u0022\u003EGUIDs of the GameObjects to group.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EName for the group object (default \u0027Group\u0027).\u003C/param\u003E\r\n\t[McpTool( \u0022group_objects\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E GroupObjects( string[] ids, string name = null )\r\n\t\t=\u003E McpGate.Run( \u0022group_objects\u0022, McpGate.Args( ( \u0022ids\u0022, ids ), ( \u0022name\u0022, name ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Measure the distance between two points or two GameObjects. Provide a/b as {x,y,z} or idA/idB as\r\n\t/// GUIDs. Returns straight-line distance, horizontal (ground) distance, and the delta vector.\r\n\t/// Read-only (works during play).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022a\u0022\u003EFirst point {x,y,z}. As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022b\u0022\u003ESecond point {x,y,z}. As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022idA\u0022\u003EFirst GameObject GUID (overrides a).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022idB\u0022\u003ESecond GameObject GUID (overrides b).\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022measure_distance\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E MeasureDistance( string a = null, string b = null, string idA = null, string idB = null )\r\n\t\t=\u003E McpGate.Run( \u0022measure_distance\u0022, McpGate.Args( ( \u0022a\u0022, a ), ( \u0022b\u0022, b ), ( \u0022idA\u0022, idA ), ( \u0022idB\u0022, idB ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Add natural variation to existing objects: random yaw and/or random uniform scale within a\r\n\t/// range. Great for breaking up repetition in placed foliage/rocks/crates. Seeded \u2014 the same seed\r\n\t/// reproduces the same layout. Returns { randomized, seed } (the count of objects changed); scale\r\n\t/// only varies when scaleMax \u0026gt; scaleMin.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022ids\u0022\u003EGUIDs of the GameObjects to randomize.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022randomYaw\u0022\u003ERandomize Z rotation (default true).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022scaleMin\u0022\u003EMin uniform scale (default 1).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022scaleMax\u0022\u003EMax uniform scale (default 1; set \u0026gt;min to vary).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022seed\u0022\u003EPRNG seed (default 1).\u003C/param\u003E\r\n\t[McpTool( \u0022randomize_transforms\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E RandomizeTransforms( string[] ids, bool? randomYaw = null, double? scaleMin = null, double? scaleMax = null, int? seed = null )\r\n\t\t=\u003E McpGate.Run( \u0022randomize_transforms\u0022, McpGate.Args( ( \u0022ids\u0022, ids ), ( \u0022randomYaw\u0022, randomYaw ), ( \u0022scaleMin\u0022, scaleMin ), ( \u0022scaleMax\u0022, scaleMax ), ( \u0022seed\u0022, seed ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Change the display name of a GameObject identified by its GUID (the GUID itself never changes,\r\n\t/// so existing references stay valid). Returns { renamed, id, oldName, newName }. Name-based\r\n\t/// lookups (e.g. find_objects) will see the new name immediately.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003ENew display name.\u003C/param\u003E\r\n\t[McpTool( \u0022rename_gameobject\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E RenameGameobject( string id, string name )\r\n\t\t=\u003E McpGate.Run( \u0022rename_gameobject\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022name\u0022, name ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Swap the model on one object (id) or many (ids) \u2014 e.g. retheme a row of props in one call.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022model\u0022\u003ENew model path, e.g. \u0027models/dev/sphere.vmdl\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003ESingle GameObject GUID.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022ids\u0022\u003EMultiple GameObject GUIDs.\u003C/param\u003E\r\n\t[McpTool( \u0022replace_model\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E ReplaceModel( string model, string id = null, string[] ids = null )\r\n\t\t=\u003E McpGate.Run( \u0022replace_model\u0022, McpGate.Args( ( \u0022model\u0022, model ), ( \u0022id\u0022, id ), ( \u0022ids\u0022, ids ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Scatter seeded model copies inside a radius. Existing calls mutate immediately and keep the\r\n\t/// legacy { scattered, groupId, seed } result. Use dryRun:true to resolve random transforms and\r\n\t/// ground traces once, returning per-slot transforms, ground status, warnings, model bounds, and\r\n\t/// planId; commit_placement_plan creates exactly that preview with rollback on failure.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022model\u0022\u003EModel path to scatter, e.g. \u0027models/dev/box.vmdl\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022center\u0022\u003ECentre of the scatter area (default origin). As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022radius\u0022\u003EScatter radius in units (default 256).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022count\u0022\u003EHow many to place (default 10, max 300).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022randomYaw\u0022\u003ERandomly rotate each around Z (default true).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022snapToGround\u0022\u003ERaycast each onto the surface below (default true).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022scaleMin\u0022\u003EMin uniform scale (default 1).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022scaleMax\u0022\u003EMax uniform scale (default 1; set \u0026gt;min for size variation).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022tint\u0022\u003ETint applied to every copy \u2014 object {r,g,b,a} or comma string \u0022r,g,b,a\u0022. As \u0022r,g,b[,a]\u0022 (0-1 floats).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022seed\u0022\u003EPRNG seed for a reproducible layout (default 1).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022group\u0022\u003EParent all copies under one group object (default true).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EBase name for the props/group (default \u0027Prop\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022dryRun\u0022\u003EPreview only: return deterministic transforms and planId without creating props.\u003C/param\u003E\r\n\t[McpTool( \u0022scatter_props\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E ScatterProps( string model, string center = null, double? radius = null, int? count = null, bool? randomYaw = null, bool? snapToGround = null, double? scaleMin = null, double? scaleMax = null, string tint = null, int? seed = null, bool? group = null, string name = null, bool? dryRun = null )\r\n\t\t=\u003E McpGate.Run( \u0022scatter_props\u0022, McpGate.Args( ( \u0022model\u0022, model ), ( \u0022center\u0022, center ), ( \u0022radius\u0022, radius ), ( \u0022count\u0022, count ), ( \u0022randomYaw\u0022, randomYaw ), ( \u0022snapToGround\u0022, snapToGround ), ( \u0022scaleMin\u0022, scaleMin ), ( \u0022scaleMax\u0022, scaleMax ), ( \u0022tint\u0022, tint ), ( \u0022seed\u0022, seed ), ( \u0022group\u0022, group ), ( \u0022name\u0022, name ), ( \u0022dryRun\u0022, dryRun ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Select a GameObject in the editor (highlights it in the hierarchy and scene view). Replaces the\r\n\t/// current selection unless addToSelection=true. Returns { selected, id }; confirm the result with\r\n\t/// get_selected_objects.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject to select.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022addToSelection\u0022\u003EIf true, adds to current selection instead of replacing it.\u003C/param\u003E\r\n\t[McpTool( \u0022select_object\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SelectObject( string id, bool? addToSelection = null )\r\n\t\t=\u003E McpGate.Run( \u0022select_object\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022addToSelection\u0022, addToSelection ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Enable or disable a GameObject (disabled objects are invisible and inactive, including their\r\n\t/// components and children). Returns { id, enabled } confirming the new state.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022enabled\u0022\u003Etrue to enable, false to disable.\u003C/param\u003E\r\n\t[McpTool( \u0022set_enabled\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SetEnabled( string id, bool enabled )\r\n\t\t=\u003E McpGate.Run( \u0022set_enabled\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022enabled\u0022, enabled ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Reparent a GameObject. Set parentId to null or omit to move to scene root.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject to reparent.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022parentId\u0022\u003EGUID of the new parent. Null or omitted = scene root.\u003C/param\u003E\r\n\t[McpTool( \u0022set_parent\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SetParent( string id, string parentId = null )\r\n\t\t=\u003E McpGate.Run( \u0022set_parent\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022parentId\u0022, parentId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Add, remove, and/or clear gameplay tags on one object (id) or many (ids). Tags drive collision\r\n\t/// groups, queries, and triggers.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003ESingle GameObject GUID.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022ids\u0022\u003EMultiple GameObject GUIDs.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022add\u0022\u003ETags to add.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022remove\u0022\u003ETags to remove.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022clear\u0022\u003ERemove all existing tags first.\u003C/param\u003E\r\n\t[McpTool( \u0022set_tags\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SetTags( string id = null, string[] ids = null, string[] add = null, string[] remove = null, bool? clear = null )\r\n\t\t=\u003E McpGate.Run( \u0022set_tags\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022ids\u0022, ids ), ( \u0022add\u0022, add ), ( \u0022remove\u0022, remove ), ( \u0022clear\u0022, clear ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Set the renderer tint colour on one object (id) or many (ids) at once. Works on any\r\n\t/// ModelRenderer/SkinnedModelRenderer. Pass the colour as \u0022tint\u0022 (or its alias \u0022color\u0022); each\r\n\t/// accepts an object {r,g,b,a} OR a comma string \u0022r,g,b,a\u0022.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003ESingle GameObject GUID.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022ids\u0022\u003EMultiple GameObject GUIDs.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022tint\u0022\u003ETint colour to apply (object or comma string). As \u0022r,g,b[,a]\u0022 (0-1 floats).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022color\u0022\u003EAlias for \u0022tint\u0022 (object or comma string). As \u0022r,g,b[,a]\u0022 (0-1 floats).\u003C/param\u003E\r\n\t[McpTool( \u0022set_tint\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SetTint( string id = null, string[] ids = null, string tint = null, string color = null )\r\n\t\t=\u003E McpGate.Run( \u0022set_tint\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022ids\u0022, ids ), ( \u0022tint\u0022, tint ), ( \u0022color\u0022, color ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Atomically set position, rotation, and/or scale on a GameObject. All supplied values are parsed\r\n\t/// before mutation; values apply in world space by default. Prefer space=\u0027local\u0027 or space=\u0027world\u0027;\r\n\t/// local remains a legacy alias. Returns legacy { transformed, gameObject } plus before/after\r\n\t/// transform and bounds receipts.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022position\u0022\u003ENew position. As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022rotation\u0022\u003ENew rotation. As \u0022pitch,yaw,roll\u0022 degrees.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022scale\u0022\u003ENew scale \u2014 uniform number, per-axis object {x,y,z}, or comma string \u0022x,y,z\u0022. As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022local\u0022\u003ELegacy alias: true selects local space and false selects world space.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022space\u0022\u003EExplicit transform space. If supplied with local, both values must agree. One of: world | local.\u003C/param\u003E\r\n\t[McpTool( \u0022set_transform\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SetTransform( string id, string position = null, string rotation = null, string scale = null, bool? local = null, string space = null )\r\n\t\t=\u003E McpGate.Run( \u0022set_transform\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022position\u0022, position ), ( \u0022rotation\u0022, rotation ), ( \u0022scale\u0022, scale ), ( \u0022local\u0022, local ), ( \u0022space\u0022, space ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Drop a GameObject straight down onto the surface below it (physics raycast). Works best on\r\n\t/// collider-less props (an object with its own collider may self-hit). Optional offset lifts it off\r\n\t/// the surface. Returns { snapped, groundZ, gameObject } with the object\u0027s updated transform \u2014 or {\r\n\t/// snapped: false, reason } (not an error) when no ground was hit below.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject to snap.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022offset\u0022\u003EHeight above the surface to place it (default 0).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022startHeight\u0022\u003EHow far above the object to start the trace (default 2000).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022maxDistance\u0022\u003EMax trace distance downward (default 20000).\u003C/param\u003E\r\n\t[McpTool( \u0022snap_to_ground\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SnapToGround( string id, double? offset = null, double? startHeight = null, double? maxDistance = null )\r\n\t\t=\u003E McpGate.Run( \u0022snap_to_ground\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022offset\u0022, offset ), ( \u0022startHeight\u0022, startHeight ), ( \u0022maxDistance\u0022, maxDistance ) ) );\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/Mcp/BridgeMovieMakerTools.cs","FileName":"BridgeMovieMakerTools.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"// AUTO-GENERATED by scripts/emit-mcp-wrappers.mjs \u2014 DO NOT EDIT.\r\n// Regenerate: node scripts/extract-manifest.mjs \u0026\u0026 node scripts/emit-mcp-wrappers.mjs\r\n// Source of truth: sbox-mcp-server/src/tools/ (zod schemas) \u2192 scripts/tools-manifest.json\r\n\r\nusing System.Text.Json.Nodes;\r\nusing System.Threading.Tasks;\r\nusing Editor.Mcp;\r\n\r\n/// \u003Csummary\u003E\r\n/// Wire and control Sandbox.MovieMaker cutscene playback: list .movie clips, add MoviePlayer\r\n/// components, play and stop clips.\r\n/// \u003C/summary\u003E\r\n[McpToolset( \u0022bridge_moviemaker\u0022, \u0022Wire and control Sandbox.MovieMaker cutscene playback: list .movie clips, add MoviePlayer components, play and stop clips.\u0022 )]\r\npublic static class BridgeMovieMakerTools\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Add a Sandbox.MovieMaker.MoviePlayer component and optionally wire a .movie resource into it \u2014\r\n\t/// the cutscene playback primitive. Creates a new \u0027Movie Player\u0027 GameObject when no id is given.\r\n\t/// Set playOnStart to begin playback the moment play mode starts (intro cinematics), or leave it\r\n\t/// and trigger via play_movie (scripted cutscenes \u2014 call it from a trigger zone or dialogue beat).\r\n\t/// isLooping \u002B timeScale map straight onto the component. Movies must already exist as .movie\r\n\t/// assets (list_movies; author in the Movie Maker dock). Scene-mutating \u2014 refused during play mode.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGameObject GUID to attach to. Omit to create a new \u0027Movie Player\u0027 object.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022moviePath\u0022\u003EAsset-relative path of the .movie resource to wire (see list_movies).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022isLooping\u0022\u003ELoop playback.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022timeScale\u0022\u003EPlayback speed multiplier (1 = normal).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022createTargets\u0022\u003ELet the player create missing track-target objects on play.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022playOnStart\u0022\u003EBegin playing as soon as play mode starts (intro cinematic).\u003C/param\u003E\r\n\t[McpTool( \u0022add_movie_player\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E AddMoviePlayer( string id = null, string moviePath = null, bool? isLooping = null, double? timeScale = null, bool? createTargets = null, bool? playOnStart = null )\r\n\t\t=\u003E McpGate.Run( \u0022add_movie_player\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022moviePath\u0022, moviePath ), ( \u0022isLooping\u0022, isLooping ), ( \u0022timeScale\u0022, timeScale ), ( \u0022createTargets\u0022, createTargets ), ( \u0022playOnStart\u0022, playOnStart ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Author a MovieMaker .movie cutscene clip from a declarative shot list \u2014 EDIT MODE ONLY, no Movie\r\n\t/// Maker dock, no play mode, no real-time waiting (a 30s clip bakes in one call, typically \u0026lt;1s).\r\n\t/// Builds a hold\u002Bblend keyframe timeline from the shots (smoothstep ease by default), steps a\r\n\t/// camera through it, and hand-pumps MovieRecorder Advance/Capture per synthetic frame, then saves\r\n\t/// Assets/\u0026lt;folder\u0026gt;/\u0026lt;clipName\u0026gt;.movie (registered \u002B compiled; errors if the file exists \u2014\r\n\t/// the scene itself is NOT saved). Returns { authored, path, name, durationSeconds, frames,\r\n\t/// sampleRate, shots, tracks, bakeMs, compiled, loadable, camera, nextSteps }. Camera: omit\r\n\t/// cameraId for a temp camera (destroyed after the bake \u2014 play back with add_movie_player\r\n\t/// createTargets:true so the missing target is recreated), or pass cameraId of an existing camera\r\n\t/// GameObject (transform \u002B FOV restored EXACTLY afterwards; the clip then animates THAT object on\r\n\t/// playback). fovDegrees is baked for real (the clip carries a FieldOfView track). Authored clips\r\n\t/// animate ONLY the camera the bake moves \u2014 other scene objects don\u0027t move in edit mode (that\u0027s\r\n\t/// what record_gameplay_clip is for). Total timeline capped at 120s, max 32 shots. Errors during\r\n\t/// play mode (stop_play first). Verify with list_movies; play via add_movie_player \u002B play_movie in\r\n\t/// play mode.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022shots\u0022\u003EThe shot list in order (1-32 shots). Timeline = hold\u2080, then blend\u1D62 \u002B hold\u1D62 per following shot. JSON array.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022clipName\u0022\u003EAsset name without extension (default authored_\u0026lt;UTC timestamp\u0026gt;; sanitized to [A-Za-z0-9_-]).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022folder\u0022\u003EAssets subfolder to save into (default \u0022movies\u0022).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022sampleRate\u0022\u003EClip samples per second (default 30, clamped 1-120).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022cameraId\u0022\u003EGUID of an existing camera GameObject (must have a CameraComponent) to bake through \u2014 restored EXACTLY afterwards, and playback then animates that object. Omit for a temp camera that is destroyed after the bake.\u003C/param\u003E\r\n\t[McpTool( \u0022author_movie_clip\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E AuthorMovieClip( JsonNode shots, string clipName = null, string folder = null, int? sampleRate = null, string cameraId = null )\r\n\t\t=\u003E McpGate.Run( \u0022author_movie_clip\u0022, McpGate.Args( ( \u0022shots\u0022, shots ), ( \u0022clipName\u0022, clipName ), ( \u0022folder\u0022, folder ), ( \u0022sampleRate\u0022, sampleRate ), ( \u0022cameraId\u0022, cameraId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a sealed killcam Component: a rolling-buffer MovieRecorder keeps ONLY the last\r\n\t/// MaxBufferSeconds of a target\u0027s gameplay (BufferDuration verified live: the compiled clip\u0027s\r\n\t/// Duration equals the buffer, re-based to 0), and TriggerReplay() plays that history back through\r\n\t/// a MoviePlayer while the main camera chase-follows the target (Scene.Camera takeover in\r\n\t/// OnPreRender, restored exactly afterwards; static OnReplayFinished event \u002B IsReplaying flag).\r\n\t/// Sandbox-safe: live-verified that GAME code can construct and drive MovieRecorder/MoviePlayer at\r\n\t/// runtime, and killcams/replays are the official recording-api use case \u2014 this is the real\r\n\t/// MovieMaker path, not a transform-history approximation. The replay REWINDS THE LIVE TARGET\r\n\t/// through its recorded past (classic killcam \u2014 the target is dead/inactive when it runs; disable a\r\n\t/// still-alive controller for the duration). wholeScene:true makes the generated component default\r\n\t/// to MovieRecorderOptions.Default (all renderers/cameras/sound points/particles \u2014 the replay\r\n\t/// rewinds everything, killer included; heavy in dense scenes), and it stays toggleable\r\n\t/// per-instance via the RecordWholeScene property. Returns { created, path, className,\r\n\t/// bufferSeconds, sampleRate, cameraDistance, cameraHeight, nextSteps }. Then: trigger_hotload \u2192\r\n\t/// attach to a MANAGER object \u2192 set_component_reference Target to the player \u2192 arm via WatchOnStart\r\n\t/// or StartWatching() from spawn code \u2192 call TriggerReplay() from death code (pairs with\r\n\t/// create_health_system). LOCAL/visual-only \u2014 wrap in an [Rpc.Broadcast] for all clients. Refuses\r\n\t/// if the file already exists.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EComponent class/file name (default \u0022Killcam\u0022).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003EProject folder for the .cs file (default \u0022Code\u0022).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022bufferSeconds\u0022\u003ERolling-buffer length in seconds \u2014 the replay shows at most this much history (default 10, clamped 2-120).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022sampleRate\u0022\u003ERecorder samples per second (default 30, clamped 1-120).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022cameraDistance\u0022\u003EReplay chase-camera distance behind the target (default 150, clamped 10-2000).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022cameraHeight\u0022\u003EReplay chase-camera height above the target (default 60, clamped 0-2000).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022wholeScene\u0022\u003EGenerated default for RecordWholeScene: true = buffer the WHOLE scene via MovieRecorderOptions.Default (replay rewinds everything; heavy in dense scenes), false = only the Target hierarchy (default).\u003C/param\u003E\r\n\t[McpTool( \u0022create_killcam\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateKillcam( string name = null, string directory = null, double? bufferSeconds = null, int? sampleRate = null, double? cameraDistance = null, double? cameraHeight = null, bool? wholeScene = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_killcam\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022bufferSeconds\u0022, bufferSeconds ), ( \u0022sampleRate\u0022, sampleRate ), ( \u0022cameraDistance\u0022, cameraDistance ), ( \u0022cameraHeight\u0022, cameraHeight ), ( \u0022wholeScene\u0022, wholeScene ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Poll the gameplay recording job. While recording returns { recording:true, jobId, elapsedSeconds\r\n\t/// (clip-timeline seconds), framesWithData, maxSeconds, sampleRate, capture, trackedObjectCount }\r\n\t/// (trackedObjectCount is -1 for whole-scene capture). After an auto-stop (maxSeconds cap / play\r\n\t/// mode ended) returns { stopped:true, pendingSave:true, reason } \u2014 the clip is in memory awaiting\r\n\t/// stop_gameplay_recording. After a save/discard returns that last summary (assetPath etc.).\r\n\t/// Read-only; works during play. No params.\r\n\t/// \u003C/summary\u003E\r\n\t[McpTool( \u0022gameplay_recording_status\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E GameplayRecordingStatus()\r\n\t\t=\u003E McpGate.Run( \u0022gameplay_recording_status\u0022, McpGate.Args() );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// List the project\u0027s .movie resources (Sandbox.MovieMaker clips authored in the editor\u0027s Movie\r\n\t/// Maker dock: Window \u2192 Movie Maker). Scans the ENTIRE Assets folder recursively and returns every\r\n\t/// .movie found \u2014 no limit or paging. Returns { count, movies, note } where each movie has { path\r\n\t/// (asset-relative \u2014 the form add_movie_player/play_movie expect), name, loadable (resolves via\r\n\t/// ResourceLibrary), hasCompiledClip }. Start here before add_movie_player / play_movie \u2014 if the\r\n\t/// list is empty, the movie has to be authored in the dock first (the bridge plays movies; it\r\n\t/// doesn\u0027t author keyframes).\r\n\t/// \u003C/summary\u003E\r\n\t[McpTool.ReadOnly( \u0022list_movies\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E ListMovies()\r\n\t\t=\u003E McpGate.Run( \u0022list_movies\u0022, McpGate.Args() );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Start MoviePlayer playback. Targets the MoviePlayer on the given GameObject, or the first\r\n\t/// MoviePlayer in the scene when id is omitted. Pass moviePath to load-and-play a different .movie\r\n\t/// on the same player; positionSeconds seeks before playing; isLooping/timeScale apply immediately.\r\n\t/// Clips genuinely advance in PLAY MODE (start_play first, then verify with capture_view) \u2014 in edit\r\n\t/// mode this only sets state, which the response calls out. NOT scene-mutating, so it works during\r\n\t/// play mode.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGameObject GUID holding the MoviePlayer. Omit to use the first MoviePlayer in the scene.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022moviePath\u0022\u003EAsset-relative .movie path to load and play (otherwise plays the wired Resource).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022positionSeconds\u0022\u003ESeek to this time (seconds) before playing.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022timeScale\u0022\u003EPlayback speed multiplier (1 = normal).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022isLooping\u0022\u003ELoop playback.\u003C/param\u003E\r\n\t[McpTool( \u0022play_movie\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E PlayMovie( string id = null, string moviePath = null, double? positionSeconds = null, double? timeScale = null, bool? isLooping = null )\r\n\t\t=\u003E McpGate.Run( \u0022play_movie\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022moviePath\u0022, moviePath ), ( \u0022positionSeconds\u0022, positionSeconds ), ( \u0022timeScale\u0022, timeScale ), ( \u0022isLooping\u0022, isLooping ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Start recording live play-mode gameplay into a Sandbox.MovieMaker clip \u2014 REQUIRES play mode\r\n\t/// (start_play first; errors otherwise). Captures the given GameObjects (ids \u2014 recommended: small\r\n\t/// focused clips) or, when ids is omitted, the WHOLE scene (heavy: every object becomes tracks).\r\n\t/// Returns { started, jobId, sampleRate, maxSeconds, capture, discarded, note } immediately;\r\n\t/// recording runs ASYNC in the editor frame loop until stop_gameplay_recording or the maxSeconds\r\n\t/// safety cap (default 60s of clip time, max 600s). Only one recording at a time (a second call\r\n\t/// errors while active; a stopped-but-unsaved clip is discarded by a new start, reported in\r\n\t/// \u0027discarded\u0027). Combine with playtest or drive_player to record a SCRIPTED run, then\r\n\t/// stop_gameplay_recording to save the .movie and play_movie to replay it.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022ids\u0022\u003EGameObject GUIDs to capture (from get_scene_hierarchy WHILE PLAYING \u2014 play-mode ids can differ from editor ids). Omit to capture the whole scene (heavy).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022sampleRate\u0022\u003ESamples per second (default 30, clamped 1-120).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022maxSeconds\u0022\u003ESafety cap \u2014 auto-stops the recording once the clip timeline reaches this many seconds (default 60, clamped 1-600). The clip stays in memory until stop_gameplay_recording saves it.\u003C/param\u003E\r\n\t[McpTool( \u0022record_gameplay_clip\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E RecordGameplayClip( string[] ids = null, int? sampleRate = null, double? maxSeconds = null )\r\n\t\t=\u003E McpGate.Run( \u0022record_gameplay_clip\u0022, McpGate.Args( ( \u0022ids\u0022, ids ), ( \u0022sampleRate\u0022, sampleRate ), ( \u0022maxSeconds\u0022, maxSeconds ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Run a scripted playtest AND record the same run to a .movie clip in ONE call \u2014 automated\r\n\t/// regression footage: a failing playtest comes with a replayable clip of exactly what happened.\r\n\t/// REQUIRES play mode (start_play first). steps uses the EXACT playtest schema (one verb per step:\r\n\t/// move / look / lookDelta / action / jump / set / wait / capture / assert \u2014 see the playtest tool\r\n\t/// for the full verb reference). The recording defaults to the playtest\u0027s resolved player\r\n\t/// hierarchy; pass ids to record other objects, or nothing resolvable falls back to whole-scene\r\n\t/// capture (heavy). Returns { started, steps, recordingJobId, capture, sampleRate, clipName,\r\n\t/// folder, recorderCapSeconds, note } immediately; both jobs run ASYNC in the editor frame loop and\r\n\t/// the clip AUTO-SAVES the moment the playtest finishes (a failing or aborted run still saves its\r\n\t/// footage; play mode ending early is also saved). THE POLL CHAIN: 1) playtest_status until\r\n\t/// finished:true \u2192 the per-step pass/fail transcript. 2) gameplay_recording_status \u2192 the saved clip\r\n\t/// summary { saved, assetPath, durationSeconds, trackCount } (if it still says pendingSave, the\r\n\t/// save is a frame away \u2014 poll again; a save error there means name collision: call\r\n\t/// stop_gameplay_recording yourself with a new name). Replay the footage with add_movie_player \u002B\r\n\t/// play_movie. Errors if a playtest or gameplay recording is already active. Only one at a time.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022steps\u0022\u003EOrdered playtest step objects \u2014 identical schema to the playtest tool (move/look/lookDelta/action/jump/set/wait/capture/assert). Runs top-to-bottom in the frame loop. JSON array.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the player/controller GameObject the playtest drives. Omit to auto-resolve the first PlayerController.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022component\u0022\u003EController component type to target (e.g. \u0027PlayerController\u0027). Omit to auto-detect.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022ids\u0022\u003EGameObject GUIDs to RECORD (from get_scene_hierarchy WHILE PLAYING). Omit to record the playtest\u0027s player hierarchy (the default and usually what you want).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022sampleRate\u0022\u003ERecording samples per second (default 30, clamped 1-120).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022clipName\u0022\u003ESaved .movie asset name without extension (default playtest_\u0026lt;UTC timestamp\u0026gt;; sanitized to [A-Za-z0-9_-]).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022folder\u0022\u003EAssets subfolder to save the clip into (default \u0022recordings\u0022).\u003C/param\u003E\r\n\t[McpTool( \u0022record_playtest\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E RecordPlaytest( JsonNode steps, string id = null, string component = null, string[] ids = null, int? sampleRate = null, string clipName = null, string folder = null )\r\n\t\t=\u003E McpGate.Run( \u0022record_playtest\u0022, McpGate.Args( ( \u0022steps\u0022, steps ), ( \u0022id\u0022, id ), ( \u0022component\u0022, component ), ( \u0022ids\u0022, ids ), ( \u0022sampleRate\u0022, sampleRate ), ( \u0022clipName\u0022, clipName ), ( \u0022folder\u0022, folder ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Stop the active gameplay recording and persist it as a project .movie asset the editor can load\r\n\t/// (written to Assets/\u0026lt;folder\u0026gt;/\u0026lt;name\u0026gt;.movie, registered \u002B compiled \u2014 list_movies then\r\n\t/// shows it with hasCompiledClip). Also saves a job that already auto-stopped (maxSeconds cap, or\r\n\t/// play mode ended). Returns { saved, assetPath, durationSeconds, trackCount, sampleRate, compiled,\r\n\t/// stopReason, wired, note } \u2014 a trackCount of 0 means nothing was captured and the response warns\r\n\t/// about it. Pass wireToId to auto-wire a MoviePlayer on that GameObject pointed at the new clip\r\n\t/// (during play mode that wiring is RUNTIME-ONLY and discarded on stop_play; the .movie asset\r\n\t/// itself always persists). Errors if the target file already exists (the clip stays in memory \u2014\r\n\t/// retry with another name). discard:true throws the recording away instead. Replay:\r\n\t/// add_movie_player \u002B play_movie in play mode.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EAsset name without extension (default recording_\u0026lt;UTC timestamp\u0026gt;; sanitized to [A-Za-z0-9_-]).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022folder\u0022\u003EAssets subfolder to save into (default \u0022recordings\u0022).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022wireToId\u0022\u003EGameObject GUID to auto-wire a MoviePlayer at the new clip (runtime-only if done during play mode).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022discard\u0022\u003EThrow the recording away instead of saving it.\u003C/param\u003E\r\n\t[McpTool( \u0022stop_gameplay_recording\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E StopGameplayRecording( string name = null, string folder = null, string wireToId = null, bool? discard = null )\r\n\t\t=\u003E McpGate.Run( \u0022stop_gameplay_recording\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022folder\u0022, folder ), ( \u0022wireToId\u0022, wireToId ), ( \u0022discard\u0022, discard ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Stop MoviePlayer playback (the counterpart to play_movie). Targets the MoviePlayer on the given\r\n\t/// GameObject, or the first MoviePlayer in the scene when id is omitted. Pass rewind to also reset\r\n\t/// the playhead to 0 so the next play_movie starts from the top. Works during play mode.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGameObject GUID holding the MoviePlayer. Omit to use the first MoviePlayer in the scene.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022rewind\u0022\u003EAlso reset the playhead to 0.\u003C/param\u003E\r\n\t[McpTool( \u0022stop_movie\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E StopMovie( string id = null, bool? rewind = null )\r\n\t\t=\u003E McpGate.Run( \u0022stop_movie\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022rewind\u0022, rewind ) ) );\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/Mcp/BridgeNpcTools.cs","FileName":"BridgeNpcTools.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"// AUTO-GENERATED by scripts/emit-mcp-wrappers.mjs \u2014 DO NOT EDIT.\r\n// Regenerate: node scripts/extract-manifest.mjs \u0026\u0026 node scripts/emit-mcp-wrappers.mjs\r\n// Source of truth: sbox-mcp-server/src/tools/ (zod schemas) \u2192 scripts/tools-manifest.json\r\n\r\nusing System.Text.Json.Nodes;\r\nusing System.Threading.Tasks;\r\nusing Editor.Mcp;\r\n\r\n/// \u003Csummary\u003E\r\n/// NPC brains (state machines), spawners, patrol routes, and perception simulation.\r\n/// \u003C/summary\u003E\r\n[McpToolset( \u0022bridge_npc\u0022, \u0022NPC brains (state machines), spawners, patrol routes, and perception simulation.\u0022 )]\r\npublic static class BridgeNpcTools\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Wire a placed route (or an arbitrary ordered GUID list) into an NpcBrain\u0027s Waypoints list on a\r\n\t/// target NPC. This is the list-of-GameObject-references case that plain set_property can\u0027t\r\n\t/// express. Pass either waypointIds (explicit order) or routeId (a route parent whose children\r\n\t/// become the waypoints in hierarchy order). The list count is returned; List\u0026lt;GameObject\u0026gt;\r\n\t/// refs may read back as handles/GUIDs via get_property, so trust the count or confirm patrol in\r\n\t/// play mode.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022npcId\u0022\u003EGUID of the GameObject holding the NpcBrain (or any component with a List\u0026lt;GameObject\u0026gt; waypoint property).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022waypointIds\u0022\u003EOrdered waypoint GameObject GUIDs (e.g. from place_patrol_route). Takes precedence over routeId.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022routeId\u0022\u003EA route parent GUID whose children (in hierarchy order) become the waypoints.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022property\u0022\u003EThe List\u0026lt;GameObject\u0026gt; property name to set. Defaults to \u0027Waypoints\u0027. (Use \u0027SpawnPoints\u0027 to wire spawn points on a spawner.).\u003C/param\u003E\r\n\t[McpTool( \u0022assign_patrol_route\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E AssignPatrolRoute( string npcId, string[] waypointIds = null, string routeId = null, string property = null )\r\n\t\t=\u003E McpGate.Run( \u0022assign_patrol_route\u0022, McpGate.Args( ( \u0022npcId\u0022, npcId ), ( \u0022waypointIds\u0022, waypointIds ), ( \u0022routeId\u0022, routeId ), ( \u0022property\u0022, property ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate an NpcBrain Component: a behavior state machine\r\n\t/// (Idle/Patrol/Wander/Chase/Search/Flee/Ambush) driven by occlusion-aware perception \u2014 FOV cone \u002B\r\n\t/// sight range \u002B a line-of-sight trace (respects walls/trees) \u002B proximity hearing \u2014 with\r\n\t/// last-known-position memory (lose-LOS -\u0026gt; search -\u0026gt; give up -\u0026gt; resume). This is the\r\n\t/// decision layer on top of bake_navmesh / NavMeshAgent movement. Pick a behavior preset, then tune\r\n\t/// via the generated [Property] fields with set_property. After generating: trigger_hotload \u002B\r\n\t/// get_compile_errors, place a route with place_patrol_route \u002B assign_patrol_route, bake_navmesh,\r\n\t/// and verify perception in EDIT mode with simulate_npc_perception (chase/search behavior needs\r\n\t/// play mode). The component is added to a GameObject like any other; it auto-adds a NavMeshAgent\r\n\t/// in OnStart.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass/file name. Defaults to \u0027NpcBrain\u0027. Sanitized to a valid C# identifier.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory under the project root for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022behavior\u0022\u003EPreset (sets StartState \u002B flee toggle): \u0027patrol\u0027 (walk waypoints), \u0027guard\u0027 (Ambush near spawn until a target enters range), \u0027hunter\u0027 (patrol-\u0026gt;chase-\u0026gt;search, the Sasquatch), \u0027swarm\u0027 (wander/idle-\u0026gt;chase nearest, RUN mobs), \u0027skittish\u0027 (chase but flee on low health). The generated file is the same shape; the preset just changes defaults. Defaults to \u0027hunter\u0027. One of: patrol | guard | hunter | swarm | skittish.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetTag\u0022\u003ETag the NPC hunts (its candidates are GameObjects with this tag). Defaults to \u0027player\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022moveSpeed\u0022\u003EPatrol/wander speed (NavMeshAgent MaxSpeed). Default 130.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022chaseSpeed\u0022\u003EChase/flee speed. Default 200.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022sightRange\u0022\u003EMax sight distance. Default 1500.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022fovDegrees\u0022\u003EFull field-of-view cone angle in degrees. Default 110. (Baked into a cosine threshold for cheap, trig-free checks.).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022eyeHeight\u0022\u003ETrace origin height above the NPC\u0027s feet. Default 64.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022hearingRadius\u0022\u003EProximity-hearing radius \u2014 a target within it is investigated (sets last-known-pos) but NOT instantly aggroed. Default 600.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022giveUpTime\u0022\u003ESeconds to search after losing line-of-sight before giving up and resuming the start state. Default 6.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022searchRadius\u0022\u003EWander radius around the last-known position while searching. Default 400.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022waypointStopDistance\u0022\u003EHow close the NPC must get to a waypoint/target before it counts as reached. Default 80.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022canFlee\u0022\u003EEnable the Flee state (else the NPC never flees). Defaults from the preset.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022fleeHealthFrac\u0022\u003EFlee when CurrentHealthFrac drops to/below this (the game sets CurrentHealthFrac 0..1). Default 0.25.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022networked\u0022\u003EWhen true (default), emit a host-authoritative brain: \u0027if (IsProxy) return;\u0027 \u002B [Sync] CurrentState. NOTE: a no-session solo playtest makes everything a proxy, so a networked brain won\u0027t think until a host session exists \u2014 pass false to iterate solo in the edit scene.\u003C/param\u003E\r\n\t[McpTool( \u0022create_npc_brain\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateNpcBrain( string name = null, string directory = null, string behavior = null, string targetTag = null, double? moveSpeed = null, double? chaseSpeed = null, double? sightRange = null, double? fovDegrees = null, double? eyeHeight = null, double? hearingRadius = null, double? giveUpTime = null, double? searchRadius = null, double? waypointStopDistance = null, bool? canFlee = null, double? fleeHealthFrac = null, bool? networked = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_npc_brain\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022behavior\u0022, behavior ), ( \u0022targetTag\u0022, targetTag ), ( \u0022moveSpeed\u0022, moveSpeed ), ( \u0022chaseSpeed\u0022, chaseSpeed ), ( \u0022sightRange\u0022, sightRange ), ( \u0022fovDegrees\u0022, fovDegrees ), ( \u0022eyeHeight\u0022, eyeHeight ), ( \u0022hearingRadius\u0022, hearingRadius ), ( \u0022giveUpTime\u0022, giveUpTime ), ( \u0022searchRadius\u0022, searchRadius ), ( \u0022waypointStopDistance\u0022, waypointStopDistance ), ( \u0022canFlee\u0022, canFlee ), ( \u0022fleeHealthFrac\u0022, fleeHealthFrac ), ( \u0022networked\u0022, networked ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a daily-routine NPC brain: a [Property] list of schedule entries (startHour/endHour\r\n\t/// 0..24, taskName, target = named scene GameObject or fixed position), the hour read from any\r\n\t/// create_day_night_clock component (capability match: a float TimeOfDay property, same GameObject\r\n\t/// first then scene-wide) with an HONEST fallback to its own internal clock when none exists (check\r\n\t/// the generated UsingClockComponent bool), walking the NPC to the active entry\u0027s target and idling\r\n\t/// outside the schedule, plus a static OnTaskChanged(brain, taskName) event and [Sync(FromHost)]\r\n\t/// CurrentTask. Entries with endHour \u0026lt; startHour wrap past midnight. Returns {created, path,\r\n\t/// className, tasks[], propertyNames[], note}. Next: trigger_hotload \u002B get_compile_errors, attach\r\n\t/// (targetId or add_component_with_properties), create the named target GameObjects (e.g.\r\n\t/// \u0027WorkSpot\u0027), pair with create_day_night_clock for shared time, verify via get_runtime_property\r\n\t/// CurrentTask in play mode. Limits: default movement is a direct transform walk (walks through\r\n\t/// walls) \u2014 pass useNavMeshAgent:true for pathfinding (then bake_navmesh is REQUIRED); a clock with\r\n\t/// a different shape (e.g. 0..1 DayProgress) will NOT bind; networked default true won\u0027t tick in a\r\n\t/// no-session solo playtest (networked:false to iterate). Refused during play mode; refuses to\r\n\t/// overwrite an existing file.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass/file name. Defaults to \u0027NpcScheduleBrain\u0027. Sanitized to a valid C# identifier.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory under the project root for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022schedule\u0022\u003ESchedule entries baked as inspector-editable defaults. Defaults to Work 8-17 @ \u0027WorkSpot\u0027, Relax 17-22 @ \u0027HomeSpot\u0027 (idles/sleeps otherwise). JSON array.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022moveSpeed\u0022\u003EWalk speed in world units/s. Defaults to 100.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022arriveDistance\u0022\u003EDistance at which the NPC counts as arrived and idles at the spot. Defaults to 32.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022useNavMeshAgent\u0022\u003Etrue: move via NavMeshAgent.MoveTo (real pathfinding \u2014 REQUIRES bake_navmesh or the NPC won\u0027t move). Defaults to false (direct transform walk, no navmesh needed, walks through walls).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022fallbackDayLengthSeconds\u0022\u003EInternal fallback clock only: real seconds per 24 in-game hours when NO TimeOfDay clock component exists. Defaults to 600.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022fallbackStartHour\u0022\u003EInternal fallback clock only: starting hour 0..24. Defaults to 8.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022networked\u0022\u003Etrue (default): host-authoritative (IsProxy guard) \u002B [Sync(FromHost)] CurrentTask. false: local build for solo iteration.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of the NPC GameObject to attach to (only attaches if the type is already in the TypeLibrary \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_npc_schedule_brain\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateNpcScheduleBrain( string name = null, string directory = null, JsonNode schedule = null, double? moveSpeed = null, double? arriveDistance = null, bool? useNavMeshAgent = null, double? fallbackDayLengthSeconds = null, double? fallbackStartHour = null, bool? networked = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_npc_schedule_brain\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022schedule\u0022, schedule ), ( \u0022moveSpeed\u0022, moveSpeed ), ( \u0022arriveDistance\u0022, arriveDistance ), ( \u0022useNavMeshAgent\u0022, useNavMeshAgent ), ( \u0022fallbackDayLengthSeconds\u0022, fallbackDayLengthSeconds ), ( \u0022fallbackStartHour\u0022, fallbackStartHour ), ( \u0022networked\u0022, networked ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a spawner Component that instantiates an NPC prefab over time / in escalating waves at\r\n\t/// spawn points, capped by maxAlive. RUN\u0027s swarm backbone and Sasquatched\u0027s round-start spawn.\r\n\t/// After generating: set NpcPrefab via set_prefab_ref, set SpawnPoints (reuse place_patrol_route to\r\n\t/// make a set of empties, then assign_patrol_route with property=\u0027SpawnPoints\u0027), trigger_hotload \u002B\r\n\t/// get_compile_errors. Verify by watching the GameObject count over time in play mode. Networked\r\n\t/// spawns use NetworkSpawn() and are host-only.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass/file name. Defaults to \u0027NpcSpawner\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory under the project root. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022mode\u0022\u003E\u0027continuous\u0027 (one every interval), \u0027waves\u0027 (a batch every interval, waveCount times), \u0027burst\u0027 (one batch then stop). Default \u0027waves\u0027. One of: continuous | waves | burst.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022count\u0022\u003ENPCs per wave (waves) or per batch (burst/continuous batch). Default 5.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022interval\u0022\u003ESeconds between spawns (continuous) or between waves (waves). Default 8.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022waveCount\u0022\u003ENumber of waves (waves mode). Default 3.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022waveGrowth\u0022\u003EMultiply count each wave (\u0026gt;1 = escalating). Default 1.0.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022radius\u0022\u003ERandom scatter radius around a spawn point. Default 200.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022maxAlive\u0022\u003ECap on concurrent live NPCs (important so swarms don\u0027t melt the frame rate). Default 12.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022networked\u0022\u003EWhen true (default), spawn via NetworkSpawn() (host-only, try/catch solo-safe) so clients see the NPCs; false = a plain local Clone for solo/edit testing.\u003C/param\u003E\r\n\t[McpTool( \u0022create_npc_spawner\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateNpcSpawner( string name = null, string directory = null, string mode = null, double? count = null, double? interval = null, double? waveCount = null, double? waveGrowth = null, double? radius = null, double? maxAlive = null, bool? networked = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_npc_spawner\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022mode\u0022, mode ), ( \u0022count\u0022, count ), ( \u0022interval\u0022, interval ), ( \u0022waveCount\u0022, waveCount ), ( \u0022waveGrowth\u0022, waveGrowth ), ( \u0022radius\u0022, radius ), ( \u0022maxAlive\u0022, maxAlive ), ( \u0022networked\u0022, networked ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a utility-AI (scored-action) brain: one file with an abstract {name}Action : Component\r\n\t/// base (Score() 0..1 \u002B Begin/Tick/End lifecycle), a sealed {name}Brain that every EvaluateInterval\r\n\t/// picks the highest-scoring sibling action (score \u00D7 ScoreWeight, current action gets\r\n\t/// \u002BHysteresisBonus so near-ties don\u0027t flip-flop), and two example actions \u2014 {name}IdleAction\r\n\t/// (constant fallback score) and {name}WanderAction (desire builds while idle, walks to random\r\n\t/// points by direct transform movement, no navmesh). How it differs from create_npc_brain: the FSM\r\n\t/// has a FIXED transition table; here behavior EMERGES from per-frame scores \u2014 add behaviors by\r\n\t/// subclassing the base on the same GameObject, no transition wiring. Returns {created, path,\r\n\t/// classNames[4], propertyNames[], note}. Next: trigger_hotload \u002B get_compile_errors, attach the\r\n\t/// brain AND example actions to one GameObject (targetId attaches only the brain), verify in play\r\n\t/// mode via get_runtime_property CurrentActionName. Limits: networked default true =\r\n\t/// host-authoritative (won\u0027t tick in a no-session solo playtest \u2014 use networked:false); actions\r\n\t/// Tick on the simulating machine only. Refused during play mode; refuses to overwrite an existing\r\n\t/// file.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003ESystem prefix \u2014 generates {name}Action / {name}Brain / {name}IdleAction / {name}WanderAction in {name}Ai.cs. Defaults to \u0027Utility\u0027. Sanitized to a valid C# identifier.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory under the project root for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022evaluateInterval\u0022\u003ESeconds between score evaluations (the active action still Ticks every frame). Defaults to 0.25.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022hysteresisBonus\u0022\u003EScore bonus the current action gets during evaluation \u2014 stickiness that prevents flip-flopping between near-tied actions. Defaults to 0.15.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022moveSpeed\u0022\u003EExample WanderAction walk speed in world units/s. Defaults to 80.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022wanderRadius\u0022\u003EExample WanderAction roam radius around its start position. Defaults to 300.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022networked\u0022\u003Etrue (default): host-authoritative brain (IsProxy guard) \u002B [Sync(FromHost)] CurrentActionName. false: local build for solo iteration.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of a GameObject to attach the BRAIN to (actions must be added separately; only attaches if the type is already in the TypeLibrary \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_utility_ai\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateUtilityAi( string name = null, string directory = null, double? evaluateInterval = null, double? hysteresisBonus = null, double? moveSpeed = null, double? wanderRadius = null, bool? networked = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_utility_ai\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022evaluateInterval\u0022, evaluateInterval ), ( \u0022hysteresisBonus\u0022, hysteresisBonus ), ( \u0022moveSpeed\u0022, moveSpeed ), ( \u0022wanderRadius\u0022, wanderRadius ), ( \u0022networked\u0022, networked ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Place a set of waypoint GameObjects (tagged empties) for a patrol route and group them under a\r\n\t/// parent route object \u2014 authorable in one call. Optionally snaps each point to the ground (raycast\r\n\t/// down) so waypoints sit on the navmesh, not floating. Returns the route parent GUID \u002B ordered\r\n\t/// waypoint GUIDs to feed into assign_patrol_route. Validate connectivity afterward with\r\n\t/// get_navmesh_path between consecutive waypoints (catches a \u0027point in a wall\u0027).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022points\u0022\u003EOrdered world positions for the route (at least 2). JSON array.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003ERoute name. Defaults to \u0027PatrolRoute\u0027. Waypoints are named \u0026lt;route\u0026gt;_WP0, _WP1, ...\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022tag\u0022\u003ETag applied to each waypoint. Defaults to \u0027waypoint\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022snapToGround\u0022\u003EDrop each point onto the surface below via a downward raycast. Default true.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022parentId\u0022\u003EExisting parent GameObject GUID to nest the waypoints under; otherwise a new route empty is created at the points\u0027 centroid.\u003C/param\u003E\r\n\t[McpTool( \u0022place_patrol_route\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E PlacePatrolRoute( JsonNode points, string name = null, string tag = null, bool? snapToGround = null, string parentId = null )\r\n\t\t=\u003E McpGate.Run( \u0022place_patrol_route\u0022, McpGate.Args( ( \u0022points\u0022, points ), ( \u0022name\u0022, name ), ( \u0022tag\u0022, tag ), ( \u0022snapToGround\u0022, snapToGround ), ( \u0022parentId\u0022, parentId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// READ-ONLY edit-mode verifier: evaluate the NPC\u0027s perception math RIGHT NOW without entering play\r\n\t/// mode. Given an NPC (reads its NpcBrain SightRange/FovDegrees/EyeHeight/TargetTag \u002B transform)\r\n\t/// and either a targetId or a point, it runs the SAME line-of-sight check the brain uses \u2014 FOV cone\r\n\t/// (dot vs the baked cosine), sight-range gate, and an occlusion trace from the eye to the target \u2014\r\n\t/// and reports the result AND why. This is the keystone verifier: it makes the perception layer\r\n\t/// checkable in edit mode (no flaky screenshot timing) \u2014 e.g. place the Sasquatch, place a camper\r\n\t/// behind a tree, and confirm the tree blocks LOS. Call params override the brain\u0027s values, so it\r\n\t/// also works before/without an NpcBrain (uses defaults). Safe in play mode too (read-only, like\r\n\t/// raycast).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022npcId\u0022\u003EGUID of the NPC GameObject (ideally with an NpcBrain; its perception [Property] values are read).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of the target GameObject to test visibility to (e.g. a player). Provide this OR point.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022point\u0022\u003EA raw world point to test visibility to. Provide this OR targetId. As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022sightRange\u0022\u003EOverride the sight range for this check (else read from the NpcBrain / default 1500).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022fovDegrees\u0022\u003EOverride the FOV cone angle for this check (else read from the NpcBrain / default 110).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022eyeHeight\u0022\u003EOverride the eye height for this check (else read from the NpcBrain / default 64).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetTag\u0022\u003EOverride the target tag (canSee also requires the target to carry this tag; else read from the NpcBrain / default \u0027player\u0027).\u003C/param\u003E\r\n\t[McpTool( \u0022simulate_npc_perception\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SimulateNpcPerception( string npcId, string targetId = null, string point = null, double? sightRange = null, double? fovDegrees = null, double? eyeHeight = null, string targetTag = null )\r\n\t\t=\u003E McpGate.Run( \u0022simulate_npc_perception\u0022, McpGate.Args( ( \u0022npcId\u0022, npcId ), ( \u0022targetId\u0022, targetId ), ( \u0022point\u0022, point ), ( \u0022sightRange\u0022, sightRange ), ( \u0022fovDegrees\u0022, fovDegrees ), ( \u0022eyeHeight\u0022, eyeHeight ), ( \u0022targetTag\u0022, targetTag ) ) );\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Code/MyLibraryComponent.cs","FileName":"MyLibraryComponent.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":335526,"Code":"using Sandbox;\r\n\r\n/// \u003Csummary\u003E\r\n/// This is a component - in your library!\r\n/// \u003C/summary\u003E\r\n[Title( \u0022claude bridge - My Component\u0022 )]\r\npublic class MyLibraryComponent : Component\r\n{\r\n\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/Mcp/BridgePrefabTools.cs","FileName":"BridgePrefabTools.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"// AUTO-GENERATED by scripts/emit-mcp-wrappers.mjs \u2014 DO NOT EDIT.\r\n// Regenerate: node scripts/extract-manifest.mjs \u0026\u0026 node scripts/emit-mcp-wrappers.mjs\r\n// Source of truth: sbox-mcp-server/src/tools/ (zod schemas) \u2192 scripts/tools-manifest.json\r\n\r\nusing System.Text.Json.Nodes;\r\nusing System.Threading.Tasks;\r\nusing Editor.Mcp;\r\n\r\n/// \u003Csummary\u003E\r\n/// Create prefabs from scene objects, instantiate them, list and inspect them, and wire prefab\r\n/// references into component properties.\r\n/// \u003C/summary\u003E\r\n[McpToolset( \u0022bridge_prefab\u0022, \u0022Create prefabs from scene objects, instantiate them, list and inspect them, and wire prefab references into component properties.\u0022 )]\r\npublic static class BridgePrefabTools\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Save an existing GameObject as a real .prefab file \u2014 FULL engine serialization: every component\r\n\t/// with its property values, and all children, in the same JSON format the editor writes. Returns {\r\n\t/// created, path, sourceId, components, children } \u2014 pass path to instantiate_prefab to spawn\r\n\t/// copies or get_prefab_info to inspect. Errors if the source GameObject is missing; overwrites an\r\n\t/// existing file at path.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject to save as prefab.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022path\u0022\u003EPath for the prefab file relative to project root (e.g. \u0027prefabs/enemies/grunt.prefab\u0027).\u003C/param\u003E\r\n\t[McpTool( \u0022create_prefab\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreatePrefab( string id, string path )\r\n\t\t=\u003E McpGate.Run( \u0022create_prefab\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022path\u0022, path ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Inspect a prefab file as a structured summary: { path, name, size, modified, totalObjects,\r\n\t/// maxDepth, referencedPrefabs, tree } \u2014 tree is the object hierarchy with per-node component type\r\n\t/// lists (children capped at 8 per node with a truncation count). referencedPrefabs lists other\r\n\t/// .prefab files this one links to. Use before instantiate_prefab; find prefabs with list_prefabs;\r\n\t/// raw JSON via read_file if needed.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022path\u0022\u003EPath to the .prefab file (e.g. \u0027prefabs/enemies/grunt.prefab\u0027).\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022get_prefab_info\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E GetPrefabInfo( string path )\r\n\t\t=\u003E McpGate.Run( \u0022get_prefab_info\u0022, McpGate.Args( ( \u0022path\u0022, path ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Spawn a FULL prefab instance into the active scene \u2014 components and children recreated. Uses the\r\n\t/// engine\u0027s GameObject.Clone for registered prefabs, with a guid-remapped deserialize fallback for\r\n\t/// freshly-written files (repeat instantiations never collide). Returns { instantiated, prefab,\r\n\t/// method, gameObject, components, childCount } \u2014 gameObject.id is the new GUID for\r\n\t/// set_transform/set_property follow-ups. Optional name/position/rotation override the spawned\r\n\t/// root.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022path\u0022\u003EPath to the .prefab file (e.g. \u0027prefabs/enemies/grunt.prefab\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003ERename the spawned root (defaults to the prefab\u0027s root name).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022position\u0022\u003EWorld position to spawn at \u2014 object {x,y,z} or comma string \u0022x,y,z\u0022. Defaults to origin. As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022rotation\u0022\u003ERotation as euler angles. Defaults to identity. As \u0022pitch,yaw,roll\u0022 degrees.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022scale\u0022\u003EUniform scale multiplier. Defaults to 1.0.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022parent\u0022\u003EGUID of parent GameObject to attach to.\u003C/param\u003E\r\n\t[McpTool( \u0022instantiate_prefab\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E InstantiatePrefab( string path, string name = null, string position = null, string rotation = null, double? scale = null, string parent = null )\r\n\t\t=\u003E McpGate.Run( \u0022instantiate_prefab\u0022, McpGate.Args( ( \u0022path\u0022, path ), ( \u0022name\u0022, name ), ( \u0022position\u0022, position ), ( \u0022rotation\u0022, rotation ), ( \u0022scale\u0022, scale ), ( \u0022parent\u0022, parent ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// List all .prefab files in the project. Filter by name or path.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022filter\u0022\u003ESearch filter for prefab name or path.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022maxResults\u0022\u003EMaximum results to return. Defaults to 100.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022list_prefabs\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E ListPrefabs( string filter = null, double? maxResults = null )\r\n\t\t=\u003E McpGate.Run( \u0022list_prefabs\u0022, McpGate.Args( ( \u0022filter\u0022, filter ), ( \u0022maxResults\u0022, maxResults ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Set a GameObject-typed property on a component to a loaded prefab. Use this when set_property\r\n\t/// can\u0027t handle prefab references (which it can\u0027t, because prefabs are GameObjects not primitives).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject holding the component.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022component\u0022\u003EComponent type name.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022property\u0022\u003EProperty name to set (must be GameObject-typed).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022prefabPath\u0022\u003EPrefab asset path (e.g. \u0027prefabs/player.prefab\u0027).\u003C/param\u003E\r\n\t[McpTool( \u0022set_prefab_ref\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SetPrefabRef( string id, string component, string property, string prefabPath )\r\n\t\t=\u003E McpGate.Run( \u0022set_prefab_ref\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022component\u0022, component ), ( \u0022property\u0022, property ), ( \u0022prefabPath\u0022, prefabPath ) ) );\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/Mcp/BridgeScaffoldGameplayTools.cs","FileName":"BridgeScaffoldGameplayTools.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"// AUTO-GENERATED by scripts/emit-mcp-wrappers.mjs \u2014 DO NOT EDIT.\r\n// Regenerate: node scripts/extract-manifest.mjs \u0026\u0026 node scripts/emit-mcp-wrappers.mjs\r\n// Source of truth: sbox-mcp-server/src/tools/ (zod schemas) \u2192 scripts/tools-manifest.json\r\n\r\nusing System.Text.Json.Nodes;\r\nusing System.Threading.Tasks;\r\nusing Editor.Mcp;\r\n\r\n/// \u003Csummary\u003E\r\n/// Generate complete, compile-verified gameplay C# components: player/NPC controllers, game\r\n/// managers, health, pickups, inventory, save systems, economy, loot tables, round/phase machines,\r\n/// interaction systems, placement mode, and more. Each tool writes a .cs file into the project;\r\n/// follow with trigger_hotload \u002B compile_status.\r\n/// \u003C/summary\u003E\r\n[McpToolset( \u0022bridge_scaffold_gameplay\u0022, \u0022Generate complete, compile-verified gameplay C# components: player/NPC controllers, game managers, health, pickups, inventory, save systems, economy, loot tables, round/phase machines, interaction systems, placement mode, and more. Each tool writes a .cs file into the project; follow with trigger_hotload \u002B compile_status.\u0022 )]\r\npublic static class BridgeScaffoldGameplayTools\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// SCENE-MUTATING: generate a data-driven achievement trigger-zone component AND create its\r\n\t/// GameObject now (named zone with a sized BoxCollider, IsTrigger=true, at the given position).\r\n\t/// When an object tagged triggerTag enters, the component calls\r\n\t/// \u0026lt;achievementSetClass\u0026gt;.Instance.Progress(achievementId, amount) \u2014 or Unlock() when\r\n\t/// unlock=true \u2014 with a once-only latch and optional destroy-after-fire. Returns { created, path,\r\n\t/// className, achievementSetClass, achievementId, gameObject, attached, note, nextSteps }. The\r\n\t/// generated component only attaches to the zone after trigger_hotload \u2014 until then \u0060attached\u0060 is\r\n\t/// false and nextSteps carries the exact add_component_with_properties follow-up. The generated\r\n\t/// code references the set class BY NAME: run create_achievement_set first or the project will not\r\n\t/// compile (the result warns via \u0060note\u0060). Re-running with the same name fails unless\r\n\t/// reuseClass=true, which skips codegen and just places another zone (attaching \u002B configuring\r\n\t/// immediately since the class is already compiled). Refused during play mode.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022achievementId\u0022\u003EId of the achievement to progress/unlock (sanitized to [a-z0-9_-]).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated trigger component. Defaults to \u0027AchievementTrigger\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022achievementSetClass\u0022\u003EClass name of the achievement set the zone reports to (from create_achievement_set). Defaults to \u0027AchievementSet\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022amount\u0022\u003EProgress amount added per fire (ignored when unlock=true). Defaults to 1.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022unlock\u0022\u003ECall Unlock() instead of Progress(). Defaults to false.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022triggerTag\u0022\u003ETag the entering object must carry (put it on the player via set_tags). Defaults to \u0027player\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022onceOnly\u0022\u003EOnly the first tagged entry fires. Defaults to true.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022destroyAfterFire\u0022\u003EDestroy the zone GameObject after firing. Defaults to false.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022createObject\u0022\u003ECreate the zone GameObject now (with BoxCollider). Defaults to true; false = code-gen only.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022objectName\u0022\u003EName for the zone GameObject. Defaults to \u0027\u0026lt;name\u0026gt;Zone\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022position\u0022\u003EWorld position of the zone GameObject. As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022scale\u0022\u003EBoxCollider size \u2014 uniform number, object {x,y,z}, or comma string \u0022x,y,z\u0022. Defaults to 100,100,100. As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022reuseClass\u0022\u003EIf the .cs already exists, skip codegen and just place another zone with the existing class. Defaults to false.\u003C/param\u003E\r\n\t[McpTool( \u0022add_achievement_trigger\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E AddAchievementTrigger( string achievementId, string name = null, string directory = null, string achievementSetClass = null, double? amount = null, bool? unlock = null, string triggerTag = null, bool? onceOnly = null, bool? destroyAfterFire = null, bool? createObject = null, string objectName = null, string position = null, string scale = null, bool? reuseClass = null )\r\n\t\t=\u003E McpGate.Run( \u0022add_achievement_trigger\u0022, McpGate.Args( ( \u0022achievementId\u0022, achievementId ), ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022achievementSetClass\u0022, achievementSetClass ), ( \u0022amount\u0022, amount ), ( \u0022unlock\u0022, unlock ), ( \u0022triggerTag\u0022, triggerTag ), ( \u0022onceOnly\u0022, onceOnly ), ( \u0022destroyAfterFire\u0022, destroyAfterFire ), ( \u0022createObject\u0022, createObject ), ( \u0022objectName\u0022, objectName ), ( \u0022position\u0022, position ), ( \u0022scale\u0022, scale ), ( \u0022reuseClass\u0022, reuseClass ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate an eye-traced interaction-prompt HUD \u2014 a PanelComponent (.razor \u002B .razor.scss pair,\r\n\t/// like create_leaderboard_panel) that every frame traces a ray from the scene camera\r\n\t/// (Scene.Trace.Ray, out to [Property] float Range) and, when the crosshair is on a component\r\n\t/// implementing Component.IPressable, shows a centered \u0022Press E\u0022-style pill. The prompt text comes\r\n\t/// from the target\u0027s IPressable.GetTooltip() when it overrides it (most don\u0027t), else a [Property]\r\n\t/// DefaultPrompt built from the action. This is the visible half of the interaction loop: it PAIRS\r\n\t/// with create_interactable / add_interaction_station (which implement IPressable) \u2014 this tool\r\n\t/// tells the player they CAN press, those tools handle the press. Host it under a ScreenPanel\r\n\t/// (add_screen_panel), then add the component to that panel object. The generated Razor is\r\n\t/// razor_lint-safe by construction: PanelComponent \u002B BuildHash override folding the visible state,\r\n\t/// no switch-expressions and no non-ASCII in @code, and a class root selector in the SCSS.\r\n\t/// LOCAL/visual-only (no [Sync]).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass/file name for the generated .razor. Defaults to \u0027InteractionPrompt\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .razor \u002B .razor.scss. Defaults to \u0027Code/UI\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022action\u0022\u003EVerb woven into the default prompt text (\u0027Press E to \u0026lt;action\u0026gt;\u0027). Defaults to \u0027use\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022range\u0022\u003EEye-trace reach in world units \u2014 how close the crosshair must be to a pressable to show the prompt. Defaults to 120.\u003C/param\u003E\r\n\t[McpTool( \u0022add_interaction_prompt\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E AddInteractionPrompt( string name = null, string directory = null, string action = null, double? range = null )\r\n\t\t=\u003E McpGate.Run( \u0022add_interaction_prompt\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022action\u0022, action ), ( \u0022range\u0022, range ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a Component.IPressable \u0027station\u0027 prop (crafting bench / shop till / arcade cabinet)\r\n\t/// that ONE user occupies at a time. Occupancy is host-authoritative: the occupant is a\r\n\t/// [Sync(SyncFlags.FromHost)] Guid (GameObject/Connection aren\u0027t [Sync]-able) and Press() routes\r\n\t/// the claim to the host via an [Rpc.Host] Occupy(). Includes a reservation grace window (the\r\n\t/// station stays reserved for its last user for graceSeconds after they leave, so a brief walk-away\r\n\t/// can\u0027t jump the queue), an optional unlock-level gate (users below requiredLevel can\u0027t use it \u2014\r\n\t/// wire the static ResolveUserLevel hook to your progression system to activate it), and an\r\n\t/// overlay-open hook (a static OnStationOpened(GameObject) event to open your UI, plus an opt-in\r\n\t/// [Rpc.Broadcast] mirror). Single-player safe. Optionally attached to an existing GameObject by\r\n\t/// GUID (only after a trigger_hotload). Give the prop a Collider so the player\u0027s use key can\r\n\t/// raycast it. Mined from interaction-station patterns across shipped s\u0026amp;box games.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated component. Defaults to \u0027InteractionStation\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the .cs file (path override). Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022graceSeconds\u0022\u003ESeconds the station stays reserved for its last user after they leave, before anyone else can claim it. 0 = no grace window. Defaults to 5.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022requiredLevel\u0022\u003EUnlock-level gate: users below this level can\u0027t use the station. 0 = no gate. The gate only bites once you wire the static ResolveUserLevel hook to your progression system. Defaults to 0.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of an existing GameObject to attach the station component to (only attaches if the type is already loaded \u2014 generate, trigger_hotload, then it places; otherwise add it after the hotload).\u003C/param\u003E\r\n\t[McpTool( \u0022add_interaction_station\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E AddInteractionStation( string name = null, string directory = null, double? graceSeconds = null, int? requiredLevel = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022add_interaction_station\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022graceSeconds\u0022, graceSeconds ), ( \u0022requiredLevel\u0022, requiredLevel ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a batched write-side stat reporter component for Sandbox.Services.Stats \u2014 the write\r\n\t/// partner of create_leaderboard_panel. Gameplay code calls the static \u0026lt;Name\u0026gt;.Report(\u0022kills\u0022,\r\n\t/// 1) from anywhere; amounts accumulate locally and flush as Stats.Increment deltas on a timer\r\n\t/// (default every 12 s, also on disable/destroy). Baseline-delta bookkeeping means a partial flush\r\n\t/// retries the un-sent remainder instead of double-counting, and deltas larger than maxChunk are\r\n\t/// sent in chunks. Returns { created, path, className, placedOn, note, nextSteps }. Place ONE in\r\n\t/// the scene after trigger_hotload (add_component_to_new_object), or pass targetId to attach\r\n\t/// immediately when the type is already compiled. Stats are PER LOCAL PLAYER (each client reports\r\n\t/// its own) and only exist on leaderboards once the stat is registered for the project ident on\r\n\t/// sbox.game. Fails if the file already exists.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated component. Defaults to \u0027StatReporter\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022flushIntervalSeconds\u0022\u003ESeconds between batched flushes to the backend. Defaults to 12, clamped to \u0026gt;= 1.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022maxChunk\u0022\u003ELargest amount sent in a single Stats.Increment call; bigger deltas are chunked. Defaults to 1000, clamped to \u0026gt;= 1.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of a GameObject to attach to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022add_leaderboard_stat\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E AddLeaderboardStat( string name = null, string directory = null, double? flushIntervalSeconds = null, double? maxChunk = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022add_leaderboard_stat\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022flushIntervalSeconds\u0022, flushIntervalSeconds ), ( \u0022maxChunk\u0022, maxChunk ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a currency component (sealed) persisted over Sandbox.Services.Stats \u2014 Steam-cloud\r\n\t/// persistence, per Steam account, per package ident, with NO local save file. The stat stores the\r\n\t/// ABSOLUTE balance: every Add(double)/TrySpend(double) pushes Stats.SetValue(statName, balance);\r\n\t/// Flush() (and OnDestroy) pushes the buffered writes. On start it reads the balance back\r\n\t/// asynchronously via Stats.GetLocalPlayerStats(ident) -\u0026gt; Refresh() -\u0026gt; Get(statName).Value\r\n\t/// and fires the static OnBalanceLoaded(double); wait for IsLoaded before showing the balance.\r\n\t/// CLOUD SEMANTICS (surprising): stat writes are buffered/rate-limited by the backend and apply\r\n\t/// ONLY to the LOCAL Steam user \u2014 calling this for another player silently does nothing, so attach\r\n\t/// it to the LOCAL player\u0027s GameObject (IsProxy guards keep remote copies inert); read-back is\r\n\t/// eventually consistent and can lag minutes behind writes \u2014 the in-session Balance property is the\r\n\t/// runtime truth. Dev sessions without a real published package ident may read back nothing\r\n\t/// (balance starts 0 with a log line). packageIdent defaults to the running package (Game.Ident).\r\n\t/// Returns { created, path, className, statName, packageIdent, flushEveryChange, placedOn, note,\r\n\t/// nextSteps }. Next: trigger_hotload, attach to the local player, bind OnBalanceChanged for the\r\n\t/// HUD. Refused during play mode. Use create_economy_wallet/create_currency_account for in-run\r\n\t/// networked money, create_signed_save for offline local persistence; pair with\r\n\t/// create_leaderboard_panel (the same stat can back a leaderboard).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated component. Defaults to \u0027SteamStatCurrency\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022statName\u0022\u003ESandbox.Services stat that stores the balance (the stat-name string is the contract between write and read-back). Defaults to \u0027currency\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022packageIdent\u0022\u003EPackage ident to read stats from. Omit/empty = the running package (Game.Ident).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022flushEveryChange\u0022\u003ECall Stats.Flush() after every balance change instead of relying on the buffered flush \u002B OnDestroy flush (the backend rate-limits flushes). Defaults to false.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of the LOCAL player\u0027s GameObject to attach to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022add_steam_stat_currency\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E AddSteamStatCurrency( string name = null, string directory = null, string statName = null, string packageIdent = null, bool? flushEveryChange = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022add_steam_stat_currency\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022statName\u0022, statName ), ( \u0022packageIdent\u0022, packageIdent ), ( \u0022flushEveryChange\u0022, flushEveryChange ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate an achievement engine: a component with an AchievementDef list\r\n\t/// (id/title/description/target), per-achievement progress persisted via FileSystem.Data JSON\r\n\t/// (survives restarts), Progress(id, amount) / Unlock(id) API on a static Instance, a static\r\n\t/// OnAchievementUnlocked event, and an optional Stats.Increment mirror (\u0027ach-\u0026lt;id\u0026gt;\u0027 \u002B= 1) on\r\n\t/// unlock. Also emits a Razor unlock-toast HUD (\u0026lt;Name\u0026gt;Toast.razor \u002B .razor.scss, razor_lint\r\n\t/// clean) unless makeToast=false. Returns { created, path, className, toastRazorPath,\r\n\t/// toastScssPath, toastClassName, achievements, placedOn, note, nextSteps }. Ids are sanitized to\r\n\t/// [a-z0-9_-]; omitting achievements bakes 3 editable samples. After trigger_hotload: place ONE set\r\n\t/// in the scene, and host the toast under a ScreenPanel (add_screen_panel). Pair with\r\n\t/// add_achievement_trigger for world-trigger unlocks. LOCAL-only: achievements belong to each\r\n\t/// client\u0027s local player. Fails if the .cs or toast .razor already exists.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated engine component (toast panel becomes \u0026lt;name\u0026gt;Toast). Defaults to \u0027AchievementSet\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for all generated files. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022achievements\u0022\u003EAchievement definitions baked into the component. Omit for 3 editable samples (first_steps, collector, veteran). JSON array.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022fileName\u0022\u003ESave file name inside FileSystem.Data. Defaults to \u0027achievements.json\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022mirrorToStats\u0022\u003EMirror each unlock into Sandbox.Services.Stats as \u0027ach-\u0026lt;id\u0026gt;\u0027 \u002B= 1. Defaults to true.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022makeToast\u0022\u003EAlso emit the \u0026lt;name\u0026gt;Toast.razor \u002B .razor.scss unlock toast HUD. Defaults to true.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022toastSeconds\u0022\u003ESeconds each unlock toast stays on screen. Defaults to 4, clamped to \u0026gt;= 0.5.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of a GameObject to attach the engine to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_achievement_set\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateAchievementSet( string name = null, string directory = null, JsonNode achievements = null, string fileName = null, bool? mirrorToStats = null, bool? makeToast = null, double? toastSeconds = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_achievement_set\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022achievements\u0022, achievements ), ( \u0022fileName\u0022, fileName ), ( \u0022mirrorToStats\u0022, mirrorToStats ), ( \u0022makeToast\u0022, makeToast ), ( \u0022toastSeconds\u0022, toastSeconds ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a first-person pickup / carry / throw component (sealed Component) for physics props.\r\n\t/// Attach it to the PLAYER (the object that owns the camera). It eye-traces from Scene.Camera for a\r\n\t/// Rigidbody-bearing GameObject tagged [Property] CarryTag (default \u0027carryable\u0027) within [Property]\r\n\t/// Range; grabbing routes a host-authoritative [Rpc.Host] request that re-validates the target and\r\n\t/// caller, hands the object\u0027s network ownership to the carrier\r\n\t/// (GameObject.Network.AssignOwnership), and disables the rigidbody\u0027s MotionEnabled while held. The\r\n\t/// held object follows a hold point ([Property] Vector3 HoldOffset in front of the camera) each\r\n\t/// FixedUpdate; dropping restores physics, throwing applies an impulse ([Property] float\r\n\t/// ThrowForce). The held-object id is [Sync(SyncFlags.FromHost)] so proxies see the carrying state,\r\n\t/// and static OnPickedUp / OnDropped events fire uniformly for SFX/VFX. PAIRS with physics props \u2014\r\n\t/// give each carryable a Rigidbody \u002B Collider and the CarryTag (set_tags); network-spawn them for\r\n\t/// multiplayer so ownership \u002B transform replicate. Single-player safe (IsProxy is false and RPCs\r\n\t/// run locally with no session). Inputs: GrabAction (default \u0027use\u0027) grabs/drops, ThrowAction\r\n\t/// (default \u0027attack1\u0027) throws. Optionally attach to an existing player GameObject by GUID after a\r\n\t/// hotload.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated component. Defaults to \u0027CarrySystem\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022range\u0022\u003EEye-trace reach for grabbing a carryable, in world units. Defaults to 130.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022throwForce\u0022\u003EImpulse magnitude applied on throw (scales with the prop\u0027s mass \u2014 tune per game). Defaults to 20000.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022carryTag\u0022\u003EOnly objects with this tag (and a Rigidbody) can be picked up; lower-cased/underscored to match s\u0026amp;box tag convention. Defaults to \u0027carryable\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of the PLAYER GameObject (the one with the camera) to attach to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_carry_system\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateCarrySystem( string name = null, string directory = null, double? range = null, double? throwForce = null, string carryTag = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_carry_system\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022range\u0022, range ), ( \u0022throwForce\u0022, throwForce ), ( \u0022carryTag\u0022, carryTag ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a host-authoritative currency ACCOUNT component (sealed) \u2014 the audited sibling of\r\n\t/// create_economy_wallet (wallet = simple money, account = money \u002B a ledger). Balance is\r\n\t/// [Sync(SyncFlags.FromHost)] so clients can\u0027t author their own money; host-guarded Deposit(amount,\r\n\t/// reason), Withdraw(amount, reason) -\u0026gt; bool, and TryTransfer(otherAccount, amount, reason)\r\n\t/// -\u0026gt; bool each record a Transaction { Time (Time.Now), signed Amount, Reason, BalanceAfter }\r\n\t/// into a fixed-size ring buffer (historySize, default 32; oldest entries overwritten SILENTLY).\r\n\t/// GetRecentTransactions(max) returns them NEWEST FIRST \u2014 the ledger is HOST-SIDE ONLY and does not\r\n\t/// replicate (Balance does); proxies get an empty list. Bind the instance OnBalanceChanged(long)\r\n\t/// for HUD labels. Single-player safe. Returns { created, path, className, startingBalance,\r\n\t/// historySize, placedOn, note, nextSteps }. Next: trigger_hotload, then attach via targetId re-run\r\n\t/// or add_component_to_new_object. Refuses if the file already exists; refused during play mode.\r\n\t/// Use create_economy_wallet when you don\u0027t need the audit trail; pair with create_idle_economy (it\r\n\t/// auto-wires this account\u0027s Money/TrySpend).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated component. Defaults to \u0027CurrencyAccount\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022startingBalance\u0022\u003EBalance the account opens with (host seeds it in OnStart). Defaults to 0.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022historySize\u0022\u003ETransaction ring-buffer capacity (clamped 1..4096); fixed once the first transaction is recorded, oldest overwritten silently after that. Defaults to 32.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of a per-player/bank GameObject to attach to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_currency_account\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateCurrencyAccount( string name = null, string directory = null, int? startingBalance = null, int? historySize = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_currency_account\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022startingBalance\u0022, startingBalance ), ( \u0022historySize\u0022, historySize ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a networked coin / currency pickup component (sealed, Component.ITriggerListener).\r\n\t/// Host-spawned; when a GameObject carrying PlayerTag (\u0027player\u0027) enters its trigger the HOST\r\n\t/// validates and grants Value (default 1) into a wallet on the player, then destroys the pickup\r\n\t/// network-wide (the host Destroy() replicates \u2014 there is no NetworkDestroy on this SDK). Optional\r\n\t/// magnet: while MagnetRadius (default 0 = off) is \u0026gt; 0 the coin accelerates toward the nearest\r\n\t/// player each FixedUpdate (host-side, capped by MaxMagnetSpeed). IsProxy guards keep the grant \u002B\r\n\t/// despawn host-only in multiplayer (NetworkSpawn the coin on the host); single-player works with\r\n\t/// no networking. The deposit is reflection-free and dependency-free: a static Grant seam is wired\r\n\t/// ONCE to the direct typed call \u2014 player.Components.Get\u0026lt;EconomyWallet\u0026gt;()?.AddMoney(amount) \u2014\r\n\t/// so the component compiles with NO hard reference to a specific wallet class (rename the wallet\r\n\t/// type if yours differs; mirrors create_pickup\u0027s self-contained convention). WalletComponentName\r\n\t/// (default \u0027EconomyWallet\u0027) is used to locate the wallet and name the fix if Grant is left unwired\r\n\t/// (never silent). Pairs with create_economy_wallet (AddMoney/TrySpend/CanAfford) and\r\n\t/// create_floating_combat_text (spawn a \u0027\u002BN\u0027 popup from OnCollected).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated component. Defaults to \u0027CurrencyPickup\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022value\u0022\u003EHow much currency the pickup grants into the wallet. Defaults to 1.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022magnetRadius\u0022\u003EMagnet range in world units \u2014 within it the coin flies to the nearest player each FixedUpdate. 0 = magnet off. Defaults to 0.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022walletComponentName\u0022\u003EType name of the wallet component to deposit into (used to locate it and to name the fix if the Grant seam is left unwired). Defaults to \u0027EconomyWallet\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of a coin GameObject to attach to \u2014 give it a trigger Collider (SphereCollider, IsTrigger=true). Only attaches if the type is already loaded \u2014 hotload first.\u003C/param\u003E\r\n\t[McpTool( \u0022create_currency_pickup\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateCurrencyPickup( string name = null, string directory = null, int? value = null, double? magnetRadius = null, string walletComponentName = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_currency_pickup\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022value\u0022, value ), ( \u0022magnetRadius\u0022, magnetRadius ), ( \u0022walletComponentName\u0022, walletComponentName ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a host-authoritative time-of-day clock: [Sync(SyncFlags.FromHost)] TimeOfDay (0\u201324) \u002B\r\n\t/// Day advancing by Time.Delta, IsDay/IsNight from sunrise/sunset hours, and static OnNewDay /\r\n\t/// OnDayNightChanged events to drive lighting, NPC schedules, or spawns. Single-player safe. Pairs\r\n\t/// with create_round_phase_machine. Optionally attached to a GameObject by GUID (after a hotload).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name. Defaults to \u0027DayNightClock\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022dayLengthSeconds\u0022\u003EReal seconds per in-game day. Defaults to 600 (10 min).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022startHour\u0022\u003EHour the clock starts at (0\u201324). Defaults to 8.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022sunriseHour\u0022\u003EHour day begins. Defaults to 6.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022sunsetHour\u0022\u003EHour night begins. Defaults to 20.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of an existing GameObject to attach to (hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_day_night_clock\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateDayNightClock( string name = null, string directory = null, double? dayLengthSeconds = null, double? startHour = null, double? sunriseHour = null, double? sunsetHour = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_day_night_clock\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022dayLengthSeconds\u0022, dayLengthSeconds ), ( \u0022startHour\u0022, startHour ), ( \u0022sunriseHour\u0022, sunriseHour ), ( \u0022sunsetHour\u0022, sunsetHour ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a host-authoritative currency Wallet component: a [Sync(SyncFlags.FromHost)] Money\r\n\t/// balance (only the host can write it \u2014 plain [Sync] money is the classic economy exploit) with\r\n\t/// AddMoney / TrySpend / SetMoney / CanAfford and an OnMoneyChanged event. Single-player safe.\r\n\t/// Optionally attached to an existing GameObject by GUID (after a hotload). Pairs with a save\r\n\t/// system for persistence. Mined from the most-requested currency pattern across 51 games.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name. Defaults to \u0027Wallet\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022startingMoney\u0022\u003EInitial balance the host seeds on start. Defaults to 0.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of an existing GameObject to attach the Wallet to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_economy_wallet\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateEconomyWallet( string name = null, string directory = null, int? startingMoney = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_economy_wallet\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022startingMoney\u0022, startingMoney ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a self-contained, host-authoritative elo rating component: standard elo math (expected\r\n\t/// = 1/(1\u002B10^((Rb-Ra)/400)), delta = K * (score - expected)) with a [Property] K-factor, ratings in\r\n\t/// a [Sync(SyncFlags.FromHost)] NetDictionary\u0026lt;long,float\u0026gt; keyed by SteamId, and host-side\r\n\t/// persistence via FileSystem.Data JSON. API on a static Instance: ReportMatch(winnerSteamId,\r\n\t/// loserSteamId) for 1v1 and ReportTeamMatch(winnerIds, loserIds) for teams (team-average elo,\r\n\t/// uniform delta per member) \u2014 both are IsProxy-guarded no-ops on clients; GetRating(steamId) works\r\n\t/// anywhere (unknown players = defaultRating); the static OnRatingChanged(steamId, newRating) fires\r\n\t/// on EVERY machine via an [Rpc.Broadcast]. Returns { created, path, className, kFactor,\r\n\t/// defaultRating, placedOn, note, nextSteps }. After trigger_hotload: place ONE in the scene and\r\n\t/// network its GameObject (network_spawn) or the [Sync] never replicates. Only the HOST\u0027s disk\r\n\t/// holds the ratings ledger. Fails if the file already exists.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated component. Defaults to \u0027EloRatingSystem\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022kFactor\u0022\u003EElo K-factor \u2014 how far one result moves ratings (32 = fast, 16 = stable). Defaults to 32, clamped to \u0026gt;= 1.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022defaultRating\u0022\u003ERating assigned to players with no recorded matches. Defaults to 1000.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022fileName\u0022\u003ESave file name inside FileSystem.Data (host-side ledger). Defaults to \u0027elo_ratings.json\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of a GameObject to attach to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_elo_rating_system\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateEloRatingSystem( string name = null, string directory = null, double? kFactor = null, double? defaultRating = null, string fileName = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_elo_rating_system\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022kFactor\u0022, kFactor ), ( \u0022defaultRating\u0022, defaultRating ), ( \u0022fileName\u0022, fileName ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a typed LOCAL pub/sub event bus: a pure STATIC class (NOT a Component \u2014 nothing to\r\n\t/// place in the scene) with Subscribe\u0026lt;T\u0026gt;(owner, Action\u0026lt;T\u0026gt;), Unsubscribe(owner) (removes\r\n\t/// all of that owner\u0027s handlers across every event type), Publish\u0026lt;T\u0026gt;(evt) (synchronous,\r\n\t/// exact-type-T subscribers only, snapshot-iterated so handlers may subscribe/unsubscribe\r\n\t/// mid-publish), Count\u0026lt;T\u0026gt;() and Clear(), keyed by a plain Dictionary\u0026lt;Type,\r\n\t/// List\u0026lt;(object, Delegate)\u0026gt;\u0026gt; \u2014 plus a tiny example event record ({name}Ping). Decouples\r\n\t/// game systems: the quest system publishes \u0027EnemyDied\u0027, UI and achievements subscribe, neither\r\n\t/// knows the other. Returns {created, path, className, exampleEvent, api[], note}. Next:\r\n\t/// trigger_hotload \u002B get_compile_errors, then Subscribe in components\u0027 OnStart and \u2014 REQUIRED \u2014\r\n\t/// Unsubscribe(this) in OnDestroy: handler lists hold PLAIN references (no weak refs), so a\r\n\t/// component that never unsubscribes leaks itself for the scene\u0027s life; call Clear() on scene\r\n\t/// teardown. Limits: LOCAL only \u2014 Publish reaches the calling machine\u0027s subscribers, NOT other\r\n\t/// clients; for networked events pair with [Rpc.Broadcast]/[Rpc.Host] methods that Publish on\r\n\t/// arrival. No base-type dispatch (Publish\u0026lt;Base\u0026gt; won\u0027t reach Subscribe\u0026lt;Derived\u0026gt;).\r\n\t/// Refuses to overwrite an existing file; refused during play mode.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EStatic class/file name. Defaults to \u0027EventBus\u0027. The example event record is named {name}Ping. Sanitized to a valid C# identifier.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory under the project root for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t[McpTool( \u0022create_event_bus\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateEventBus( string name = null, string directory = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_event_bus\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a generalized L4D-style AI/pacing director component (host-authoritative). On a\r\n\t/// configurable interval the host rolls a weighted pick over a [Property] List\u0026lt;GameObject\u0026gt;\r\n\t/// EventPrefabs (with a parallel List\u0026lt;float\u0026gt; Weights), skips any event already active\r\n\t/// (dedupe) and anything past a MaxActive concurrency cap, clones the chosen prefab, NetworkSpawns\r\n\t/// it, and attaches a generated {name}TimedEvent companion so each spawned event self-destructs\r\n\t/// after EventLifetime seconds. Great for ambient events, waves, and world events. Single-player\r\n\t/// safe (IsProxy guard; NetworkSpawn falls back to a local clone). Fill EventPrefabs/Weights in the\r\n\t/// inspector or via the bridge after a hotload; edit the RollInterval() stub to make pacing\r\n\t/// adaptive (player-count/inactivity/time-pressure factors) per the ai-director cookbook.\r\n\t/// Optionally attached to an existing GameObject by GUID (after a hotload). NOTE: emits ONE .cs\r\n\t/// file containing two classes ({name} \u002B {name}TimedEvent); the type only resolves after\r\n\t/// trigger_hotload.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the director (a {name}TimedEvent companion is generated alongside it). Defaults to \u0027EventDirector\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022path\u0022\u003ESubdirectory for the generated .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022intervalSeconds\u0022\u003EBase seconds between director rolls. Defaults to 30 (clamped to \u0026gt;= 0.1).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022maxActive\u0022\u003EMaximum number of concurrently-live events. Defaults to 3 (clamped to \u0026gt;= 1).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022eventLifetime\u0022\u003ESeconds before each spawned event self-destructs. Defaults to 60 (clamped to \u0026gt;= 0.1).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of an existing GameObject to attach the director to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_event_director\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateEventDirector( string name = null, string path = null, double? intervalSeconds = null, int? maxActive = null, double? eventLifetime = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_event_director\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022path\u0022, path ), ( \u0022intervalSeconds\u0022, intervalSeconds ), ( \u0022maxActive\u0022, maxActive ), ( \u0022eventLifetime\u0022, eventLifetime ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a host-authoritative gacha / loot-box roller component. Two-level pick: parallel\r\n\t/// [Property] lists RarityNames \u002B RarityWeights select a RARITY by cumulative weight (the\r\n\t/// create_weighted_loot_table shape), then a flat \u0027Rarity:Item\u0027 [Property] list (e.g.\r\n\t/// \u0027Legendary:Dragon Fang\u0027) picks an ITEM uniformly within that rarity \u2014 simple and\r\n\t/// inspector-editable. A pity counter (PityAfter, default 50) guarantees the rarest tier (the LAST\r\n\t/// entry in RarityNames) after N rolls without it and resets on a hit. Duplicate detection against\r\n\t/// an owned-items set fires a host-side OnDuplicate hook (marked TODO: convert dupes to\r\n\t/// shards/currency). Roll() routes to the host via an [Rpc.Host] RequestRoll (Rpc.Caller\r\n\t/// re-validated \u2014 NetFlags is not security) and the result fans out via [Rpc.Broadcast] so every\r\n\t/// machine fires the static OnRolled(rarity, item, isDuplicate) event; single-player safe (RPCs run\r\n\t/// locally). Use create_weighted_loot_table instead for a simpler single-tier weighted pick with no\r\n\t/// pity/dupe/networking. Pairs with create_economy_wallet (spend currency to roll) and\r\n\t/// create_inventory (store the pulls).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated component. Defaults to \u0027GachaDropTable\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022pityAfter\u0022\u003ERolls without a rarest-tier hit before the next roll is guaranteed rarest. 0 disables pity. Defaults to 50.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of a per-player/manager GameObject to attach to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_gacha_drop_table\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateGachaDropTable( string name = null, string directory = null, int? pityAfter = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_gacha_drop_table\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022pityAfter\u0022, pityAfter ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a minimal game-manager Component: a static Instance singleton, [Property] MaxPlayers /\r\n\t/// GameState, and a Component.INetworkListener OnActive hook that logs player connects. Writes\r\n\t/// \u0026lt;name\u0026gt;.cs and returns { created, path, className }. NOTE: the\r\n\t/// includeScore/includeTimer/includeSpawning params are not currently applied \u2014 the same minimal\r\n\t/// manager is always generated (for richer game-loop scaffolds see create_round_phase_machine /\r\n\t/// create_objective_system / create_economy_wallet). Follow with trigger_hotload, then\r\n\t/// get_compile_errors, then place via add_component_to_new_object.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name. Defaults to \u0027GameManager\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory under code/ for the file.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022includeScore\u0022\u003EInclude score tracking (currently not applied by the handler).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022includeTimer\u0022\u003EInclude round timer with countdown (currently not applied by the handler).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022includeSpawning\u0022\u003EInclude player spawning from prefab at spawn point (currently not applied by the handler).\u003C/param\u003E\r\n\t[McpTool( \u0022create_game_manager\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateGameManager( string name = null, string directory = null, bool? includeScore = null, bool? includeTimer = null, bool? includeSpawning = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_game_manager\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022includeScore\u0022, includeScore ), ( \u0022includeTimer\u0022, includeTimer ), ( \u0022includeSpawning\u0022, includeSpawning ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a Health component: MaxHealth, [Sync] CurrentHealth, TakeDamage/Heal, an OnDeath event,\r\n\t/// optional regen and respawn. Host-authoritative damage when networked, single-player safe.\r\n\t/// Optionally attached to an existing GameObject by GUID.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name. Defaults to \u0027Health\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022maxHealth\u0022\u003EStarting/maximum health. Defaults to 100.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022regen\u0022\u003EInclude passive health regeneration after a delay. Defaults to false.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022respawn\u0022\u003EOn death, respawn at a RespawnPoint (wire it with set_component_reference) instead of disabling. Defaults to false.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of an existing GameObject to attach the Health component to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_health_system\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateHealthSystem( string name = null, string directory = null, double? maxHealth = null, bool? regen = null, bool? respawn = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_health_system\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022maxHealth\u0022, maxHealth ), ( \u0022regen\u0022, regen ), ( \u0022respawn\u0022, respawn ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a hold-to-confirm action component (sealed Component). While a named input action is\r\n\t/// held (Input.Down), a public Progress value fills 0\u21921 over [Property] float HoldSeconds;\r\n\t/// releasing early snaps back to 0, or drains down if [Property] bool DecayOnRelease. Reaching 1\r\n\t/// fires the static OnConfirmed(GameObject) event, then a short CooldownSeconds blocks\r\n\t/// re-triggering. The classic \u0027hold E to disarm / open / revive\u0027 interaction. No UI is generated \u2014\r\n\t/// read the public Progress (0..1) from your own HUD to draw a radial or bar; a #region Feedback\r\n\t/// hook marks where to tie in a sound/effect. LOCAL/owner-only: input is IsProxy-guarded so it\r\n\t/// never fires on proxies and is single-player safe. For a host-authoritative outcome, call an\r\n\t/// [Rpc.Host] from inside the OnConfirmed subscriber. Attach to the player (or any owned object\r\n\t/// that reads input); optionally attach to an existing GameObject by GUID after a hotload.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated component. Defaults to \u0027HoldToConfirm\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022action\u0022\u003EInput action name that must be held (must exist in the project\u0027s Input settings \u2014 see ensure_input_action). Defaults to \u0027use\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022holdSeconds\u0022\u003ESeconds of continuous hold required to confirm. Defaults to 1.5.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022decayOnRelease\u0022\u003EBaked default for DecayOnRelease: if true, releasing early drains Progress back down instead of snapping to 0 (editable per-instance). Defaults to false.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of a GameObject to attach the component to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_hold_to_confirm\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateHoldToConfirm( string name = null, string directory = null, string action = null, double? holdSeconds = null, bool? decayOnRelease = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_hold_to_confirm\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022action\u0022, action ), ( \u0022holdSeconds\u0022, holdSeconds ), ( \u0022decayOnRelease\u0022, decayOnRelease ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a geometric idle-economy component (sealed): generators on the classic BaseCost *\r\n\t/// Growth^Owned cost curve with Buy 1 / Buy N / Buy Max \u2014 CostOf(index, count),\r\n\t/// MaxAffordable(index), TryBuy(index, count) and BuyMax(index) all use the CLOSED-FORM geometric\r\n\t/// series (cost = c0*(g^n-1)/(g-1), buyMax = floor(log_g(funds*(g-1)/c0\u002B1))) \u2014 no per-copy loops,\r\n\t/// Buy 1000 is the same math as Buy 1. Wallet wiring is TypeLibrary reflection with NO compile-time\r\n\t/// wallet dependency (the shipped create_idle_income pattern): each income tick invokes\r\n\t/// AddMoney(long|int) on the first sibling component that has one, purchases invoke\r\n\t/// TrySpend(long|int), Buy Max reads the sibling\u0027s Money (or Balance) property \u2014 works out of the\r\n\t/// box next to create_economy_wallet or create_currency_account; with NO wallet sibling, purchases\r\n\t/// are refused with a Log.Warning (never silent) while TotalEarned still accumulates.\r\n\t/// Host-authoritative: mutations IsProxy-guarded; owned counts are HOST-SIDE state (not\r\n\t/// replicated); TotalEarned is [Sync(FromHost)]. Static events OnPurchased(index, count, cost) and\r\n\t/// OnIncomeTick(amount, total). BuyMax steps down once past a whole-currency rounding edge rather\r\n\t/// than failing. Returns { created, path, className, generators, tickSeconds, placedOn, note,\r\n\t/// nextSteps }. Next: trigger_hotload, place it NEXT TO a wallet on the same GameObject, tune the\r\n\t/// parallel GeneratorNames/BaseCosts/Growths/IncomesPerSecond lists with set_property. Refused\r\n\t/// during play mode. Pair with create_offline_progress for away-time earnings; use\r\n\t/// create_idle_income for a bare income ticker with no purchasing.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated component. Defaults to \u0027IdleEconomy\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022tickSeconds\u0022\u003ESeconds between income grants (floored at 0.1). Defaults to 1.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022generators\u0022\u003EBaked-in generator defaults (inspector-tunable after generation). Omit for a starter trio: Cursor 15/1.15/0.5, Farm 200/1.15/4, Factory 3000/1.12/30. JSON array.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of the GameObject to attach to \u2014 put it on the SAME GameObject as the wallet so the reflection wiring finds it (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_idle_economy\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateIdleEconomy( string name = null, string directory = null, double? tickSeconds = null, JsonNode generators = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_idle_economy\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022tickSeconds\u0022, tickSeconds ), ( \u0022generators\u0022, generators ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a host-authoritative passive income component: every tickSeconds the host grants\r\n\t/// incomePerTick \u00D7 Multiplier, auto-wiring the first sibling component with an AddMoney(int) method\r\n\t/// (a create_economy_wallet scaffold plugs in with zero code) or an overridable Grant() seam;\r\n\t/// TotalEarned is [Sync(FromHost)] and static OnIncomeTick fires per grant. The idle-game kit:\r\n\t/// wallet (create_economy_wallet) \u002B this \u002B create_offline_progress. Writes a .cs file and returns {\r\n\t/// created, path, className, nextSteps } \u2014 follow with trigger_hotload \u002B compile_status.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass/file name (default \u0027IdleIncome\u0027 -\u0026gt; Code/IdleIncome.cs). Errors if the file exists.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003EDirectory for the .cs file. Default \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022incomePerTick\u0022\u003EAmount granted per tick. Default 1.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022tickSeconds\u0022\u003ESeconds between grants. Default 1.\u003C/param\u003E\r\n\t[McpTool( \u0022create_idle_income\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateIdleIncome( string name = null, string directory = null, double? incomePerTick = null, double? tickSeconds = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_idle_income\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022incomePerTick\u0022, incomePerTick ), ( \u0022tickSeconds\u0022, tickSeconds ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a Component.IPressable interactable: the built-in PlayerController \u0027use\u0027 key drives\r\n\t/// Press()/Hover()/Blur() with no custom player code. Includes a static OnPressed event, an\r\n\t/// optional cooldown (TimeUntil), and a private OnPress() extensionpoint for effects. For\r\n\t/// host-authoritative side-effects call an [Rpc.Host] from OnPress(). The Prompt property is left\r\n\t/// to your game\u0027s HUD. Optionally attached to an existing GameObject by GUID (after a hotload).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name. Defaults to \u0027Interactable\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022prompt\u0022\u003EPrompt string shown by the game\u0027s HUD when hovering. Defaults to \u0027Press\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022cooldownSeconds\u0022\u003ESeconds before the interactable can be pressed again. 0 = no cooldown. Defaults to 0.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of an existing GameObject to attach the component to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_interactable\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateInteractable( string name = null, string directory = null, string prompt = null, double? cooldownSeconds = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_interactable\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022prompt\u0022, prompt ), ( \u0022cooldownSeconds\u0022, cooldownSeconds ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a slot-based inventory component using parallel List\u0026lt;string\u0026gt; ItemIds /\r\n\t/// List\u0026lt;int\u0026gt; Counts (serialization-safe, inspector-editable). Includes TryAdd (stack-first,\r\n\t/// partial-add rejected), TryRemove, CountOf, Move (swap or merge same-id slots), and Clear. Static\r\n\t/// OnChanged event fires after every successful mutation. Host-authoritative usage note: mutate on\r\n\t/// the host in multiplayer, replicate via your own [Sync]/RPC. Pairs with create_pickup.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name. Defaults to \u0027Inventory\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022capacity\u0022\u003ETotal slot count. Defaults to 24.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022maxStack\u0022\u003EMaximum items per slot (stack cap). Defaults to 99.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of an existing GameObject to attach to (hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_inventory\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateInventory( string name = null, string directory = null, int? capacity = null, int? maxStack = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_inventory\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022capacity\u0022, capacity ), ( \u0022maxStack\u0022, maxStack ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a Razor PanelComponent that fetches and displays a Sandbox.Services leaderboard derived\r\n\t/// from a stat name. Produces TWO files: {name}.razor and {name}.razor.scss. The panel\r\n\t/// auto-refreshes every 30 s, shows rank/displayName/value rows, handles loading state, and\r\n\t/// includes a BuildHash() override (razor-lint clean). Must be hosted under a ScreenPanel or\r\n\t/// WorldPanel. Stats must be configured for the project ident on sbox.game. Uses\r\n\t/// Leaderboards.Get(statName) \u002B board.Refresh() -- the exact API from ServicesQueryHandler. Returns\r\n\t/// { created, razorPath, scssPath, className, note }. Follow with trigger_hotload, then\r\n\t/// get_compile_errors, then host it via add_screen_panel (panelComponent=className).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the panel component. Defaults to \u0027LeaderboardPanel\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated files. Defaults to \u0027Code/UI\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022statName\u0022\u003ESandbox.Services stat name the leaderboard is derived from. Defaults to \u0027score\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022title\u0022\u003EDisplay title shown at the top of the panel. Defaults to \u0027Leaderboard\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022maxRows\u0022\u003EMaximum leaderboard rows to fetch and display. Defaults to 10.\u003C/param\u003E\r\n\t[McpTool( \u0022create_leaderboard_panel\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateLeaderboardPanel( string name = null, string directory = null, string statName = null, string title = null, int? maxRows = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_leaderboard_panel\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022statName\u0022, statName ), ( \u0022title\u0022, title ), ( \u0022maxRows\u0022, maxRows ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate GameResource-based loot tables \u2014 the data-asset sibling of create_weighted_loot_table.\r\n\t/// One .cs file containing THREE types: an entry POCO { Name, Weight, optional NestedTable\r\n\t/// reference }, a [AssetType]-registered GameResource loot-table class (designers author \u0027.loot\u0027\r\n\t/// files in the editor asset browser \u2014 New \u0026gt; Loot Table \u2014 after the hotload; NOTE:\r\n\t/// [AssetType(Name=..., Extension=..., Category=...)] is used because GameResourceAttribute is\r\n\t/// [Obsolete] on this SDK), and a \u0027\u0026lt;name\u0026gt;Resolver\u0027 Component that rolls an assigned table by\r\n\t/// cumulative weight. Nested tables: an entry with a NestedTable rolls INTO that table instead of\r\n\t/// dropping its Name, capped at maxDepth (default 4) with a self-reference guard so cycles\r\n\t/// terminate (at the cap the deepest entry\u0027s Name is returned). Resolver.Roll() returns the item\r\n\t/// name (null \u002B warning when no Table is assigned or the table is empty; entries with weight \u0026lt;=\r\n\t/// 0 never win; all-zero weights fall back to the first entry) and fires the static\r\n\t/// OnLoot(GameObject, item) event; roll HOST-SIDE and replicate the result yourself. targetId\r\n\t/// attaches the RESOLVER (the resource is an asset type, not a component). SURPRISING: pick an\r\n\t/// extension that is NOT a suffix of a built-in one (e.g. avoid \u0027cfg\u0027) or ResourceLibrary picks up\r\n\t/// engine files as phantom instances. Returns { created, path, className, resolverClass, extension,\r\n\t/// maxDepth, placedOn, note, nextSteps }. Next: trigger_hotload -\u0026gt; author .loot assets in the\r\n\t/// editor -\u0026gt; assign the resolver\u0027s Table (set_property with the asset path). Refused during play\r\n\t/// mode. Use create_weighted_loot_table for a single inline component with no asset files;\r\n\t/// create_gacha_drop_table for pity \u002B duplicate mechanics.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated GameResource (the resolver becomes \u0027\u0026lt;name\u0026gt;Resolver\u0027). Defaults to \u0027LootTableResource\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022extension\u0022\u003EAsset file extension (lowercase alphanumerics; avoid suffixes of built-in extensions like \u0027cfg\u0027). Defaults to \u0027loot\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022title\u0022\u003EDisplay name of the asset type in the editor\u0027s New-asset menu. Defaults to \u0027Loot Table\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022maxDepth\u0022\u003EDefault nested-table resolve depth cap baked into the resolver (clamped 0..16; also a [Property]). Defaults to 4.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of a GameObject to attach the RESOLVER component to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_loot_table_resource\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateLootTableResource( string name = null, string directory = null, string extension = null, string title = null, int? maxDepth = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_loot_table_resource\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022extension\u0022, extension ), ( \u0022title\u0022, title ), ( \u0022maxDepth\u0022, maxDepth ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a between-runs roguelite meta-progression component (sealed, owner-only): persistent\r\n\t/// meta-currency \u002B an unlock-flag dictionary saved to FileSystem.Data JSON (dirty-flag autosave \u002B\r\n\t/// OnDestroy, the create_save_system shape). API: Grant(long), TrySpend(long) -\u0026gt; bool,\r\n\t/// Unlock(key) (idempotent \u2014 the static OnUnlocked(key) event fires only on the FIRST unlock, and\r\n\t/// unlocks write through to disk immediately), IsUnlocked(key) -\u0026gt; bool, and the run-end seam\r\n\t/// BankRun(int earned) which converts a finished run\u0027s earnings into meta-currency, bumps\r\n\t/// RunsBanked, and saves immediately \u2014 call it from your round machine\u0027s end-of-run transition\r\n\t/// (create_round_state_machine / create_round_phase_machine). Instance OnCurrencyChanged(long)\r\n\t/// drives meta-shop balance labels. Versioned payload: old-version files start fresh.\r\n\t/// IsProxy-guarded \u2014 in multiplayer each machine banks only its own local meta file (this is\r\n\t/// per-machine persistence, not a server economy). Returns { created, path, className, fileName,\r\n\t/// version, placedOn, note, nextSteps }. Next: trigger_hotload, attach to a persistent\r\n\t/// hub/menu-scene manager GameObject, gate content with IsUnlocked when building the player.\r\n\t/// Refused during play mode. Pair with create_currency_account (in-run money) and\r\n\t/// create_signed_save (if the meta file needs tamper evidence \u2014 this one is unsigned).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated component. Defaults to \u0027MetaProgression\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022fileName\u0022\u003EFileSystem.Data path the meta state is written to. Defaults to \u0027meta.json\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022version\u0022\u003EPayload version; mismatched files start fresh. Defaults to 1.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022autosaveSeconds\u0022\u003EDirty-flag autosave cadence in seconds; 0 disables the heartbeat (unlocks and BankRun still write through immediately). Defaults to 10.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of a persistent manager GameObject to attach to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_meta_progression\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateMetaProgression( string name = null, string directory = null, string fileName = null, int? version = null, double? autosaveSeconds = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_meta_progression\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022fileName\u0022, fileName ), ( \u0022version\u0022, version ), ( \u0022autosaveSeconds\u0022, autosaveSeconds ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a sim/tycoon needs engine component: a [Property] list of need definitions (name, decay\r\n\t/// rate/s, critical threshold, weight) with per-need 0..100 values that decay over Time.Delta,\r\n\t/// Satisfy(name, amount) to restore, an aggregate Happiness (weighted mean, [Sync(FromHost)] when\r\n\t/// networked), and static OnNeedCritical (edge-triggered: fires once crossing below threshold,\r\n\t/// re-arms above) \u002B OnHappinessChanged (\u0026gt;0.25-point moves) events. Returns {created, path,\r\n\t/// className, needs[], propertyNames[], note}. Next: trigger_hotload, get_compile_errors, then\r\n\t/// attach via targetId re-call or add_component_with_properties; drive from game code (e.g. a\r\n\t/// create_interactable that calls Satisfy). Limits: per-need values live on the simulating machine\r\n\t/// only (host) \u2014 sync per-need UI yourself via RPCs; events fire on the simulating machine only;\r\n\t/// networked default true means a no-session solo playtest won\u0027t tick (everything is a proxy) \u2014\r\n\t/// pass networked:false to iterate solo. Refused during play mode; refuses to overwrite an existing\r\n\t/// file.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass/file name. Defaults to \u0027NeedsSystem\u0027. Sanitized to a valid C# identifier.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory under the project root for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022needs\u0022\u003ENeed definitions baked as inspector-editable defaults. Defaults to the classic sim trio: Hunger(0.8/s), Energy(0.5/s), Fun(0.3/s). JSON array.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022networked\u0022\u003Etrue (default): host-authoritative (IsProxy guard) \u002B [Sync(FromHost)] Happiness \u2014 needs a host session. false: local build that ticks in a solo playtest.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of a GameObject to attach the component to (only attaches if the type is already in the TypeLibrary \u2014 hotload first, then re-call or use add_component_with_properties).\u003C/param\u003E\r\n\t[McpTool( \u0022create_needs_system\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateNeedsSystem( string name = null, string directory = null, JsonNode needs = null, bool? networked = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_needs_system\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022needs\u0022, needs ), ( \u0022networked\u0022, networked ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate an NPC controller script with NavMeshAgent pathfinding. Supports patrol, chase, and\r\n\t/// patrol-chase behaviors.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name. Defaults to \u0027NpcController\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory under code/ for the file.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022behavior\u0022\u003EAI behavior: \u0027patrol\u0027 (follow waypoints), \u0027chase\u0027 (follow player), \u0027patrol_chase\u0027 (patrol until player nearby). Defaults to \u0027patrol\u0027. One of: patrol | chase | patrol_chase.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022moveSpeed\u0022\u003EMovement speed. Defaults to 150.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022chaseRange\u0022\u003EDetection range for chase behavior. Defaults to 500.\u003C/param\u003E\r\n\t[McpTool( \u0022create_npc_controller\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateNpcController( string name = null, string directory = null, string behavior = null, double? moveSpeed = null, double? chaseRange = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_npc_controller\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022behavior\u0022, behavior ), ( \u0022moveSpeed\u0022, moveSpeed ), ( \u0022chaseRange\u0022, chaseRange ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate an ObjectiveManager component \u2014 the win/lose brain of a game. Tracks an objective\r\n\t/// (collect_all / reach_goal / survive_time / eliminate_all), fires a win, and handles a lose\r\n\t/// condition (fall below kill-Z / timer / out of lives). Self-contained C#; other systems call\r\n\t/// ObjectiveManager.Instance. Optionally placed as a scene singleton. Returns { created, path,\r\n\t/// className, gameObject, note } \u2014 gameObject is the placed singleton, or null with a note when the\r\n\t/// fresh type isn\u0027t in the TypeLibrary yet. Follow with trigger_hotload, then get_compile_errors;\r\n\t/// if placement was skipped, place with add_component_to_new_object after the hotload.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name. Defaults to \u0027ObjectiveManager\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022objective\u0022\u003EWin condition. Defaults to \u0027reach_goal\u0027. One of: collect_all | reach_goal | survive_time | eliminate_all.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetCount\u0022\u003EHow many to collect/eliminate (for collect_all / eliminate_all). Defaults to 3.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022timeLimit\u0022\u003ESeconds \u2014 survive this long to win (survive_time) or before losing (loseOn=timer). Defaults to 60.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022loseOn\u0022\u003ELose condition. \u0027fall\u0027 = player drops below killZ. Defaults to \u0027fall\u0027. One of: fall | timer | lives | none.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022killZ\u0022\u003EWorld Z below which the player is considered fallen out of the world. Defaults to -1000.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022lives\u0022\u003ELives before game over (loseOn=lives). Defaults to 1.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022placeInScene\u0022\u003EPlace the manager as a scene singleton. Defaults to true. (Only attaches if the type is already loaded \u2014 generate, hotload, then it places; otherwise add it after hotload.).\u003C/param\u003E\r\n\t[McpTool( \u0022create_objective_system\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateObjectiveSystem( string name = null, string directory = null, string objective = null, int? targetCount = null, double? timeLimit = null, string loseOn = null, double? killZ = null, int? lives = null, bool? placeInScene = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_objective_system\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022objective\u0022, objective ), ( \u0022targetCount\u0022, targetCount ), ( \u0022timeLimit\u0022, timeLimit ), ( \u0022loseOn\u0022, loseOn ), ( \u0022killZ\u0022, killZ ), ( \u0022lives\u0022, lives ), ( \u0022placeInScene\u0022, placeInScene ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate an offline / idle-progress component (sealed, owner/host-only) \u2014 the idle-game staple.\r\n\t/// Persists LastSeenUtc (DateTime) to FileSystem.Data JSON on a dirty-flag autosave heartbeat\r\n\t/// (AutosaveSeconds) and on OnDisabled, copying create_save_system\u0027s persistence patterns. On\r\n\t/// enable it computes elapsed = now \u2212 LastSeenUtc, guards a clock rollback (negative \u2192 0), clamps\r\n\t/// to MaxOfflineHours (default 8), then replays that time through a SimulateOffline(double seconds)\r\n\t/// TODO hook in fixed TickSeconds chunks (default 1) so idle accumulation is deterministic\r\n\t/// (frame-rate independent), and fires the static OnOfflineProgressApplied(seconds) event (drive a\r\n\t/// \u0027welcome back, you earned X\u0027 screen). IsProxy-guarded so a client can\u0027t author their own offline\r\n\t/// earnings. Fill in the SimulateOffline hook with your idle math (e.g.\r\n\t/// wallet.AddMoney(rate*seconds)). Pairs with create_economy_wallet / create_save_system /\r\n\t/// create_stat_modifier_system.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated component. Defaults to \u0027OfflineProgress\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022maxOfflineHours\u0022\u003EOffline time is clamped to this many hours (stops a week-away paying out a week). Defaults to 8.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022tickSeconds\u0022\u003ESimulateOffline chunk size in seconds \u2014 smaller = finer-grained deterministic replay (floored at 0.1). Defaults to 1.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of an idle/save-manager GameObject to attach to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_offline_progress\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateOfflineProgress( string name = null, string directory = null, double? maxOfflineHours = null, double? tickSeconds = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_offline_progress\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022maxOfflineHours\u0022, maxOfflineHours ), ( \u0022tickSeconds\u0022, tickSeconds ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a trigger-based collectible component. On enter by a tagged object it raises\r\n\t/// OnCollected (wire it to your objective/score system) and despawns. Optionally builds a visible\r\n\t/// pickup GameObject with a trigger SphereCollider (\u002B a model) in one call. Returns { created,\r\n\t/// path, className, gameObject, note } \u2014 gameObject is the placed pickup (null unless\r\n\t/// placeInScene=true); a note flags when the component couldn\u0027t attach because the fresh type needs\r\n\t/// a hotload. Follow with trigger_hotload, then get_compile_errors.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name. Defaults to \u0027Pickup\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022action\u0022\u003EEffect flavour (all self-contained; the heal/item branches show the typed call to a companion system in comments). Defaults to \u0027score\u0027. One of: score | heal | item | custom.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022amount\u0022\u003EMagnitude of the effect (score points, heal amount). Defaults to 1.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022filterTag\u0022\u003EOnly collect for objects with this tag. Defaults to \u0027player\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022placeInScene\u0022\u003EAlso build a pickup GameObject (trigger SphereCollider \u002B optional model). Defaults to false.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022position\u0022\u003EWorld position when placeInScene is true. As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022radius\u0022\u003ETrigger sphere radius when placed. Defaults to 24.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022model\u0022\u003EOptional model path for a visible pickup (e.g. \u0027models/dev/box.vmdl\u0027). Cloud assets must be installed first.\u003C/param\u003E\r\n\t[McpTool( \u0022create_pickup\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreatePickup( string name = null, string directory = null, string action = null, double? amount = null, string filterTag = null, bool? placeInScene = null, string position = null, double? radius = null, string model = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_pickup\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022action\u0022, action ), ( \u0022amount\u0022, amount ), ( \u0022filterTag\u0022, filterTag ), ( \u0022placeInScene\u0022, placeInScene ), ( \u0022position\u0022, position ), ( \u0022radius\u0022, radius ), ( \u0022model\u0022, model ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a ghost-preview \u002B commit placement component (single class). StartPlacing() clones\r\n\t/// GhostPrefab as a NetworkMode.Never preview with colliders disabled and ModelRenderers tinted\r\n\t/// semi-transparent. Each frame while placing: ray from Scene.Camera.GetMouseRay(),\r\n\t/// IgnoreGameObjectHierarchy(ghost), snap hit position to GridSize (0 = freeform), move ghost. On\r\n\t/// Input.Pressed(\u0027attack1\u0027) TryPlace() re-validates distance and commits a real clone.\r\n\t/// StopPlacing() destroys the ghost. Static OnPlaced(GameObject, Vector3) event. Includes a\r\n\t/// multiplayer RPC note. API grounded in building-placement cookbook (enifun.shop_manager pattern).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name. Defaults to \u0027PlacementMode\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022gridSize\u0022\u003ESnap grid size in world units (0 = freeform placement). Defaults to 0.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of an existing GameObject to attach to (hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_placement_mode\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreatePlacementMode( string name = null, string directory = null, double? gridSize = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_placement_mode\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022gridSize\u0022, gridSize ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a player controller script with WASD movement, mouse look, jumping, and sprint.\r\n\t/// Supports first-person, third-person, and top-down movement modes. Optionally places a player rig\r\n\t/// (GameObject \u002B CharacterController \u002B Camera) in the scene \u2014 note the generated component is\r\n\t/// attached AFTER a trigger_hotload (it isn\u0027t in the TypeLibrary until a recompile).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name. Defaults to \u0027PlayerController\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory under code/ for the file.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022type\u0022\u003EMovement mode: \u0027first_person\u0027 (mouse-look body\u002Bcamera, WASD relative to facing), \u0027third_person\u0027 (mouse yaw, WASD relative to facing, boom camera), or \u0027top_down\u0027 (screen-relative WASD, fixed overhead camera, no jump). Defaults to \u0027first_person\u0027. One of: first_person | third_person | top_down.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022moveSpeed\u0022\u003EMovement speed in units/sec. Defaults to 300.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022jumpForce\u0022\u003EJump force (ignored for top_down). Defaults to 350.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022sprintMultiplier\u0022\u003ESprint speed multiplier (held \u0027run\u0027 action). Defaults to 1.5.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022placeInScene\u0022\u003EIf true, build a player rig in the scene: a GameObject (tagged \u0027player\u0027) with a CharacterController and (unless createCamera=false) a Camera. The generated controller component is NOT attached in this call \u2014 trigger_hotload then add_component_with_properties on the returned GameObject. Defaults to false (file-only).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022createCamera\u0022\u003EWhen placeInScene is true, also create a Camera (FP/TP: child at eye/boom offset; top_down: fixed overhead). Defaults to true.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022spawnPosition\u0022\u003EWhen placeInScene is true, the world position to spawn the player rig at \u2014 object {x,y,z} or comma string \u0022x,y,z\u0022. Defaults to the origin. As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t[McpTool( \u0022create_player_controller\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreatePlayerController( string name = null, string directory = null, string type = null, double? moveSpeed = null, double? jumpForce = null, double? sprintMultiplier = null, bool? placeInScene = null, bool? createCamera = null, string spawnPosition = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_player_controller\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022type\u0022, type ), ( \u0022moveSpeed\u0022, moveSpeed ), ( \u0022jumpForce\u0022, jumpForce ), ( \u0022sprintMultiplier\u0022, sprintMultiplier ), ( \u0022placeInScene\u0022, placeInScene ), ( \u0022createCamera\u0022, createCamera ), ( \u0022spawnPosition\u0022, spawnPosition ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a host-authoritative round/phase machine: a [Sync(SyncFlags.FromHost)] CurrentPhase\r\n\t/// cycled through your named phases on a per-phase timer (host-only), with a static OnPhaseChanged\r\n\t/// event that fires on every machine. Great for round/match flow, match phases, or a day/night\r\n\t/// cycle. Single-player safe. Optionally attached to an existing GameObject by GUID (after a\r\n\t/// hotload). Mined from the round-flow pattern across the 51 games.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name. Defaults to \u0027GameDirector\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022phases\u0022\u003EOrdered phase names (become an enum), e.g. [\u0022Lobby\u0022,\u0022Day\u0022,\u0022Night\u0022,\u0022Payout\u0022]. Defaults to [\u0022Lobby\u0022,\u0022Active\u0022,\u0022Ended\u0022].\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022duration\u0022\u003EDefault seconds per phase (each phase also gets its own tunable [Property]). Defaults to 60.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022loop\u0022\u003ELoop back to the first phase after the last (true) or hold on the last phase (false). Defaults to true.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of an existing GameObject to attach to (only if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_round_phase_machine\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateRoundPhaseMachine( string name = null, string directory = null, string[] phases = null, double? duration = null, bool? loop = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_round_phase_machine\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022phases\u0022, phases ), ( \u0022duration\u0022, duration ), ( \u0022loop\u0022, loop ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a host-authoritative MULTI-STATE round machine (the complex variant of\r\n\t/// create_round_phase_machine). Produces one .cs file: a RoundManager singleton component \u002B an\r\n\t/// abstract RoundState base (Begin/Tick/OnTimeUp/Finish lifecycle with a per-state\r\n\t/// [Sync(SyncFlags.FromHost)] TimeUntil timer) \u002B one sealed stub class per named state. The manager\r\n\t/// auto-attaches the state components on start (you only place the manager), ticks ONLY the active\r\n\t/// state on the host, Advance()s on timeout with index-wrap, SKIPS any state whose CanEnter()\r\n\t/// returns false, and announces every transition via a static OnStateChanged event plus an\r\n\t/// [Rpc.Broadcast] mirror so the host fires immediately and proxies converge without waiting a\r\n\t/// snapshot (the [Sync] index reconciles late joiners). Single-player safe. USE THIS (not\r\n\t/// create_round_phase_machine) when each phase needs its OWN behaviour \u2014 entry side-effects,\r\n\t/// per-frame Tick logic, a skip condition, or copy-data-out-on-exit; use the phase machine for 3\u20135\r\n\t/// light phases that differ only in duration. Optionally attached to an existing GameObject by GUID\r\n\t/// (after a hotload).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EManager class name. Defaults to \u0027RoundManager\u0027. The abstract base is derived from it (RoundManager \u2192 RoundState).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the .cs file (path override). Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022states\u0022\u003EOrdered state names \u2014 each becomes a sealed {Name}State stub class. Defaults to [\u0022Waiting\u0022,\u0022Active\u0022,\u0022PostRound\u0022].\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022duration\u0022\u003EDefault seconds each state lasts (each state also gets its own tunable [Property] Duration). 0 = no auto-advance for a state. Defaults to 30.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022durations\u0022\u003EOptional per-state duration override: an array aligned to \u0060states\u0060 ([10,120,8]) OR an object keyed by state name ({\u0022Waiting\u0022:10,\u0022Active\u0022:120}). Any state not covered falls back to \u0060duration\u0060. JSON value.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022loop\u0022\u003ELoop back to the first state after the last (true) or hold on the last state (false). Defaults to true.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of an existing GameObject to attach the manager to (only if the type is already loaded \u2014 trigger_hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_round_state_machine\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateRoundStateMachine( string name = null, string directory = null, string[] states = null, double? duration = null, JsonNode durations = null, bool? loop = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_round_state_machine\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022states\u0022, states ), ( \u0022duration\u0022, duration ), ( \u0022durations\u0022, durations ), ( \u0022loop\u0022, loop ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a multi-slot save MANAGER component (the slot-picker sibling of create_save_system).\r\n\t/// Use this when the game needs SEVERAL named save slots the player chooses between (New Game /\r\n\t/// Load Game menu, per-character or per-run saves) \u2014 not one silent autosave. Use\r\n\t/// create_save_system instead when a single implicit save file is enough. Emits one sealed\r\n\t/// Component that lists / creates / loads / saves / deletes N slots: a lightweight manifest file\r\n\t/// (saveslots.json) holds per-slot metadata for the picker (Used flag \u002B Name \u002B SavedAtUnix\r\n\t/// timestamp \u002B PlaytimeSeconds) so listing never loads a heavy payload, and each slot\u0027s game state\r\n\t/// lives in its own saveslot_\u0026lt;i\u0026gt;.json. Versioned SlotData POCO with clamp-on-load Sanitize()\r\n\t/// and delete-on-version-mismatch; runs only on the owning machine (IsProxy guard). Static\r\n\t/// OnSlotLoaded / OnSlotSaved / OnSlotDeleted hooks for HUD. Storage stays within the verified\r\n\t/// FileSystem.Data.ReadJsonOrDefault / WriteJson / DeleteFile surface (index-file pattern, no\r\n\t/// directory enumeration). Set sceneReconciliation:true to also reconcile scene objects by\r\n\t/// GameObject.Id on load \u2014 records the save marks destroyed are destroyed, survivors repositioned,\r\n\t/// missing skipped (good for a placeable-world tycoon). Optionally attached to an existing\r\n\t/// GameObject by GUID (after a hotload).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated component. Defaults to \u0027SaveSlotManager\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022maxSlots\u0022\u003EHow many save slots the manager manages (manifest is normalized to exactly this many, indexed 0..N-1). Clamped to 1..100. Defaults to 3.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022sceneReconciliation\u0022\u003EIf true, saved records carry each object\u0027s GameObject.Id GUID and load reconciles the live scene against them (destroy the save\u0027s destroyed records via Scene.Directory.FindByGuid, reposition survivors, skip missing) \u2014 call RecordObject(go) to track a placeable. If false (default), the slot save is a plain payload with no scene reconciliation. Defaults to false.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of an existing GameObject to attach the manager to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_save_slots\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateSaveSlots( string name = null, string directory = null, int? maxSlots = null, bool? sceneReconciliation = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_save_slots\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022maxSlots\u0022, maxSlots ), ( \u0022sceneReconciliation\u0022, sceneReconciliation ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a versioned save-system component: a SaveData POCO with Version bump on schema change,\r\n\t/// dirty-flag autosave on a TimeUntil timer, clamp-on-load Sanitize() for corrupt/hand-edited\r\n\t/// saves, and delete-on-version-mismatch to start fresh instead of crashing. Runs only on the\r\n\t/// owning machine (IsProxy guard). Fires static OnLoaded/OnSaved hooks for HUD and analytics.\r\n\t/// FileSystem.Data.ReadJsonOrDefault/WriteJson verified live on the current SDK. Optionally\r\n\t/// attached to an existing GameObject by GUID (after a hotload).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name. Defaults to \u0027SaveSystem\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022fileName\u0022\u003ESave file name under FileSystem.Data (e.g. \u0027save.json\u0027). Defaults to \u0027save.json\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022version\u0022\u003ESchema version embedded in SaveData. Old saves with a different version start fresh. Defaults to 1.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022autosaveSeconds\u0022\u003ESeconds between autosave ticks (0 disables autosave). Defaults to 10.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of an existing GameObject to attach to (only if the type is already loaded -- hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_save_system\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateSaveSystem( string name = null, string directory = null, string fileName = null, int? version = null, double? autosaveSeconds = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_save_system\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022fileName\u0022, fileName ), ( \u0022version\u0022, version ), ( \u0022autosaveSeconds\u0022, autosaveSeconds ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a tamper-evident, versioned save-system component (sealed, owner-only). The SaveData\r\n\t/// payload POCO is serialized to JSON (Sandbox.Json), FNV-1a-64 hashed over payload \u002B version \u002B\r\n\t/// salt, and written as a signed envelope { Version, Payload, Signature } to FileSystem.Data.\r\n\t/// Load() re-verifies: a signature mismatch (hand-edited/corrupt file) triggers a FORCED RESET \u2014\r\n\t/// the save file is DELETED, defaults are used, and the static OnTampered(reason) event fires\r\n\t/// (destructive and deliberate; tell the player). A version mismatch starts fresh without the\r\n\t/// tamper event (add migrations in Load). Loaded values pass a Sanitize() clamp hook so even a\r\n\t/// re-signed save can\u0027t smuggle absurd values. Dirty-flag autosave (autosaveSeconds, default 10;\r\n\t/// MarkDirty() to arm) \u002B a final save in OnDestroy. HONEST LIMIT: the salt ships inside the game\r\n\t/// assembly, so this is tamper-EVIDENT (stops notepad edits), NOT cryptographically secure. If you\r\n\t/// omit salt, a unique random one is baked into the generated file \u2014 changing it later invalidates\r\n\t/// existing saves. Returns { created, path, className, fileName, version, autosaveSeconds,\r\n\t/// placedOn, note, nextSteps }. Next: trigger_hotload, attach, add your fields to SaveData \u002B clamps\r\n\t/// to Sanitize(), bump version on shape changes. Refused during play mode. Use create_save_system\r\n\t/// for a plain unsigned save, create_save_slots for multi-slot UI flows, create_meta_progression\r\n\t/// for roguelite meta-state.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated component. Defaults to \u0027SignedSave\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022fileName\u0022\u003EFileSystem.Data path the signed envelope is written to. Defaults to \u0027save_signed.json\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022version\u0022\u003ESave-shape version baked into the file and the signature; mismatched files start fresh. Defaults to 1.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022salt\u0022\u003ESigning salt baked into the generated code. Omit to bake a unique random salt (recommended); changing it later invalidates existing saves.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022autosaveSeconds\u0022\u003EDirty-flag autosave cadence in seconds; 0 disables the heartbeat (OnDestroy still saves). Defaults to 10.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of a save-manager GameObject to attach to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_signed_save\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateSignedSave( string name = null, string directory = null, string fileName = null, int? version = null, string salt = null, double? autosaveSeconds = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_signed_save\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022fileName\u0022, fileName ), ( \u0022version\u0022, version ), ( \u0022salt\u0022, salt ), ( \u0022autosaveSeconds\u0022, autosaveSeconds ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a speedrun timer component plus a leaderboard display panel. The timer\r\n\t/// (\u0026lt;Name\u0026gt;.cs) is TimeSince-based with a static Instance: StartTimer() at run start,\r\n\t/// StopTimer() at the finish (pairs with a trigger zone), ResetTimer() to abort. StopTimer persists\r\n\t/// the local best via FileSystem.Data and submits Stats.SetValue(statName, seconds) ONLY when the\r\n\t/// run beats it \u2014 configure the stat with MIN aggregation on sbox.game so the global board keeps\r\n\t/// best times. The panel (\u0026lt;Name\u0026gt;Panel.razor \u002B .razor.scss, razor_lint clean) fetches via\r\n\t/// Leaderboards.GetFromStat with min aggregation \u002B ascending sort, has a clickable Friends-only\r\n\t/// filter button, and overlays a local-best row read from the same save file. Returns { created,\r\n\t/// path, className, panelRazorPath, panelScssPath, panelClassName, statName, placedOn, note,\r\n\t/// nextSteps }. After trigger_hotload: place ONE timer (add_component_to_new_object or targetId)\r\n\t/// and host the panel under a ScreenPanel/WorldPanel (add_screen_panel). maxRows clamps to 1..50;\r\n\t/// makePanel=false skips the panel files. Fails if the .cs or panel .razor already exists.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the generated timer component (panel becomes \u0026lt;name\u0026gt;Panel). Defaults to \u0027SpeedrunTimer\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for all generated files. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022statName\u0022\u003ESandbox.Services stat the best time is written to (sanitized to [a-z0-9_-]). Defaults to \u0027best_time\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022fileName\u0022\u003ESave file name inside FileSystem.Data for the local best. Defaults to \u0027speedrun.json\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022title\u0022\u003EPanel title text. Defaults to \u0027Best Times\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022maxRows\u0022\u003ELeaderboard rows fetched/shown. Defaults to 10, clamped to 1..50.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022makePanel\u0022\u003EAlso emit the \u0026lt;name\u0026gt;Panel.razor \u002B .razor.scss display panel. Defaults to true.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of a GameObject to attach the timer to (only attaches if the type is already loaded \u2014 hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_speedrun_leaderboard\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateSpeedrunLeaderboard( string name = null, string directory = null, string statName = null, string fileName = null, string title = null, double? maxRows = null, bool? makePanel = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_speedrun_leaderboard\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022statName\u0022, statName ), ( \u0022fileName\u0022, fileName ), ( \u0022title\u0022, title ), ( \u0022maxRows\u0022, maxRows ), ( \u0022makePanel\u0022, makePanel ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate an enum-keyed stat modifier system with three modifier layers: SET\r\n\t/// (highest-priority-wins hard override), ADD (summed bonuses), MULT (multiplied factors applied\r\n\t/// last). Modifier storage uses parallel private Lists of primitive types (serialization-safe).\r\n\t/// RemoveModifiersFrom(source) cleans up all mods from a buff/debuff source by reference. Static\r\n\t/// OnStatChanged(stat, value) event fires after every add/remove. Mined from RPG/buff/debuff\r\n\t/// patterns across shipped s\u0026amp;box games. Returns { created, path, className, stats, placedOn,\r\n\t/// note } \u2014 stats echoes the sanitized stat names ({name}Stat enum values); placedOn is the target\r\n\t/// GameObject when attached (needs the type hotloaded). Follow with trigger_hotload, then\r\n\t/// get_compile_errors.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name prefix -- generates {name}Stat enum \u002B {name} Component. Defaults to \u0027StatSystem\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022stats\u0022\u003EStat names as a JSON array or comma-separated string. Defaults to \u0027Health,Speed,Damage\u0027. JSON value.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of an existing GameObject to attach to (hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_stat_modifier_system\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateStatModifierSystem( string name = null, string directory = null, JsonNode stats = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_stat_modifier_system\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022stats\u0022, stats ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a host-authoritative balanced team assigner component (smallest-bucket draft):\r\n\t/// AssignSmallest(steamId) drops a joining player into the emptiest team, announces via\r\n\t/// [Rpc.Broadcast] so every client\u0027s roster agrees, and fires static OnTeamAssigned(steamId, index,\r\n\t/// name); plus Rebalance(), GetTeam, GetMembers. Writes a .cs file and returns { created, path,\r\n\t/// className, teams, nextSteps } \u2014 follow with trigger_hotload \u002B compile_status, attach to your\r\n\t/// game manager, call AssignSmallest from your join hook (e.g. INetworkListener.OnActive).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass/file name (default \u0027TeamAssigner\u0027 -\u0026gt; Code/TeamAssigner.cs). Errors if the file exists.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003EDirectory for the .cs file. Default \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022teams\u0022\u003ETeam names in index order. Default [\u0022Red\u0022, \u0022Blue\u0022].\u003C/param\u003E\r\n\t[McpTool( \u0022create_team_assigner\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateTeamAssigner( string name = null, string directory = null, string[] teams = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_team_assigner\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022teams\u0022, teams ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a trigger-zone Component (Component.ITriggerListener): auto-adds a trigger BoxCollider\r\n\t/// on start, filters entrants by a TriggerTag [Property] (default \u0027player\u0027), and logs enter/exit\r\n\t/// via private OnPlayerEnter/OnPlayerExit extension points you fill in. Writes \u0026lt;name\u0026gt;.cs and\r\n\t/// returns { created, path, className }. NOTE: the action/filterTag params are not currently\r\n\t/// applied at generation time \u2014 the zone always logs; implement teleport/damage/spawn in the\r\n\t/// generated methods (edit_script). Follow with trigger_hotload, then get_compile_errors.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name. Defaults to \u0027TriggerZone\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory under code/ for the file.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022action\u0022\u003EWhat happens on trigger (currently not applied by the handler \u2014 the generated zone always logs; implement the effect in OnPlayerEnter yourself). One of: log | teleport | damage | spawn.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022filterTag\u0022\u003EOnly trigger for objects with this tag (currently not applied at generation \u2014 the generated TriggerTag [Property] defaults to \u0027player\u0027; change it per-instance with set_property).\u003C/param\u003E\r\n\t[McpTool( \u0022create_trigger_zone\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateTriggerZone( string name = null, string directory = null, string action = null, string filterTag = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_trigger_zone\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022action\u0022, action ), ( \u0022filterTag\u0022, filterTag ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Generate a cumulative-weight random loot picker: parallel Name/Weight lists\r\n\t/// (inspector-editable), a Roll() method that returns a winning entry name and fires a static\r\n\t/// OnLoot event, and optional pity (guarantee the last/rarest entry after PityAfter consecutive\r\n\t/// non-rare rolls). Roll() is host-authoritative -- only call it on the host and replicate the\r\n\t/// result (clients rolling their own loot is equivalent to clients writing their own money\r\n\t/// balance). Optionally attached to an existing GameObject by GUID (after a hotload).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name. Defaults to \u0027LootTable\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the .cs file. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022entries\u0022\u003ELoot table entries. Defaults to common:70 / uncommon:25 / rare:5. JSON value.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022pity\u0022\u003EIf true, guarantee the last (rarest) entry after PityAfter consecutive non-rare rolls. Defaults to false.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of an existing GameObject to attach to (only if the type is already loaded -- hotload first).\u003C/param\u003E\r\n\t[McpTool( \u0022create_weighted_loot_table\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateWeightedLootTable( string name = null, string directory = null, JsonNode entries = null, bool? pity = null, string targetId = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_weighted_loot_table\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022entries\u0022, entries ), ( \u0022pity\u0022, pity ), ( \u0022targetId\u0022, targetId ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Scaffold an end-of-round map vote. Three files: \u0026lt;Name\u0026gt;.cs (sealed host-authoritative\r\n\t/// controller) \u002B \u0026lt;Name\u0026gt;Panel.razor \u002B \u0026lt;Name\u0026gt;Panel.razor.scss (vote UI: one button per\r\n\t/// map, live tallies, countdown, own-pick highlight, winner banner). Flow: host calls StartVote()\r\n\t/// (usually from a post-round phase/state, or set the AutoStart [Property]) -\u0026gt; clients click\r\n\t/// -\u0026gt; votes route client-to-host via [Rpc.Host] SubmitVote with the caller re-resolved HOST-SIDE\r\n\t/// from Rpc.Caller (null-checked \u2014 Connection has no IsValid on this SDK) and the map index\r\n\t/// re-validated (re-votes overwrite, keyed by SteamId) -\u0026gt; tallies replicate via [Sync(FromHost)]\r\n\t/// NetList\u0026lt;int\u0026gt; -\u0026gt; when the [Sync] TimeUntil countdown expires the host picks the winner\r\n\t/// (most votes; ties break deterministically via one LCG scramble of a time seed \u2014 no\r\n\t/// System.Random) -\u0026gt; after resultLingerSeconds the HOST calls Scene.LoadFromFile(winner) (API\r\n\t/// verified live on this SDK; clients follow via the scene networking layer \u2014 verify the client\r\n\t/// hand-off in a real multi-client session). Static event OnVoteFinished(sceneFile) fires on every\r\n\t/// machine. Returns { created, componentPath, razorPath, scssPath, className, panelClassName, maps,\r\n\t/// voteDurationSeconds, resultLingerSeconds, autoStart, note, nextSteps }. REQUIREMENTS: the\r\n\t/// controller must sit on a NETWORK-SPAWNED object in multiplayer or [Sync] never replicates; if\r\n\t/// maps is omitted the MapScenes list is generated EMPTY and StartVote() refuses with a warning\r\n\t/// until you fill it in the inspector. Follow with trigger_hotload, attach via\r\n\t/// add_component_with_properties, host the panel under add_screen_panel.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EClass name for the controller; the panel is generated as \u0026lt;Name\u0026gt;Panel. Defaults to \u0027MapVote\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022directory\u0022\u003ESubdirectory for the generated .cs \u002B .razor \u002B .razor.scss. Defaults to \u0027Code\u0027.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022maps\u0022\u003EScene files to vote between, e.g. [\u0022scenes/arena.scene\u0022, \u0022scenes/docks.scene\u0022] (find them with list_scenes). Baked into the MapScenes [Property] list, editable later in the inspector. Defaults to an EMPTY list (StartVote() then refuses until it\u0027s filled).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022voteDurationSeconds\u0022\u003ESeconds the vote stays open once StartVote() is called (clamped to \u0026gt;= 3). Defaults to 20.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022resultLingerSeconds\u0022\u003ESeconds the winner banner shows before the host loads the winning scene (clamped to \u0026gt;= 0). Defaults to 4.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022autoStart\u0022\u003EStart the vote automatically on spawn (host only). Usually false \u2014 call StartVote() from your round machine\u0027s post-round state instead. Defaults to false.\u003C/param\u003E\r\n\t[McpTool( \u0022scaffold_map_vote_flow\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E ScaffoldMapVoteFlow( string name = null, string directory = null, string[] maps = null, double? voteDurationSeconds = null, double? resultLingerSeconds = null, bool? autoStart = null )\r\n\t\t=\u003E McpGate.Run( \u0022scaffold_map_vote_flow\u0022, McpGate.Args( ( \u0022name\u0022, name ), ( \u0022directory\u0022, directory ), ( \u0022maps\u0022, maps ), ( \u0022voteDurationSeconds\u0022, voteDurationSeconds ), ( \u0022resultLingerSeconds\u0022, resultLingerSeconds ), ( \u0022autoStart\u0022, autoStart ) ) );\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/NetPrimitivesHandlers.cs","FileName":"NetPrimitivesHandlers.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"using Editor;\r\nusing Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Text;\r\nusing System.Text.Json;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\n\r\n// =============================================================================\r\n//  Networking primitives pack (v1.20.0, Track B) -- four multiplayer scaffolds\r\n//  (code-gen; scene-mutating):\r\n//\r\n//    create_host_rpc_action       validated \u002B rate-limited [Rpc.Host] action skeleton\r\n//    add_targeted_rpc             Rpc.FilterInclude single-client (unicast) side-effect\r\n//    create_local_player_resolver proxy-safe \u0022who is MY player\u0022 resolver (online \u002B offline)\r\n//    add_host_migration_recovery  proxy-\u003Eauthority transition detector \u002B OnBecameHost hook\r\n//\r\n//  Compiles into the SAME editor assembly as MyEditorMenu.cs / ScaffoldHandlers.cs,\r\n//  so it reuses the shared statics on ClaudeBridge (TryResolveProjectPath,\r\n//  SanitizeIdentifier, SerializeGo) and ScaffoldHelpers (PrepareCodeFile /\r\n//  WriteCode / Utf8NoBom). Handler code here is UNSANDBOXED editor code.\r\n//\r\n//  The C# *strings these handlers WRITE TO DISK* are SANDBOXED game code and must\r\n//  obey the s\u0026box sandbox rules:\r\n//    - System.Math/MathF/MathX all compile on the current SDK; Array.Clone() is\r\n//      whitelist-blocked (not used here).\r\n//    - Fully-qualify System.Collections.Generic.Dictionary (dodges a missing using).\r\n//    - TimeSince/TimeUntil for timers; float literals formatted InvariantCulture \u002B \u0027f\u0027.\r\n//    - Guard Networking access: check Networking.IsActive before Networking.IsHost\r\n//      (IsHost can throw with no session). Rpc.Caller re-resolved host-side, never\r\n//      trusting client args for identity.\r\n//    - VERIFIED live against the installed SDK before codegen (describe_type \u002B\r\n//      networking-authority cookbook): Connection.Local (static), Connection.All,\r\n//      Connection.SteamId (NOTE: Connection has NO IsValid member on this SDK \u2014\r\n//      null-check it; caught live by the v1.20.0 verify-gate), Rpc.Caller (Connection) / Rpc.CallerId (Guid),\r\n//      Rpc.FilterInclude(Connection) -\u003E IDisposable, GameObject.Network (NetworkAccessor)\r\n//      -\u003E Owner (Connection) / OwnerId (Guid) / IsOwner / IsProxy, [Sync(SyncFlags.FromHost)],\r\n//      [Rpc.Host] / [Rpc.Broadcast], (ulong)SteamId cast.\r\n//\r\n//  Register(...) lines \u002B the _sceneMutatingCommands additions live in\r\n//  MyEditorMenu.cs (Batch 45) to keep the files decoupled.\r\n// =============================================================================\r\n\r\n// -----------------------------------------------------------------------------\r\n// create_host_rpc_action -- the validated, rate-limited host-action skeleton.\r\n//\r\n// The safe answer to \u0022a client asks the host to DO something\u0022: a client-callable\r\n// Request() forwards to an [Rpc.Host] body that re-resolves the caller via\r\n// Rpc.Caller (NEVER trusting client args for identity), enforces a per-SteamId\r\n// cooldown from a Dictionary\u003Culong, TimeSince\u003E, runs a clearly-marked TODO hook,\r\n// and fires a static OnActionExecuted event. Covers the backlog\u0027s\r\n// add_rate_limited_rpc.\r\n// -----------------------------------------------------------------------------\r\npublic class CreateHostRpcActionHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar ci = System.Globalization.CultureInfo.InvariantCulture;\r\n\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022HostRpcAction\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tfloat cooldown = p.TryGetProperty( \u0022cooldownSeconds\u0022, out var cv ) \u0026\u0026 cv.TryGetSingle( out var cf ) ? cf : 1f;\r\n\t\t\tif ( cooldown \u003C 0f ) cooldown = 0f;   // a negative cooldown would emit nonsense\r\n\r\n\t\t\tvar code = BuildCode( className, cooldown, ci );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\tobject placedOn = null; string note = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022targetId\u0022, out var tid ) \u0026\u0026 tid.ValueKind == JsonValueKind.String )\r\n\t\t\t\tplacedOn = NetPrimitivesHelpers.PlaceOnTarget( tid.GetString(), className, out note );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tcooldownSeconds = cooldown,\r\n\t\t\t\tplacedOn,\r\n\t\t\t\tnote,\r\n\t\t\t\tnextSteps = new[]\r\n\t\t\t\t{\r\n\t\t\t\t\t$\u0022trigger_hotload to compile {className} into the game assembly.\u0022,\r\n\t\t\t\t\tplacedOn != null\r\n\t\t\t\t\t\t? $\u0022{className} was attached to the target GameObject.\u0022\r\n\t\t\t\t\t\t: $\u0022Attach it to the object that owns this action (a player, a station, or your game manager): add_component_with_properties (component=\\\u0022{className}\\\u0022) after the hotload, or re-run with targetId.\u0022,\r\n\t\t\t\t\t$\u0022Fire it from the owning client (input / UI button): GetComponent\u003C{className}\u003E()?.Request(); -- it routes to the host, which re-validates and rate-limits.\u0022,\r\n\t\t\t\t\t$\u0022Fill in the TODO host block with your authoritative action (spend currency, NetworkSpawn, grant a reward). Re-clamp any gameplay args there -- forged client args bypass NetFlags.\u0022,\r\n\t\t\t\t\t$\u0022React to accepted actions: {className}.OnActionExecuted \u002B= conn =\u003E Log.Info( $\\\u0022action by {{conn.DisplayName}}\\\u0022 ); (fires on the host). Wrap an [Rpc.Broadcast] if every client should react.\u0022,\r\n\t\t\t\t\t\u0022Tune CooldownSeconds with set_property. The per-SteamId cooldown is host-only runtime state (not [Sync]).\u0022\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_host_rpc_action failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string BuildCode( string className, float cooldown, System.Globalization.CultureInfo ci )\r\n\t{\r\n\t\tstring cd = cooldown.ToString( ci ) \u002B \u0022f\u0022;\r\n\r\n\t\treturn $@\u0022using Sandbox;\r\nusing System;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} -- a validated, rate-limited host action. The safe skeleton for\r\n/// \u0022\u0022a client asks the host to DO something\u0022\u0022 (buy, use, vote, interact).\r\n///\r\n/// Flow:  client calls Request()  -\u0026gt;  [Rpc.Host] SubmitRequest() runs ON THE HOST\r\n///        -\u0026gt;  host re-resolves WHO called it via Rpc.Caller (never trusts client\r\n///        args for identity)  -\u0026gt;  enforces a per-SteamId cooldown  -\u0026gt;  runs your\r\n///        host-authoritative action  -\u0026gt;  fires OnActionExecuted.\r\n///\r\n/// [Rpc.Host] is callable by ANY client with forged args -- NetFlags restrict who\r\n/// may INVOKE, which is not security. That is why identity \u002B cooldown \u002B your\r\n/// validation all live INSIDE the host body. Single-player safe (no session -\u0026gt; the\r\n/// RPC just runs locally; the caller falls back to Connection.Local).\r\n///\r\n/// Usage:\r\n///   GetComponent\u0026lt;{className}\u0026gt;()?.Request();   // from input / a UI button, on the owning client\r\n///   {className}.OnActionExecuted \u002B= conn =\u0026gt; Log.Info( $\u0022\u0022action by {{conn.DisplayName}}\u0022\u0022 );\r\n/// \u003C/summary\u003E\r\npublic sealed class {className} : Component\r\n{{\r\n\t/// \u003Csummary\u003EMinimum seconds between accepted requests, per calling player.\u003C/summary\u003E\r\n\t[Property] public float CooldownSeconds {{ get; set; }} = {cd};\r\n\r\n\t/// \u003Csummary\u003EFires ON THE HOST after an accepted request. Arg = the validated caller.\u003C/summary\u003E\r\n\tpublic static Action\u003CConnection\u003E OnActionExecuted {{ get; set; }}\r\n\r\n\t// Host-only runtime state: last-accept time keyed by the caller\u0027s SteamId.\r\n\t// NOT [Sync] -- it is the host\u0027s own rate-limit bookkeeping, never replicated.\r\n\tprivate readonly System.Collections.Generic.Dictionary\u003Culong, TimeSince\u003E _cooldowns = new();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Client entry point. Call this on the owning client (input handler / UI button).\r\n\t/// It routes to the host; do NOT put authoritative logic here -- a client controls\r\n\t/// this machine and could call anything. The real work happens host-side.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void Request()\r\n\t{{\r\n\t\tSubmitRequest();   // [Rpc.Host] -- executes on the host (or locally in solo)\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Host-authoritative handler. Public so the RPC source generator is happy; the\r\n\t/// re-validation below is what actually protects it. NEVER trust args passed from\r\n\t/// the client for identity -- re-resolve the caller here.\r\n\t/// \u003C/summary\u003E\r\n\t[Rpc.Host]\r\n\tpublic void SubmitRequest()\r\n\t{{\r\n\t\t// Re-resolve the caller SERVER-SIDE. Read Rpc.Caller only when a session is\r\n\t\t// active (offline it is meaningless); fall back to us in solo.\r\n\t\tvar caller = Networking.IsActive ? Rpc.Caller : Connection.Local;\r\n\t\tif ( caller == null ) caller = Connection.Local;\r\n\t\tif ( caller == null ) return;   // no identity at all -- refuse\r\n\r\n\t\t// FOOTGUN (some SDK builds): Rpc.Caller can return the HOST\u0027s own connection\r\n\t\t// for a proxy-initiated call. If identity is security-critical, resolve the\r\n\t\t// acting player from the OWNING component\u0027s Network.Owner instead.\r\n\r\n\t\tulong callerId = (ulong)caller.SteamId;\r\n\r\n\t\t// Per-SteamId rate limit -- spamming the RPC cannot bypass the cooldown.\r\n\t\tif ( _cooldowns.TryGetValue( callerId, out var since ) \u0026\u0026 since \u003C CooldownSeconds )\r\n\t\t\treturn;   // still cooling down for this caller\r\n\t\t_cooldowns[callerId] = 0f;   // reset this caller\u0027s timer\r\n\r\n\t\t// --- TODO: your host-authoritative action goes here ---------------------\r\n\t\t// Runs ONLY on the host. Re-validate \u002B re-clamp any gameplay values, then\r\n\t\t// mutate [Sync(SyncFlags.FromHost)] state / NetworkSpawn() / grant rewards.\r\n\t\t// Example: GetComponent\u003CWallet\u003E()?.AddMoney( 10 );\r\n\t\t// ------------------------------------------------------------------------\r\n\r\n\t\tOnActionExecuted?.Invoke( caller );\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n// -----------------------------------------------------------------------------\r\n// add_targeted_rpc -- the Rpc.FilterInclude single-client (unicast) pattern.\r\n//\r\n// A host-side SendTo(Connection, string) wraps an [Rpc.Broadcast] call in\r\n// using ( Rpc.FilterInclude( target ) ) so ONLY that one connection executes the\r\n// body, which raises a static OnReceived event.\r\n// -----------------------------------------------------------------------------\r\npublic class AddTargetedRpcHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022TargetedRpc\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tvar code = BuildCode( className );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\tobject placedOn = null; string note = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022targetId\u0022, out var tid ) \u0026\u0026 tid.ValueKind == JsonValueKind.String )\r\n\t\t\t\tplacedOn = NetPrimitivesHelpers.PlaceOnTarget( tid.GetString(), className, out note );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tplacedOn,\r\n\t\t\t\tnote,\r\n\t\t\t\tnextSteps = new[]\r\n\t\t\t\t{\r\n\t\t\t\t\t$\u0022trigger_hotload to compile {className} into the game assembly.\u0022,\r\n\t\t\t\t\tplacedOn != null\r\n\t\t\t\t\t\t? $\u0022{className} was attached to the target GameObject.\u0022\r\n\t\t\t\t\t\t: $\u0022Attach it to a networked manager object: add_component_with_properties (component=\\\u0022{className}\\\u0022) after the hotload, or re-run with targetId. The object must be NetworkSpawn\u0027d for the RPC to route.\u0022,\r\n\t\t\t\t\t$\u0022Send to ONE player from the host: GetComponent\u003C{className}\u003E()?.SendTo( player.Network.Owner, \\\u0022You\u0027re up next!\\\u0022 ); -- only that client runs the body.\u0022,\r\n\t\t\t\t\t$\u0022Receive on the target: {className}.OnReceived \u002B= msg =\u003E ShowToast( msg ); -- fires only on the filtered client (and locally in solo).\u0022,\r\n\t\t\t\t\t\u0022Use this instead of [Rpc.Broadcast] \u002B a client-side \u0027is this for me?\u0027 check -- FilterInclude scopes it server-side, so no data leaks and no wasted bandwidth.\u0022\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022add_targeted_rpc failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string BuildCode( string className )\r\n\t{\r\n\t\treturn $@\u0022using Sandbox;\r\nusing System;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} -- send a message to exactly ONE client using Rpc.FilterInclude.\r\n///\r\n/// A normal [Rpc.Broadcast] runs on EVERY machine. Wrapping the call in\r\n/// using ( Rpc.FilterInclude( target ) ) scopes it server-side so ONLY the target\r\n/// connection executes the RPC body -- the right way to unicast (a private prompt,\r\n/// a personal reward toast, a per-player cutscene) instead of broadcasting to all\r\n/// and filtering on the client (which leaks data \u002B wastes bandwidth).\r\n///\r\n/// Call SendTo on the host. Single-player safe (with no session it just runs locally).\r\n///\r\n/// Usage (host-side):\r\n///   GetComponent\u0026lt;{className}\u0026gt;()?.SendTo( somePlayer.Network.Owner, \u0022\u0022You\u0027re up next!\u0022\u0022 );\r\n///   {className}.OnReceived \u002B= msg =\u0026gt; Log.Info( $\u0022\u0022(only me) {{msg}}\u0022\u0022 );\r\n/// \u003C/summary\u003E\r\npublic sealed class {className} : Component\r\n{{\r\n\t/// \u003Csummary\u003EFires on the TARGET client only (and locally in solo) when a message arrives.\u003C/summary\u003E\r\n\tpublic static Action\u003Cstring\u003E OnReceived {{ get; set; }}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Host-side: deliver \u003Cparamref name=\u0022\u0022message\u0022\u0022/\u003E to exactly one connection.\r\n\t/// FilterInclude scopes the broadcast so only \u003Cparamref name=\u0022\u0022target\u0022\u0022/\u003E runs it.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void SendTo( Connection target, string message )\r\n\t{{\r\n\t\tif ( target == null ) return;\r\n\r\n\t\t// Only the host should originate a targeted message in a host-authoritative\r\n\t\t// game. Guarded behind IsActive because Networking.IsHost can throw with no\r\n\t\t// session; in solo this falls through and just runs locally.\r\n\t\tif ( Networking.IsActive \u0026\u0026 !Networking.IsHost ) return;\r\n\r\n\t\tusing ( Rpc.FilterInclude( target ) )\r\n\t\t\tReceive( message );\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The unicast body. Public so the RPC source generator is happy. Runs ONLY on the\r\n\t/// filtered target connection (FilterInclude decided that server-side).\r\n\t/// \u003C/summary\u003E\r\n\t[Rpc.Broadcast]\r\n\tpublic void Receive( string message )\r\n\t{{\r\n\t\tOnReceived?.Invoke( message );\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n// -----------------------------------------------------------------------------\r\n// create_local_player_resolver -- proxy-safe \u0022who is MY player\u0022.\r\n//\r\n// Static Local property that lazily finds the player GameObject owned by the local\r\n// connection ( Network.Owner == Connection.Local, or Network.IsOwner ) when\r\n// networking is active, and falls back to the first/only tagged player when it is\r\n// NOT (offline/solo). Cached with an IsValid() revalidation. The corpus footgun\r\n// killer -- running \u0022my player\u0022 logic against a proxy of someone else\u0027s player.\r\n// -----------------------------------------------------------------------------\r\npublic class CreateLocalPlayerResolverHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022LocalPlayerResolver\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tvar tag = p.TryGetProperty( \u0022playerTag\u0022, out var tv ) \u0026\u0026 !string.IsNullOrWhiteSpace( tv.GetString() )\r\n\t\t\t\t? tv.GetString().Trim() : \u0022player\u0022;\r\n\t\t\tvar tagLiteral = NetPrimitivesHelpers.EscapeStringLiteral( tag );\r\n\r\n\t\t\tvar code = BuildCode( className, tagLiteral );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\tobject placedOn = null; string note = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022targetId\u0022, out var tid ) \u0026\u0026 tid.ValueKind == JsonValueKind.String )\r\n\t\t\t\tplacedOn = NetPrimitivesHelpers.PlaceOnTarget( tid.GetString(), className, out note );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tplayerTag = tag,\r\n\t\t\t\tplacedOn,\r\n\t\t\t\tnote,\r\n\t\t\t\tnextSteps = new[]\r\n\t\t\t\t{\r\n\t\t\t\t\t$\u0022trigger_hotload to compile {className} into the game assembly.\u0022,\r\n\t\t\t\t\tplacedOn != null\r\n\t\t\t\t\t\t? $\u0022{className} was attached to the target GameObject.\u0022\r\n\t\t\t\t\t\t: $\u0022Attach ONE to a persistent object (your game manager): add_component_with_properties (component=\\\u0022{className}\\\u0022) after the hotload, or re-run with targetId. Placing it lets you set PlayerTag in the inspector.\u0022,\r\n\t\t\t\t\t$\u0022Tag each player GameObject with \\\u0022{tag}\\\u0022 (set_tags) so the resolver can find them.\u0022,\r\n\t\t\t\t\t$\u0022Read your player from anywhere: var me = {className}.Local; -- online it is the object you OWN, offline it is the only player. Cached \u002B revalidated automatically.\u0022,\r\n\t\t\t\t\t$\u0022Filter events to your own player: if ( {className}.IsLocal( someGameObject ) ) {{ ... }} -- kills the \u0027ran my UI/logic against a proxy\u0027 footgun.\u0022\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_local_player_resolver failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string BuildCode( string className, string tagLiteral )\r\n\t{\r\n\t\treturn $@\u0022using Sandbox;\r\nusing System;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} -- \u0022\u0022who is MY player?\u0022\u0022, the proxy-safe way. Resolves the player\r\n/// GameObject that belongs to THIS machine, both online and offline.\r\n///\r\n/// Online: your player is the tagged object whose Network.Owner is the local\r\n/// connection ( Network.Owner == Connection.Local, or Network.IsOwner ). Offline /\r\n/// solo (no session), there is exactly one player, so it returns the first tagged\r\n/// object. The result is cached and revalidated with IsValid() so a destroyed /\r\n/// respawned player is re-resolved automatically.\r\n///\r\n/// Attach ONE of these to a persistent object (your game manager) so PlayerTag is\r\n/// configurable; the resolver itself is static and callable from anywhere:\r\n///   var me = {className}.Local;                 // my player GameObject (or null)\r\n///   if ( {className}.IsLocal( someGo ) ) ...    // filter events to my own player\r\n///\r\n/// This kills the #1 multiplayer footgun -- running \u0022\u0022my player\u0022\u0022 logic against a\r\n/// proxy of someone else\u0027s player.\r\n/// \u003C/summary\u003E\r\npublic sealed class {className} : Component\r\n{{\r\n\t/// \u003Csummary\u003ETag that marks a player GameObject. Players must carry this tag.\u003C/summary\u003E\r\n\t[Property] public string PlayerTag {{ get; set; }} = \u0022\u0022{tagLiteral}\u0022\u0022;\r\n\r\n\tprivate static {className} _instance;\r\n\tprivate static string _tag = \u0022\u0022{tagLiteral}\u0022\u0022;\r\n\tprivate static GameObject _cached;\r\n\r\n\tprotected override void OnEnabled()\r\n\t{{\r\n\t\t_instance = this;\r\n\t\t_tag = PlayerTag;\r\n\t}}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{{\r\n\t\tif ( _instance == this ) _instance = null;\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003EThe local machine\u0027s player GameObject, or null if not found yet.\u003C/summary\u003E\r\n\tpublic static GameObject Local\r\n\t{{\r\n\t\tget\r\n\t\t{{\r\n\t\t\tif ( IsLocal( _cached ) ) return _cached;   // cache hit, still valid \u002B still ours\r\n\t\t\t_cached = Resolve();\r\n\t\t\treturn _cached;\r\n\t\t}}\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003ETrue if \u003Cparamref name=\u0022\u0022go\u0022\u0022/\u003E is the local machine\u0027s player.\u003C/summary\u003E\r\n\tpublic static bool IsLocal( GameObject go )\r\n\t{{\r\n\t\tif ( !go.IsValid() ) return false;\r\n\t\tif ( !Networking.IsActive ) return true;   // solo: the only player is mine\r\n\t\treturn go.Network.Owner == Connection.Local || go.Network.IsOwner;\r\n\t}}\r\n\r\n\tprivate static GameObject Resolve()\r\n\t{{\r\n\t\tvar scene = Game.ActiveScene;\r\n\t\tif ( !scene.IsValid() ) return null;\r\n\r\n\t\tif ( !Networking.IsActive )\r\n\t\t{{\r\n\t\t\t// Offline / solo: the first tagged player is ours.\r\n\t\t\tforeach ( var go in scene.GetAllObjects( true ) )\r\n\t\t\t\tif ( go.Tags.Has( _tag ) ) return go;\r\n\t\t\treturn null;\r\n\t\t}}\r\n\r\n\t\t// Online: our player is the tagged object owned by the local connection.\r\n\t\tforeach ( var go in scene.GetAllObjects( true ) )\r\n\t\t{{\r\n\t\t\tif ( !go.Tags.Has( _tag ) ) continue;\r\n\t\t\tif ( go.Network.Owner == Connection.Local || go.Network.IsOwner )\r\n\t\t\t\treturn go;\r\n\t\t}}\r\n\t\treturn null;\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n// -----------------------------------------------------------------------------\r\n// add_host_migration_recovery -- proxy-\u003Eauthority transition detector.\r\n//\r\n// Tracks previous IsProxy each frame; when it flips from true to false (we became\r\n// the authority for this object, i.e. host migration promoted us), it fires a\r\n// static OnBecameHost event and runs a virtual-style TODO rebuild hook, then -- a\r\n// short settle delay later -- a deferred validation hook. Inert offline (IsProxy\r\n// is always false with no session).\r\n// -----------------------------------------------------------------------------\r\npublic class AddHostMigrationRecoveryHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar ci = System.Globalization.CultureInfo.InvariantCulture;\r\n\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022HostMigrationRecovery\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\t// Settle delay is fixed at the cookbook-recommended ~1s but exposed as a\r\n\t\t\t// [Property] so it is tunable; no param for it (keeps the schema to name/directory).\r\n\t\t\tfloat settle = 1f;\r\n\r\n\t\t\tvar code = BuildCode( className, settle, ci );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\tobject placedOn = null; string note = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022targetId\u0022, out var tid ) \u0026\u0026 tid.ValueKind == JsonValueKind.String )\r\n\t\t\t\tplacedOn = NetPrimitivesHelpers.PlaceOnTarget( tid.GetString(), className, out note );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tplacedOn,\r\n\t\t\t\tnote,\r\n\t\t\t\tnextSteps = new[]\r\n\t\t\t\t{\r\n\t\t\t\t\t$\u0022trigger_hotload to compile {className} into the game assembly.\u0022,\r\n\t\t\t\t\tplacedOn != null\r\n\t\t\t\t\t\t? $\u0022{className} was attached to the target GameObject.\u0022\r\n\t\t\t\t\t\t: $\u0022Attach it to your host-authoritative manager object: add_component_with_properties (component=\\\u0022{className}\\\u0022) after the hotload, or re-run with targetId. The object should be NetworkSpawn\u0027d.\u0022,\r\n\t\t\t\t\t$\u0022React to becoming host: {className}.OnBecameHost \u002B= go =\u003E Log.Info( \\\u0022I am the host now -- rebuilding\\\u0022 );\u0022,\r\n\t\t\t\t\t\u0022Fill in the RebuildAfterMigration() TODO region: re-arm host-only loops/timers against your clock, TakeOwnership of orphans, rebuild handle maps by world position, reconcile your [Sync] registry against the real scene.\u0022,\r\n\t\t\t\t\t\u0022Fill in the deferred ValidateAfterMigration() TODO: sanity-check expected-vs-actual and hard-reset the round if it looks corrupt (SettleSeconds delay lets in-flight packets land first).\u0022,\r\n\t\t\t\t\t\u0022Requires a real host migration to fire (a second client that becomes host when the first leaves) -- it is inert in solo/offline play.\u0022\r\n\t\t\t\t}\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022add_host_migration_recovery failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string BuildCode( string className, float settle, System.Globalization.CultureInfo ci )\r\n\t{\r\n\t\tstring st = settle.ToString( ci ) \u002B \u0022f\u0022;\r\n\r\n\t\treturn $@\u0022using Sandbox;\r\nusing System;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} -- detects when THIS machine takes authority over this object\r\n/// (proxy -\u0026gt; owner), which is what happens to a host-authoritative manager during\r\n/// host migration, and gives you a clean hook to rebuild host-only state.\r\n///\r\n/// It tracks IsProxy each frame; when it flips from true (someone else was the\r\n/// authority) to false (now it is us), it fires OnBecameHost and runs the rebuild\r\n/// hook, then -- after a short settle delay so in-flight packets can land -- runs a\r\n/// deferred validation hook. Inert offline (IsProxy is always false with no session).\r\n///\r\n/// Attach to your host-authoritative manager object. Fill in the two TODO regions.\r\n///\r\n/// Usage:\r\n///   {className}.OnBecameHost \u002B= go =\u0026gt; Log.Info( \u0022\u0022I am the host now -- rebuilding\u0022\u0022 );\r\n/// \u003C/summary\u003E\r\npublic sealed class {className} : Component\r\n{{\r\n\t/// \u003Csummary\u003ESeconds to wait after becoming host before the deferred validation runs.\u003C/summary\u003E\r\n\t[Property] public float SettleSeconds {{ get; set; }} = {st};\r\n\r\n\t/// \u003Csummary\u003EFires on the machine that just gained authority. Arg = this GameObject.\u003C/summary\u003E\r\n\tpublic static Action\u003CGameObject\u003E OnBecameHost {{ get; set; }}\r\n\r\n\tprivate bool _wasProxy;\r\n\tprivate bool _initialized;\r\n\tprivate bool _pendingValidate;\r\n\tprivate TimeSince _sinceBecameHost;\r\n\r\n\tprotected override void OnEnabled()\r\n\t{{\r\n\t\t_wasProxy = IsProxy;   // baseline so we only fire on a real transition\r\n\t\t_initialized = true;\r\n\t}}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{{\r\n\t\tbool proxyNow = IsProxy;\r\n\t\tif ( _initialized \u0026\u0026 _wasProxy \u0026\u0026 !proxyNow )\r\n\t\t\tBecameHost();\r\n\t\t_wasProxy = proxyNow;\r\n\r\n\t\tif ( _pendingValidate \u0026\u0026 _sinceBecameHost \u003E SettleSeconds )\r\n\t\t{{\r\n\t\t\t_pendingValidate = false;\r\n\t\t\tValidateAfterMigration();\r\n\t\t}}\r\n\t}}\r\n\r\n\tprivate void BecameHost()\r\n\t{{\r\n\t\t_sinceBecameHost = 0f;\r\n\t\t_pendingValidate = true;\r\n\t\tRebuildAfterMigration();\r\n\t\tOnBecameHost?.Invoke( GameObject );\r\n\t}}\r\n\r\n\t// virtual-style rebuild hook -- edit this body (the component is sealed, so there\r\n\t// is nothing to override; this region IS your override point).\r\n\tprivate void RebuildAfterMigration()\r\n\t{{\r\n\t\t// TODO: rebuild host-only state now that YOU are the authority. The previous\r\n\t\t// host is gone; anything it owned or was mid-computing is now your job. Typical\r\n\t\t// moves (networking-authority cookbook, pattern 17):\r\n\t\t//   - Re-arm host-only loops / spawners. A [Sync] TimeUntil stores the DEAD\r\n\t\t//     host\u0027s clock epoch -- read its .Relative remaining and re-arm it here.\r\n\t\t//   - Network.TakeOwnership() any orphaned objects you must now manage/destroy.\r\n\t\t//   - Rebuild handle-\u003Ehandle maps by world-position matching (object Ids do not\r\n\t\t//     survive migration).\r\n\t\t//   - Reconcile your [Sync] registry against the REAL scene (drop dead entries,\r\n\t\t//     add visible objects the list is missing).\r\n\t}}\r\n\r\n\t// deferred sanity check -- runs SettleSeconds after becoming host so in-flight\r\n\t// packets that have not applied yet do not make a healthy scene look broken.\r\n\tprivate void ValidateAfterMigration()\r\n\t{{\r\n\t\t// TODO: compare expected-vs-actual (child counts, roster tags) and hard-reset\r\n\t\t// the round rather than limping along if it looks corrupt. (cookbook pattern 17)\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Shared helpers for the networking-primitives handlers -- mirrors the standard\r\n/// scaffold placement (GameFeelHelpers / create_event_director) plus a tiny\r\n/// string-literal escaper for baked-in tag defaults.\r\n/// \u003C/summary\u003E\r\ninternal static class NetPrimitivesHelpers\r\n{\r\n\tpublic static object PlaceOnTarget( string targetId, string className, out string note )\r\n\t{\r\n\t\tnote = null;\r\n\t\tvar scene = SceneEditorSession.Active?.Scene;\r\n\t\tif ( scene == null ) { note = \u0022No active scene to place into.\u0022; return null; }\r\n\t\tif ( !Guid.TryParse( targetId, out var guid ) ) { note = \u0022Invalid targetId GUID.\u0022; return null; }\r\n\t\tvar go = scene.Directory.FindByGuid( guid );\r\n\t\tif ( go == null ) { note = $\u0022Target GameObject not found: {targetId}\u0022; return null; }\r\n\t\tvar typeDesc = Game.TypeLibrary.GetType( className );\r\n\t\tif ( typeDesc == null )\r\n\t\t{\r\n\t\t\tnote = $\u0022Generated {className}.cs but it is not in the TypeLibrary yet -- trigger_hotload, then add it with add_component_with_properties.\u0022;\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\ttry { go.Components.Create( typeDesc ); return ClaudeBridge.SerializeGo( go ); }\r\n\t\tcatch ( Exception ex ) { note = $\u0022Placement failed ({ex.Message}).\u0022; return null; }\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EEscape a user string so it can be baked as a C# double-quoted literal.\u003C/summary\u003E\r\n\tpublic static string EscapeStringLiteral( string s )\r\n\t{\r\n\t\tif ( string.IsNullOrEmpty( s ) ) return \u0022player\u0022;\r\n\t\treturn s.Replace( \u0022\\\\\u0022, \u0022\\\\\\\\\u0022 ).Replace( \u0022\\\u0022\u0022, \u0022\\\\\\\u0022\u0022 );\r\n\t}\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/ProjectAuditHandlers.cs","FileName":"ProjectAuditHandlers.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"using Editor;\r\nusing Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Text.Json;\r\nusing System.Threading.Tasks;\r\n\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n// Batch 51 \u2014 Project audit \u0026 batch operations (v2 relaunch wave 1)\r\n//   find_broken_references  \u2014 scene-wide broken/dead reference scan\r\n//   batch_set_property      \u2014 one property across many objects, with dry-run\r\n//   describe_project        \u2014 one-call project orientation summary\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n\r\n/// \u003Csummary\u003E\r\n/// find_broken_references \u2014 scan the open scene for null models on renderers,\r\n/// destroyed-but-still-referenced GameObjects/Components in component properties,\r\n/// and unresolvable (null) component entries.\r\n/// \u003C/summary\u003E\r\npublic class FindBrokenReferencesHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\tvar scene = SceneEditorSession.Active?.Scene;\r\n\t\tif ( scene == null )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022No active scene\u0022 } );\r\n\r\n\t\tint limit = p.TryGetProperty( \u0022limit\u0022, out var l ) ? l.GetInt32() : 100;\r\n\t\tif ( limit \u003C 1 ) limit = 1; if ( limit \u003E 500 ) limit = 500;\r\n\r\n\t\tvar issues = new List\u003Cobject\u003E();\r\n\t\tint total = 0;\r\n\t\tint objectsScanned = 0;\r\n\r\n\t\tvoid AddIssue( GameObject go, string component, string kind, string detail )\r\n\t\t{\r\n\t\t\ttotal\u002B\u002B;\r\n\t\t\tif ( issues.Count \u003C limit )\r\n\t\t\t\tissues.Add( new { id = go.Id.ToString(), name = go.Name, component, kind, detail } );\r\n\t\t}\r\n\r\n\t\tforeach ( var go in scene.GetAllObjects( true ) )\r\n\t\t{\r\n\t\t\tif ( go == null ) continue;\r\n\t\t\tobjectsScanned\u002B\u002B;\r\n\r\n\t\t\tforeach ( var comp in go.Components.GetAll() )\r\n\t\t\t{\r\n\t\t\t\tif ( comp == null )\r\n\t\t\t\t{\r\n\t\t\t\t\tAddIssue( go, \u0022(null)\u0022, \u0022missing_component\u0022, \u0022Component entry is null \u2014 its type may no longer exist/compile\u0022 );\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif ( comp is ModelRenderer mr \u0026\u0026 mr.Model == null )\r\n\t\t\t\t\tAddIssue( go, comp.GetType().Name, \u0022missing_model\u0022, \u0022Renderer has no Model assigned\u0022 );\r\n\r\n\t\t\t\t// Destroyed-but-referenced objects/components: a null ref is usually a\r\n\t\t\t\t// legitimate \u0027unset optional\u0027, but a ref to a DESTROYED thing is broken.\r\n\t\t\t\tvar typeDesc = Game.TypeLibrary.GetType( comp.GetType().Name );\r\n\t\t\t\tif ( typeDesc == null ) continue;\r\n\t\t\t\tforeach ( var prop in typeDesc.Properties )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar pt = prop.PropertyType;\r\n\t\t\t\t\tbool isGo = pt == typeof( GameObject );\r\n\t\t\t\t\tbool isComp = typeof( Component ).IsAssignableFrom( pt );\r\n\t\t\t\t\tif ( !isGo \u0026\u0026 !isComp ) continue;\r\n\r\n\t\t\t\t\tobject val;\r\n\t\t\t\t\ttry { val = prop.GetValue( comp ); }\r\n\t\t\t\t\tcatch { continue; }\r\n\t\t\t\t\tif ( val == null ) continue;\r\n\r\n\t\t\t\t\tif ( val is GameObject g \u0026\u0026 !g.IsValid() )\r\n\t\t\t\t\t\tAddIssue( go, comp.GetType().Name, \u0022dead_gameobject_ref\u0022, $\u0022{prop.Name} references a destroyed GameObject\u0022 );\r\n\t\t\t\t\telse if ( val is Component c \u0026\u0026 !c.IsValid() )\r\n\t\t\t\t\t\tAddIssue( go, comp.GetType().Name, \u0022dead_component_ref\u0022, $\u0022{prop.Name} references a destroyed Component\u0022 );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// v2 round 2: scan .scene/.prefab FILES for prefab references to files that no\r\n\t\t// longer exist ({\u0022_type\u0022:\u0022gameobject\u0022,\u0022prefab\u0022:\u0022prefabs/x.prefab\u0022} with x deleted\r\n\t\t// or renamed) \u2014 the break class scene-level checks can\u0027t see.\r\n\t\tint filesScanned = 0;\r\n\t\tbool scanFiles = !( p.TryGetProperty( \u0022scanFiles\u0022, out var sf ) \u0026\u0026 sf.ValueKind == JsonValueKind.False );\r\n\t\tif ( scanFiles )\r\n\t\t{\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tvar root = Project.Current?.GetRootPath();\r\n\t\t\t\tif ( root != null )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar rx = new System.Text.RegularExpressions.Regex( \u0022\\\u0022prefab\\\u0022:\\\\s*\\\u0022([^\\\u0022]\u002B)\\\u0022\u0022 );\r\n\t\t\t\t\tvar files = Directory.GetFiles( root, \u0022*.scene\u0022, SearchOption.AllDirectories )\r\n\t\t\t\t\t\t.Concat( Directory.GetFiles( root, \u0022*.prefab\u0022, SearchOption.AllDirectories ) )\r\n\t\t\t\t\t\t.Where( f =\u003E { var r = Path.GetRelativePath( root, f ).Replace( \u0027\\\\\u0027, \u0027/\u0027 ); return !r.StartsWith( \u0022Libraries/\u0022 ) \u0026\u0026 !r.StartsWith( \u0022.sbox/\u0022 ); } );\r\n\t\t\t\t\tforeach ( var file in files )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tfilesScanned\u002B\u002B;\r\n\t\t\t\t\t\tvar rel = Path.GetRelativePath( root, file ).Replace( \u0027\\\\\u0027, \u0027/\u0027 );\r\n\t\t\t\t\t\tforeach ( System.Text.RegularExpressions.Match m in rx.Matches( File.ReadAllText( file ) ) )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tvar refPath = m.Groups[1].Value;\r\n\t\t\t\t\t\t\tbool exists = File.Exists( Path.Combine( root, refPath ) )\r\n\t\t\t\t\t\t\t\t|| File.Exists( Path.Combine( root, \u0022Assets\u0022, refPath ) );\r\n\t\t\t\t\t\t\tif ( !exists )\r\n\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\ttotal\u002B\u002B;\r\n\t\t\t\t\t\t\t\tif ( issues.Count \u003C limit )\r\n\t\t\t\t\t\t\t\t\tissues.Add( new { id = (string)null, name = rel, component = \u0022(file)\u0022, kind = \u0022missing_prefab_file\u0022, detail = $\u0022references \u0027{refPath}\u0027 which does not exist in the project\u0022 } );\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tcatch { /* file scan is best-effort \u2014 scene checks above already reported */ }\r\n\t\t}\r\n\r\n\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t{\r\n\t\t\ttotal,\r\n\t\t\tshowing = issues.Count,\r\n\t\t\ttruncated = total \u003E issues.Count,\r\n\t\t\tobjectsScanned,\r\n\t\t\tfilesScanned,\r\n\t\t\tissues,\r\n\t\t\tnote = total == 0\r\n\t\t\t\t? \u0022No broken references found.\u0022\r\n\t\t\t\t: \u0022Fix missing_model with assign_model; clear dead refs with set_property (value null) or set_component_reference to a live target; missing_prefab_file means a .scene/.prefab references a deleted/renamed prefab \u2014 fix the path or recreate it with create_prefab.\u0022\r\n\t\t} );\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// batch_set_property \u2014 set one component property to the same value across many\r\n/// GameObjects, with a dry-run mode that validates and reports without applying.\r\n/// \u003C/summary\u003E\r\npublic class BatchSetPropertyHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\tvar scene = SceneEditorSession.Active?.Scene;\r\n\t\tif ( scene == null )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022No active scene\u0022 } );\r\n\r\n\t\tif ( !p.TryGetProperty( \u0022ids\u0022, out var idsEl ) || idsEl.ValueKind != JsonValueKind.Array || idsEl.GetArrayLength() == 0 )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022ids (non-empty array of GameObject GUIDs) is required\u0022 } );\r\n\t\tvar componentType = p.TryGetProperty( \u0022component\u0022, out var ct ) ? ct.GetString() : null;\r\n\t\tif ( string.IsNullOrWhiteSpace( componentType ) )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022component (type name) is required\u0022 } );\r\n\t\tvar propertyName = p.TryGetProperty( \u0022property\u0022, out var pn ) ? pn.GetString() : null;\r\n\t\tif ( string.IsNullOrWhiteSpace( propertyName ) )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022property (name) is required\u0022 } );\r\n\t\tif ( !p.TryGetProperty( \u0022value\u0022, out var valueEl ) )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022value is required\u0022 } );\r\n\t\tbool dryRun = p.TryGetProperty( \u0022dryRun\u0022, out var dr ) \u0026\u0026 dr.ValueKind == JsonValueKind.True;\r\n\r\n\t\tvar results = new List\u003Cobject\u003E();\r\n\t\tint succeeded = 0, failed = 0, changed = 0, unchanged = 0;\r\n\r\n\t\tforeach ( var idEl in idsEl.EnumerateArray() )\r\n\t\t{\r\n\t\t\tvar id = idEl.GetString();\r\n\t\t\tvoid Fail( string why ) { failed\u002B\u002B; results.Add( new { id, ok = false, error = why } ); }\r\n\r\n\t\t\tvar go = ClaudeBridge.ResolveGameObject( scene, id );\r\n\t\t\tif ( go == null ) { Fail( \u0022GameObject not found\u0022 ); continue; }\r\n\r\n\t\t\tvar component = go.Components.GetAll()\r\n\t\t\t\t.FirstOrDefault( c =\u003E c != null \u0026\u0026 c.GetType().Name.Equals( componentType, StringComparison.OrdinalIgnoreCase ) );\r\n\t\t\tif ( component == null ) { Fail( $\u0022No \u0027{componentType}\u0027 component\u0022 ); continue; }\r\n\r\n\t\t\tvar typeDesc = Game.TypeLibrary.GetType( component.GetType().Name );\r\n\t\t\tvar propDesc = typeDesc?.Properties.FirstOrDefault( pp =\u003E pp.Name.Equals( propertyName, StringComparison.OrdinalIgnoreCase ) );\r\n\t\t\tif ( propDesc == null ) { Fail( $\u0022Property \u0027{propertyName}\u0027 not found on {componentType}\u0022 ); continue; }\r\n\r\n\t\t\tobject current = null;\r\n\t\t\ttry { current = propDesc.GetValue( component ); } catch { }\r\n\r\n\t\t\t// Resolve the exact typed value before either a dry-run receipt or a write.\r\n\t\t\t// Previously dry-run skipped coercion entirely and always claimed a change.\r\n\t\t\tobject proposed = null;\r\n\t\t\tvar valueStr = ClaudeBridge.ElementToValueString( valueEl );\r\n\t\t\tif ( !ClaudeBridge.CoercePropertyAndSet(\r\n\t\t\t\tpropDesc.PropertyType,\r\n\t\t\t\tv =\u003E proposed = v,\r\n\t\t\t\tpropDesc.Name,\r\n\t\t\t\tvalueStr,\r\n\t\t\t\tout var coerceError ) )\r\n\t\t\t{\r\n\t\t\t\tFail( coerceError );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tbool wouldChange = !Equals( current, proposed );\r\n\r\n\t\t\tif ( dryRun )\r\n\t\t\t{\r\n\t\t\t\tsucceeded\u002B\u002B;\r\n\t\t\t\tif ( wouldChange ) changed\u002B\u002B; else unchanged\u002B\u002B;\r\n\t\t\t\tresults.Add( new\r\n\t\t\t\t{\r\n\t\t\t\t\tid,\r\n\t\t\t\t\tok = true,\r\n\t\t\t\t\twouldChange,\r\n\t\t\t\t\tcurrentValue = current?.ToString(),\r\n\t\t\t\t\tproposedValue = proposed?.ToString()\r\n\t\t\t\t} );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\t// Keep apply aligned with dry-run and avoid needless setter side effects.\r\n\t\t\t\tif ( !wouldChange )\r\n\t\t\t\t{\r\n\t\t\t\t\tsucceeded\u002B\u002B;\r\n\t\t\t\t\tunchanged\u002B\u002B;\r\n\t\t\t\t\tresults.Add( new { id, ok = true, changed = false, previous = current?.ToString(), value = proposed?.ToString() } );\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tpropDesc.SetValue( component, proposed );\r\n\t\t\t\tsucceeded\u002B\u002B;\r\n\t\t\t\tchanged\u002B\u002B;\r\n\t\t\t\tresults.Add( new { id, ok = true, changed = true, previous = current?.ToString(), value = proposed?.ToString() } );\r\n\t\t\t}\r\n\t\t\tcatch ( Exception ex )\r\n\t\t\t{\r\n\t\t\t\tFail( ex.Message );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t{\r\n\t\t\ttotal = results.Count,\r\n\t\t\tsucceeded,\r\n\t\t\tfailed,\r\n\t\t\tdryRun,\r\n\t\t\tchanged,\r\n\t\t\tunchanged,\r\n\t\t\tresults,\r\n\t\t\tnote = dryRun\r\n\t\t\t\t? $\u0022Dry run - nothing was changed. {changed} would change; {unchanged} already match.\u0022\r\n\t\t\t\t: $\u0022Applied {changed} change(s); {unchanged} object(s) already matched; {failed} failed.\u0022\r\n\t\t} );\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// describe_project \u2014 a one-call orientation summary: project identity, scenes,\r\n/// prefabs, code footprint, custom component types, and installed libraries.\r\n/// \u003C/summary\u003E\r\npublic class DescribeProjectHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\tvar project = Project.Current;\r\n\t\tif ( project == null )\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = \u0022No current project\u0022 } );\r\n\r\n\t\tvar root = project.GetRootPath();\r\n\t\tvar scene = SceneEditorSession.Active?.Scene;\r\n\r\n\t\tstring[] Rel( IEnumerable\u003Cstring\u003E paths, int cap ) =\u003E\r\n\t\t\tpaths.Select( f =\u003E Path.GetRelativePath( root, f ).Replace( \u0027\\\\\u0027, \u0027/\u0027 ) )\r\n\t\t\t\t.Where( f =\u003E !f.StartsWith( \u0022Libraries/\u0022 ) \u0026\u0026 !f.StartsWith( \u0022.sbox/\u0022 ) )\r\n\t\t\t\t.Take( cap ).ToArray();\r\n\r\n\t\tstring[] scenes = Array.Empty\u003Cstring\u003E(), prefabs = Array.Empty\u003Cstring\u003E();\r\n\t\tint codeFiles = 0, razorFiles = 0;\r\n\t\ttry { scenes = Rel( Directory.GetFiles( root, \u0022*.scene\u0022, SearchOption.AllDirectories ), 50 ); } catch { }\r\n\t\ttry { prefabs = Rel( Directory.GetFiles( root, \u0022*.prefab\u0022, SearchOption.AllDirectories ), 50 ); } catch { }\r\n\t\ttry { codeFiles = Directory.GetFiles( Path.Combine( root, \u0022Code\u0022 ), \u0022*.cs\u0022, SearchOption.AllDirectories ).Length; } catch { }\r\n\t\ttry { razorFiles = Directory.GetFiles( Path.Combine( root, \u0022Code\u0022 ), \u0022*.razor\u0022, SearchOption.AllDirectories ).Length; } catch { }\r\n\r\n\t\t// Custom components = Component subclasses outside the engine namespaces.\r\n\t\tstring[] customComponents = Array.Empty\u003Cstring\u003E();\r\n\t\ttry\r\n\t\t{\r\n\t\t\tcustomComponents = Game.TypeLibrary.GetTypes\u003CComponent\u003E()\r\n\t\t\t\t.Where( t =\u003E !t.IsAbstract \u0026\u0026 t.FullName != null\r\n\t\t\t\t\t\u0026\u0026 !t.FullName.StartsWith( \u0022Sandbox.\u0022 ) \u0026\u0026 !t.FullName.StartsWith( \u0022Editor.\u0022 )\r\n\t\t\t\t\t\u0026\u0026 !t.FullName.StartsWith( \u0022Facepunch.\u0022 ) )\r\n\t\t\t\t.Select( t =\u003E t.Name ).OrderBy( n =\u003E n ).Take( 100 ).ToArray();\r\n\t\t}\r\n\t\tcatch { }\r\n\r\n\t\tstring[] libraries = Array.Empty\u003Cstring\u003E();\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar libDir = Path.Combine( root, \u0022Libraries\u0022 );\r\n\t\t\tif ( Directory.Exists( libDir ) )\r\n\t\t\t\tlibraries = Directory.GetDirectories( libDir ).Select( Path.GetFileName ).OrderBy( n =\u003E n ).ToArray();\r\n\t\t}\r\n\t\tcatch { }\r\n\r\n\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t{\r\n\t\t\tname = project.Config.Title,\r\n\t\t\tident = project.Config.Ident,\r\n\t\t\torg = project.Config.Org,\r\n\t\t\ttype = project.Config.Type,\r\n\t\t\trootPath = root.Replace( \u0027\\\\\u0027, \u0027/\u0027 ),\r\n\t\t\topenScene = scene == null ? null : new { name = scene.Name, objectCount = scene.GetAllObjects( true ).Count() },\r\n\t\t\tscenes = new { total = scenes.Length, files = scenes },\r\n\t\t\tprefabs = new { total = prefabs.Length, files = prefabs },\r\n\t\t\tcode = new { csFiles = codeFiles, razorFiles },\r\n\t\t\tcustomComponents = new { total = customComponents.Length, names = customComponents },\r\n\t\t\tlibraries,\r\n\t\t\tnote = \u0022Orient here, then: get_scene_hierarchy for the open scene, describe_type for any component, list_prefabs/get_prefab_info for prefabs, find_broken_references for health.\u0022\r\n\t\t} );\r\n\t}\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/AiSystemsHandlers.cs","FileName":"AiSystemsHandlers.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"using Editor;\r\nusing Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Text;\r\nusing System.Text.Json;\r\nusing System.Threading.Tasks;\r\n\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n//  AI \u0026 Systems \u2014 Feature Wave (create_needs_system / create_utility_ai /\r\n//  create_npc_schedule_brain / create_event_bus / add_tts_voice)\r\n//\r\n//  Compiles into the SAME editor assembly as MyEditorMenu.cs, so it uses the\r\n//  shared helpers directly: ClaudeBridge.TryResolveProjectPath / SanitizeIdentifier /\r\n//  ParseVector3 / SerializeGo, ScaffoldHelpers.PrepareCodeFile / WriteCode, and the\r\n//  IBridgeHandler dispatch contract. Handler code here is UNSANDBOXED editor code.\r\n//\r\n//  The C# *strings these handlers generate* run in the SANDBOX (the game). Every\r\n//  template below was live-compile-verified on 2026-07-12 (written into the live\r\n//  project with default params, hotloaded, compile clean, TypeLibrary-load confirmed\r\n//  for every class, then deleted): sealed Components \u002B [Sync(SyncFlags.FromHost)],\r\n//  nested data classes in [Property] List\u003CT\u003E, an abstract Component base with virtual\r\n//  members, a static (non-Component) class, a C# record, TypeLibrary.GetType(Type) \u002B\r\n//  PropertyDescription.GetValue in game code, Rotation.LookAt(Vector3),\r\n//  Sandbox.Speech.Synthesizer (fluent TrySetVoice/WithText/WithRate/Play), and\r\n//  SoundHandle (Stop(fade)/IsPlaying/IsValid/SetParent/ListenLocal/LipSync.Enabled).\r\n//\r\n//  Registration lines \u002B the _sceneMutatingCommands additions are wired by the main\r\n//  agent in MyEditorMenu.cs (see this wave\u0027s summary) to avoid a merge conflict.\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n\r\n/// \u003Csummary\u003E\r\n/// Shared helpers for the AI \u0026amp; Systems generators. Kept internal to this file so\r\n/// it does not collide with anything in MyEditorMenu.cs or sibling handler files.\r\n/// \u003C/summary\u003E\r\ninternal static class AiSystemsHelpers\r\n{\r\n\t/// \u003Csummary\u003ERead an optional float param \u2014 tolerates a JSON number OR a numeric string.\u003C/summary\u003E\r\n\tpublic static float Float( JsonElement p, string key, float fallback )\r\n\t{\r\n\t\tif ( !p.TryGetProperty( key, out var e ) ) return fallback;\r\n\t\tif ( e.ValueKind == JsonValueKind.Number \u0026\u0026 e.TryGetSingle( out var f ) ) return f;\r\n\t\tif ( e.ValueKind == JsonValueKind.String\r\n\t\t     \u0026\u0026 float.TryParse( e.GetString(), System.Globalization.NumberStyles.Float,\r\n\t\t                        System.Globalization.CultureInfo.InvariantCulture, out var fs ) ) return fs;\r\n\t\treturn fallback;\r\n\t}\r\n\r\n\tpublic static int Int( JsonElement p, string key, int fallback )\r\n\t{\r\n\t\tif ( !p.TryGetProperty( key, out var e ) ) return fallback;\r\n\t\tif ( e.ValueKind == JsonValueKind.Number \u0026\u0026 e.TryGetInt32( out var i ) ) return i;\r\n\t\tif ( e.ValueKind == JsonValueKind.String \u0026\u0026 int.TryParse( e.GetString(), out var iss ) ) return iss;\r\n\t\treturn fallback;\r\n\t}\r\n\r\n\tpublic static bool Bool( JsonElement p, string key, bool fallback )\r\n\t{\r\n\t\tif ( !p.TryGetProperty( key, out var e ) ) return fallback;\r\n\t\tif ( e.ValueKind == JsonValueKind.True ) return true;\r\n\t\tif ( e.ValueKind == JsonValueKind.False ) return false;\r\n\t\tif ( e.ValueKind == JsonValueKind.String \u0026\u0026 bool.TryParse( e.GetString(), out var b ) ) return b;\r\n\t\treturn fallback;\r\n\t}\r\n\r\n\tpublic static string Str( JsonElement p, string key, string fallback )\r\n\t{\r\n\t\tif ( p.TryGetProperty( key, out var e ) \u0026\u0026 e.ValueKind == JsonValueKind.String )\r\n\t\t{\r\n\t\t\tvar s = e.GetString();\r\n\t\t\tif ( !string.IsNullOrWhiteSpace( s ) ) return s;\r\n\t\t}\r\n\t\treturn fallback;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Format a float as an invariant-culture C# literal with an \u0027f\u0027 suffix (130 -\u003E \u0022130f\u0022).\r\n\t/// Invariant culture matters: a comma-decimal locale must not emit \u00220,25f\u0022.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string F( float v )\r\n\t{\r\n\t\tvar s = v.ToString( \u00220.0###\u0022, System.Globalization.CultureInfo.InvariantCulture );\r\n\t\treturn s \u002B \u0022f\u0022;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Escape a user string for embedding inside a REGULAR C# string literal (\u0022...\u0022) in\r\n\t/// generated code: backslash-escape \\ and \u0022, strip/escape control chars. (EscVerbatim-style\r\n\t/// quote-doubling is only valid inside @\u0022\u0022 literals \u2014 the generated property defaults and\r\n\t/// list initializers are regular literals, caught live by the quote-in-need-name test.)\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string EscString( string raw )\r\n\t{\r\n\t\treturn ( raw ?? \u0022\u0022 )\r\n\t\t\t.Replace( \u0022\\\\\u0022, \u0022\\\\\\\\\u0022 )\r\n\t\t\t.Replace( \u0022\\\u0022\u0022, \u0022\\\\\\\u0022\u0022 )\r\n\t\t\t.Replace( \u0022\\r\u0022, \u0022\\\\r\u0022 )\r\n\t\t\t.Replace( \u0022\\n\u0022, \u0022\\\\n\u0022 )\r\n\t\t\t.Replace( \u0022\\t\u0022, \u0022\\\\t\u0022 );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Attach the generated component to a scene GameObject by GUID \u2014 only possible if\r\n\t/// the type is ALREADY in the TypeLibrary (i.e. after a hotload). Mirrors the proven\r\n\t/// PlaceOnTarget in ScaffoldHandlers/EconomySaveHandlers.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static object PlaceOnTarget( string targetId, string className, out string note )\r\n\t{\r\n\t\tnote = null;\r\n\t\tvar scene = SceneEditorSession.Active?.Scene;\r\n\t\tif ( scene == null ) { note = \u0022No active scene to place into.\u0022; return null; }\r\n\t\tif ( !Guid.TryParse( targetId, out var guid ) ) { note = \u0022Invalid targetId GUID.\u0022; return null; }\r\n\t\tvar go = scene.Directory.FindByGuid( guid );\r\n\t\tif ( go == null ) { note = $\u0022Target GameObject not found: {targetId}\u0022; return null; }\r\n\t\tvar typeDesc = Game.TypeLibrary.GetType( className );\r\n\t\tif ( typeDesc == null )\r\n\t\t{\r\n\t\t\tnote = $\u0022Generated {className}.cs but it is not in the TypeLibrary yet \u2014 trigger_hotload, then add it with add_component_with_properties.\u0022;\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\ttry { go.Components.Create( typeDesc ); return ClaudeBridge.SerializeGo( go ); }\r\n\t\tcatch ( Exception ex ) { note = $\u0022Placement failed ({ex.Message}).\u0022; return null; }\r\n\t}\r\n}\r\n\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n//  1. create_needs_system  (code-gen; scene-mutating)\r\n//     Sim/tycoon needs engine: [Property] list of need definitions, per-need\r\n//     0..100 values decaying over Time.Delta, Satisfy(name, amount), weighted-\r\n//     mean Happiness, static OnNeedCritical / OnHappinessChanged events.\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\npublic class CreateNeedsSystemHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022NeedsSystem\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tvar networked = AiSystemsHelpers.Bool( p, \u0022networked\u0022, true );\r\n\r\n\t\t\t// \u2500\u2500 Need definitions: explicit \u0060needs\u0060 array wins, else the classic sim trio.\r\n\t\t\tvar needLines = new StringBuilder();\r\n\t\t\tvar needNames = new List\u003Cstring\u003E();\r\n\t\t\tif ( p.TryGetProperty( \u0022needs\u0022, out var arr ) \u0026\u0026 arr.ValueKind == JsonValueKind.Array \u0026\u0026 arr.GetArrayLength() \u003E 0 )\r\n\t\t\t{\r\n\t\t\t\tforeach ( var e in arr.EnumerateArray() )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar nName  = AiSystemsHelpers.Str(   e, \u0022name\u0022, \u0022Need\u0022 );\r\n\t\t\t\t\tvar decay  = AiSystemsHelpers.Float( e, \u0022decayPerSecond\u0022, 0.5f );\r\n\t\t\t\t\tvar crit   = AiSystemsHelpers.Float( e, \u0022criticalThreshold\u0022, 20f );\r\n\t\t\t\t\tvar weight = AiSystemsHelpers.Float( e, \u0022weight\u0022, 1f );\r\n\t\t\t\t\tneedNames.Add( nName );\r\n\t\t\t\t\tneedLines.Append( \u0022\\t\\tnew NeedDefinition { Name = \\\u0022\u0022 \u002B AiSystemsHelpers.EscString( nName )\r\n\t\t\t\t\t\t\u002B \u0022\\\u0022, DecayPerSecond = \u0022 \u002B AiSystemsHelpers.F( decay )\r\n\t\t\t\t\t\t\u002B \u0022, CriticalThreshold = \u0022 \u002B AiSystemsHelpers.F( crit )\r\n\t\t\t\t\t\t\u002B \u0022, Weight = \u0022 \u002B AiSystemsHelpers.F( weight ) \u002B \u0022 },\\n\u0022 );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tneedNames.AddRange( new[] { \u0022Hunger\u0022, \u0022Energy\u0022, \u0022Fun\u0022 } );\r\n\t\t\t\tneedLines.Append( \u0022\\t\\tnew NeedDefinition { Name = \\\u0022Hunger\\\u0022, DecayPerSecond = 0.8f, CriticalThreshold = 20f, Weight = 1f },\\n\u0022 );\r\n\t\t\t\tneedLines.Append( \u0022\\t\\tnew NeedDefinition { Name = \\\u0022Energy\\\u0022, DecayPerSecond = 0.5f, CriticalThreshold = 15f, Weight = 1f },\\n\u0022 );\r\n\t\t\t\tneedLines.Append( \u0022\\t\\tnew NeedDefinition { Name = \\\u0022Fun\\\u0022, DecayPerSecond = 0.3f, CriticalThreshold = 10f, Weight = 0.5f },\\n\u0022 );\r\n\t\t\t}\r\n\r\n\t\t\tvar code = BuildSource( className, networked, needLines.ToString() );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\tobject placedOn = null; string placeNote = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022targetId\u0022, out var tid ) \u0026\u0026 tid.ValueKind == JsonValueKind.String )\r\n\t\t\t\tplacedOn = AiSystemsHelpers.PlaceOnTarget( tid.GetString(), className, out placeNote );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tnetworked,\r\n\t\t\t\tneeds = needNames,\r\n\t\t\t\tpropertyNames = new[] { \u0022Needs\u0022, \u0022Happiness\u0022 },\r\n\t\t\t\tplacedOn,\r\n\t\t\t\tplacementNote = placeNote,\r\n\t\t\t\tnote = \u0022Per-need values live on the simulating machine only (read with GetNeed(name), restore with Satisfy(name, amount)); \u0022 \u002B\r\n\t\t\t\t       \u0022the aggregate Happiness (weighted mean 0..100) \u0022 \u002B\r\n\t\t\t\t       ( networked\r\n\t\t\t\t         ? \u0022is [Sync(FromHost)] so clients can read it. Host-authoritative: decay \u002B Satisfy only run on the host \u2014 route client actions through an [Rpc.Host] method that calls Satisfy. A no-session solo playtest makes everything a proxy (use networked:false to iterate solo). \u0022\r\n\t\t\t\t         : \u0022updates locally (networked:false build \u2014 no [Sync], no proxy guard; ticks in a single-machine playtest). \u0022 ) \u002B\r\n\t\t\t\t       \u0022OnNeedCritical is edge-triggered (fires once crossing below threshold, re-arms above it); OnHappinessChanged fires on \u003E0.25-point moves. \u0022 \u002B\r\n\t\t\t\t       \u0022Both static events fire on the simulating machine only. Needs list is inspector-editable per instance.\u0022\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_needs_system failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tprivate static string BuildSource( string className, bool networked, string needLines )\r\n\t{\r\n\t\tvar syncAttr    = networked ? \u0022[Sync( SyncFlags.FromHost )] \u0022 : \u0022\u0022;\r\n\t\tvar updateGuard = networked ? \u0022\\t\\tif ( IsProxy ) return;   // host-authoritative \u2014 only the host decays\\n\\n\u0022 : \u0022\u0022;\r\n\t\tvar satisfyGuard= networked ? \u0022\\t\\tif ( IsProxy ) return;\\n\u0022 : \u0022\u0022;\r\n\t\tvar headerNote  = networked\r\n\t\t\t? \u0022// Host-authoritative needs engine. Only the host decays/mutates needs; the aggregate\\n// Happiness is [Sync]\u0027d so clients can read it. Per-need values live host-side only.\\n\u0022\r\n\t\t\t: \u0022// Local needs engine (networked:false \u2014 no [Sync], no proxy guard). Ticks in a\\n// single-machine playtest; every machine runs its own copy if used networked.\\n\u0022;\r\n\r\n\t\treturn\r\n$@\u0022using Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\n\r\n{headerNote}public sealed class {className} : Component\r\n{{\r\n\t/// \u003Csummary\u003EOne tunable need: value starts at 100 and decays toward 0 at DecayPerSecond.\u003C/summary\u003E\r\n\tpublic sealed class NeedDefinition\r\n\t{{\r\n\t\tpublic string Name {{ get; set; }} = \u0022\u0022Need\u0022\u0022;\r\n\t\tpublic float DecayPerSecond {{ get; set; }} = 0.5f;    // points lost per second (0..100 scale)\r\n\t\tpublic float CriticalThreshold {{ get; set; }} = 20f;  // OnNeedCritical fires when value falls below this\r\n\t\tpublic float Weight {{ get; set; }} = 1f;              // contribution to the Happiness weighted mean\r\n\t}}\r\n\r\n\t[Property] public List\u003CNeedDefinition\u003E Needs {{ get; set; }} = new()\r\n\t{{\r\n{needLines}\t}};\r\n\r\n\t/// \u003Csummary\u003EWeighted mean of all need values, 0..100.\u003C/summary\u003E\r\n\t{syncAttr}public float Happiness {{ get; private set; }} = 100f;\r\n\r\n\t/// \u003Csummary\u003EFires on the simulating machine when a need first crosses below its critical threshold. Re-arms when satisfied back above it.\u003C/summary\u003E\r\n\tpublic static Action\u003C{className}, string\u003E OnNeedCritical {{ get; set; }}\r\n\t/// \u003Csummary\u003EFires when Happiness moves by more than 0.25 points. Arg = new happiness.\u003C/summary\u003E\r\n\tpublic static Action\u003C{className}, float\u003E OnHappinessChanged {{ get; set; }}\r\n\r\n\tprivate readonly Dictionary\u003Cstring, float\u003E _values = new();\r\n\tprivate readonly HashSet\u003Cstring\u003E _critical = new();\r\n\tprivate float _lastHappiness = -1f;\r\n\r\n\tprotected override void OnStart()\r\n\t{{\r\n\t\tforeach ( var need in Needs )\r\n\t\t\tif ( need != null \u0026\u0026 !string.IsNullOrEmpty( need.Name ) \u0026\u0026 !_values.ContainsKey( need.Name ) )\r\n\t\t\t\t_values[need.Name] = 100f;\r\n\t}}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{{\r\n{updateGuard}\t\tforeach ( var need in Needs )\r\n\t\t{{\r\n\t\t\tif ( need == null || string.IsNullOrEmpty( need.Name ) ) continue;\r\n\t\t\tif ( !_values.TryGetValue( need.Name, out var v ) ) {{ v = 100f; }}\r\n\r\n\t\t\tvar nv = MathX.Clamp( v - need.DecayPerSecond * Time.Delta, 0f, 100f );\r\n\t\t\t_values[need.Name] = nv;\r\n\r\n\t\t\t// Edge-triggered: fires once on crossing below threshold, re-arms above it.\r\n\t\t\tif ( nv \u003C need.CriticalThreshold )\r\n\t\t\t{{\r\n\t\t\t\tif ( _critical.Add( need.Name ) ) OnNeedCritical?.Invoke( this, need.Name );\r\n\t\t\t}}\r\n\t\t\telse\r\n\t\t\t{{\r\n\t\t\t\t_critical.Remove( need.Name );\r\n\t\t\t}}\r\n\t\t}}\r\n\r\n\t\tRecomputeHappiness();\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003ECurrent value (0..100) of a need by name, or -1 if unknown.\u003C/summary\u003E\r\n\tpublic float GetNeed( string name )\r\n\t\t=\u003E name != null \u0026\u0026 _values.TryGetValue( name, out var v ) ? v : -1f;\r\n\r\n\t/// \u003Csummary\u003ERestore a need by amount (clamped 0..100).\u003C/summary\u003E\r\n\tpublic void Satisfy( string name, float amount )\r\n\t{{\r\n{satisfyGuard}\t\tif ( name == null || !_values.ContainsKey( name ) ) return;\r\n\t\t_values[name] = MathX.Clamp( _values[name] \u002B amount, 0f, 100f );\r\n\t\tRecomputeHappiness();\r\n\t}}\r\n\r\n\tprivate void RecomputeHappiness()\r\n\t{{\r\n\t\tfloat total = 0f, weight = 0f;\r\n\t\tforeach ( var need in Needs )\r\n\t\t{{\r\n\t\t\tif ( need == null || string.IsNullOrEmpty( need.Name ) ) continue;\r\n\t\t\tif ( !_values.TryGetValue( need.Name, out var v ) ) continue;\r\n\t\t\ttotal \u002B= v * need.Weight;\r\n\t\t\tweight \u002B= need.Weight;\r\n\t\t}}\r\n\t\tvar h = weight \u003E 0f ? total / weight : 100f;\r\n\t\tif ( System.MathF.Abs( h - _lastHappiness ) \u003E 0.25f )\r\n\t\t{{\r\n\t\t\t_lastHappiness = h;\r\n\t\t\tHappiness = h;\r\n\t\t\tOnHappinessChanged?.Invoke( this, h );\r\n\t\t}}\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n//  2. create_utility_ai  (code-gen; scene-mutating)\r\n//     Scored-action brain: abstract {Prefix}Action base (Score 0..1 \u002B\r\n//     Begin/Tick/End) \u002B sealed {Prefix}Brain that picks the highest-scoring\r\n//     sibling action every EvaluateInterval (hysteresis bonus prevents\r\n//     flip-flopping) \u002B two example actions (Idle, Wander).\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\npublic class CreateUtilityAiHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar rawName   = AiSystemsHelpers.Str( p, \u0022name\u0022, \u0022Utility\u0022 );\r\n\t\t\tif ( rawName.EndsWith( \u0022.cs\u0022, StringComparison.OrdinalIgnoreCase ) )\r\n\t\t\t\trawName = rawName.Substring( 0, rawName.Length - 3 );\r\n\t\t\tvar directory = AiSystemsHelpers.Str( p, \u0022directory\u0022, \u0022Code\u0022 );\r\n\r\n\t\t\tvar prefix   = ClaudeBridge.SanitizeIdentifier( rawName, \u0022Utility\u0022 );\r\n\t\t\tvar fileName = $\u0022{prefix}Ai.cs\u0022;\r\n\t\t\tif ( !ClaudeBridge.TryResolveProjectPath( Path.Combine( directory, fileName ), out var fullPath, out var pathErr ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = pathErr } );\r\n\t\t\tif ( File.Exists( fullPath ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022File already exists: {directory}/{fileName}. Choose a different name.\u0022 } );\r\n\r\n\t\t\tvar evaluateInterval = AiSystemsHelpers.Float( p, \u0022evaluateInterval\u0022, 0.25f );\r\n\t\t\tvar hysteresisBonus  = AiSystemsHelpers.Float( p, \u0022hysteresisBonus\u0022,  0.15f );\r\n\t\t\tvar moveSpeed        = AiSystemsHelpers.Float( p, \u0022moveSpeed\u0022,        80f );\r\n\t\t\tvar wanderRadius     = AiSystemsHelpers.Float( p, \u0022wanderRadius\u0022,     300f );\r\n\t\t\tvar networked        = AiSystemsHelpers.Bool(  p, \u0022networked\u0022,        true );\r\n\r\n\t\t\tvar brainName  = $\u0022{prefix}Brain\u0022;\r\n\t\t\tvar actionBase = $\u0022{prefix}Action\u0022;\r\n\t\t\tvar idleName   = $\u0022{prefix}IdleAction\u0022;\r\n\t\t\tvar wanderName = $\u0022{prefix}WanderAction\u0022;\r\n\r\n\t\t\tvar code = BuildSource( brainName, actionBase, idleName, wanderName, networked,\r\n\t\t\t\tevaluateInterval, hysteresisBonus, moveSpeed, wanderRadius );\r\n\r\n\t\t\tDirectory.CreateDirectory( Path.GetDirectoryName( fullPath ) );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\tobject placedOn = null; string placeNote = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022targetId\u0022, out var tid ) \u0026\u0026 tid.ValueKind == JsonValueKind.String )\r\n\t\t\t\tplacedOn = AiSystemsHelpers.PlaceOnTarget( tid.GetString(), brainName, out placeNote );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = $\u0022{directory}/{fileName}\u0022,\r\n\t\t\t\tclassNames = new[] { actionBase, brainName, idleName, wanderName },\r\n\t\t\t\tnetworked,\r\n\t\t\t\tpropertyNames = new[] { \u0022EvaluateInterval\u0022, \u0022HysteresisBonus\u0022, \u0022ScoreWeight\u0022, \u0022BaseScore\u0022, \u0022MoveSpeed\u0022, \u0022WanderRadius\u0022, \u0022SecondsToFullDesire\u0022 },\r\n\t\t\t\tplacedOn,\r\n\t\t\t\tplacementNote = placeNote,\r\n\t\t\t\tnote = $\u0022Utility AI vs create_npc_brain: the FSM brain has FIXED transitions (Idle\u2192Chase\u2192Search\u2026); this brain has NO transition table \u2014 \u0022 \u002B\r\n\t\t\t\t       $\u0022every {actionBase} sibling self-scores 0..1 each EvaluateInterval and the highest (score \u00D7 ScoreWeight, current action \u002BHysteresisBonus) wins, \u0022 \u002B\r\n\t\t\t\t       \u0022so behavior emerges from the scores. Add behaviors by subclassing the abstract base ON THE SAME GameObject as the brain \u0022 \u002B\r\n\t\t\t\t       \u0022(targetId placement attaches ONLY the brain \u2014 add the example actions with add_component_with_properties after a hotload). \u0022 \u002B\r\n\t\t\t\t       \u0022The two examples alternate emergently: Wander desire builds while idle, collapses on arrival. Wander moves by direct transform walk (no navmesh, walks through walls). \u0022 \u002B\r\n\t\t\t\t       ( networked\r\n\t\t\t\t         ? \u0022Networked: host-authoritative (IsProxy guard) \u002B [Sync] CurrentActionName \u2014 needs a host session; use networked:false to iterate solo.\u0022\r\n\t\t\t\t         : \u0022Solo/local build: no proxy guard, ticks in a single-machine playtest.\u0022 )\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_utility_ai failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tprivate static string BuildSource(\r\n\t\tstring brainName, string actionBase, string idleName, string wanderName, bool networked,\r\n\t\tfloat evaluateInterval, float hysteresisBonus, float moveSpeed, float wanderRadius )\r\n\t{\r\n\t\tstring F( float v ) =\u003E AiSystemsHelpers.F( v );\r\n\r\n\t\tvar syncAttr   = networked ? \u0022[Sync( SyncFlags.FromHost )] \u0022 : \u0022\u0022;\r\n\t\tvar proxyGuard = networked ? \u0022\\t\\tif ( IsProxy ) return;   // host-authoritative \u2014 only the host thinks\\n\\n\u0022 : \u0022\u0022;\r\n\t\tvar headerNote = networked\r\n\t\t\t? \u0022// Host-authoritative: only the host evaluates \u002B ticks actions; CurrentActionName is\\n// [Sync]\u0027d for client UI. A no-session solo playtest makes everything a proxy \u2014\\n// generate with networked:false to iterate solo.\\n\u0022\r\n\t\t\t: \u0022// Solo / local brain (networked:false \u2014 no proxy guard). Ticks in a single-machine playtest.\\n\u0022;\r\n\r\n\t\treturn\r\n$@\u0022using Sandbox;\r\nusing System;\r\n\r\n// Utility AI \u2014 scored-action brain. Unlike an FSM (fixed transition table), actions\r\n// self-score 0..1 every EvaluateInterval and the highest score wins (emergent switching).\r\n// Add more actions by subclassing {actionBase} on the same GameObject.\r\n{headerNote}\r\n/// \u003Csummary\u003EBase class for utility actions. Put subclasses on the SAME GameObject as the brain.\u003C/summary\u003E\r\npublic abstract class {actionBase} : Component\r\n{{\r\n\t/// \u003Csummary\u003EMultiplier applied to Score() \u2014 raise to bias this action.\u003C/summary\u003E\r\n\t[Property] public float ScoreWeight {{ get; set; }} = 1f;\r\n\r\n\t/// \u003Csummary\u003EDesirability this instant, 0..1. Highest-scoring sibling action wins.\u003C/summary\u003E\r\n\tpublic abstract float Score();\r\n\r\n\t/// \u003Csummary\u003ECalled once when this action becomes the active one.\u003C/summary\u003E\r\n\tpublic virtual void Begin() {{ }}\r\n\t/// \u003Csummary\u003ECalled every frame while this action is active.\u003C/summary\u003E\r\n\tpublic virtual void Tick() {{ }}\r\n\t/// \u003Csummary\u003ECalled once when a better-scoring action takes over.\u003C/summary\u003E\r\n\tpublic virtual void End() {{ }}\r\n}}\r\n\r\n/// \u003Csummary\u003EPicks and runs the highest-scoring sibling {actionBase}.\u003C/summary\u003E\r\npublic sealed class {brainName} : Component\r\n{{\r\n\t/// \u003Csummary\u003ESeconds between score evaluations (the active action Ticks every frame regardless).\u003C/summary\u003E\r\n\t[Property] public float EvaluateInterval {{ get; set; }} = {F( evaluateInterval )};\r\n\t/// \u003Csummary\u003EScore bonus the CURRENT action gets during evaluation \u2014 hysteresis so near-ties don\u0027t flip-flop.\u003C/summary\u003E\r\n\t[Property] public float HysteresisBonus {{ get; set; }} = {F( hysteresisBonus )};\r\n\r\n\t{syncAttr}public string CurrentActionName {{ get; private set; }} = \u0022\u0022\u0022\u0022;\r\n\r\n\tpublic {actionBase} Current {{ get; private set; }}\r\n\r\n\t/// \u003Csummary\u003EFires on the simulating machine when the active action changes. Args = brain, new action type name.\u003C/summary\u003E\r\n\tpublic static Action\u003C{brainName}, string\u003E OnActionChanged {{ get; set; }}\r\n\r\n\tprivate TimeSince _sinceEval;\r\n\r\n\tprotected override void OnStart()\r\n\t{{\r\n\t\t_sinceEval = 999f;   // evaluate on the first eligible frame\r\n\t}}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{{\r\n{proxyGuard}\t\tif ( _sinceEval \u003E= EvaluateInterval )\r\n\t\t{{\r\n\t\t\t_sinceEval = 0f;\r\n\t\t\tEvaluate();\r\n\t\t}}\r\n\r\n\t\tif ( Current != null \u0026\u0026 Current.IsValid() \u0026\u0026 Current.Active )\r\n\t\t\tCurrent.Tick();\r\n\t}}\r\n\r\n\tprivate void Evaluate()\r\n\t{{\r\n\t\t{actionBase} best = null;\r\n\t\tfloat bestScore = float.MinValue;\r\n\r\n\t\tforeach ( var action in Components.GetAll\u003C{actionBase}\u003E() )\r\n\t\t{{\r\n\t\t\tif ( action == null || !action.IsValid() || !action.Active ) continue;\r\n\t\t\tfloat score = MathX.Clamp( action.Score(), 0f, 1f ) * action.ScoreWeight;\r\n\t\t\tif ( action == Current ) score \u002B= HysteresisBonus;\r\n\t\t\tif ( score \u003E bestScore ) {{ bestScore = score; best = action; }}\r\n\t\t}}\r\n\r\n\t\tif ( best == Current ) return;\r\n\r\n\t\tif ( Current != null \u0026\u0026 Current.IsValid() ) Current.End();\r\n\t\tCurrent = best;\r\n\t\tCurrentActionName = best != null ? best.GetType().Name : \u0022\u0022\u0022\u0022;\r\n\t\tif ( best != null ) best.Begin();\r\n\t\tOnActionChanged?.Invoke( this, CurrentActionName );\r\n\t}}\r\n}}\r\n\r\n/// \u003Csummary\u003EExample action: constant low score \u2014 the fallback when nothing else wants to run.\u003C/summary\u003E\r\npublic sealed class {idleName} : {actionBase}\r\n{{\r\n\t[Property] public float BaseScore {{ get; set; }} = 0.1f;\r\n\r\n\tpublic override float Score() =\u003E BaseScore;\r\n}}\r\n\r\n/// \u003Csummary\u003EExample action: desire builds while not wandering; walks to random points near home, then resets.\u003C/summary\u003E\r\npublic sealed class {wanderName} : {actionBase}\r\n{{\r\n\t[Property] public float MoveSpeed {{ get; set; }} = {F( moveSpeed )};\r\n\t[Property] public float WanderRadius {{ get; set; }} = {F( wanderRadius )};\r\n\t/// \u003Csummary\u003ESeconds of not-wandering until desire reaches 1.0.\u003C/summary\u003E\r\n\t[Property] public float SecondsToFullDesire {{ get; set; }} = 6f;\r\n\r\n\tprivate Vector3 _home;\r\n\tprivate Vector3 _target;\r\n\tprivate TimeSince _sinceSatisfied;\r\n\r\n\tprotected override void OnStart()\r\n\t{{\r\n\t\t_home = WorldPosition;\r\n\t\t_target = WorldPosition;\r\n\t\t_sinceSatisfied = 0f;\r\n\t}}\r\n\r\n\tpublic override float Score()\r\n\t\t=\u003E MathX.Clamp( _sinceSatisfied / System.MathF.Max( SecondsToFullDesire, 0.1f ), 0f, 1f );\r\n\r\n\tpublic override void Begin() =\u003E PickTarget();\r\n\r\n\tpublic override void Tick()\r\n\t{{\r\n\t\tvar flat = ( _target - WorldPosition ).WithZ( 0f );\r\n\t\tif ( flat.Length \u003C= 8f )\r\n\t\t{{\r\n\t\t\t_sinceSatisfied = 0f;   // reached \u2014 desire collapses, idle takes over until it rebuilds\r\n\t\t\tPickTarget();\r\n\t\t\treturn;\r\n\t\t}}\r\n\r\n\t\tvar step = flat.Normal * MoveSpeed * Time.Delta;\r\n\t\tif ( step.Length \u003E flat.Length ) step = flat;\r\n\t\tWorldPosition \u002B= step;\r\n\t\tWorldRotation = Rotation.LookAt( flat.Normal );\r\n\t}}\r\n\r\n\tprivate void PickTarget()\r\n\t{{\r\n\t\t_target = _home \u002B new Vector3(\r\n\t\t\tRandom.Shared.Float( -WanderRadius, WanderRadius ),\r\n\t\t\tRandom.Shared.Float( -WanderRadius, WanderRadius ),\r\n\t\t\t0f );\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n//  3. create_npc_schedule_brain  (code-gen; scene-mutating)\r\n//     Daily-routine NPC: schedule entries (startHour/endHour/task/target),\r\n//     reads the hour from any create_day_night_clock component (capability\r\n//     match: float TimeOfDay), falls back to an internal clock, walks to the\r\n//     active task\u0027s target, idles outside the schedule. Static OnTaskChanged.\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\npublic class CreateNpcScheduleBrainHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022NpcScheduleBrain\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tvar moveSpeed      = AiSystemsHelpers.Float( p, \u0022moveSpeed\u0022, 100f );\r\n\t\t\tvar arriveDistance = AiSystemsHelpers.Float( p, \u0022arriveDistance\u0022, 32f );\r\n\t\t\tvar fallbackDayLen = AiSystemsHelpers.Float( p, \u0022fallbackDayLengthSeconds\u0022, 600f );\r\n\t\t\tvar fallbackStart  = AiSystemsHelpers.Float( p, \u0022fallbackStartHour\u0022, 8f );\r\n\t\t\tvar useNavMesh     = AiSystemsHelpers.Bool(  p, \u0022useNavMeshAgent\u0022, false );\r\n\t\t\tvar networked      = AiSystemsHelpers.Bool(  p, \u0022networked\u0022, true );\r\n\r\n\t\t\t// \u2500\u2500 Schedule entries: explicit \u0060schedule\u0060 array wins, else a work/relax default.\r\n\t\t\tvar entryLines = new StringBuilder();\r\n\t\t\tvar taskNames  = new List\u003Cstring\u003E();\r\n\t\t\tif ( p.TryGetProperty( \u0022schedule\u0022, out var arr ) \u0026\u0026 arr.ValueKind == JsonValueKind.Array \u0026\u0026 arr.GetArrayLength() \u003E 0 )\r\n\t\t\t{\r\n\t\t\t\tforeach ( var e in arr.EnumerateArray() )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar start  = AiSystemsHelpers.Float( e, \u0022startHour\u0022, 8f );\r\n\t\t\t\t\tvar end    = AiSystemsHelpers.Float( e, \u0022endHour\u0022, 17f );\r\n\t\t\t\t\tvar task   = AiSystemsHelpers.Str(   e, \u0022taskName\u0022, \u0022Task\u0022 );\r\n\t\t\t\t\tvar target = AiSystemsHelpers.Str(   e, \u0022targetName\u0022, \u0022\u0022 );\r\n\t\t\t\t\ttaskNames.Add( task );\r\n\r\n\t\t\t\t\tvar line = \u0022\\t\\tnew ScheduleEntry { StartHour = \u0022 \u002B AiSystemsHelpers.F( start )\r\n\t\t\t\t\t\t\u002B \u0022, EndHour = \u0022 \u002B AiSystemsHelpers.F( end )\r\n\t\t\t\t\t\t\u002B \u0022, TaskName = \\\u0022\u0022 \u002B AiSystemsHelpers.EscString( task ) \u002B \u0022\\\u0022\u0022;\r\n\t\t\t\t\tif ( !string.IsNullOrEmpty( target ) )\r\n\t\t\t\t\t\tline \u002B= \u0022, TargetName = \\\u0022\u0022 \u002B AiSystemsHelpers.EscString( target ) \u002B \u0022\\\u0022\u0022;\r\n\t\t\t\t\tif ( e.TryGetProperty( \u0022targetPosition\u0022, out var posEl ) \u0026\u0026 posEl.ValueKind != JsonValueKind.Null )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar v = ClaudeBridge.ParseVector3( posEl );\r\n\t\t\t\t\t\tline \u002B= \u0022, TargetPosition = new Vector3( \u0022 \u002B AiSystemsHelpers.F( v.x ) \u002B \u0022, \u0022 \u002B AiSystemsHelpers.F( v.y ) \u002B \u0022, \u0022 \u002B AiSystemsHelpers.F( v.z ) \u002B \u0022 )\u0022;\r\n\t\t\t\t\t}\r\n\t\t\t\t\tentryLines.Append( line \u002B \u0022 },\\n\u0022 );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\ttaskNames.AddRange( new[] { \u0022Work\u0022, \u0022Relax\u0022 } );\r\n\t\t\t\tentryLines.Append( \u0022\\t\\tnew ScheduleEntry { StartHour = 8f, EndHour = 17f, TaskName = \\\u0022Work\\\u0022, TargetName = \\\u0022WorkSpot\\\u0022 },\\n\u0022 );\r\n\t\t\t\tentryLines.Append( \u0022\\t\\tnew ScheduleEntry { StartHour = 17f, EndHour = 22f, TaskName = \\\u0022Relax\\\u0022, TargetName = \\\u0022HomeSpot\\\u0022 },\\n\u0022 );\r\n\t\t\t}\r\n\r\n\t\t\tvar code = BuildSource( className, networked, useNavMesh, entryLines.ToString(),\r\n\t\t\t\tmoveSpeed, arriveDistance, fallbackDayLen, fallbackStart );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\tobject placedOn = null; string placeNote = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022targetId\u0022, out var tid ) \u0026\u0026 tid.ValueKind == JsonValueKind.String )\r\n\t\t\t\tplacedOn = AiSystemsHelpers.PlaceOnTarget( tid.GetString(), className, out placeNote );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tnetworked,\r\n\t\t\t\tuseNavMeshAgent = useNavMesh,\r\n\t\t\t\ttasks = taskNames,\r\n\t\t\t\tpropertyNames = new[] { \u0022Schedule\u0022, \u0022MoveSpeed\u0022, \u0022ArriveDistance\u0022, \u0022FallbackDayLengthSeconds\u0022, \u0022FallbackStartHour\u0022 },\r\n\t\t\t\tplacedOn,\r\n\t\t\t\tplacementNote = placeNote,\r\n\t\t\t\tnote = \u0022Time source: binds by CAPABILITY to any component exposing a float TimeOfDay property (the create_day_night_clock contract) \u2014 \u0022 \u002B\r\n\t\t\t\t       \u0022same GameObject first, then scene-wide, re-scanned every 5s while unbound. If NO clock exists it honestly falls back to its own \u0022 \u002B\r\n\t\t\t\t       \u0022internal clock (FallbackDayLengthSeconds per 24h, starting at FallbackStartHour) \u2014 check UsingClockComponent at runtime. \u0022 \u002B\r\n\t\t\t\t       \u0022A clock with a different shape (e.g. a 0..1 DayProgress) will NOT bind \u2014 generate a create_day_night_clock or match the contract. \u0022 \u002B\r\n\t\t\t\t       \u0022Entries with EndHour \u003C StartHour wrap past midnight. TargetName resolves a scene GameObject by name (case-insensitive, cached per task); \u0022 \u002B\r\n\t\t\t\t       \u0022missing names mean the NPC idles. Outside every entry the NPC idles in place. \u0022 \u002B\r\n\t\t\t\t       ( useNavMesh\r\n\t\t\t\t         ? \u0022Movement: NavMeshAgent.MoveTo \u2014 REQUIRES a baked navmesh (bake_navmesh) or the NPC won\u0027t move. \u0022\r\n\t\t\t\t         : \u0022Movement: direct transform walk (no navmesh, walks through walls \u2014 pass useNavMeshAgent:true for pathfinding). \u0022 ) \u002B\r\n\t\t\t\t       ( networked\r\n\t\t\t\t         ? \u0022Networked: host-authoritative (IsProxy guard) \u002B [Sync] CurrentTask \u2014 needs a host session; use networked:false to iterate solo.\u0022\r\n\t\t\t\t         : \u0022Solo/local build: no proxy guard, ticks in a single-machine playtest.\u0022 )\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_npc_schedule_brain failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tprivate static string BuildSource(\r\n\t\tstring className, bool networked, bool useNavMesh, string entryLines,\r\n\t\tfloat moveSpeed, float arriveDistance, float fallbackDayLen, float fallbackStart )\r\n\t{\r\n\t\tstring F( float v ) =\u003E AiSystemsHelpers.F( v );\r\n\r\n\t\tvar syncAttr   = networked ? \u0022[Sync( SyncFlags.FromHost )] \u0022 : \u0022\u0022;\r\n\t\tvar proxyGuard = networked ? \u0022\\t\\tif ( IsProxy ) return;   // host-authoritative \u2014 only the host routes\\n\\n\u0022 : \u0022\u0022;\r\n\t\tvar headerNote = networked\r\n\t\t\t? \u0022// Host-authoritative daily-routine brain. Only the host reads the clock and moves the\\n// NPC; CurrentTask is [Sync]\u0027d for client UI. A no-session solo playtest makes everything\\n// a proxy \u2014 generate with networked:false to iterate solo.\\n\u0022\r\n\t\t\t: \u0022// Solo / local daily-routine brain (networked:false \u2014 no proxy guard).\\n\u0022;\r\n\r\n\t\t// NavMeshAgent variant swaps the movement body; MoveTo/Stop/MaxSpeed are the same\r\n\t\t// calls the shipped create_npc_brain generator emits (proven sandbox surface).\r\n\t\tvar agentField   = useNavMesh ? \u0022\\tprivate NavMeshAgent _agent;\\n\u0022 : \u0022\u0022;\r\n\t\tvar agentOnStart = useNavMesh ? \u0022\\t\\t_agent = GetOrAddComponent\u003CNavMeshAgent\u003E();\\n\u0022 : \u0022\u0022;\r\n\t\tvar moveBody = useNavMesh\r\n\t\t\t?\r\n@\u0022\t\tvar flat = ( target - WorldPosition ).WithZ( 0f );\r\n\t\tif ( flat.Length \u003C= ArriveDistance ) { _agent.Stop(); return; }   // arrived \u2014 idle at the task spot\r\n\t\t_agent.MaxSpeed = MoveSpeed;\r\n\t\t_agent.MoveTo( target );\u0022\r\n\t\t\t:\r\n@\u0022\t\tvar flat = ( target - WorldPosition ).WithZ( 0f );\r\n\t\tif ( flat.Length \u003C= ArriveDistance ) return;   // arrived \u2014 idle at the task spot\r\n\r\n\t\tvar step = flat.Normal * MoveSpeed * Time.Delta;\r\n\t\tif ( step.Length \u003E flat.Length ) step = flat;\r\n\t\tWorldPosition \u002B= step;\r\n\t\tWorldRotation = Rotation.LookAt( flat.Normal );\u0022;\r\n\r\n\t\treturn\r\n$@\u0022using Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n\r\n// Daily-routine NPC brain. Reads the hour from any create_day_night_clock component\r\n// (capability match: a float TimeOfDay property) found on this GameObject or in the\r\n// scene; falls back to its own internal clock when none exists. Walks the NPC to the\r\n// active schedule entry\u0027s target and idles outside the schedule.\r\n{headerNote}public sealed class {className} : Component\r\n{{\r\n\t/// \u003Csummary\u003EOne routine block. EndHour smaller than StartHour wraps past midnight (e.g. 22 -\u003E 6).\u003C/summary\u003E\r\n\tpublic sealed class ScheduleEntry\r\n\t{{\r\n\t\tpublic float StartHour {{ get; set; }} = 8f;    // inclusive, 0..24\r\n\t\tpublic float EndHour {{ get; set; }} = 17f;     // exclusive\r\n\t\tpublic string TaskName {{ get; set; }} = \u0022\u0022Task\u0022\u0022;\r\n\t\tpublic string TargetName {{ get; set; }} = \u0022\u0022\u0022\u0022;  // named scene GameObject to walk to (wins over TargetPosition)\r\n\t\tpublic Vector3 TargetPosition {{ get; set; }}   // fixed world position, used when TargetName is empty\r\n\t}}\r\n\r\n\t[Property] public List\u003CScheduleEntry\u003E Schedule {{ get; set; }} = new()\r\n\t{{\r\n{entryLines}\t}};\r\n\r\n\t[Property] public float MoveSpeed {{ get; set; }} = {F( moveSpeed )};\r\n\t[Property] public float ArriveDistance {{ get; set; }} = {F( arriveDistance )};\r\n\r\n\t// Internal fallback clock \u2014 used ONLY when no TimeOfDay clock component is found.\r\n\t[Property] public float FallbackDayLengthSeconds {{ get; set; }} = {F( fallbackDayLen )};\r\n\t[Property] public float FallbackStartHour {{ get; set; }} = {F( fallbackStart )};\r\n\r\n\t{syncAttr}public string CurrentTask {{ get; private set; }} = \u0022\u0022\u0022\u0022;\r\n\r\n\t/// \u003Csummary\u003EThe hour (0..24) currently driving the schedule.\u003C/summary\u003E\r\n\tpublic float CurrentHour {{ get; private set; }}\r\n\t/// \u003Csummary\u003ETrue when bound to a scene clock component, false when on the internal fallback.\u003C/summary\u003E\r\n\tpublic bool UsingClockComponent =\u003E _clock != null \u0026\u0026 _clock.IsValid();\r\n\r\n\t/// \u003Csummary\u003EFires on the simulating machine when the active task changes. Args = brain, new task name (\u0022\u0022\u0022\u0022 = idle).\u003C/summary\u003E\r\n\tpublic static Action\u003C{className}, string\u003E OnTaskChanged {{ get; set; }}\r\n\r\n\tprivate Component _clock;\r\n\tprivate PropertyDescription _hourProp;\r\n\tprivate float _fallbackHour;\r\n\tprivate GameObject _targetGo;\r\n\tprivate string _resolvedTargetName;\r\n\tprivate RealTimeSince _sinceClockScan;\r\n{agentField}\r\n\tprotected override void OnStart()\r\n\t{{\r\n{agentOnStart}\t\t_fallbackHour = MathX.Clamp( FallbackStartHour, 0f, 24f );\r\n\t\t_sinceClockScan = 999f;\r\n\t}}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{{\r\n{proxyGuard}\t\t// Bind (and occasionally re-bind) to a clock \u2014 one may hotload/spawn later.\r\n\t\tif ( ( _clock == null || !_clock.IsValid() ) \u0026\u0026 _sinceClockScan \u003E 5f )\r\n\t\t\tTryBindClock();\r\n\r\n\t\tCurrentHour = ReadHour();\r\n\r\n\t\tvar entry = ActiveEntry( CurrentHour );\r\n\t\tvar task = entry != null ? ( entry.TaskName ?? \u0022\u0022\u0022\u0022 ) : \u0022\u0022\u0022\u0022;\r\n\t\tif ( task != CurrentTask )\r\n\t\t{{\r\n\t\t\tCurrentTask = task;\r\n\t\t\t_targetGo = null;\r\n\t\t\t_resolvedTargetName = null;\r\n\t\t\tOnTaskChanged?.Invoke( this, task );\r\n\t\t}}\r\n\r\n\t\tif ( entry == null ) return;   // outside every schedule block \u2014 idle in place\r\n\r\n\t\tvar target = ResolveTarget( entry );\r\n\t\tif ( target == null ) return;\r\n\t\tMoveToward( target.Value );\r\n\t}}\r\n\r\n\tprivate void TryBindClock()\r\n\t{{\r\n\t\t_sinceClockScan = 0f;\r\n\t\t_clock = null;\r\n\t\t_hourProp = null;\r\n\t\tif ( Scene == null ) return;\r\n\r\n\t\t// Same-GameObject components first, then the whole scene. Capability match:\r\n\t\t// a float TimeOfDay property (the create_day_night_clock contract).\r\n\t\tvar candidates = Components.GetAll\u003CComponent\u003E().Concat( Scene.GetAllComponents\u003CComponent\u003E() );\r\n\t\tforeach ( var c in candidates )\r\n\t\t{{\r\n\t\t\tif ( c == null || c == this || !c.IsValid() ) continue;\r\n\t\t\tvar td = TypeLibrary.GetType( c.GetType() );\r\n\t\t\tif ( td == null ) continue;\r\n\t\t\tvar hour = td.Properties.FirstOrDefault( x =\u003E x.Name == \u0022\u0022TimeOfDay\u0022\u0022 \u0026\u0026 x.PropertyType == typeof( float ) );\r\n\t\t\tif ( hour == null ) continue;\r\n\t\t\t_clock = c;\r\n\t\t\t_hourProp = hour;\r\n\t\t\treturn;\r\n\t\t}}\r\n\t}}\r\n\r\n\tprivate float ReadHour()\r\n\t{{\r\n\t\tif ( _clock != null \u0026\u0026 _clock.IsValid() \u0026\u0026 _hourProp != null )\r\n\t\t{{\r\n\t\t\tvar v = _hourProp.GetValue( _clock );\r\n\t\t\tif ( v is float f ) return MathX.Clamp( f, 0f, 24f );\r\n\t\t}}\r\n\r\n\t\t// Internal fallback: 24 in-game hours elapse per FallbackDayLengthSeconds.\r\n\t\t_fallbackHour \u002B= ( 24f / MathX.Clamp( FallbackDayLengthSeconds, 1f, 86400f ) ) * Time.Delta;\r\n\t\twhile ( _fallbackHour \u003E= 24f ) _fallbackHour -= 24f;\r\n\t\treturn _fallbackHour;\r\n\t}}\r\n\r\n\tprivate ScheduleEntry ActiveEntry( float hour )\r\n\t{{\r\n\t\tif ( Schedule == null ) return null;\r\n\t\tforeach ( var e in Schedule )\r\n\t\t{{\r\n\t\t\tif ( e == null ) continue;\r\n\t\t\tbool active = e.StartHour \u003C= e.EndHour\r\n\t\t\t\t? hour \u003E= e.StartHour \u0026\u0026 hour \u003C e.EndHour\r\n\t\t\t\t: hour \u003E= e.StartHour || hour \u003C e.EndHour;   // wraps past midnight\r\n\t\t\tif ( active ) return e;\r\n\t\t}}\r\n\t\treturn null;\r\n\t}}\r\n\r\n\tprivate Vector3? ResolveTarget( ScheduleEntry entry )\r\n\t{{\r\n\t\tif ( !string.IsNullOrEmpty( entry.TargetName ) )\r\n\t\t{{\r\n\t\t\tif ( _targetGo != null \u0026\u0026 _targetGo.IsValid() \u0026\u0026 _resolvedTargetName == entry.TargetName )\r\n\t\t\t\treturn _targetGo.WorldPosition;\r\n\r\n\t\t\t_targetGo = FindByNameRecursive( Scene, entry.TargetName );\r\n\t\t\t_resolvedTargetName = entry.TargetName;\r\n\t\t\tif ( _targetGo != null \u0026\u0026 _targetGo.IsValid() ) return _targetGo.WorldPosition;\r\n\t\t\treturn null;   // named target missing from the scene \u2014 idle\r\n\t\t}}\r\n\t\treturn entry.TargetPosition;\r\n\t}}\r\n\r\n\tprivate static GameObject FindByNameRecursive( GameObject root, string name )\r\n\t{{\r\n\t\tif ( root == null ) return null;\r\n\t\tforeach ( var child in root.Children )\r\n\t\t{{\r\n\t\t\tif ( child == null ) continue;\r\n\t\t\tif ( string.Equals( child.Name, name, StringComparison.OrdinalIgnoreCase ) ) return child;\r\n\t\t\tvar found = FindByNameRecursive( child, name );\r\n\t\t\tif ( found != null ) return found;\r\n\t\t}}\r\n\t\treturn null;\r\n\t}}\r\n\r\n\tprivate void MoveToward( Vector3 target )\r\n\t{{\r\n{moveBody}\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n//  4. create_event_bus  (code-gen; scene-mutating [writes a file])\r\n//     Typed LOCAL pub/sub: static class with Subscribe\u003CT\u003E(owner, Action\u003CT\u003E),\r\n//     Unsubscribe(owner), Publish\u003CT\u003E(evt). Plain owner-keyed handler lists \u2014\r\n//     no weak refs; owners must Unsubscribe in OnDestroy. Not a Component.\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\npublic class CreateEventBusHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022EventBus\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tvar code = BuildSource( className );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\texampleEvent = $\u0022{className}Ping\u0022,\r\n\t\t\t\tapi = new[] { \u0022Subscribe\u003CT\u003E(object owner, Action\u003CT\u003E handler)\u0022, \u0022Unsubscribe(object owner)\u0022, \u0022Publish\u003CT\u003E(T evt)\u0022, \u0022Count\u003CT\u003E()\u0022, \u0022Clear()\u0022 },\r\n\t\t\t\tnote = \u0022Pure STATIC class \u2014 nothing to place in the scene (no targetId). LOCAL only: Publish runs handlers synchronously on the \u0022 \u002B\r\n\t\t\t\t       \u0022publishing machine, exact-type-T subscribers only (no base-type dispatch); NOT networked \u2014 pair with [Rpc.Broadcast]/[Rpc.Host] \u0022 \u002B\r\n\t\t\t\t       \u0022methods that Publish on arrival for networked events. Handler lists hold PLAIN references (no weak refs): every subscriber MUST \u0022 \u002B\r\n\t\t\t\t       \u0022call Unsubscribe(this) in OnDestroy or the handler AND the owner leak for the scene\u0027s life; call Clear() on scene teardown. \u0022 \u002B\r\n\t\t\t\t       $\u0022A tiny example event record ({className}Ping) is included \u2014 define your own events as small records/classes.\u0022\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022create_event_bus failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tprivate static string BuildSource( string className )\r\n\t{\r\n\t\treturn\r\n$@\u0022using System;\r\nusing System.Collections.Generic;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} \u2014 typed LOCAL pub/sub. Subscribe with an owner object, publish typed\r\n/// events, handlers run synchronously on the publishing machine. NOT networked \u2014 pair\r\n/// with [Rpc.Broadcast] / [Rpc.Host] methods that Publish on arrival for networked events.\r\n///\r\n/// Handler lists hold PLAIN references (no weak refs): every subscriber MUST call\r\n/// Unsubscribe(this) in OnDestroy, or the handler AND the owner leak for the scene\u0027s life.\r\n/// \u003C/summary\u003E\r\npublic static class {className}\r\n{{\r\n\tprivate static readonly Dictionary\u003CType, List\u003C(object Owner, Delegate Handler)\u003E\u003E _subs = new();\r\n\r\n\t/// \u003Csummary\u003ERegister a handler for events of type T. owner is your component (used by Unsubscribe).\u003C/summary\u003E\r\n\tpublic static void Subscribe\u003CT\u003E( object owner, Action\u003CT\u003E handler )\r\n\t{{\r\n\t\tif ( owner == null || handler == null ) return;\r\n\t\tif ( !_subs.TryGetValue( typeof( T ), out var list ) )\r\n\t\t{{\r\n\t\t\tlist = new List\u003C(object, Delegate)\u003E();\r\n\t\t\t_subs[typeof( T )] = list;\r\n\t\t}}\r\n\t\tlist.Add( (owner, handler) );\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003ERemove ALL handlers registered by this owner, across every event type. Call in OnDestroy.\u003C/summary\u003E\r\n\tpublic static void Unsubscribe( object owner )\r\n\t{{\r\n\t\tif ( owner == null ) return;\r\n\t\tforeach ( var list in _subs.Values )\r\n\t\t\tlist.RemoveAll( s =\u003E ReferenceEquals( s.Owner, owner ) );\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003EDeliver evt to every exact-type-T subscriber, synchronously, in subscribe order.\u003C/summary\u003E\r\n\tpublic static void Publish\u003CT\u003E( T evt )\r\n\t{{\r\n\t\tif ( !_subs.TryGetValue( typeof( T ), out var list ) || list.Count == 0 ) return;\r\n\r\n\t\t// Snapshot so a handler may Subscribe/Unsubscribe mid-publish safely.\r\n\t\tforeach ( var sub in list.ToArray() )\r\n\t\t{{\r\n\t\t\tif ( sub.Handler is Action\u003CT\u003E a ) a( evt );\r\n\t\t}}\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003EHandlers currently registered for T (diagnostics).\u003C/summary\u003E\r\n\tpublic static int Count\u003CT\u003E() =\u003E _subs.TryGetValue( typeof( T ), out var l ) ? l.Count : 0;\r\n\r\n\t/// \u003Csummary\u003EDrop every subscription \u2014 call on scene teardown / game restart.\u003C/summary\u003E\r\n\tpublic static void Clear() =\u003E _subs.Clear();\r\n}}\r\n\r\n/// \u003Csummary\u003EExample event \u2014 define your own as small records and Publish them.\u003C/summary\u003E\r\npublic record {className}Ping( string Message );\r\n\u0022;\r\n\t}\r\n}\r\n\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n//  5. add_tts_voice  (code-gen; scene-mutating)\r\n//     TTS speaker component over the verified Sandbox.Speech.Synthesizer:\r\n//     Say(text) \u2192 TrySetVoice \u2192 WithText \u2192 WithRate \u2192 Play() \u2192 SoundHandle,\r\n//     stop-previous-on-say, positional/2D routing, optional viseme-data\r\n//     extraction (Handle.LipSync.Enabled). Audio-only \u2014 see note for why\r\n//     Sandbox.LipSync is not auto-wired.\r\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\npublic class AddTtsVoiceHandler : IBridgeHandler\r\n{\r\n\tpublic Task\u003Cobject\u003E Execute( JsonElement p )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !ScaffoldHelpers.PrepareCodeFile( p, \u0022TtsSpeaker\u0022, out var fullPath, out var relPath, out var className, out var err ) )\r\n\t\t\t\treturn Task.FromResult\u003Cobject\u003E( err );\r\n\r\n\t\t\tvar voiceName     = AiSystemsHelpers.Str(   p, \u0022voiceName\u0022, \u0022\u0022 );\r\n\t\t\tvar voiceGender   = AiSystemsHelpers.Str(   p, \u0022voiceGender\u0022, \u0022\u0022 );\r\n\t\t\tvar voiceAge      = AiSystemsHelpers.Str(   p, \u0022voiceAge\u0022, \u0022\u0022 );\r\n\t\t\tvar rate          = AiSystemsHelpers.Int(   p, \u0022rate\u0022, 0 );\r\n\t\t\tvar volume        = AiSystemsHelpers.Float( p, \u0022volume\u0022, 1f );\r\n\t\t\tvar positional    = AiSystemsHelpers.Bool(  p, \u0022positional\u0022, true );\r\n\t\t\tvar stopPrevious  = AiSystemsHelpers.Bool(  p, \u0022stopPreviousOnSay\u0022, true );\r\n\t\t\tvar stopFade      = AiSystemsHelpers.Float( p, \u0022stopFadeSeconds\u0022, 0.1f );\r\n\t\t\tvar enableVisemes = AiSystemsHelpers.Bool(  p, \u0022enableVisemeData\u0022, false );\r\n\r\n\t\t\tvar code = BuildSource( className,\r\n\t\t\t\tAiSystemsHelpers.EscString( voiceName ),\r\n\t\t\t\tAiSystemsHelpers.EscString( voiceGender ),\r\n\t\t\t\tAiSystemsHelpers.EscString( voiceAge ),\r\n\t\t\t\trate, volume, positional, stopPrevious, stopFade, enableVisemes );\r\n\t\t\tScaffoldHelpers.WriteCode( fullPath, code );\r\n\r\n\t\t\tobject placedOn = null; string placeNote = null;\r\n\t\t\tif ( p.TryGetProperty( \u0022targetId\u0022, out var tid ) \u0026\u0026 tid.ValueKind == JsonValueKind.String )\r\n\t\t\t\tplacedOn = AiSystemsHelpers.PlaceOnTarget( tid.GetString(), className, out placeNote );\r\n\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new\r\n\t\t\t{\r\n\t\t\t\tcreated = true,\r\n\t\t\t\tpath = relPath,\r\n\t\t\t\tclassName,\r\n\t\t\t\tpropertyNames = new[] { \u0022VoiceName\u0022, \u0022VoiceGender\u0022, \u0022VoiceAge\u0022, \u0022Rate\u0022, \u0022Volume\u0022, \u0022Positional\u0022, \u0022StopPreviousOnSay\u0022, \u0022StopFadeSeconds\u0022, \u0022EnableVisemeData\u0022 },\r\n\t\t\t\tplacedOn,\r\n\t\t\t\tplacementNote = placeNote,\r\n\t\t\t\tnote = \u0022Call \u003Cclass\u003E.Say(\\\u0022text\\\u0022) from game code (LOCAL audio \u2014 wrap in [Rpc.Broadcast] for everyone to hear). \u0022 \u002B\r\n\t\t\t\t       \u0022The Synthesizer API surface compiles (verified live) but the editor cannot playtest audio, so RUNTIME behavior \u0022 \u002B\r\n\t\t\t\t       \u0022(actual speech, voice selection, viseme data) is UNVERIFIED \u2014 verify in play mode with your ears. \u0022 \u002B\r\n\t\t\t\t       \u0022Voice availability is machine/OS-specific: call LogVoices() in play mode to list installed voices; TrySetVoice is \u0022 \u002B\r\n\t\t\t\t       \u0022best-effort (falls back to the OS default). Gender/age hint strings (e.g. \\\u0022Female\\\u0022/\\\u0022Adult\\\u0022) are passed through unvalidated. \u0022 \u002B\r\n\t\t\t\t       \u0022LIPSYNC: audio-only by design \u2014 s\u0026box\u0027s Sandbox.LipSync component consumes a BaseSoundComponent (verified), not the raw \u0022 \u002B\r\n\t\t\t\t       \u0022SoundHandle TTS produces, and Synthesizer.OnVisemeReached\u0027s delegate arg types can\u0027t be confirmed via reflection, so neither is \u0022 \u002B\r\n\t\t\t\t       \u0022auto-wired. enableVisemeData:true sets Handle.LipSync.Enabled so your own mouth-drive code can read Handle.LipSync.Visemes (runtime-unverified).\u0022\r\n\t\t\t} );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\treturn Task.FromResult\u003Cobject\u003E( new { error = $\u0022add_tts_voice failed: {ex.Message}\u0022 } );\r\n\t\t}\r\n\t}\r\n\r\n\tprivate static string BuildSource(\r\n\t\tstring className, string voiceNameLit, string voiceGenderLit, string voiceAgeLit,\r\n\t\tint rate, float volume, bool positional, bool stopPrevious, float stopFade, bool enableVisemes )\r\n\t{\r\n\t\tstring F( float v ) =\u003E AiSystemsHelpers.F( v );\r\n\t\tstring B( bool b ) =\u003E b ? \u0022true\u0022 : \u0022false\u0022;\r\n\r\n\t\treturn\r\n$@\u0022using Sandbox;\r\nusing System;\r\n\r\n/// \u003Csummary\u003E\r\n/// {className} \u2014 speaks text through the OS speech synthesizer (Sandbox.Speech.Synthesizer).\r\n/// LOCAL audio only: Say() synthesizes and plays on the calling machine. For networked\r\n/// voice, call Say from inside an [Rpc.Broadcast] handler so every client speaks it.\r\n/// \u003C/summary\u003E\r\npublic sealed class {className} : Component\r\n{{\r\n\t/// \u003Csummary\u003EExact installed OS voice name (see LogVoices). Empty = use VoiceGender/VoiceAge, or the OS default.\u003C/summary\u003E\r\n\t[Property] public string VoiceName {{ get; set; }} = \u0022\u0022{voiceNameLit}\u0022\u0022;\r\n\t/// \u003Csummary\u003EVoice gender hint, used only when VoiceName is empty (e.g. \u0022\u0022Female\u0022\u0022, \u0022\u0022Male\u0022\u0022). Needs VoiceAge too.\u003C/summary\u003E\r\n\t[Property] public string VoiceGender {{ get; set; }} = \u0022\u0022{voiceGenderLit}\u0022\u0022;\r\n\t/// \u003Csummary\u003EVoice age hint paired with VoiceGender (e.g. \u0022\u0022Adult\u0022\u0022, \u0022\u0022Child\u0022\u0022, \u0022\u0022Senior\u0022\u0022).\u003C/summary\u003E\r\n\t[Property] public string VoiceAge {{ get; set; }} = \u0022\u0022{voiceAgeLit}\u0022\u0022;\r\n\t/// \u003Csummary\u003ESpeaking rate offset: negative = slower, positive = faster, 0 = normal.\u003C/summary\u003E\r\n\t[Property] public int Rate {{ get; set; }} = {rate};\r\n\t[Property] public float Volume {{ get; set; }} = {F( volume )};\r\n\t/// \u003Csummary\u003ETrue: 3D sound parented to this GameObject (follows the speaker). False: flat 2D voice on the listener.\u003C/summary\u003E\r\n\t[Property] public bool Positional {{ get; set; }} = {B( positional )};\r\n\t/// \u003Csummary\u003EFade out any still-playing previous line when Say is called again.\u003C/summary\u003E\r\n\t[Property] public bool StopPreviousOnSay {{ get; set; }} = {B( stopPrevious )};\r\n\t[Property] public float StopFadeSeconds {{ get; set; }} = {F( stopFade )};\r\n\t/// \u003Csummary\u003EEnable viseme extraction on the played handle (read Handle.LipSync.Visemes from your own mouth-drive code).\u003C/summary\u003E\r\n\t[Property] public bool EnableVisemeData {{ get; set; }} = {B( enableVisemes )};\r\n\r\n\t/// \u003Csummary\u003EThe most recent line\u0027s SoundHandle (null before the first Say).\u003C/summary\u003E\r\n\tpublic SoundHandle Handle {{ get; private set; }}\r\n\tpublic bool IsSpeaking =\u003E Handle != null \u0026\u0026 Handle.IsValid \u0026\u0026 Handle.IsPlaying;\r\n\r\n\t/// \u003Csummary\u003ESynthesize and play a line. Repeated calls interrupt the previous line when StopPreviousOnSay.\u003C/summary\u003E\r\n\tpublic void Say( string text )\r\n\t{{\r\n\t\tif ( string.IsNullOrWhiteSpace( text ) ) return;\r\n\r\n\t\tif ( StopPreviousOnSay \u0026\u0026 Handle != null \u0026\u0026 Handle.IsPlaying )\r\n\t\t\tHandle.Stop( StopFadeSeconds );\r\n\r\n\t\tvar synth = new Sandbox.Speech.Synthesizer();\r\n\r\n\t\tif ( !string.IsNullOrWhiteSpace( VoiceName ) )\r\n\t\t\tsynth.TrySetVoice( VoiceName );\r\n\t\telse if ( !string.IsNullOrWhiteSpace( VoiceGender ) \u0026\u0026 !string.IsNullOrWhiteSpace( VoiceAge ) )\r\n\t\t\tsynth.TrySetVoice( VoiceGender, VoiceAge );\r\n\r\n\t\tvar handle = synth.WithText( text ).WithRate( Rate ).Play();\r\n\t\tif ( handle == null ) return;\r\n\r\n\t\thandle.Volume = Volume;\r\n\t\tif ( Positional )\r\n\t\t{{\r\n\t\t\thandle.Position = WorldPosition;\r\n\t\t\thandle.SetParent( GameObject );   // follows the speaker as it moves\r\n\t\t}}\r\n\t\telse\r\n\t\t{{\r\n\t\t\thandle.ListenLocal = true;\r\n\t\t}}\r\n\r\n\t\tif ( EnableVisemeData )\r\n\t\t\thandle.LipSync.Enabled = true;\r\n\r\n\t\tHandle = handle;\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003EFade out the current line (no-op when nothing is playing).\u003C/summary\u003E\r\n\tpublic void StopSpeaking()\r\n\t{{\r\n\t\tif ( Handle != null \u0026\u0026 Handle.IsPlaying ) Handle.Stop( StopFadeSeconds );\r\n\t}}\r\n\r\n\t/// \u003Csummary\u003ELog every installed OS voice \u002B the currently selected one (voice availability is machine-specific).\u003C/summary\u003E\r\n\tpublic void LogVoices()\r\n\t{{\r\n\t\tvar synth = new Sandbox.Speech.Synthesizer();\r\n\t\tif ( !string.IsNullOrWhiteSpace( VoiceName ) ) synth.TrySetVoice( VoiceName );\r\n\t\tforeach ( var v in synth.InstalledVoices )\r\n\t\t\tLog.Info( $\u0022\u0022[{className}] voice: {{v}}\u0022\u0022 );\r\n\t\tLog.Info( $\u0022\u0022[{className}] selected: {{synth.CurrentVoice}}\u0022\u0022 );\r\n\t}}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{{\r\n\t\tif ( Handle != null \u0026\u0026 Handle.IsPlaying ) Handle.Stop( 0f );\r\n\t}}\r\n}}\r\n\u0022;\r\n\t}\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/Mcp/BridgeComponentTools.cs","FileName":"BridgeComponentTools.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"// AUTO-GENERATED by scripts/emit-mcp-wrappers.mjs \u2014 DO NOT EDIT.\r\n// Regenerate: node scripts/extract-manifest.mjs \u0026\u0026 node scripts/emit-mcp-wrappers.mjs\r\n// Source of truth: sbox-mcp-server/src/tools/ (zod schemas) \u2192 scripts/tools-manifest.json\r\n\r\nusing System.Text.Json.Nodes;\r\nusing System.Threading.Tasks;\r\nusing Editor.Mcp;\r\n\r\n/// \u003Csummary\u003E\r\n/// Add, configure, inspect and invoke components on GameObjects: set/get properties, wire\r\n/// cross-component references, call methods and editor buttons.\r\n/// \u003C/summary\u003E\r\n[McpToolset( \u0022bridge_component\u0022, \u0022Add, configure, inspect and invoke components on GameObjects: set/get properties, wire cross-component references, call methods and editor buttons.\u0022 )]\r\npublic static class BridgeComponentTools\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Create a new GameObject, add a component, set its properties, and optionally parent/position/tag\r\n\t/// it \u2014 all in one atomic call. Collapses the create_gameobject \u2192 add_component_with_properties \u2192\r\n\t/// set_parent sequence. NOTE: a freshly GENERATED component type only resolves after a\r\n\t/// trigger_hotload; generate the script, hotload, THEN call this.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022component\u0022\u003EComponent type name to add (e.g. \u0027CameraComponent\u0027, \u0027ObjectiveManager\u0027). Use list_available_components to find valid types.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EDisplay name for the new GameObject. Defaults to the component type name.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022properties\u0022\u003EKey-value map of property names to values, auto-converted to the right type (same convention as add_component_with_properties). JSON value.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022position\u0022\u003EWorld position. As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022rotation\u0022\u003EWorld rotation. As \u0022pitch,yaw,roll\u0022 degrees.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022scale\u0022\u003EWorld scale (per-axis). As \u0022x,y,z\u0022 (or JSON {x,y,z}).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022parentId\u0022\u003EGUID of a parent GameObject. Omit for scene root.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022tags\u0022\u003ETags to add to the new GameObject (e.g. [\u0027player\u0027]).\u003C/param\u003E\r\n\t[McpTool( \u0022add_component_to_new_object\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E AddComponentToNewObject( string component, string name = null, JsonNode properties = null, string position = null, string rotation = null, string scale = null, string parentId = null, string[] tags = null )\r\n\t\t=\u003E McpGate.Run( \u0022add_component_to_new_object\u0022, McpGate.Args( ( \u0022component\u0022, component ), ( \u0022name\u0022, name ), ( \u0022properties\u0022, properties ), ( \u0022position\u0022, position ), ( \u0022rotation\u0022, rotation ), ( \u0022scale\u0022, scale ), ( \u0022parentId\u0022, parentId ), ( \u0022tags\u0022, tags ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Add a component to a GameObject and configure its properties in one call (properties PERSIST\r\n\t/// through save\u002Breload). Use list_available_components to find valid types. Returns\r\n\t/// appliedProperties \u002B failedProperties so you can see exactly what stuck.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022component\u0022\u003EComponent type name (e.g. \u0027ModelRenderer\u0027, \u0027Rigidbody\u0027, \u0027BoxCollider\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022properties\u0022\u003EKey-value map of property names to values, each auto-converted to the property\u0027s real type. Primitives \u00275\u0027/true; Color/Vector3 as comma strings \u00271,0,0,1\u0027; enum member names; ASSET refs as a path (\u0027Model\u0027:\u0027models/dev/box.vmdl\u0027, \u0027MaterialOverride\u0027:\u0027materials/x.vmat\u0027); GameObject/Component refs as a target GUID. Best-effort per key \u2014 failures are reported in failedProperties, not silently dropped. JSON value.\u003C/param\u003E\r\n\t[McpTool( \u0022add_component_with_properties\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E AddComponentWithProperties( string id, string component, JsonNode properties = null )\r\n\t\t=\u003E McpGate.Run( \u0022add_component_with_properties\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022component\u0022, component ), ( \u0022properties\u0022, properties ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Dump all public properties of every component on a GameObject. Returns { id, components } where\r\n\t/// each entry is { component, properties: [{ name, type, value }] } \u2014 values are stringified\r\n\t/// (unreadable ones show \u0027\u0026lt;error\u0026gt;\u0027). Use the exact component/property names it reports with\r\n\t/// set_property or get_property; can be large on component-heavy objects.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022get_all_properties\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E GetAllProperties( string id )\r\n\t\t=\u003E McpGate.Run( \u0022get_all_properties\u0022, McpGate.Args( ( \u0022id\u0022, id ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Read a single property value from a component on a GameObject.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022component\u0022\u003EComponent type name (e.g. \u0027ModelRenderer\u0027, \u0027PlayerController\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022property\u0022\u003EProperty name to read.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022get_property\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E GetProperty( string id, string component, string property )\r\n\t\t=\u003E McpGate.Run( \u0022get_property\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022component\u0022, component ), ( \u0022property\u0022, property ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Call a public method on a component. Matching is tried in order: (1) a [Button] attribute label,\r\n\t/// (2) the exact method NAME, (3) case-insensitive name with spaces stripped. Calls ANY public\r\n\t/// method, not only [Button]-attributed ones (e.g. \u0027StartGame\u0027). Pass \u0060args\u0060 to call methods that\r\n\t/// take parameters \u2014 the arg count must match and each value is coerced to the parameter type\r\n\t/// (primitives: string/number/bool work; complex types like Vector3 may not coerce). Omit args (or\r\n\t/// []) for parameterless methods. (list_component_buttons only lists [Button] methods, so a plain\r\n\t/// method may be invokable yet not appear there.).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022component\u0022\u003EComponent type name (e.g. \u0027MapBuilder\u0027, \u0027SasquatchedGame\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022button\u0022\u003EA [Button] label OR a public method name (e.g. \u0027Build Terrain\u0027, \u0027StartGame\u0027); case- and space-insensitive.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EOptional GameObject GUID \u2014 if omitted, finds first matching component in scene.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022args\u0022\u003EArguments to pass (must match the method\u0027s parameter count); coerced to each parameter type. JSON array.\u003C/param\u003E\r\n\t[McpTool( \u0022invoke_button\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E InvokeButton( string component, string button, string id = null, JsonNode args = null )\r\n\t\t=\u003E McpGate.Run( \u0022invoke_button\u0022, McpGate.Args( ( \u0022component\u0022, component ), ( \u0022button\u0022, button ), ( \u0022id\u0022, id ), ( \u0022args\u0022, args ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Call a public method BY NAME on a component of a live scene GameObject, passing ARGUMENTS. The\r\n\t/// with-args sibling of invoke_button (which only calls parameterless [Button]/methods on a scene\r\n\t/// component). Finds a public method matching name \u002B arg-count, coerces each JSON arg to the\r\n\t/// parameter type (primitives/enums; Color/Vector3 as comma strings \u00271,0,0,1\u0027; asset refs as a\r\n\t/// path; GameObject/Component refs as a target GUID), invokes it, and returns the method\u0027s return\r\n\t/// value as a string (null for void). Returns success=false with a clear error on\r\n\t/// resolve/coerce/throw.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022method\u0022\u003EName of the public method to call (e.g. \u0027TakeDamage\u0027, \u0027AddGold\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022component\u0022\u003EComponent type name to target (e.g. \u0027Health\u0027, \u0027PlayerController\u0027). Omit to search all components on the object for a method matching name \u002B arg-count.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022args\u0022\u003EOrdered arguments, each coerced to the matching parameter\u0027s type. Numbers/bools/strings pass through; Color/Vector3/Rotation as comma strings \u00271,0,0,1\u0027; enum member names; ASSET refs as a path (\u0027models/dev/box.vmdl\u0027); GameObject/Component refs as a target GUID. Omit (or []) for a no-arg method. JSON array.\u003C/param\u003E\r\n\t[McpTool( \u0022invoke_method\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E InvokeMethod( string id, string method, string component = null, JsonNode args = null )\r\n\t\t=\u003E McpGate.Run( \u0022invoke_method\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022method\u0022, method ), ( \u0022component\u0022, component ), ( \u0022args\u0022, args ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// List all instantiable component types in the TypeLibrary \u2014 built-in AND your project\u0027s custom\r\n\t/// components (abstract types excluded); filter does a substring match on the type name. Returns {\r\n\t/// count, components } with { name, title, description, fullName } per type, sorted by name \u2014 the\r\n\t/// unfiltered list is LARGE, so pass filter. Use the returned name with\r\n\t/// add_component_with_properties, and describe_type for a type\u0027s full property list.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022filter\u0022\u003ESearch filter \u2014 matches against component name and title.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022category\u0022\u003EFilter by category/group (e.g. \u0027Rendering\u0027, \u0027Physics\u0027, \u0027Audio\u0027).\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022list_available_components\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E ListAvailableComponents( string filter = null, string category = null )\r\n\t\t=\u003E McpGate.Run( \u0022list_available_components\u0022, McpGate.Args( ( \u0022filter\u0022, filter ), ( \u0022category\u0022, category ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// List the [Button]-attributed methods on a component. NOTE: this only finds methods decorated\r\n\t/// with [Button]; invoke_button can ALSO call any plain public no-arg method by name, so a method\r\n\t/// missing here may still be invokable. Use describe_type / get_method_signature to find non-button\r\n\t/// methods.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022component\u0022\u003EComponent type name.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EOptional GameObject GUID.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022list_component_buttons\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E ListComponentButtons( string component, string id = null )\r\n\t\t=\u003E McpGate.Run( \u0022list_component_buttons\u0022, McpGate.Args( ( \u0022component\u0022, component ), ( \u0022id\u0022, id ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Wire a component\u0027s GameObject/Component-typed property to ANOTHER live object in the scene by\r\n\t/// GUID (e.g. ObjectiveManager.Player = the player, a camera\u0027s follow target, a door\u0027s hinge).\r\n\t/// Preferred for object/component refs (can pick a specific component type off the target via\r\n\t/// targetComponent, and validates). set_property also accepts a GUID for ref props; set_prefab_ref\r\n\t/// is for prefab assets. Set clear:true to null the reference.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject that HOLDS the component you\u0027re writing into.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022component\u0022\u003EComponent type name on that object (e.g. \u0027ObjectiveManager\u0027, \u0027CameraComponent\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022property\u0022\u003EThe property to set (must be a GameObject- or Component-typed property).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetId\u0022\u003EGUID of the GameObject to reference. Required unless clear:true.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022targetComponent\u0022\u003EIf the property is a Component subtype, the specific component type to pull off the target object. Omit to auto-match by the property\u0027s type.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022clear\u0022\u003EIf true, set the reference to null instead of assigning a target.\u003C/param\u003E\r\n\t[McpTool( \u0022set_component_reference\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SetComponentReference( string id, string component, string property, string targetId = null, string targetComponent = null, bool? clear = null )\r\n\t\t=\u003E McpGate.Run( \u0022set_component_reference\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022component\u0022, component ), ( \u0022property\u0022, property ), ( \u0022targetId\u0022, targetId ), ( \u0022targetComponent\u0022, targetComponent ), ( \u0022clear\u0022, clear ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Set a property value on a component (editor mode), and PERSIST it (survives save\u002Breload).\r\n\t/// Handles primitives, enums, value types (Color/Vector3 as comma strings), AND references: pass an\r\n\t/// asset PATH for Model/Material/Texture/SoundEvent props, or a GameObject GUID for\r\n\t/// GameObject/Component-typed props (resolved like set_component_reference). Returns success=false\r\n\t/// with a clear error if a path/GUID can\u0027t be resolved (no more silent null). For wiring object\r\n\t/// refs prefer set_component_reference; for prefab refs use set_prefab_ref.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022component\u0022\u003EComponent type name.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022property\u0022\u003EProperty name to set.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022value\u0022\u003ENew value. Primitive: \u00275\u0027, \u0027true\u0027. Color/Vector3: a comma string (\u00271,0,0,1\u0027 / \u00270,0,200\u0027), an array ([0,0,200]), or an object ({r,g,b,a} / {x,y,z}). Enum: the member name. Asset ref (Model/Material/...): the asset path e.g. \u0027models/dev/box.vmdl\u0027. GameObject/Component ref: the target GameObject\u0027s GUID. Empty/\u0027null\u0027 clears the property. JSON value.\u003C/param\u003E\r\n\t[McpTool( \u0022set_property\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SetProperty( string id, string component, string property, JsonNode value )\r\n\t\t=\u003E McpGate.Run( \u0022set_property\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022component\u0022, component ), ( \u0022property\u0022, property ), ( \u0022value\u0022, value ) ) );\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/Mcp/BridgeDiscoveryTools.cs","FileName":"BridgeDiscoveryTools.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"// AUTO-GENERATED by scripts/emit-mcp-wrappers.mjs \u2014 DO NOT EDIT.\r\n// Regenerate: node scripts/extract-manifest.mjs \u0026\u0026 node scripts/emit-mcp-wrappers.mjs\r\n// Source of truth: sbox-mcp-server/src/tools/ (zod schemas) \u2192 scripts/tools-manifest.json\r\n\r\nusing System.Text.Json.Nodes;\r\nusing System.Threading.Tasks;\r\nusing Editor.Mcp;\r\n\r\n/// \u003Csummary\u003E\r\n/// Reflect over the s\u0026amp;box API: describe types, search types, get method signatures, list\r\n/// installed libraries, and search project files. Use before writing C# against unfamiliar SDK\r\n/// types.\r\n/// \u003C/summary\u003E\r\n[McpToolset( \u0022bridge_discovery\u0022, \u0022Reflect over the s\u0026box API: describe types, search types, get method signatures, list installed libraries, and search project files. Use before writing C# against unfamiliar SDK types.\u0022 )]\r\npublic static class BridgeDiscoveryTools\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Inspect a type\u0027s full surface \u2014 properties, methods, events, attributes \u2014 via reflection on\r\n\t/// Game.TypeLibrary and loaded assemblies. Use this before writing code touching an unfamiliar\r\n\t/// component or s\u0026amp;box API. Examples: \u0027MeshComponent\u0027, \u0027PlayerController\u0027, \u0027NetworkHelper\u0027,\r\n\t/// \u0027Vector3\u0027.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022name\u0022\u003EType name (short or fully-qualified).\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022describe_type\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E DescribeType( string name )\r\n\t\t=\u003E McpGate.Run( \u0022describe_type\u0022, McpGate.Args( ( \u0022name\u0022, name ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Grep the user\u0027s s\u0026amp;box project for a symbol (case-sensitive substring; skips .git/bin/obj).\r\n\t/// Useful for finding usage examples of an API or seeing how the project already does something.\r\n\t/// Returns \u0060symbol\u0060, \u0060count\u0060, and \u0060results\u0060 [{file, line, text}], capped at \u0060max_results\u0060 (default\r\n\t/// 25) \u2014 raise it if you may be missing hits. Follow up with read_file on a result\u0027s file path.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022symbol\u0022\u003ESubstring or symbol to search for.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022extension\u0022\u003EFile extension filter. Default: \u0022.cs\u0022.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022max_results\u0022\u003EMaximum hits to return (default 25); the search stops once reached.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022find_in_project\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E FindInProject( string symbol, string extension = \u0022.cs\u0022, int max_results = 25 )\r\n\t\t=\u003E McpGate.Run( \u0022find_in_project\u0022, McpGate.Args( ( \u0022symbol\u0022, symbol ), ( \u0022extension\u0022, extension ), ( \u0022max_results\u0022, max_results ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Get the formal signature(s) of a method on a type \u2014 parameter names, types, defaults, return\r\n\t/// type, all overloads. Use before invoking an API you\u0027re unsure of.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022type\u0022\u003EType name (e.g. \u0027Scene\u0027, \u0027GameObject\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022method\u0022\u003EMethod name (case-sensitive).\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022get_method_signature\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E GetMethodSignature( string type, string method )\r\n\t\t=\u003E McpGate.Run( \u0022get_method_signature\u0022, McpGate.Args( ( \u0022type\u0022, type ), ( \u0022method\u0022, method ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// List the s\u0026amp;box libraries/addons installed in this project (reads Libraries/ \u002B each .sbproj).\r\n\t/// Discovers what\u0027s available to build ON \u2014 e.g. character controllers (fish.scc = Shrimple\r\n\t/// Character Controller, facepunch.playercontroller), world/spline/road tools \u2014 so you can leverage\r\n\t/// an installed library (add its components via add_component_with_properties, or generate code\r\n\t/// against its API) instead of writing from scratch. Returns \u0060count\u0060 and \u0060libraries\u0060 [{folder,\r\n\t/// ident, org, title, type, enabled}] \u2014 ALL libraries, no limit or pagination; \u0060enabled\u0060 is false\r\n\t/// when the library\u0027s .sbproj has been disabled (renamed .sbproj.disabled). Read-only.\r\n\t/// \u003C/summary\u003E\r\n\t[McpTool.ReadOnly( \u0022list_libraries\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E ListLibraries()\r\n\t\t=\u003E McpGate.Run( \u0022list_libraries\u0022, McpGate.Args() );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Find loaded types matching a name pattern. Useful for discovering \u0027is there a built-in X for\r\n\t/// this?\u0027. Returns \u0060count\u0060 and \u0060matches\u0060 with each type\u0027s name, fullName, isComponent, and\r\n\t/// isAbstract \u2014 results are silently truncated at \u0060limit\u0060 (default 50), so narrow the pattern if\r\n\t/// you hit the cap. Pass a match\u0027s name to describe_type for its full member surface.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022pattern\u0022\u003ESubstring to match against type name (case-insensitive).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022namespace\u0022\u003EOptional namespace filter (case-insensitive substring).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022components_only\u0022\u003EOnly return Component subclasses (default false).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022limit\u0022\u003EMaximum matches to return (default 50); the search stops silently at this cap.\u003C/param\u003E\r\n\t[McpTool.ReadOnly( \u0022search_types\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SearchTypes( string pattern, string @namespace = null, bool components_only = false, int limit = 50 )\r\n\t\t=\u003E McpGate.Run( \u0022search_types\u0022, McpGate.Args( ( \u0022pattern\u0022, pattern ), ( \u0022namespace\u0022, @namespace ), ( \u0022components_only\u0022, components_only ), ( \u0022limit\u0022, limit ) ) );\r\n}\r\n"},{"Ident":"sboxskinsgg.claudebridge","Path":"Editor/Mcp/BridgeMaterialTools.cs","FileName":"BridgeMaterialTools.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":335526,"Code":"// AUTO-GENERATED by scripts/emit-mcp-wrappers.mjs \u2014 DO NOT EDIT.\r\n// Regenerate: node scripts/extract-manifest.mjs \u0026\u0026 node scripts/emit-mcp-wrappers.mjs\r\n// Source of truth: sbox-mcp-server/src/tools/ (zod schemas) \u2192 scripts/tools-manifest.json\r\n\r\nusing System.Text.Json.Nodes;\r\nusing System.Threading.Tasks;\r\nusing Editor.Mcp;\r\n\r\n/// \u003Csummary\u003E\r\n/// Assign models and materials to renderers, author .vmat materials, and set material properties.\r\n/// \u003C/summary\u003E\r\n[McpToolset( \u0022bridge_material\u0022, \u0022Assign models and materials to renderers, author .vmat materials, and set material properties.\u0022 )]\r\npublic static class BridgeMaterialTools\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Apply a material to a GameObject by setting its ModelRenderer\u0027s MaterialOverride (overrides the\r\n\t/// whole model\u0027s material). Requires an existing ModelRenderer (assign_model first) and errors if\r\n\t/// the material path can\u0027t be loaded. Returns { assigned, id, material } \u2014 tweak values afterwards\r\n\t/// with set_material_property.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022material\u0022\u003EMaterial path (e.g. \u0027materials/walls/brick.vmat\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022slot\u0022\u003EMaterial slot index. Defaults to 0 (first slot).\u003C/param\u003E\r\n\t[McpTool( \u0022assign_material\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E AssignMaterial( string id, string material, double? slot = null )\r\n\t\t=\u003E McpGate.Run( \u0022assign_material\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022material\u0022, material ), ( \u0022slot\u0022, slot ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Set a 3D model on a GameObject\u0027s ModelRenderer. Creates the renderer component if it doesn\u0027t\r\n\t/// exist; errors if the model path can\u0027t be loaded. Returns { assigned, id, model } \u2014 follow with\r\n\t/// assign_material / set_material_property to style it, or take a screenshot to verify.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022model\u0022\u003EModel path (e.g. \u0027models/citizen/citizen.vmdl\u0027, \u0027models/dev/box.vmdl\u0027).\u003C/param\u003E\r\n\t[McpTool( \u0022assign_model\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E AssignModel( string id, string model )\r\n\t\t=\u003E McpGate.Run( \u0022assign_model\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022model\u0022, model ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Create a new material file (.vmat, KV1 format) with a shader and properties like color,\r\n\t/// roughness, metallic, texture. Errors if the file already exists; when no properties are given it\r\n\t/// writes sensible PBR defaults (g_flMetalness 0, g_flRoughness 1). Returns { created, path,\r\n\t/// shader, propertiesWritten } \u2014 pass the returned path to recompile_asset (so the editor compiles\r\n\t/// it) and then assign_material.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022path\u0022\u003ERelative path for the material (e.g. \u0027materials/walls/brick.vmat\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022shader\u0022\u003EShader to use. Defaults to \u0027shaders/complex.shader\u0027 (PBR).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022properties\u0022\u003EMaterial properties as key-value pairs (e.g. { \u0022Color\u0022: \u0022#ff0000\u0022, \u0022Roughness\u0022: 0.8 }). JSON value.\u003C/param\u003E\r\n\t[McpTool( \u0022create_material\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E CreateMaterial( string path, string shader = null, JsonNode properties = null )\r\n\t\t=\u003E McpGate.Run( \u0022create_material\u0022, McpGate.Args( ( \u0022path\u0022, path ), ( \u0022shader\u0022, shader ), ( \u0022properties\u0022, properties ) ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Change a property on the material assigned to a GameObject \u2014 color, roughness, metallic,\r\n\t/// texture, etc. Operates on the ModelRenderer\u0027s MaterialOverride; if none is assigned it\r\n\t/// auto-creates one from the default complex shader (no separate assign_material step needed).\r\n\t/// Returns { set, id, property, autoCreatedMaterial } \u2014 screenshot to verify the visual change.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022id\u0022\u003EGUID of the GameObject.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022property\u0022\u003EMaterial property name (e.g. \u0027Color\u0027, \u0027Roughness\u0027, \u0027Metalness\u0027, \u0027Normal\u0027).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022value\u0022\u003EProperty value \u2014 number for floats, string for texture paths/colors, {r,g,b,a} for colors. JSON value.\u003C/param\u003E\r\n\t[McpTool( \u0022set_material_property\u0022 )]\r\n\tpublic static Task\u003Cobject\u003E SetMaterialProperty( string id, string property, JsonNode value )\r\n\t\t=\u003E McpGate.Run( \u0022set_material_property\u0022, McpGate.Args( ( \u0022id\u0022, id ), ( \u0022property\u0022, property ), ( \u0022value\u0022, value ) ) );\r\n}\r\n"}]}