Editor/SceneQueryHelpers.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace SboxMcpServer;
/// <summary>
/// Pure scene-data helpers: path building, component/tag enumeration, and
/// the canonical summary/detail object builders used by all tool handlers.
/// </summary>
internal static class SceneQueryHelpers
{
/// <summary>Returns the scene-path of a GameObject, e.g. "Root/Parent/Child".</summary>
internal static string GetObjectPath( GameObject go )
{
var parts = new List<string>();
var current = go;
while ( current != null )
{
parts.Insert( 0, current.Name );
current = current.Parent;
}
return string.Join( "/", parts );
}
/// <summary>Returns a compact list of component type names for a GameObject.</summary>
internal static List<string> GetComponentNames( GameObject go )
{
var names = new List<string>();
foreach ( var comp in go.Components.GetAll() )
names.Add( comp.GetType().Name );
return names;
}
/// <summary>Returns all tags on a GameObject as a list of strings.</summary>
internal static List<string> GetTags( GameObject go )
{
return go.Tags.TryGetAll().ToList();
}
/// <summary>
/// Recursively walks all GameObjects in the scene, including those inside
/// disabled parents. Use this instead of scene.GetAllObjects(true) whenever
/// you need to find disabled objects.
/// </summary>
internal static IEnumerable<GameObject> WalkAll( Scene scene, bool includeDisabled = true )
{
foreach ( var root in scene.Children )
foreach ( var go in WalkSubtree( root, includeDisabled ) )
yield return go;
}
/// <summary>
/// Recursively walks a subtree rooted at <paramref name="root"/>.
/// </summary>
/// <summary>Name marker that causes MCP to skip an object and its entire subtree.</summary>
internal const string IgnoreMarker = "(MCP IGNORE)";
/// <summary>Tag that causes MCP to skip an object and its entire subtree.</summary>
internal const string IgnoreTag = "mcp_ignore";
/// <summary>Max children before auto-skipping subtree walk (parent still returned).</summary>
internal const int MaxAutoWalkChildren = 25;
internal static IEnumerable<GameObject> WalkSubtree( GameObject root, bool includeDisabled = true )
{
if ( !includeDisabled && !root.Enabled ) yield break;
if ( root.Name != null && root.Name.IndexOf( IgnoreMarker, StringComparison.OrdinalIgnoreCase ) >= 0 ) yield break;
if ( root.Tags.Has( IgnoreTag ) ) yield break;
yield return root;
// Auto-skip children of objects with too many children (performance guard)
if ( root.Children.Count > MaxAutoWalkChildren ) yield break;
foreach ( var child in root.Children )
foreach ( var go in WalkSubtree( child, includeDisabled ) )
yield return go;
}
/// <summary>
/// Compact summary used in list results (find_game_objects, get_scene_hierarchy).
/// </summary>
internal static Dictionary<string, object> BuildObjectSummary( GameObject go )
{
var pos = go.WorldPosition;
return new Dictionary<string, object>
{
["id"] = go.Id.ToString(),
["name"] = go.Name,
["path"] = GetObjectPath( go ),
["enabled"] = go.Enabled,
["active"] = go.Active,
["tags"] = GetTags( go ),
["components"] = GetComponentNames( go ),
["position"] = new Dictionary<string, object>
{
["x"] = MathF.Round( pos.x, 2 ),
["y"] = MathF.Round( pos.y, 2 ),
["z"] = MathF.Round( pos.z, 2 )
},
["childCount"] = go.Children.Count,
["isPrefabInstance"] = go.IsPrefabInstance,
["prefabSource"] = go.IsPrefabInstance ? go.PrefabInstanceSource : null,
["isNetworkRoot"] = go.IsNetworkRoot,
["networkMode"] = go.NetworkMode.ToString()
};
}
/// <summary>
/// Full detail object used by get_game_object_details.
/// </summary>
internal static Dictionary<string, object> BuildObjectDetail( GameObject go, bool includeChildrenRecursive = false )
{
var wp = go.WorldPosition;
var wr = go.WorldRotation;
var ws = go.WorldScale;
var lp = go.LocalPosition;
var lr = go.LocalRotation;
var ls = go.LocalScale;
var components = new List<Dictionary<string, object>>();
foreach ( var comp in go.Components.GetAll() )
{
components.Add( new Dictionary<string, object>
{
["type"] = comp.GetType().Name,
["enabled"] = comp.Enabled
} );
}
List<object> children;
if ( includeChildrenRecursive )
{
children = go.Children
.Select( c => (object)BuildObjectDetail( c, true ) )
.ToList();
}
else
{
children = go.Children.Select( c => (object)new Dictionary<string, object>
{
["id"] = c.Id.ToString(),
["name"] = c.Name,
["enabled"] = c.Enabled,
["components"] = GetComponentNames( c )
} ).ToList();
}
return new Dictionary<string, object>
{
["id"] = go.Id.ToString(),
["name"] = go.Name,
["path"] = GetObjectPath( go ),
["enabled"] = go.Enabled,
["active"] = go.Active,
["tags"] = GetTags( go ),
["components"] = components,
["worldTransform"] = new Dictionary<string, object>
{
["position"] = Vec3Dict( wp ),
["rotation"] = RotDict( wr ),
["scale"] = Vec3Dict( ws )
},
["localTransform"] = new Dictionary<string, object>
{
["position"] = Vec3Dict( lp ),
["rotation"] = RotDict( lr ),
["scale"] = Vec3Dict( ls )
},
["parent"] = go.Parent != null ? new Dictionary<string, object>
{
["id"] = go.Parent.Id.ToString(),
["name"] = go.Parent.Name
} : null,
["children"] = children,
["isRoot"] = go.IsRoot,
["isNetworkRoot"] = go.IsNetworkRoot,
["isPrefabInstance"] = go.IsPrefabInstance,
["prefabSource"] = go.IsPrefabInstance ? go.PrefabInstanceSource : null,
["networkMode"] = go.NetworkMode.ToString()
};
}
// ── Private formatting helpers ─────────────────────────────────────────
private static Dictionary<string, object> Vec3Dict( Vector3 v ) => new()
{
["x"] = MathF.Round( v.x, 2 ),
["y"] = MathF.Round( v.y, 2 ),
["z"] = MathF.Round( v.z, 2 )
};
private static Dictionary<string, object> RotDict( Rotation r ) => new()
{
["pitch"] = MathF.Round( r.Pitch(), 2 ),
["yaw"] = MathF.Round( r.Yaw(), 2 ),
["roll"] = MathF.Round( r.Roll(), 2 )
};
}