Editor/PhysicsToolHandlers.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Editor;
using Sandbox;
namespace SboxMcpServer;
/// <summary>
/// Physics & Collider MCP tools: add_collider, configure_collider, add_rigidbody.
/// </summary>
internal static class PhysicsToolHandlers
{
// ── add_collider ──────────────────────────────────────────────────────
internal static object AddCollider( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );
string id = OzmiumSceneHelpers.Get( args, "id", (string)null );
string name = OzmiumSceneHelpers.Get( args, "name", (string)null );
string ctype = OzmiumSceneHelpers.Get( args, "colliderType", "BoxCollider" );
var go = OzmiumSceneHelpers.FindGo( scene, id, name );
if ( go == null ) return OzmiumSceneHelpers.Txt( "Object not found." );
try
{
Collider collider;
switch ( ctype.ToLowerInvariant() )
{
case "boxcollider":
case "box":
{
var c = go.Components.Create<BoxCollider>();
if ( args.TryGetProperty( "size", out var szEl ) && szEl.ValueKind == JsonValueKind.Object )
{
c.Scale = ParseV3( szEl );
}
if ( args.TryGetProperty( "center", out var ctEl ) && ctEl.ValueKind == JsonValueKind.Object )
{
c.Center = ParseV3( ctEl );
}
collider = c;
break;
}
case "spherecollider":
case "sphere":
{
var c = go.Components.Create<SphereCollider>();
if ( args.TryGetProperty( "center", out var ctEl ) && ctEl.ValueKind == JsonValueKind.Object )
{
c.Center = ParseV3( ctEl );
}
c.Radius = OzmiumSceneHelpers.Get( args, "radius", 32f );
collider = c;
break;
}
case "capsulecollider":
case "capsule":
{
var c = go.Components.Create<CapsuleCollider>();
c.Start = OzmiumSceneHelpers.Get( args, "start", Vector3.Zero );
c.End = OzmiumSceneHelpers.Get( args, "end", new Vector3( 0, 64, 0 ) );
c.Radius = OzmiumSceneHelpers.Get( args, "radius", 16f );
collider = c;
break;
}
case "modelcollider":
case "model":
{
collider = go.Components.Create<ModelCollider>();
break;
}
default:
return OzmiumSceneHelpers.Txt( $"Unknown collider type '{ctype}'. Use: BoxCollider, SphereCollider, CapsuleCollider, ModelCollider." );
}
// Common collider properties
if ( args.TryGetProperty( "isTrigger", out var trigEl ) )
collider.IsTrigger = trigEl.GetBoolean();
if ( args.TryGetProperty( "friction", out var frEl ) && frEl.ValueKind != JsonValueKind.Null )
collider.Friction = frEl.GetSingle();
if ( args.TryGetProperty( "elasticity", out var elEl ) && elEl.ValueKind != JsonValueKind.Null )
collider.Elasticity = elEl.GetSingle();
if ( args.TryGetProperty( "surfaceVelocity", out var svEl ) && svEl.ValueKind == JsonValueKind.Object )
collider.SurfaceVelocity = ParseV3( svEl );
return OzmiumSceneHelpers.Txt( $"Added {ctype} to '{go.Name}'." );
}
catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
}
// ── configure_collider ───────────────────────────────────────────────────
internal static object ConfigureCollider( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );
string id = OzmiumSceneHelpers.Get( args, "id", (string)null );
string name = OzmiumSceneHelpers.Get( args, "name", (string)null );
var go = OzmiumSceneHelpers.FindGo( scene, id, name );
if ( go == null ) return OzmiumSceneHelpers.Txt( "Object not found." );
var collider = go.Components.GetAll().FirstOrDefault( c => c is Collider ) as Collider;
if ( collider == null ) return OzmiumSceneHelpers.Txt( $"No Collider component found on '{go.Name}'." );
try
{
// BoxCollider-specific
if ( collider is BoxCollider bc )
{
if ( args.TryGetProperty( "size", out var szEl ) && szEl.ValueKind == JsonValueKind.Object )
bc.Scale = ParseV3( szEl );
if ( args.TryGetProperty( "center", out var ctEl ) && ctEl.ValueKind == JsonValueKind.Object )
bc.Center = ParseV3( ctEl );
}
// SphereCollider-specific
if ( collider is SphereCollider sc )
{
if ( args.TryGetProperty( "center", out var ctEl ) && ctEl.ValueKind == JsonValueKind.Object )
sc.Center = ParseV3( ctEl );
if ( args.TryGetProperty( "radius", out var rEl ) )
sc.Radius = rEl.GetSingle();
}
// CapsuleCollider-specific
if ( collider is CapsuleCollider cc )
{
if ( args.TryGetProperty( "start", out var stEl ) )
{
if ( stEl.ValueKind == JsonValueKind.Object ) cc.Start = ParseV3( stEl );
else if ( stEl.ValueKind == JsonValueKind.Array ) cc.Start = ParseV3FromArr( stEl );
}
if ( args.TryGetProperty( "end", out var enEl ) )
{
if ( enEl.ValueKind == JsonValueKind.Object ) cc.End = ParseV3( enEl );
else if ( enEl.ValueKind == JsonValueKind.Array ) cc.End = ParseV3FromArr( enEl );
}
if ( args.TryGetProperty( "radius", out var crEl ) )
cc.Radius = crEl.GetSingle();
}
// Common properties
if ( args.TryGetProperty( "isTrigger", out var trigEl ) )
collider.IsTrigger = trigEl.GetBoolean();
if ( args.TryGetProperty( "friction", out var frEl ) && frEl.ValueKind != JsonValueKind.Null )
collider.Friction = frEl.GetSingle();
if ( args.TryGetProperty( "elasticity", out var elEl ) && elEl.ValueKind != JsonValueKind.Null )
collider.Elasticity = elEl.GetSingle();
if ( args.TryGetProperty( "surfaceVelocity", out var svEl ) && svEl.ValueKind == JsonValueKind.Object )
collider.SurfaceVelocity = ParseV3( svEl );
return OzmiumSceneHelpers.Txt( $"Configured {collider.GetType().Name} on '{go.Name}'." );
}
catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
}
// ── add_rigidbody ──────────────────────────────────────────────────────
internal static object AddRigidbody( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );
string id = OzmiumSceneHelpers.Get( args, "id", (string)null );
string name = OzmiumSceneHelpers.Get( args, "name", (string)null );
var go = OzmiumSceneHelpers.FindGo( scene, id, name );
if ( go == null ) return OzmiumSceneHelpers.Txt( "Object not found." );
try
{
var rb = go.Components.Create<Rigidbody>();
rb.MassOverride = OzmiumSceneHelpers.Get( args, "mass", 0f );
rb.LinearDamping = OzmiumSceneHelpers.Get( args, "linearDamping", 0f );
rb.AngularDamping = OzmiumSceneHelpers.Get( args, "angularDamping", 0f );
rb.Gravity = OzmiumSceneHelpers.Get( args, "gravity", true );
rb.GravityScale = OzmiumSceneHelpers.Get( args, "gravityScale", 1f );
return OzmiumSceneHelpers.Txt( $"Added Rigidbody to '{go.Name}' (massOverride={rb.MassOverride}, gravity={rb.Gravity})." );
}
catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
}
// ── Helpers ─────────────────────────────────────────────────────────────
private static Vector3 ParseV3( JsonElement el )
{
float vx = 0, vy = 0, vz = 0;
if ( el.TryGetProperty( "x", out var xp ) ) vx = xp.GetSingle();
if ( el.TryGetProperty( "y", out var yp ) ) vy = yp.GetSingle();
if ( el.TryGetProperty( "z", out var zp ) ) vz = zp.GetSingle();
return new Vector3( vx, vy, vz );
}
private static Vector3 ParseV3FromArr( JsonElement el )
{
if ( el.ValueKind != JsonValueKind.Array ) return Vector3.Zero;
var arr = el.EnumerateArray().ToList();
return new Vector3(
arr.Count > 0 ? arr[0].GetSingle() : 0,
arr.Count > 1 ? arr[1].GetSingle() : 0,
arr.Count > 2 ? arr[2].GetSingle() : 0 );
}
// ── Schemas ─────────────────────────────────────────────────────────────
private static readonly Dictionary<string, object> ColliderTypes = new()
{
["type"] = "string",
["description"] = "Type of collider to add.",
["enum"] = new[] { "BoxCollider", "SphereCollider", "CapsuleCollider", "ModelCollider" }
};
private static readonly Dictionary<string, object> V3Prop = new()
{
["type"] = "object", ["description"] = "Vector3 {x, y, z}."
};
internal static Dictionary<string, object> SchemaAddCollider => S( "add_collider",
"Adds a collider component to a GameObject with configured properties.",
new Dictionary<string, object>
{
["id"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "GUID." },
["name"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Exact name." },
["colliderType"] = ColliderTypes,
["size"] = V3Prop,
["center"] = V3Prop,
["radius"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Radius (SphereCollider/CapsuleCollider)." },
["start"] = V3Prop,
["end"] = V3Prop,
["isTrigger"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Whether this is a trigger volume." },
["friction"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Friction (0-1)." },
["elasticity"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Bounciness (0-1)." },
["surfaceVelocity"] = V3Prop
},
new[] { "colliderType" } );
internal static Dictionary<string, object> SchemaConfigureCollider => S( "configure_collider",
"Modifies properties on an existing Collider component.",
new Dictionary<string, object>
{
["id"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "GUID." },
["name"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Exact name." },
["size"] = V3Prop,
["center"] = V3Prop,
["radius"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Radius." },
["start"] = V3Prop,
["end"] = V3Prop,
["isTrigger"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Trigger volume." },
["friction"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Friction." },
["elasticity"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Bounciness." },
["surfaceVelocity"] = V3Prop
} );
internal static Dictionary<string, object> SchemaAddRigidbody => S( "add_rigidbody",
"Adds a Rigidbody component to a GameObject for physics simulation.",
new Dictionary<string, object>
{
["id"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "GUID." },
["name"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Exact name." },
["mass"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Mass override (0 = auto)." },
["linearDamping"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Linear damping." },
["angularDamping"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Angular damping." },
["gravity"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Enable gravity (default true)." },
["gravityScale"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Gravity scale (default 1)." }
} );
private static Dictionary<string, object> S( string name, string desc, Dictionary<string, object> props, string[] req = null )
{
var schema = new Dictionary<string, object> { ["type"] = "object", ["properties"] = props };
if ( req != null ) schema["required"] = req;
return new Dictionary<string, object> { ["name"] = name, ["description"] = desc, ["inputSchema"] = schema };
}
// ── create_character_controller ───────────────────────────────────────
internal static object CreateCharacterController( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );
float x = OzmiumSceneHelpers.Get( args, "x", 0f );
float y = OzmiumSceneHelpers.Get( args, "y", 0f );
float z = OzmiumSceneHelpers.Get( args, "z", 0f );
string name = OzmiumSceneHelpers.Get( args, "name", "Character Controller" );
try
{
var go = scene.CreateObject();
go.Name = name;
go.WorldPosition = new Vector3( x, y, z );
var cc = go.Components.Create<CharacterController>();
cc.Radius = OzmiumSceneHelpers.Get( args, "radius", 16f );
cc.Height = OzmiumSceneHelpers.Get( args, "height", 64f );
cc.StepHeight = OzmiumSceneHelpers.Get( args, "stepHeight", 18f );
cc.GroundAngle = OzmiumSceneHelpers.Get( args, "groundAngle", 45f );
cc.Acceleration = OzmiumSceneHelpers.Get( args, "acceleration", 10f );
cc.Bounciness = OzmiumSceneHelpers.Get( args, "bounciness", 0.3f );
return OzmiumSceneHelpers.Txt( $"Created CharacterController on '{go.Name}' (radius={cc.Radius}, height={cc.Height})." );
}
catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
}
// ── add_plane_collider ───────────────────────────────────────────────
internal static object AddPlaneCollider( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );
string id = OzmiumSceneHelpers.Get( args, "id", (string)null );
string name = OzmiumSceneHelpers.Get( args, "name", (string)null );
var go = OzmiumSceneHelpers.FindGo( scene, id, name );
if ( go == null ) return OzmiumSceneHelpers.Txt( "Object not found." );
try
{
var plane = go.Components.Create<PlaneCollider>();
if ( args.TryGetProperty( "scale", out var scEl ) && scEl.ValueKind == JsonValueKind.Object )
{
float sx = OzmiumSceneHelpers.Get( scEl, "x", 50f );
float sy = OzmiumSceneHelpers.Get( scEl, "y", 50f );
plane.Scale = new Vector2( sx, sy );
}
if ( args.TryGetProperty( "center", out var ctEl ) && ctEl.ValueKind == JsonValueKind.Object )
plane.Center = ParseV3( ctEl );
if ( args.TryGetProperty( "normal", out var nmEl ) && nmEl.ValueKind == JsonValueKind.Object )
plane.Normal = ParseV3( nmEl );
return OzmiumSceneHelpers.Txt( $"Added PlaneCollider to '{go.Name}'." );
}
catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
}
// ── add_hull_collider ────────────────────────────────────────────────
internal static object AddHullCollider( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );
string id = OzmiumSceneHelpers.Get( args, "id", (string)null );
string name = OzmiumSceneHelpers.Get( args, "name", (string)null );
string hullType = OzmiumSceneHelpers.Get( args, "hullType", "Box" );
var go = OzmiumSceneHelpers.FindGo( scene, id, name );
if ( go == null ) return OzmiumSceneHelpers.Txt( "Object not found." );
try
{
var hull = go.Components.Create<HullCollider>();
if ( Enum.TryParse<HullCollider.PrimitiveType>( hullType, true, out var pt ) )
hull.Type = pt;
if ( args.TryGetProperty( "center", out var ctEl ) && ctEl.ValueKind == JsonValueKind.Object )
hull.Center = ParseV3( ctEl );
if ( hull.Type == HullCollider.PrimitiveType.Box )
{
if ( args.TryGetProperty( "size", out var szEl ) && szEl.ValueKind == JsonValueKind.Object )
hull.BoxSize = ParseV3( szEl );
}
else
{
hull.Height = OzmiumSceneHelpers.Get( args, "height", 50f );
hull.Radius = OzmiumSceneHelpers.Get( args, "radius", 25f );
if ( hull.Type == HullCollider.PrimitiveType.Cone )
hull.Radius2 = OzmiumSceneHelpers.Get( args, "tipRadius", 0f );
hull.Slices = OzmiumSceneHelpers.Get( args, "slices", 16 );
}
return OzmiumSceneHelpers.Txt( $"Added HullCollider ({hull.Type}) to '{go.Name}'." );
}
catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
}
// ── create_model_physics ─────────────────────────────────────────────
internal static object CreateModelPhysics( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );
float x = OzmiumSceneHelpers.Get( args, "x", 0f );
float y = OzmiumSceneHelpers.Get( args, "y", 0f );
float z = OzmiumSceneHelpers.Get( args, "z", 0f );
string name = OzmiumSceneHelpers.Get( args, "name", "Model Physics" );
try
{
var go = scene.CreateObject();
go.Name = name;
go.WorldPosition = new Vector3( x, y, z );
var mp = go.Components.Create<ModelPhysics>();
if ( args.TryGetProperty( "modelPath", out var mpEl ) && mpEl.ValueKind == JsonValueKind.String )
{
var model = Model.Load( mpEl.GetString() );
if ( model != null ) mp.Model = model;
}
mp.MotionEnabled = OzmiumSceneHelpers.Get( args, "motionEnabled", true );
return OzmiumSceneHelpers.Txt( $"Created ModelPhysics on '{go.Name}' (model={mp.Model?.ResourcePath ?? "null"})." );
}
catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
}
// ── Physics extension schemas ────────────────────────────────────────
internal static Dictionary<string, object> SchemaCreateCharacterController => S( "create_character_controller",
"Create a GO with a CharacterController for collision-based movement (NPCs, custom entities). Capsule collision system.",
new Dictionary<string, object>
{
["x"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World X position." },
["y"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Y position." },
["z"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Z position." },
["name"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Name for the GO." },
["radius"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Capsule radius (default 16)." },
["height"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Capsule height (default 64)." },
["stepHeight"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Max step height (default 18)." },
["groundAngle"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Max ground angle in degrees (default 45)." },
["acceleration"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Movement acceleration (default 10)." },
["bounciness"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Bounciness (default 0.3)." }
} );
internal static Dictionary<string, object> SchemaAddPlaneCollider => S( "add_plane_collider",
"Add a PlaneCollider to an existing GO (flat ground, walls, floors). Currently missing from the standard collider tools.",
new Dictionary<string, object>
{
["id"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "GUID." },
["name"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Exact name." },
["scale"] = new Dictionary<string, object> { ["type"] = "object", ["description"] = "Scale {x,y} (default 50,50)." },
["center"] = V3Prop,
["normal"] = new Dictionary<string, object> { ["type"] = "object", ["description"] = "Normal direction {x,y,z} (default 0,0,1)." }
} );
internal static Dictionary<string, object> SchemaAddHullCollider => S( "add_hull_collider",
"Add a HullCollider to an existing GO (box/cone/cylinder primitives). Provides shapes not available in standard colliders.",
new Dictionary<string, object>
{
["id"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "GUID." },
["name"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Exact name." },
["hullType"] = new Dictionary<string, object>
{
["type"] = "string", ["description"] = "Hull shape type.",
["enum"] = new[] { "Box", "Cone", "Cylinder" }
},
["center"] = V3Prop,
["size"] = new Dictionary<string, object> { ["type"] = "object", ["description"] = "Box size {x,y,z} for Box type (default 50,50,50)." },
["height"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Height for Cone/Cylinder type (default 50)." },
["radius"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Radius for Cone/Cylinder type (default 25)." },
["tipRadius"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Tip radius for Cone type (default 0)." },
["slices"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Sides for Cone/Cylinder (default 16)." }
} );
internal static Dictionary<string, object> SchemaCreateModelPhysics => S( "create_model_physics",
"Create a GO with a ModelPhysics component for ragdolls and physics-driven models with per-bone bodies.",
new Dictionary<string, object>
{
["x"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World X position." },
["y"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Y position." },
["z"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Z position." },
["name"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Name for the GO." },
["modelPath"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Model asset path (e.g. 'models/citizen.vmdl')." },
["motionEnabled"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Enable physics motion (default true)." }
} );
}