Editor/OzmiumReadHandlers.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using Editor;
using Sandbox;
namespace SboxMcpServer;
/// <summary>
/// Handlers for all scene-read MCP tools.
/// </summary>
internal static class OzmiumReadHandlers
{
private static readonly JsonSerializerOptions _json = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
// ── get_scene_summary ──────────────────────────────────────────────────
internal static object GetSceneSummary()
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return Txt( "No active scene. Open a scene or prefab in the editor." );
var all = OzmiumSceneHelpers.WalkAll( scene, true ).ToList();
var root = scene.Children.ToList();
var compCounts = new Dictionary<string, int>();
foreach ( var go in all )
foreach ( var c in go.Components.GetAll() )
{
compCounts.TryGetValue( c.GetType().Name, out var n );
compCounts[c.GetType().Name] = n + 1;
}
var allTags = new HashSet<string>();
foreach ( var go in all )
foreach ( var t in go.Tags.TryGetAll() ) allTags.Add( t );
var prefabCounts = new Dictionary<string, int>();
foreach ( var go in all.Where( g => g.IsPrefabInstance && g.PrefabInstanceSource != null ) )
{
prefabCounts.TryGetValue( go.PrefabInstanceSource, out var n );
prefabCounts[go.PrefabInstanceSource] = n + 1;
}
var summary = new Dictionary<string, object>
{
["sceneName"] = scene.Name,
["totalObjects"] = all.Count,
["rootObjects"] = root.Count,
["enabledObjects"] = all.Count( g => g.Enabled ),
["disabledObjects"] = all.Count( g => !g.Enabled ),
["uniqueTags"] = allTags.OrderBy( t => t ).ToList(),
["componentBreakdown"] = compCounts.OrderByDescending( kv => kv.Value )
.Select( kv => new Dictionary<string, object> { ["type"] = kv.Key, ["count"] = kv.Value } ).ToList(),
["prefabBreakdown"] = prefabCounts.OrderByDescending( kv => kv.Value )
.Select( kv => new Dictionary<string, object> { ["prefab"] = kv.Key, ["instances"] = kv.Value } ).ToList(),
["rootObjectList"] = root.Select( g => new Dictionary<string, object>
{
["name"] = g.Name, ["id"] = g.Id.ToString(),
["enabled"] = g.Enabled, ["childCount"] = g.Children.Count,
["components"] = OzmiumSceneHelpers.GetComponentNames( g )
} ).ToList()
};
return Txt( JsonSerializer.Serialize( summary, _json ) );
}
// ── get_scene_hierarchy ────────────────────────────────────────────────
internal static object GetSceneHierarchy( JsonElement args )
{
bool rootOnly = Get( args, "rootOnly", false );
bool inclDisabled = Get( args, "includeDisabled", true );
string rootId = Get( args, "rootId", (string)null );
var sb = new StringBuilder();
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) { sb.Append( "No active scene. Open a scene or prefab in the editor." ); }
else
{
sb.AppendLine( $"Scene: {scene.Name}" );
if ( !string.IsNullOrEmpty( rootId ) && Guid.TryParse( rootId, out var guid ) )
{
var sub = OzmiumSceneHelpers.WalkAll( scene ).FirstOrDefault( g => g.Id == guid );
if ( sub == null ) sb.Append( $"No object with id='{rootId}'." );
else Walk( sub, 0, rootOnly, inclDisabled, sb );
}
else if ( rootOnly )
{
foreach ( var go in scene.Children )
{
if ( !inclDisabled && !go.Enabled ) continue;
if ( go.Name?.IndexOf( OzmiumSceneHelpers.IgnoreMarker, StringComparison.OrdinalIgnoreCase ) >= 0 ) continue;
if ( go.Tags.Has( OzmiumSceneHelpers.IgnoreTag ) ) continue;
OzmiumSceneHelpers.AppendHierarchyLine( sb, go, 0, true );
}
}
else
{
foreach ( var go in scene.Children ) Walk( go, 0, false, inclDisabled, sb );
}
}
return Txt( sb.ToString() );
}
private static void Walk( GameObject go, int depth, bool rootOnly, bool inclDisabled, StringBuilder sb )
{
if ( !inclDisabled && !go.Enabled ) return;
if ( go.Name?.IndexOf( OzmiumSceneHelpers.IgnoreMarker, StringComparison.OrdinalIgnoreCase ) >= 0 ) return;
if ( go.Tags.Has( OzmiumSceneHelpers.IgnoreTag ) ) return;
OzmiumSceneHelpers.AppendHierarchyLine( sb, go, depth, rootOnly );
if ( !rootOnly )
foreach ( var child in go.Children ) Walk( child, depth + 1, false, inclDisabled, sb );
}
// ── find_game_objects ──────────────────────────────────────────────────
internal static object FindGameObjects( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return Txt( "No active scene. Open a scene or prefab in the editor." );
string nameContains = Get( args, "nameContains", (string)null );
string hasTag = Get( args, "hasTag", (string)null );
string hasComponent = Get( args, "hasComponent", (string)null );
string pathContains = Get( args, "pathContains", (string)null );
bool enabledOnly = Get( args, "enabledOnly", false );
int maxResults = Get( args, "maxResults", 50 );
var matches = new List<Dictionary<string, object>>();
int searched = 0;
foreach ( var go in OzmiumSceneHelpers.WalkAll( scene, true ) )
{
searched++;
if ( enabledOnly && !go.Enabled ) continue;
if ( !string.IsNullOrEmpty( nameContains ) && go.Name.IndexOf( nameContains, StringComparison.OrdinalIgnoreCase ) < 0 ) continue;
if ( !string.IsNullOrEmpty( hasTag ) && !go.Tags.Has( hasTag ) ) continue;
if ( !string.IsNullOrEmpty( hasComponent ) && !go.Components.GetAll().Any( c => c.GetType().Name.IndexOf( hasComponent, StringComparison.OrdinalIgnoreCase ) >= 0 ) ) continue;
if ( !string.IsNullOrEmpty( pathContains ) && OzmiumSceneHelpers.GetObjectPath( go ).IndexOf( pathContains, StringComparison.OrdinalIgnoreCase ) < 0 ) continue;
if ( matches.Count >= maxResults ) break;
matches.Add( OzmiumSceneHelpers.BuildSummary( go ) );
}
return Txt( JsonSerializer.Serialize( new { summary = $"Found {matches.Count} (searched {searched})", results = matches }, _json ) );
}
// ── find_game_objects_in_radius ─────────────────────────────────────────
internal static object FindGameObjectsInRadius( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return Txt( "No active scene. Open a scene or prefab in the editor." );
float x = Get( args, "x", 0f ), y = Get( args, "y", 0f ), z = Get( args, "z", 0f );
float radius = Get( args, "radius", 1000f );
string hasTag = Get( args, "hasTag", (string)null );
int max = Get( args, "maxResults", 50 );
var origin = new Vector3( x, y, z );
float radSq = radius * radius;
var results = new List<(float d, Dictionary<string, object> s)>();
foreach ( var go in OzmiumSceneHelpers.WalkAll( scene, true ) )
{
if ( !string.IsNullOrEmpty( hasTag ) && !go.Tags.Has( hasTag ) ) continue;
var diff = go.WorldPosition - origin;
var dsq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z;
if ( dsq > radSq ) continue;
results.Add( (MathF.Sqrt( dsq ), OzmiumSceneHelpers.BuildSummary( go )) );
}
results.Sort( ( a, b ) => a.d.CompareTo( b.d ) );
var list = results.Take( max ).Select( r => { r.s["distanceFromOrigin"] = MathF.Round( r.d, 2 ); return r.s; } ).ToList();
return Txt( JsonSerializer.Serialize( new { summary = $"Found {list.Count} within radius {radius}", results = list }, _json ) );
}
// ── get_game_object_details ────────────────────────────────────────────
internal static object GetGameObjectDetails( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return Txt( "No active scene. Open a scene or prefab in the editor." );
string id = Get( args, "id", (string)null );
string name = Get( args, "name", (string)null );
bool recurse = Get( args, "includeChildrenRecursive", false );
if ( string.IsNullOrEmpty( id ) && string.IsNullOrEmpty( name ) )
return Txt( "Provide 'id' or 'name'." );
var target = OzmiumSceneHelpers.FindGo( scene, id, name );
if ( target == null ) return Txt( $"No object found: id='{id}' name='{name}'." );
return Txt( JsonSerializer.Serialize( OzmiumSceneHelpers.BuildDetail( target, recurse ), _json ) );
}
// ── get_component_properties ───────────────────────────────────────────
internal static object GetComponentProperties( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return Txt( "No active scene. Open a scene or prefab in the editor." );
string id = Get( args, "id", (string)null );
string name = Get( args, "name", (string)null );
string type = Get( args, "componentType", (string)null );
if ( string.IsNullOrEmpty( type ) ) return Txt( "Provide 'componentType'." );
var go = OzmiumSceneHelpers.FindGo( scene, id, name );
if ( go == null ) return Txt( $"No object found." );
var comp = go.Components.GetAll().FirstOrDefault( c =>
c.GetType().Name.IndexOf( type, StringComparison.OrdinalIgnoreCase ) >= 0 );
if ( comp == null ) return Txt( $"No component '{type}' on '{go.Name}'." );
var props = new Dictionary<string, object>();
foreach ( var prop in comp.GetType().GetProperties( System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance ) )
{
if ( !prop.CanRead ) continue;
try
{
var val = prop.GetValue( comp );
props[prop.Name] = val switch
{
null => null,
bool b => (object)b,
int i => (object)i,
float f => (object)MathF.Round( f, 4 ),
string s => (object)s,
Enum e => (object)e.ToString(),
Vector3 v => (object)OzmiumSceneHelpers.V3( v ),
_ => (object)val.ToString()
};
}
catch { props[prop.Name] = "<error>"; }
}
return Txt( JsonSerializer.Serialize( new
{
gameObjectId = go.Id.ToString(),
gameObjectName = go.Name,
componentType = comp.GetType().Name,
enabled = comp.Enabled,
properties = props
}, _json ) );
}
// ── get_prefab_instances ───────────────────────────────────────────────
internal static object GetPrefabInstances( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return Txt( "No active scene. Open a scene or prefab in the editor." );
string prefabPath = Get( args, "prefabPath", (string)null );
bool enabledOnly = Get( args, "enabledOnly", false );
int max = Get( args, "maxResults", 100 );
if ( string.IsNullOrEmpty( prefabPath ) )
{
var counts = new Dictionary<string, int>();
foreach ( var go in OzmiumSceneHelpers.WalkAll( scene, true ) )
{
if ( !go.IsPrefabInstance || go.PrefabInstanceSource == null ) continue;
if ( enabledOnly && !go.Enabled ) continue;
counts.TryGetValue( go.PrefabInstanceSource, out var n );
counts[go.PrefabInstanceSource] = n + 1;
}
var breakdown = counts.OrderByDescending( kv => kv.Value )
.Select( kv => new Dictionary<string, object> { ["prefab"] = kv.Key, ["instances"] = kv.Value } ).ToList();
return Txt( JsonSerializer.Serialize( new { summary = $"{counts.Count} unique prefab(s).", breakdown }, _json ) );
}
var matches = new List<Dictionary<string, object>>();
foreach ( var go in OzmiumSceneHelpers.WalkAll( scene, true ) )
{
if ( matches.Count >= max ) break;
if ( !go.IsPrefabInstance || go.PrefabInstanceSource == null ) continue;
if ( enabledOnly && !go.Enabled ) continue;
if ( go.PrefabInstanceSource.IndexOf( prefabPath, StringComparison.OrdinalIgnoreCase ) < 0 ) continue;
matches.Add( OzmiumSceneHelpers.BuildSummary( go ) );
}
return Txt( JsonSerializer.Serialize( new { summary = $"Found {matches.Count} instance(s).", results = matches }, _json ) );
}
// ── Helpers ────────────────────────────────────────────────────────────
internal static object Txt( string text ) => new { content = new object[] { new { type = "text", text } } };
private static T Get<T>( JsonElement el, string key, T def )
{
if ( el.ValueKind == JsonValueKind.Undefined ) return def;
if ( !el.TryGetProperty( key, out var p ) ) return def;
try
{
var t = typeof( T );
if ( t == typeof( string ) ) return (T)(object)p.GetString();
if ( t == typeof( bool ) ) return (T)(object)p.GetBoolean();
if ( t == typeof( int ) ) return (T)(object)p.GetInt32();
if ( t == typeof( float ) ) return (T)(object)p.GetSingle();
return def;
}
catch { return def; }
}
}