Editor/Tools/SceneManipulationTools.cs
#nullable enable
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Sandbox;

namespace Editor.Mcp
{
	public static partial class SboxMcpAssistant
	{




		[McpTool("pocketknife_test_clothing")]
		public static string TestClothing([Description("Path to the .clothing asset")] string clothingPath, [Description("true for Terry, false for standard Human")] bool useTerry = true)
		{
			try
			{
				var clothing = Sandbox.ResourceLibrary.Get<Sandbox.Clothing>(clothingPath); if (clothing is null) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Unavailable, "clothing.not_found", $"Clothing asset '{clothingPath}' could not be loaded."));
				var go = new GameObject(true, "Cosmetic Dummy"); var model = go.Components.Create<SkinnedModelRenderer>(); model.Model = Model.Load(useTerry ? "models/citizen/citizen.vmdl" : "models/citizen/human.vmdl");
				if (model.Model is null) return McpJson.Write(ToolOutcome<object>.Failure(new { Id = go.Id.ToString() }, ToolStatus.Unavailable, "clothing.model_missing", "The preview character model could not be loaded."));
				var container = new Sandbox.ClothingContainer(); container.Clothing.Add(new Sandbox.ClothingContainer.ClothingEntry(clothing)); container.Apply(model);
				return McpJson.Write(ToolOutcome<object>.Ready(new { Id = go.Id.ToString(), Model = model.Model.ToString(), Clothing = clothingPath, Applied = true }));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "clothing.failed", "Clothing preview failed.", ex)); }
		}

		private static GameObject? FindObject(System.Collections.Generic.IEnumerable<GameObject>? roots, string selector)
		{
			if (roots is null) return null;
			foreach (var item in roots) { if (item.Id.ToString().Equals(selector, StringComparison.OrdinalIgnoreCase) || item.Name.Equals(selector, StringComparison.OrdinalIgnoreCase) || GetPath(item).Equals(selector, StringComparison.OrdinalIgnoreCase)) return item; var nested = FindObject(item.Children, selector); if (nested is not null) return nested; }
			return null;
		}
		private static string? ResolveEditorObjectId(string selector)
		{
			if (string.IsNullOrWhiteSpace(selector)) return null;
			try
			{
				var direct = EditorRuntime.InvokeStatic("Editor.Mcp.SceneTools", "GetGameObject", selector.Trim(), false);
				if (direct is not null) return direct.GetType().GetProperty("Id")?.GetValue(direct)?.ToString() ?? selector.Trim();
			}
			catch (TargetInvocationException) { }
			var found = EditorRuntime.InvokeStatic("Editor.Mcp.SceneTools", "FindGameObjects", selector.Trim(), "", "", "", 100);
			var results = found?.GetType().GetProperty("Results")?.GetValue(found) as System.Collections.IEnumerable;
			if (results is null) return null;
			foreach (var item in results)
			{
				var itemName = item.GetType().GetProperty("Name")?.GetValue(item)?.ToString();
				var itemPath = item.GetType().GetProperty("Path")?.GetValue(item)?.ToString();
				if (string.Equals(itemName, selector, StringComparison.OrdinalIgnoreCase) || string.Equals(itemPath, selector, StringComparison.OrdinalIgnoreCase)) return item.GetType().GetProperty("Id")?.GetValue(item)?.ToString();
			}
			return null;
		}
		private static string? ReadProperty(object target, string propertyName)
		{
			var property = target.GetType().GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
			if (property is not null) return property.GetValue(target)?.ToString();
			var components = target.GetType().GetProperty("Components")?.GetValue(target) as System.Collections.IEnumerable;
			if (components is null) return null;
			foreach (var component in components)
			{
				var props = component.GetType().GetProperty("Properties")?.GetValue(component) as JsonObject;
				if (props is not null && props.TryGetPropertyValue(propertyName, out var node)) return node?.ToJsonString();
			}
			return null;
		}
		private static (string Id, string Type)? FindComponentProperty(object target, string propertyName)
		{
			var components = target.GetType().GetProperty("Components")?.GetValue(target) as System.Collections.IEnumerable;
			if (components is null) return null;
			foreach (var component in components)
			{
				var props = component.GetType().GetProperty("Properties")?.GetValue(component) as JsonObject;
				if (props is not null && props.ContainsKey(propertyName)) return (component.GetType().GetProperty("Id")?.GetValue(component)?.ToString() ?? "", component.GetType().GetProperty("Type")?.GetValue(component)?.ToString() ?? "");
			}
			return null;
		}
		private static JsonNode ParsePropertyValue(string value)
		{
			try { return JsonNode.Parse(value) ?? JsonValue.Create(value)!; }
			catch { return JsonValue.Create(value)!; }
		}
		private static string GetPath(GameObject go) { var names = new System.Collections.Generic.List<string>(); for (var current = go; current is not null; current = current.Parent) names.Add(current.Name); names.Reverse(); return string.Join("/", names); }
		private static object ConvertValue(string value, Type type)
		{
			if (type == typeof(string)) return value; if (type == typeof(Vector3)) { var p = value.Split(','); if (p.Length != 3) throw new FormatException("Vector3 must be x,y,z."); return new Vector3(float.Parse(p[0], CultureInfo.InvariantCulture), float.Parse(p[1], CultureInfo.InvariantCulture), float.Parse(p[2], CultureInfo.InvariantCulture)); }
			if (type.IsEnum) return Enum.Parse(type, value, true); if (type == typeof(bool)) return bool.Parse(value); if (type == typeof(float)) return float.Parse(value, CultureInfo.InvariantCulture); if (type == typeof(double)) return double.Parse(value, CultureInfo.InvariantCulture); if (type == typeof(int)) return int.Parse(value, CultureInfo.InvariantCulture); return Convert.ChangeType(value, type, CultureInfo.InvariantCulture)!;
		}
	}
}