Editor bridge handlers that scaffold vehicle, seat, tuning, and physics-grab tool code files for the editor. Each handler builds C# component source (VehicleController, Seat, PhysicsGrabTool) or applies presets to an existing vehicle component and writes or modifies editor scene/component state.
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
// ═══════════════════════════════════════════════════════════════════
// Batch 54 — bridge_vehicle (v2 wave 4): the corpus vehicles theme.
// create_vehicle_controller — make any Rigidbody prop drivable (raycast car
// with suspension, engine, steering, grip + built-in driver seat)
// create_seat_system — standalone generic seat (enter/exit/safe-exit)
// tune_vehicle — apply arcade/drift/offroad/race presets
// create_physics_grab_tool — physgun-style spring grab + throw
// Generated code APIs verified live: Rigidbody.ApplyForceAt/GetVelocityAtPoint/
// ApplyTorque/Velocity/Mass (describe_type, 2026-07-09). Driving FEEL needs a
// human playtest — compiles+runs ≠ fun (BRIDGE_GOTCHAS #1).
// ═══════════════════════════════════════════════════════════════════
/// <summary>create_vehicle_controller — scaffold a drivable raycast-car component.</summary>
public class CreateVehicleControllerHandler : IBridgeHandler
{
public Task<object> Execute( JsonElement p )
{
try
{
if ( !ScaffoldHelpers.PrepareCodeFile( p, "VehicleController", out var fullPath, out var relPath, out var className, out var err ) )
return Task.FromResult<object>( err );
float engine = p.TryGetProperty( "engineForce", out var ef ) && ef.TryGetSingle( out var eff ) ? eff : 900f;
float steer = p.TryGetProperty( "steerStrength", out var ss ) && ss.TryGetSingle( out var ssf ) ? ssf : 2.0f;
float grip = p.TryGetProperty( "grip", out var g ) && g.TryGetSingle( out var gf ) ? gf : 0.85f;
ScaffoldHelpers.WriteCode( fullPath, BuildCode( className, engine, steer, grip ) );
return Task.FromResult<object>( new
{
created = true,
path = relPath,
className,
nextSteps = new[]
{
"trigger_hotload, then check compile_status",
$"Attach {className} + a Rigidbody + a collider to your vehicle prop (batch_add_component works)",
"Enter play mode and press E on the vehicle to drive (WASD; E again to exit)",
"tune_vehicle applies arcade/drift/offroad/race presets to the attached component",
"HUMAN PLAYTEST REQUIRED for feel — tune EngineForce/SteerStrength/GripFactor from the inspector while playing"
}
} );
}
catch ( Exception ex )
{
return Task.FromResult<object>( new { error = $"create_vehicle_controller failed: {ex.Message}" } );
}
}
static string BuildCode( string className, float engine, float steer, float grip )
{
var ci = System.Globalization.CultureInfo.InvariantCulture;
return $@"using Sandbox;
using System;
/// <summary>
/// {className} — makes a Rigidbody prop drivable: a 4-corner raycast car with
/// spring/damper suspension, engine force, yaw steering, and lateral grip
/// (lower grip = drift). Built-in driver seat: press E (use) to enter — the
/// driver is hidden while driving (no controller transform fights), the host
/// assigns them vehicle ownership, and a chase camera follows — E to exit.
/// Requires a Rigidbody + collider on the same GameObject. Tune from the
/// inspector while playing; tune_vehicle applies ready-made presets.
/// </summary>
public sealed class {className} : Component, Component.IPressable
{{
[Property, Group( ""Engine"" )] public float EngineForce {{ get; set; }} = {engine.ToString( ci )}f;
[Property, Group( ""Engine"" )] public float MaxSpeed {{ get; set; }} = 800f;
[Property, Group( ""Steering"" )] public float SteerStrength {{ get; set; }} = {steer.ToString( ci )}f; // yaw rate, rad/s at full speed factor
[Property, Group( ""Handling"" ), Range( 0f, 1f )] public float GripFactor {{ get; set; }} = {grip.ToString( ci )}f;
[Property, Group( ""Suspension"" )] public float SuspensionRest {{ get; set; }} = 24f;
[Property, Group( ""Suspension"" )] public float SuspensionStrength {{ get; set; }} = 90f;
[Property, Group( ""Suspension"" )] public float SuspensionDamping {{ get; set; }} = 8f;
[Property, Group( ""Seat"" )] public Vector3 ExitOffset {{ get; set; }} = new( 0, 80, 20 );
[Property, Group( ""Camera"" )] public float CameraDistance {{ get; set; }} = 260f;
[Property, Group( ""Camera"" )] public float CameraHeight {{ get; set; }} = 110f;
[Sync] public Guid DriverId {{ get; set; }}
public bool HasDriver => DriverId != Guid.Empty;
public static event Action<GameObject, bool> OnDriverChanged; // (vehicle, entered)
Rigidbody _rb;
Vector3[] _corners;
TimeSince _sinceEnter;
protected override void OnStart()
{{
_rb = GetComponent<Rigidbody>();
if ( _rb == null )
{{
Log.Warning( $""{className} needs a Rigidbody on {{GameObject.Name}}"" );
Enabled = false;
return;
}}
var bounds = GameObject.GetBounds();
var ext = ( bounds.Size * 0.4f ).WithZ( 0 );
_corners = new[]
{{
new Vector3( ext.x, ext.y, 0 ), new Vector3( ext.x, -ext.y, 0 ),
new Vector3( -ext.x, ext.y, 0 ), new Vector3( -ext.x, -ext.y, 0 ),
}};
}}
// ── Seat (IPressable) ────────────────────────────────────────────
public bool Press( Component.IPressable.Event e )
{{
var presser = e.Source?.GameObject;
if ( presser == null ) return false;
RequestSeat( presser.Id );
return true;
}}
[Rpc.Host]
void RequestSeat( Guid pressGuid )
{{
var presser = Scene.Directory.FindByGuid( pressGuid );
if ( presser == null ) return;
if ( HasDriver && DriverId != pressGuid ) return;
if ( DriverId == pressGuid )
{{
Exit( presser );
return;
}}
// Enter: hide the player entirely while driving — parenting a live
// PlayerController to a moving vehicle makes two systems fight over the
// transform (the classic seat jitter). Hidden driver + chase camera instead.
DriverId = pressGuid;
_sinceEnter = 0;
var owner = presser.Network.Owner;
if ( owner != null ) GameObject.Network.AssignOwnership( owner );
presser.Enabled = false;
OnDriverChanged?.Invoke( GameObject, true );
}}
void Exit( GameObject driver )
{{
DriverId = Guid.Empty;
if ( driver != null )
{{
driver.WorldPosition = WorldPosition + WorldRotation * ExitOffset;
driver.Enabled = true; // their controller re-takes the camera next frame
}}
GameObject.Network.DropOwnership();
OnDriverChanged?.Invoke( GameObject, false );
}}
// Chase camera while driving (runs on the driver's client — they own the vehicle).
protected override void OnPreRender()
{{
if ( IsProxy || !HasDriver ) return;
var cam = Scene.Camera;
if ( cam == null ) return;
var targetPos = WorldPosition - WorldRotation.Forward.WithZ( 0 ).Normal * CameraDistance + Vector3.Up * CameraHeight;
cam.WorldPosition = cam.WorldPosition.LerpTo( targetPos, MathX.Clamp( Time.Delta * 6f, 0f, 1f ) );
cam.WorldRotation = Rotation.LookAt( ( WorldPosition + Vector3.Up * 30f - cam.WorldPosition ).Normal, Vector3.Up );
}}
// ── Driving (vehicle owner only) ─────────────────────────────────
protected override void OnFixedUpdate()
{{
if ( _rb == null || IsProxy || !HasDriver ) return;
// E again to exit (edge-guarded so the entering press cannot instantly exit).
if ( _sinceEnter > 0.4f && Input.Pressed( ""use"" ) )
{{
RequestSeat( DriverId );
return;
}}
var dt = Time.Delta;
var input = Input.AnalogMove; // x = forward/back, y = left/right
int grounded = 0;
// Suspension: 4 corner rays, spring + damper applied at each corner.
foreach ( var corner in _corners )
{{
var worldCorner = WorldPosition + WorldRotation * corner;
var tr = Scene.Trace.Ray( worldCorner, worldCorner + Vector3.Down * SuspensionRest * 2f )
.IgnoreGameObjectHierarchy( GameObject )
.Run();
if ( !tr.Hit ) continue;
grounded++;
var compression = 1f - ( tr.Distance / ( SuspensionRest * 2f ) );
var pointVel = _rb.GetVelocityAtPoint( worldCorner );
var force = Vector3.Up * ( compression * SuspensionStrength - pointVel.z * SuspensionDamping ) * _rb.Mass * dt * 50f;
_rb.ApplyForceAt( worldCorner, force );
}}
if ( grounded == 0 ) return; // airborne — no engine/steer/grip
var forward = WorldRotation.Forward.WithZ( 0 ).Normal;
var speed = _rb.Velocity.WithZ( 0 ).Length;
// Engine (mass-scaled so feel survives different props).
if ( MathF.Abs( input.x ) > 0.01f && speed < MaxSpeed )
_rb.ApplyForce( forward * input.x * EngineForce * _rb.Mass );
// Steering: set yaw angular velocity directly — arcade-reliable, immune to the
// prop's moment of inertia (torque was far too weak on heavy boxes — playtested).
var steerFactor = MathX.Clamp( speed / 150f, 0.25f, 1f );
var direction = _rb.Velocity.Dot( forward ) < -10f ? -1f : 1f; // reverse steers mirrored
var yawRate = MathF.Abs( input.y ) > 0.01f
? input.y * SteerStrength * steerFactor * direction
: 0f;
_rb.AngularVelocity = _rb.AngularVelocity.WithZ( MathX.Lerp( _rb.AngularVelocity.z, yawRate, MathX.Clamp( dt * 12f, 0f, 1f ) ) );
// Lateral grip: kill a fraction of sideways velocity each tick. Low grip = drift.
var right = WorldRotation.Right.WithZ( 0 ).Normal;
var lateral = right * _rb.Velocity.Dot( right );
_rb.Velocity -= lateral * GripFactor * MathX.Clamp( dt * 10f, 0f, 1f );
}}
}}
";
}
}
/// <summary>create_seat_system — scaffold a standalone enter/exit seat component.</summary>
public class CreateSeatSystemHandler : IBridgeHandler
{
public Task<object> Execute( JsonElement p )
{
try
{
if ( !ScaffoldHelpers.PrepareCodeFile( p, "Seat", out var fullPath, out var relPath, out var className, out var err ) )
return Task.FromResult<object>( err );
ScaffoldHelpers.WriteCode( fullPath, BuildCode( className ) );
return Task.FromResult<object>( new
{
created = true,
path = relPath,
className,
nextSteps = new[]
{
"trigger_hotload, then check compile_status",
$"Attach {className} to any prop (chair, bench, turret mount) — press E to sit, E to stand",
"SeatOffset positions the occupant; exit tries ExitOffsets in order and takes the first clear spot",
$"Subscribe to {className}.OnOccupantChanged for camera/UI logic"
}
} );
}
catch ( Exception ex )
{
return Task.FromResult<object>( new { error = $"create_seat_system failed: {ex.Message}" } );
}
}
static string BuildCode( string className )
{
return $@"using Sandbox;
using System;
using System.Linq;
/// <summary>
/// {className} — a networked one-occupant seat: press E (use) to sit, E again
/// to stand. Claims route through the host so two players can't share a seat;
/// the occupant is parented to the seat with their controller input disabled
/// (UseInputControls=false, restored on exit). Exit tries each ExitOffsets
/// entry and takes the first spot with clearance. Works for chairs, benches,
/// turret mounts — anything sittable.
/// </summary>
public sealed class {className} : Component, Component.IPressable
{{
[Property] public Vector3 SeatOffset {{ get; set; }} = new( 0, 0, 10 );
[Property] public System.Collections.Generic.List<Vector3> ExitOffsets {{ get; set; }} = new()
{{ new( 0, 60, 10 ), new( 0, -60, 10 ), new( 60, 0, 10 ), new( -60, 0, 10 ) }};
[Sync] public Guid OccupantId {{ get; set; }}
public bool IsOccupied => OccupantId != Guid.Empty;
public static event Action<GameObject, GameObject, bool> OnOccupantChanged; // (seat, occupant, seated)
public bool Press( Component.IPressable.Event e )
{{
var presser = e.Source?.GameObject;
if ( presser == null ) return false;
RequestSeat( presser.Id );
return true;
}}
[Rpc.Host]
void RequestSeat( Guid pressGuid )
{{
var presser = Scene.Directory.FindByGuid( pressGuid );
if ( presser == null ) return;
if ( OccupantId == pressGuid )
{{
SetControls( presser, true );
presser.SetParent( null, true );
presser.WorldPosition = FindExitSpot( presser );
OccupantId = Guid.Empty;
OnOccupantChanged?.Invoke( GameObject, presser, false );
return;
}}
if ( IsOccupied ) return;
OccupantId = pressGuid;
presser.SetParent( GameObject, true );
presser.LocalPosition = SeatOffset;
SetControls( presser, false );
OnOccupantChanged?.Invoke( GameObject, presser, true );
}}
Vector3 FindExitSpot( GameObject occupant )
{{
foreach ( var offset in ExitOffsets )
{{
var spot = WorldPosition + WorldRotation * offset;
var tr = Scene.Trace.Ray( spot + Vector3.Up * 32f, spot )
.IgnoreGameObjectHierarchy( GameObject )
.IgnoreGameObjectHierarchy( occupant )
.Run();
if ( !tr.Hit ) return spot;
}}
return WorldPosition + Vector3.Up * 48f; // all blocked — pop up top
}}
static void SetControls( GameObject occupant, bool enabled )
{{
foreach ( var comp in occupant.Components.GetAll() )
{{
if ( comp is null ) continue;
var type = Game.TypeLibrary?.GetType( comp.GetType() );
var prop = type?.Properties?.FirstOrDefault( pr => pr.Name == ""UseInputControls"" );
prop?.SetValue( comp, enabled );
}}
}}
}}
";
}
}
/// <summary>tune_vehicle — apply a handling preset to a vehicle controller component.</summary>
public class TuneVehicleHandler : IBridgeHandler
{
static readonly Dictionary<string, Dictionary<string, float>> Presets = new( StringComparer.OrdinalIgnoreCase )
{
["arcade"] = new() { ["EngineForce"] = 900f, ["MaxSpeed"] = 800f, ["SteerStrength"] = 2.0f, ["GripFactor"] = 0.85f, ["SuspensionStrength"] = 90f, ["SuspensionDamping"] = 8f },
["drift"] = new() { ["EngineForce"] = 1100f, ["MaxSpeed"] = 900f, ["SteerStrength"] = 2.8f, ["GripFactor"] = 0.35f, ["SuspensionStrength"] = 80f, ["SuspensionDamping"] = 6f },
["offroad"] = new() { ["EngineForce"] = 750f, ["MaxSpeed"] = 600f, ["SteerStrength"] = 1.5f, ["GripFactor"] = 0.7f, ["SuspensionStrength"] = 130f, ["SuspensionDamping"] = 12f },
["race"] = new() { ["EngineForce"] = 1400f, ["MaxSpeed"] = 1400f, ["SteerStrength"] = 1.7f, ["GripFactor"] = 0.95f, ["SuspensionStrength"] = 110f, ["SuspensionDamping"] = 10f },
};
public Task<object> Execute( JsonElement p )
{
var scene = SceneEditorSession.Active?.Scene;
if ( scene == null )
return Task.FromResult<object>( new { error = "No active scene" } );
var id = p.TryGetProperty( "id", out var idEl ) ? idEl.GetString() : null;
var go = ClaudeBridge.ResolveGameObject( scene, id );
if ( go == null )
return Task.FromResult<object>( new { error = $"GameObject not found: {id}" } );
var presetName = p.TryGetProperty( "preset", out var pr ) ? pr.GetString() : null;
if ( presetName == null || !Presets.TryGetValue( presetName, out var preset ) )
return Task.FromResult<object>( new { error = $"preset must be one of: {string.Join( " | ", Presets.Keys )}" } );
var compName = p.TryGetProperty( "component", out var cn ) ? cn.GetString() : null;
var component = go.Components.GetAll().FirstOrDefault( c => c != null &&
( compName != null
? c.GetType().Name.Equals( compName, StringComparison.OrdinalIgnoreCase )
: c.GetType().Name.Contains( "Vehicle", StringComparison.OrdinalIgnoreCase ) ) );
if ( component == null )
return Task.FromResult<object>( new { error = compName != null
? $"No '{compName}' component on the object"
: "No component with 'Vehicle' in its type name found — pass component explicitly" } );
var typeDesc = Game.TypeLibrary.GetType( component.GetType().Name );
var applied = new List<object>();
var missing = new List<string>();
foreach ( var (propName, value) in preset )
{
var propDesc = typeDesc?.Properties.FirstOrDefault( pp => pp.Name == propName );
if ( propDesc == null ) { missing.Add( propName ); continue; }
try
{
propDesc.SetValue( component, value );
applied.Add( new { property = propName, value } );
}
catch ( Exception ex ) { missing.Add( $"{propName} ({ex.Message})" ); }
}
return Task.FromResult<object>( new
{
tuned = applied.Count > 0,
preset = presetName.ToLowerInvariant(),
component = component.GetType().Name,
applied,
missing,
note = missing.Count > 0
? "Some preset properties don't exist on this component — presets target create_vehicle_controller scaffolds; others tune partially."
: "Preset applied. Enter play mode and drive to feel it; fine-tune the same properties with set_property."
} );
}
}
/// <summary>create_physics_grab_tool — scaffold a physgun-style spring grab + throw.</summary>
public class CreatePhysicsGrabToolHandler : IBridgeHandler
{
public Task<object> Execute( JsonElement p )
{
try
{
if ( !ScaffoldHelpers.PrepareCodeFile( p, "PhysicsGrabTool", out var fullPath, out var relPath, out var className, out var err ) )
return Task.FromResult<object>( err );
ScaffoldHelpers.WriteCode( fullPath, BuildCode( className ) );
return Task.FromResult<object>( new
{
created = true,
path = relPath,
className,
nextSteps = new[]
{
"trigger_hotload, then check compile_status",
$"Attach {className} to the player object (needs a camera child or PlayerController for aim)",
"Hold attack2 (right mouse) on a Rigidbody prop to grab; scroll-free: it follows at grab distance; attack1 throws",
"ensure_input_action if your project lacks attack1/attack2 bindings"
}
} );
}
catch ( Exception ex )
{
return Task.FromResult<object>( new { error = $"create_physics_grab_tool failed: {ex.Message}" } );
}
}
static string BuildCode( string className )
{
return $@"using Sandbox;
using System;
using System.Linq;
/// <summary>
/// {className} — a physgun-lite for the player: hold GrabAction (default
/// attack2) while looking at a Rigidbody prop to grab it; it spring-follows a
/// point in front of your view (physics stays LIVE — it collides and swings,
/// unlike a parented carry); press ThrowAction (default attack1) to launch it.
/// Grab requests route through the host, which assigns the grabber network
/// ownership of the prop. Owner-only logic; attach to the player object.
/// </summary>
public sealed class {className} : Component
{{
[Property] public float Range {{ get; set; }} = 300f;
[Property] public float SpringStrength {{ get; set; }} = 12f;
[Property] public float ThrowForce {{ get; set; }} = 600f;
[Property] public float MaxMass {{ get; set; }} = 2000f;
[Property] public string GrabAction {{ get; set; }} = ""attack2"";
[Property] public string ThrowAction {{ get; set; }} = ""attack1"";
GameObject _held;
float _holdDistance;
public bool IsHolding => _held.IsValid();
public static event Action<GameObject, GameObject, bool> OnGrabChanged; // (player, prop, grabbed)
protected override void OnFixedUpdate()
{{
if ( IsProxy ) return;
var eye = GetEye( out var dir );
if ( IsHolding && Input.Pressed( ThrowAction ) )
{{
var rb = _held.GetComponent<Rigidbody>();
rb?.ApplyImpulse( dir * ThrowForce * ( rb.Mass ) );
Release();
return;
}}
if ( Input.Down( GrabAction ) )
{{
if ( !IsHolding ) TryGrab( eye, dir );
else Hold( eye, dir );
}}
else if ( IsHolding )
{{
Release();
}}
}}
Vector3 GetEye( out Vector3 dir )
{{
var cam = Scene.Camera;
if ( cam != null )
{{
dir = cam.WorldRotation.Forward;
return cam.WorldPosition;
}}
dir = WorldRotation.Forward;
return WorldPosition + Vector3.Up * 64f;
}}
void TryGrab( Vector3 eye, Vector3 dir )
{{
var tr = Scene.Trace.Ray( eye, eye + dir * Range )
.IgnoreGameObjectHierarchy( GameObject )
.Run();
if ( !tr.Hit || tr.GameObject == null ) return;
var rb = tr.GameObject.GetComponent<Rigidbody>();
if ( rb == null || rb.Mass > MaxMass ) return;
_held = tr.GameObject;
_holdDistance = MathX.Clamp( tr.Distance, 60f, Range );
RequestGrabOwnership( _held.Id );
OnGrabChanged?.Invoke( GameObject, _held, true );
}}
void Hold( Vector3 eye, Vector3 dir )
{{
if ( !_held.IsValid() ) {{ _held = null; return; }}
var rb = _held.GetComponent<Rigidbody>();
if ( rb == null ) {{ Release(); return; }}
var target = eye + dir * _holdDistance;
// Velocity-set spring: stiff, stable, still collides with the world.
rb.Velocity = ( target - _held.WorldPosition ) * SpringStrength;
rb.AngularVelocity = rb.AngularVelocity.LerpTo( Vector3.Zero, Time.Delta * 5f );
}}
void Release()
{{
if ( _held.IsValid() )
OnGrabChanged?.Invoke( GameObject, _held, false );
_held = null;
}}
[Rpc.Host]
void RequestGrabOwnership( Guid propId )
{{
var prop = Scene.Directory.FindByGuid( propId );
var caller = Rpc.Caller;
if ( prop == null || caller is null ) return;
prop.Network.AssignOwnership( caller );
}}
}}
";
}
}