Editor/Tools/ProceduralGenerationTools.cs
#nullable enable
using System;
using System.Linq;
using System.Reflection;
using Sandbox;

namespace Editor.Mcp
{
	public static partial class SboxMcpAssistant
	{
		[McpTool("pocketknife_spawn_procedural_road")]
		public static string SpawnProceduralRoad([Description("Name of the road object")] string roadName = "New Road") => SpawnComponent("road", roadName, "RoadComponent", "RoadTool.RoadComponent", null, null);

		[McpTool("pocketknife_spawn_water_surface")]
		public static string SpawnWaterSurface([Description("Name of the water object")] string waterName = "Water Surface") => SpawnComponent("water", waterName, "WaterComponent", "WaterTool.WaterComponent", null, null);

		[McpTool("pocketknife_spawn_architecture_plan")]
		public static string SpawnArchitecturePlan([Description("Name of the building object")] string buildingName = "New Building", [Description("Width in Hammer units")] float width = 256f, [Description("Length in Hammer units")] float length = 256f)
		{
			if (width <= 0 || length <= 0) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "architecture.invalid_size", "Width and length must be positive."));
			return SpawnComponent("architecture-plan", buildingName, "ArchPlan", "sunless.architecture.ArchPlan", width, length);
		}

		[McpTool("pocketknife_add_architecture_room")]
		public static string AddArchitectureRoom([Description("Existing parent plan name, ID, or path")] string parentPlanName, [Description("Room name")] string roomName, [Description("Local X")] float x, [Description("Local Y")] float y, [Description("Local Z")] float z, [Description("Width")] float width, [Description("Length")] float length)
		{
			try
			{
				if (width <= 0 || length <= 0) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "architecture.invalid_size", "Width and length must be positive."));
				var parent = FindObjectInScene(Game.ActiveScene?.Children, parentPlanName); if (parent is null) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "architecture.parent_not_found", $"Plan '{parentPlanName}' was not found."));
				var roomType = EditorRuntime.GetComponentType("ArchRoom") ?? EditorRuntime.GetComponentType("sunless.architecture.ArchRoom"); if (roomType is null) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Unavailable, "architecture.room_dependency_missing", "ArchRoom is not loaded; no room was created."));
				var go = new GameObject(true, roomName); go.SetParent(parent); go.LocalPosition = new Vector3(x, y, z); var component = go.Components.Create((dynamic)roomType); var applied = ApplySize(component, width, length);
				return applied ? McpJson.Write(ToolOutcome<object>.Ready(new { Id = go.Id.ToString(), ParentId = parent.Id.ToString(), Position = new { x, y, z }, Width = width, Length = length })) : McpJson.Write(ToolOutcome<object>.Degraded(new { Id = go.Id.ToString(), ParentId = parent.Id.ToString(), Position = new { x, y, z }, Width = width, Length = length }, "architecture.size_property_unavailable", "The ArchRoom was created, but its component exposes no recognized width/length properties."));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "architecture.room_failed", "Architecture room creation failed.", ex)); }
		}

		private static string SpawnComponent(string kind, string name, string shortType, string fullType, float? width, float? length)
		{
			try
			{
				if (!ToolGuard.IsValidName(name)) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "component.invalid_name", "A non-empty valid object name is required."));
				var type = EditorRuntime.GetComponentType(shortType) ?? EditorRuntime.GetComponentType(fullType); if (type is null) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Unavailable, kind + ".dependency_missing", $"Required component '{shortType}' is not loaded; no object was created."));
				var go = new GameObject(true, name); var component = go.Components.Create((dynamic)type); var applied = !width.HasValue || ApplySize(component, width.Value, length!.Value);
				return applied ? McpJson.Write(ToolOutcome<object>.Ready(new { Id = go.Id.ToString(), Name = name, Component = shortType, Width = width, Length = length })) : McpJson.Write(ToolOutcome<object>.Degraded(new { Id = go.Id.ToString(), Name = name, Component = shortType }, kind + ".size_property_unavailable", "The component was created but no recognized size properties were found."));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, kind + ".create_failed", $"{kind} creation failed.", ex)); }
		}

		private static bool ApplySize(object component, float width, float length)
		{
			var type = component.GetType(); var changed = false;
			foreach (var name in new[] { "Width", "RoomWidth", "SizeX" }) { var p = type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); if (p?.CanWrite == true) { p.SetValue(component, Convert.ChangeType(width, p.PropertyType)); changed = true; break; } }
			foreach (var name in new[] { "Length", "Depth", "RoomLength", "SizeY" }) { var p = type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); if (p?.CanWrite == true) { p.SetValue(component, Convert.ChangeType(length, p.PropertyType)); changed = true; break; } }
			return changed;
		}

		private static GameObject? FindObjectInScene(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)) return item; var nested = FindObjectInScene(item.Children, selector); if (nested is not null) return nested; } return null;
		}
	}
}