Editor helper utilities for the scene editor. Provides functions to require an active editor session/scene, find GameObjects and Components by id or name, list component types, convert vectors/rotations, and produce simple serializable descriptions of GameObjects and their tree.
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
using static Sandbox.Internal.GlobalToolsNamespace;
namespace SboxMcp.Tools;
internal static class ToolHelpers
{
public static SceneEditorSession RequireSession() =>
SceneEditorSession.Active
?? throw new InvalidOperationException( "No scene is open in the editor - open or create a scene first" );
public static Scene RequireScene() => RequireSession().Scene
?? throw new InvalidOperationException( "The active editor session has no scene" );
/// <summary>
/// Resolves a GameObject by id (preferred) or by unique name.
/// </summary>
public static GameObject FindGameObject( string idOrName )
{
if ( string.IsNullOrWhiteSpace( idOrName ) )
throw new ArgumentException( "GameObject id/name must not be empty" );
var scene = RequireScene();
if ( Guid.TryParse( idOrName, out var guid ) )
{
return scene.Directory.FindByGuid( guid )
?? throw new InvalidOperationException( $"No GameObject with id '{idOrName}' - use gameobject_find to search" );
}
var matches = scene.GetAllObjects( false )
.Where( o => o is not Scene )
.Where( o => string.Equals( o.Name, idOrName, StringComparison.OrdinalIgnoreCase ) )
.ToList();
return matches.Count switch
{
1 => matches[0],
0 => throw new InvalidOperationException( $"No GameObject named '{idOrName}' - use gameobject_find to search" ),
_ => throw new InvalidOperationException(
$"{matches.Count} GameObjects are named '{idOrName}' - use an id instead: "
+ string.Join( ", ", matches.Take( 5 ).Select( m => m.Id ) ) )
};
}
public static Component FindComponent( GameObject go, string typeName )
{
var components = go.Components.GetAll<Component>( FindMode.EverythingInSelf ).ToList();
var match = components.FirstOrDefault( c => string.Equals( c.GetType().FullName, typeName, StringComparison.OrdinalIgnoreCase ) )
?? components.FirstOrDefault( c => string.Equals( c.GetType().Name, typeName, StringComparison.OrdinalIgnoreCase ) );
return match ?? throw new InvalidOperationException(
$"'{go.Name}' has no component '{typeName}'. It has: "
+ string.Join( ", ", components.Select( c => c.GetType().Name ) ) );
}
public static TypeDescription FindComponentType( string typeName )
{
var all = EditorTypeLibrary.GetTypes<Component>()
.Where( t => !t.IsAbstract && !t.IsGenericType )
.ToList();
var match = all.FirstOrDefault( t => string.Equals( t.FullName, typeName, StringComparison.OrdinalIgnoreCase ) )
?? all.FirstOrDefault( t => string.Equals( t.Name, typeName, StringComparison.OrdinalIgnoreCase ) );
if ( match is not null )
return match;
var close = all
.Where( t => t.Name.Contains( typeName, StringComparison.OrdinalIgnoreCase ) )
.Take( 8 )
.Select( t => t.Name )
.ToList();
throw new InvalidOperationException( close.Count > 0
? $"No component type '{typeName}'. Did you mean: {string.Join( ", ", close )}?"
: $"No component type '{typeName}' - use component_list_types to search" );
}
public static Vector3 ToVector3( float[] v, string argName )
{
if ( v is null || v.Length != 3 )
throw new ArgumentException( $"'{argName}' must be an array of 3 numbers [x, y, z]" );
return new Vector3( v[0], v[1], v[2] );
}
public static float[] V( Vector3 v ) => new[] { v.x, v.y, v.z };
public static float[] A( Rotation r )
{
var angles = r.Angles();
return new[] { angles.pitch, angles.yaw, angles.roll };
}
public static object Describe( GameObject go ) => new
{
id = go.Id,
name = go.Name,
enabled = go.Enabled,
position = V( go.WorldPosition ),
components = go.Components.GetAll<Component>( FindMode.EverythingInSelf )
.Select( c => c.GetType().Name ).ToArray(),
childCount = go.Children.Count,
isPrefabInstance = go.IsPrefabInstance
};
public static object DescribeTree( GameObject go, int depth )
{
var components = go.Components.GetAll<Component>( FindMode.EverythingInSelf )
.Select( c => c.GetType().Name ).ToArray();
return new
{
id = go.Id,
name = go.Name,
enabled = go.Enabled,
components,
children = depth <= 0
? (object)$"{go.Children.Count} children (increase maxDepth to see them)"
: go.Children.Select( c => DescribeTree( c, depth - 1 ) ).ToArray()
};
}
}