升级XR插件版本
This commit is contained in:
221
Packages/MCPForUnity/Editor/Tools/BatchExecute.cs
Normal file
221
Packages/MCPForUnity/Editor/Tools/BatchExecute.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes multiple MCP commands within a single Unity-side handler. Commands are executed sequentially
|
||||
/// on the main thread to preserve determinism and Unity API safety.
|
||||
/// </summary>
|
||||
[McpForUnityTool("batch_execute", AutoRegister = false)]
|
||||
public static class BatchExecute
|
||||
{
|
||||
private const int MaxCommandsPerBatch = 25;
|
||||
|
||||
public static async Task<object> HandleCommand(JObject @params)
|
||||
{
|
||||
if (@params == null)
|
||||
{
|
||||
return new ErrorResponse("'commands' payload is required.");
|
||||
}
|
||||
|
||||
var commandsToken = @params["commands"] as JArray;
|
||||
if (commandsToken == null || commandsToken.Count == 0)
|
||||
{
|
||||
return new ErrorResponse("Provide at least one command entry in 'commands'.");
|
||||
}
|
||||
|
||||
if (commandsToken.Count > MaxCommandsPerBatch)
|
||||
{
|
||||
return new ErrorResponse($"A maximum of {MaxCommandsPerBatch} commands are allowed per batch.");
|
||||
}
|
||||
|
||||
bool failFast = @params.Value<bool?>("failFast") ?? false;
|
||||
bool parallelRequested = @params.Value<bool?>("parallel") ?? false;
|
||||
int? maxParallel = @params.Value<int?>("maxParallelism");
|
||||
|
||||
if (parallelRequested)
|
||||
{
|
||||
McpLog.Warn("batch_execute parallel mode requested, but commands will run sequentially on the main thread for safety.");
|
||||
}
|
||||
|
||||
var commandResults = new List<object>(commandsToken.Count);
|
||||
int invocationSuccessCount = 0;
|
||||
int invocationFailureCount = 0;
|
||||
bool anyCommandFailed = false;
|
||||
|
||||
foreach (var token in commandsToken)
|
||||
{
|
||||
if (token is not JObject commandObj)
|
||||
{
|
||||
invocationFailureCount++;
|
||||
anyCommandFailed = true;
|
||||
commandResults.Add(new
|
||||
{
|
||||
tool = (string)null,
|
||||
callSucceeded = false,
|
||||
error = "Command entries must be JSON objects."
|
||||
});
|
||||
if (failFast)
|
||||
{
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
string toolName = commandObj["tool"]?.ToString();
|
||||
var rawParams = commandObj["params"] as JObject ?? new JObject();
|
||||
var commandParams = NormalizeParameterKeys(rawParams);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(toolName))
|
||||
{
|
||||
invocationFailureCount++;
|
||||
anyCommandFailed = true;
|
||||
commandResults.Add(new
|
||||
{
|
||||
tool = toolName,
|
||||
callSucceeded = false,
|
||||
error = "Each command must include a non-empty 'tool' field."
|
||||
});
|
||||
if (failFast)
|
||||
{
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await CommandRegistry.InvokeCommandAsync(toolName, commandParams).ConfigureAwait(true);
|
||||
bool callSucceeded = DetermineCallSucceeded(result);
|
||||
if (callSucceeded)
|
||||
{
|
||||
invocationSuccessCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
invocationFailureCount++;
|
||||
anyCommandFailed = true;
|
||||
}
|
||||
|
||||
commandResults.Add(new
|
||||
{
|
||||
tool = toolName,
|
||||
callSucceeded,
|
||||
result
|
||||
});
|
||||
|
||||
if (!callSucceeded && failFast)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
invocationFailureCount++;
|
||||
anyCommandFailed = true;
|
||||
commandResults.Add(new
|
||||
{
|
||||
tool = toolName,
|
||||
callSucceeded = false,
|
||||
error = ex.Message
|
||||
});
|
||||
|
||||
if (failFast)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool overallSuccess = !anyCommandFailed;
|
||||
var data = new
|
||||
{
|
||||
results = commandResults,
|
||||
callSuccessCount = invocationSuccessCount,
|
||||
callFailureCount = invocationFailureCount,
|
||||
parallelRequested,
|
||||
parallelApplied = false,
|
||||
maxParallelism = maxParallel
|
||||
};
|
||||
|
||||
return overallSuccess
|
||||
? new SuccessResponse("Batch execution completed.", data)
|
||||
: new ErrorResponse("One or more commands failed.", data);
|
||||
}
|
||||
|
||||
private static bool DetermineCallSucceeded(object result)
|
||||
{
|
||||
if (result == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (result is IMcpResponse response)
|
||||
{
|
||||
return response.Success;
|
||||
}
|
||||
|
||||
if (result is JObject obj)
|
||||
{
|
||||
var successToken = obj["success"];
|
||||
if (successToken != null && successToken.Type == JTokenType.Boolean)
|
||||
{
|
||||
return successToken.Value<bool>();
|
||||
}
|
||||
}
|
||||
|
||||
if (result is JToken token)
|
||||
{
|
||||
var successToken = token["success"];
|
||||
if (successToken != null && successToken.Type == JTokenType.Boolean)
|
||||
{
|
||||
return successToken.Value<bool>();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static JObject NormalizeParameterKeys(JObject source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return new JObject();
|
||||
}
|
||||
|
||||
var normalized = new JObject();
|
||||
foreach (var property in source.Properties())
|
||||
{
|
||||
string normalizedName = ToCamelCase(property.Name);
|
||||
normalized[normalizedName] = NormalizeToken(property.Value);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static JArray NormalizeArray(JArray source)
|
||||
{
|
||||
var normalized = new JArray();
|
||||
foreach (var token in source)
|
||||
{
|
||||
normalized.Add(NormalizeToken(token));
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static JToken NormalizeToken(JToken token)
|
||||
{
|
||||
return token switch
|
||||
{
|
||||
JObject obj => NormalizeParameterKeys(obj),
|
||||
JArray arr => NormalizeArray(arr),
|
||||
_ => token.DeepClone()
|
||||
};
|
||||
}
|
||||
|
||||
private static string ToCamelCase(string key) => StringCaseUtility.ToCamelCase(key);
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/BatchExecute.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/BatchExecute.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e1e2d8f3a454a37b18d06a7a7b6c3fb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
426
Packages/MCPForUnity/Editor/Tools/CommandRegistry.cs
Normal file
426
Packages/MCPForUnity/Editor/Tools/CommandRegistry.cs
Normal file
@@ -0,0 +1,426 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Resources;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds information about a registered command handler.
|
||||
/// </summary>
|
||||
class HandlerInfo
|
||||
{
|
||||
public string CommandName { get; }
|
||||
public Func<JObject, object> SyncHandler { get; }
|
||||
public Func<JObject, Task<object>> AsyncHandler { get; }
|
||||
|
||||
public bool IsAsync => AsyncHandler != null;
|
||||
|
||||
public HandlerInfo(string commandName, Func<JObject, object> syncHandler, Func<JObject, Task<object>> asyncHandler)
|
||||
{
|
||||
CommandName = commandName;
|
||||
SyncHandler = syncHandler;
|
||||
AsyncHandler = asyncHandler;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registry for all MCP command handlers via reflection.
|
||||
/// Handles both MCP tools and resources.
|
||||
/// </summary>
|
||||
public static class CommandRegistry
|
||||
{
|
||||
private static readonly Dictionary<string, HandlerInfo> _handlers = new();
|
||||
private static bool _initialized = false;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize and auto-discover all tools and resources marked with
|
||||
/// [McpForUnityTool] or [McpForUnityResource]
|
||||
/// </summary>
|
||||
public static void Initialize()
|
||||
{
|
||||
if (_initialized) return;
|
||||
|
||||
AutoDiscoverCommands();
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
private static string ToSnakeCase(string name) => StringCaseUtility.ToSnakeCase(name);
|
||||
|
||||
/// <summary>
|
||||
/// Auto-discover all types with [McpForUnityTool] or [McpForUnityResource] attributes
|
||||
/// </summary>
|
||||
private static void AutoDiscoverCommands()
|
||||
{
|
||||
try
|
||||
{
|
||||
var allTypes = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(a => !a.IsDynamic)
|
||||
.SelectMany(a =>
|
||||
{
|
||||
try { return a.GetTypes(); }
|
||||
catch { return new Type[0]; }
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// Discover tools
|
||||
var toolTypes = allTypes.Where(t => t.GetCustomAttribute<McpForUnityToolAttribute>() != null);
|
||||
int toolCount = 0;
|
||||
foreach (var type in toolTypes)
|
||||
{
|
||||
if (RegisterCommandType(type, isResource: false))
|
||||
toolCount++;
|
||||
}
|
||||
|
||||
// Discover resources
|
||||
var resourceTypes = allTypes.Where(t => t.GetCustomAttribute<McpForUnityResourceAttribute>() != null);
|
||||
int resourceCount = 0;
|
||||
foreach (var type in resourceTypes)
|
||||
{
|
||||
if (RegisterCommandType(type, isResource: true))
|
||||
resourceCount++;
|
||||
}
|
||||
|
||||
McpLog.Info($"Auto-discovered {toolCount} tools and {resourceCount} resources ({_handlers.Count} total handlers)", false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"Failed to auto-discover MCP commands: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a command type (tool or resource) with the registry.
|
||||
/// Returns true if successfully registered, false otherwise.
|
||||
/// </summary>
|
||||
private static bool RegisterCommandType(Type type, bool isResource)
|
||||
{
|
||||
string commandName;
|
||||
string typeLabel = isResource ? "resource" : "tool";
|
||||
|
||||
// Get command name from appropriate attribute
|
||||
if (isResource)
|
||||
{
|
||||
var resourceAttr = type.GetCustomAttribute<McpForUnityResourceAttribute>();
|
||||
commandName = resourceAttr.ResourceName;
|
||||
}
|
||||
else
|
||||
{
|
||||
var toolAttr = type.GetCustomAttribute<McpForUnityToolAttribute>();
|
||||
commandName = toolAttr.CommandName;
|
||||
}
|
||||
|
||||
// Auto-generate command name if not explicitly provided
|
||||
if (string.IsNullOrEmpty(commandName))
|
||||
{
|
||||
commandName = ToSnakeCase(type.Name);
|
||||
}
|
||||
|
||||
// Check for duplicate command names
|
||||
if (_handlers.ContainsKey(commandName))
|
||||
{
|
||||
McpLog.Warn(
|
||||
$"Duplicate command name '{commandName}' detected. " +
|
||||
$"{typeLabel} {type.Name} will override previously registered handler."
|
||||
);
|
||||
}
|
||||
|
||||
// Find HandleCommand method
|
||||
var method = type.GetMethod(
|
||||
"HandleCommand",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
new[] { typeof(JObject) },
|
||||
null
|
||||
);
|
||||
|
||||
if (method == null)
|
||||
{
|
||||
McpLog.Warn(
|
||||
$"MCP {typeLabel} {type.Name} is marked with [McpForUnity{(isResource ? "Resource" : "Tool")}] " +
|
||||
$"but has no public static HandleCommand(JObject) method"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
HandlerInfo handlerInfo;
|
||||
|
||||
if (typeof(Task).IsAssignableFrom(method.ReturnType))
|
||||
{
|
||||
var asyncHandler = CreateAsyncHandlerDelegate(method, commandName);
|
||||
handlerInfo = new HandlerInfo(commandName, null, asyncHandler);
|
||||
}
|
||||
else
|
||||
{
|
||||
var handler = (Func<JObject, object>)Delegate.CreateDelegate(
|
||||
typeof(Func<JObject, object>),
|
||||
method
|
||||
);
|
||||
handlerInfo = new HandlerInfo(commandName, handler, null);
|
||||
}
|
||||
|
||||
_handlers[commandName] = handlerInfo;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"Failed to register {typeLabel} {type.Name}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a command handler by name
|
||||
/// </summary>
|
||||
private static HandlerInfo GetHandlerInfo(string commandName)
|
||||
{
|
||||
if (!_handlers.TryGetValue(commandName, out var handler))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Unknown or unsupported command type: {commandName}"
|
||||
);
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a synchronous command handler by name.
|
||||
/// Throws if the command is asynchronous.
|
||||
/// </summary>
|
||||
/// <param name="commandName"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static Func<JObject, object> GetHandler(string commandName)
|
||||
{
|
||||
var handlerInfo = GetHandlerInfo(commandName);
|
||||
if (handlerInfo.IsAsync)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Command '{commandName}' is asynchronous and must be executed via ExecuteCommand"
|
||||
);
|
||||
}
|
||||
|
||||
return handlerInfo.SyncHandler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a command handler, supporting both synchronous and asynchronous (coroutine) handlers.
|
||||
/// If the handler returns an IEnumerator, it will be executed as a coroutine.
|
||||
/// </summary>
|
||||
/// <param name="commandName">The command name to execute</param>
|
||||
/// <param name="params">Command parameters</param>
|
||||
/// <param name="tcs">TaskCompletionSource to complete when async operation finishes</param>
|
||||
/// <returns>The result for synchronous commands, or null for async commands (TCS will be completed later)</returns>
|
||||
public static object ExecuteCommand(string commandName, JObject @params, TaskCompletionSource<string> tcs)
|
||||
{
|
||||
var handlerInfo = GetHandlerInfo(commandName);
|
||||
|
||||
if (handlerInfo.IsAsync)
|
||||
{
|
||||
ExecuteAsyncHandler(handlerInfo, @params, commandName, tcs);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (handlerInfo.SyncHandler == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Handler for '{commandName}' does not provide a synchronous implementation");
|
||||
}
|
||||
|
||||
return handlerInfo.SyncHandler(@params);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a command handler and return its raw result, regardless of sync or async implementation.
|
||||
/// Used internally for features like batch execution where commands need to be composed.
|
||||
/// </summary>
|
||||
/// <param name="commandName">The registered command to execute.</param>
|
||||
/// <param name="params">Parameters to pass to the command (optional).</param>
|
||||
public static Task<object> InvokeCommandAsync(string commandName, JObject @params)
|
||||
{
|
||||
var handlerInfo = GetHandlerInfo(commandName);
|
||||
var payload = @params ?? new JObject();
|
||||
|
||||
if (handlerInfo.IsAsync)
|
||||
{
|
||||
if (handlerInfo.AsyncHandler == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Async handler for '{commandName}' is not configured correctly");
|
||||
}
|
||||
|
||||
return handlerInfo.AsyncHandler(payload);
|
||||
}
|
||||
|
||||
if (handlerInfo.SyncHandler == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Handler for '{commandName}' does not provide a synchronous implementation");
|
||||
}
|
||||
|
||||
object result = handlerInfo.SyncHandler(payload);
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a delegate for an async handler method that returns Task or Task<T>.
|
||||
/// The delegate will invoke the method and await its completion, returning the result.
|
||||
/// </summary>
|
||||
/// <param name="method"></param>
|
||||
/// <param name="commandName"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
private static Func<JObject, Task<object>> CreateAsyncHandlerDelegate(MethodInfo method, string commandName)
|
||||
{
|
||||
return async (JObject parameters) =>
|
||||
{
|
||||
object rawResult;
|
||||
|
||||
try
|
||||
{
|
||||
rawResult = method.Invoke(null, new object[] { parameters });
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
throw ex.InnerException ?? ex;
|
||||
}
|
||||
|
||||
if (rawResult == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rawResult is not Task task)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Async handler '{commandName}' returned an object that is not a Task"
|
||||
);
|
||||
}
|
||||
|
||||
await task.ConfigureAwait(true);
|
||||
|
||||
var taskType = task.GetType();
|
||||
if (taskType.IsGenericType)
|
||||
{
|
||||
var resultProperty = taskType.GetProperty("Result");
|
||||
if (resultProperty != null)
|
||||
{
|
||||
return resultProperty.GetValue(task);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
private static void ExecuteAsyncHandler(
|
||||
HandlerInfo handlerInfo,
|
||||
JObject parameters,
|
||||
string commandName,
|
||||
TaskCompletionSource<string> tcs)
|
||||
{
|
||||
if (handlerInfo.AsyncHandler == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Async handler for '{commandName}' is not configured correctly");
|
||||
}
|
||||
|
||||
Task<object> handlerTask;
|
||||
|
||||
try
|
||||
{
|
||||
handlerTask = handlerInfo.AsyncHandler(parameters);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ReportAsyncFailure(commandName, tcs, ex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (handlerTask == null)
|
||||
{
|
||||
CompleteAsyncCommand(commandName, tcs, null);
|
||||
return;
|
||||
}
|
||||
|
||||
async void AwaitHandler()
|
||||
{
|
||||
try
|
||||
{
|
||||
var finalResult = await handlerTask.ConfigureAwait(true);
|
||||
CompleteAsyncCommand(commandName, tcs, finalResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ReportAsyncFailure(commandName, tcs, ex);
|
||||
}
|
||||
}
|
||||
|
||||
AwaitHandler();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Complete the TaskCompletionSource for an async command with a success result.
|
||||
/// </summary>
|
||||
/// <param name="commandName"></param>
|
||||
/// <param name="tcs"></param>
|
||||
/// <param name="result"></param>
|
||||
private static void CompleteAsyncCommand(string commandName, TaskCompletionSource<string> tcs, object result)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = new { status = "success", result };
|
||||
string json = JsonConvert.SerializeObject(response);
|
||||
|
||||
if (!tcs.TrySetResult(json))
|
||||
{
|
||||
McpLog.Warn($"TCS for async command '{commandName}' was already completed");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"Error completing async command '{commandName}': {ex.Message}\n{ex.StackTrace}");
|
||||
ReportAsyncFailure(commandName, tcs, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Report an error that occurred during async command execution.
|
||||
/// Completes the TaskCompletionSource with an error response.
|
||||
/// </summary>
|
||||
/// <param name="commandName"></param>
|
||||
/// <param name="tcs"></param>
|
||||
/// <param name="ex"></param>
|
||||
private static void ReportAsyncFailure(string commandName, TaskCompletionSource<string> tcs, Exception ex)
|
||||
{
|
||||
McpLog.Error($"Error in async command '{commandName}': {ex.Message}\n{ex.StackTrace}");
|
||||
|
||||
var errorResponse = new
|
||||
{
|
||||
status = "error",
|
||||
error = ex.Message,
|
||||
command = commandName,
|
||||
stackTrace = ex.StackTrace
|
||||
};
|
||||
|
||||
string json;
|
||||
try
|
||||
{
|
||||
json = JsonConvert.SerializeObject(errorResponse);
|
||||
}
|
||||
catch (Exception serializationEx)
|
||||
{
|
||||
McpLog.Error($"Failed to serialize error response for '{commandName}': {serializationEx.Message}");
|
||||
json = "{\"status\":\"error\",\"error\":\"Failed to complete command\"}";
|
||||
}
|
||||
|
||||
if (!tcs.TrySetResult(json))
|
||||
{
|
||||
McpLog.Warn($"TCS for async command '{commandName}' was already completed when trying to report error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/CommandRegistry.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/CommandRegistry.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b61b5a84813b5749a5c64422694a0fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
53
Packages/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs
Normal file
53
Packages/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
[McpForUnityTool("execute_menu_item", AutoRegister = false)]
|
||||
/// <summary>
|
||||
/// Tool to execute a Unity Editor menu item by its path.
|
||||
/// </summary>
|
||||
public static class ExecuteMenuItem
|
||||
{
|
||||
// Basic blacklist to prevent execution of disruptive menu items.
|
||||
private static readonly HashSet<string> _menuPathBlacklist = new HashSet<string>(
|
||||
StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"File/Quit",
|
||||
};
|
||||
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
McpLog.Info("[ExecuteMenuItem] Handling menu item command");
|
||||
string menuPath = @params["menu_path"]?.ToString() ?? @params["menuPath"]?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(menuPath))
|
||||
{
|
||||
return new ErrorResponse("Required parameter 'menu_path' or 'menuPath' is missing or empty.");
|
||||
}
|
||||
|
||||
if (_menuPathBlacklist.Contains(menuPath))
|
||||
{
|
||||
return new ErrorResponse($"Execution of menu item '{menuPath}' is blocked for safety reasons.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
bool executed = EditorApplication.ExecuteMenuItem(menuPath);
|
||||
if (!executed)
|
||||
{
|
||||
McpLog.Error($"[MenuItemExecutor] Failed to execute menu item '{menuPath}'. It might be invalid, disabled, or context-dependent.");
|
||||
return new ErrorResponse($"Failed to execute menu item '{menuPath}'. It might be invalid, disabled, or context-dependent.");
|
||||
}
|
||||
return new SuccessResponse($"Attempted to execute menu item: '{menuPath}'. Check Unity logs for confirmation or errors.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Error($"[MenuItemExecutor] Failed to setup execution for '{menuPath}': {e}");
|
||||
return new ErrorResponse($"Error setting up execution for menu item '{menuPath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 269232350d16a464091aea9e9fcc9b55
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
81
Packages/MCPForUnity/Editor/Tools/FindGameObjects.cs
Normal file
81
Packages/MCPForUnity/Editor/Tools/FindGameObjects.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Tool for searching GameObjects in the scene.
|
||||
/// Returns only instance IDs with pagination support.
|
||||
///
|
||||
/// This is a focused search tool that returns lightweight results (IDs only).
|
||||
/// For detailed GameObject data, use the unity://scene/gameobject/{id} resource.
|
||||
/// </summary>
|
||||
[McpForUnityTool("find_gameobjects")]
|
||||
public static class FindGameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the find_gameobjects command.
|
||||
/// </summary>
|
||||
/// <param name="params">Command parameters</param>
|
||||
/// <returns>Paginated list of instance IDs</returns>
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
if (@params == null)
|
||||
{
|
||||
return new ErrorResponse("Parameters cannot be null.");
|
||||
}
|
||||
|
||||
var p = new ToolParams(@params);
|
||||
|
||||
// Parse search parameters
|
||||
string searchMethod = p.Get("searchMethod", "by_name");
|
||||
|
||||
// Try searchTerm, search_term, or target (for backwards compatibility)
|
||||
string searchTerm = p.Get("searchTerm");
|
||||
if (string.IsNullOrEmpty(searchTerm))
|
||||
{
|
||||
searchTerm = p.Get("target");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(searchTerm))
|
||||
{
|
||||
return new ErrorResponse("'searchTerm' or 'target' parameter is required.");
|
||||
}
|
||||
|
||||
// Pagination parameters using standard PaginationRequest
|
||||
var pagination = PaginationRequest.FromParams(@params, defaultPageSize: 50);
|
||||
pagination.PageSize = Mathf.Clamp(pagination.PageSize, 1, 500);
|
||||
|
||||
// Search options (supports multiple parameter name variants)
|
||||
bool includeInactive = p.GetBool("includeInactive", false) ||
|
||||
p.GetBool("searchInactive", false);
|
||||
|
||||
try
|
||||
{
|
||||
// Get all matching instance IDs
|
||||
var allIds = GameObjectLookup.SearchGameObjects(searchMethod, searchTerm, includeInactive, 0);
|
||||
|
||||
// Use standard pagination response
|
||||
var paginatedResult = PaginationResponse<int>.Create(allIds, pagination);
|
||||
|
||||
return new SuccessResponse("Found GameObjects", new
|
||||
{
|
||||
instanceIDs = paginatedResult.Items,
|
||||
pageSize = paginatedResult.PageSize,
|
||||
cursor = paginatedResult.Cursor,
|
||||
nextCursor = paginatedResult.NextCursor,
|
||||
totalCount = paginatedResult.TotalCount,
|
||||
hasMore = paginatedResult.HasMore
|
||||
});
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
McpLog.Error($"[FindGameObjects] Error searching GameObjects: {ex.Message}");
|
||||
return new ErrorResponse($"Error searching GameObjects: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/FindGameObjects.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/FindGameObjects.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4511082b395b14922b34e90f7a23027e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/MCPForUnity/Editor/Tools/GameObjects.meta
Normal file
8
Packages/MCPForUnity/Editor/Tools/GameObjects.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b61d0e8082ed14c1fb500648007bba7a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,142 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Component resolver that delegates to UnityTypeResolver.
|
||||
/// Kept for backwards compatibility.
|
||||
/// </summary>
|
||||
internal static class ComponentResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolve a Component/MonoBehaviour type by short or fully-qualified name.
|
||||
/// Delegates to UnityTypeResolver.TryResolve with Component constraint.
|
||||
/// </summary>
|
||||
public static bool TryResolve(string nameOrFullName, out Type type, out string error)
|
||||
{
|
||||
return UnityTypeResolver.TryResolve(nameOrFullName, out type, out error, typeof(Component));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all accessible property and field names from a component type.
|
||||
/// </summary>
|
||||
public static List<string> GetAllComponentProperties(Type componentType)
|
||||
{
|
||||
if (componentType == null) return new List<string>();
|
||||
|
||||
var properties = componentType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead && p.CanWrite)
|
||||
.Select(p => p.Name);
|
||||
|
||||
var fields = componentType.GetFields(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(f => !f.IsInitOnly && !f.IsLiteral)
|
||||
.Select(f => f.Name);
|
||||
|
||||
// Also include SerializeField private fields (common in Unity)
|
||||
var serializeFields = componentType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
.Where(f => f.GetCustomAttribute<SerializeField>() != null)
|
||||
.Select(f => f.Name);
|
||||
|
||||
return properties.Concat(fields).Concat(serializeFields).Distinct().OrderBy(x => x).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suggests the most likely property matches for a user's input using fuzzy matching.
|
||||
/// Uses Levenshtein distance, substring matching, and common naming pattern heuristics.
|
||||
/// </summary>
|
||||
public static List<string> GetFuzzyPropertySuggestions(string userInput, List<string> availableProperties)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userInput) || !availableProperties.Any())
|
||||
return new List<string>();
|
||||
|
||||
var cacheKey = $"{userInput.ToLowerInvariant()}:{string.Join(",", availableProperties)}";
|
||||
if (PropertySuggestionCache.TryGetValue(cacheKey, out var cached))
|
||||
return cached;
|
||||
|
||||
try
|
||||
{
|
||||
var suggestions = GetRuleBasedSuggestions(userInput, availableProperties);
|
||||
PropertySuggestionCache[cacheKey] = suggestions;
|
||||
return suggestions;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"[Property Matching] Error getting suggestions for '{userInput}': {ex.Message}");
|
||||
return new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, List<string>> PropertySuggestionCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Rule-based suggestions that mimic AI behavior for property matching.
|
||||
/// This provides immediate value while we could add real AI integration later.
|
||||
/// </summary>
|
||||
private static List<string> GetRuleBasedSuggestions(string userInput, List<string> availableProperties)
|
||||
{
|
||||
var suggestions = new List<string>();
|
||||
var cleanedInput = userInput.ToLowerInvariant().Replace(" ", "").Replace("-", "").Replace("_", "");
|
||||
|
||||
foreach (var property in availableProperties)
|
||||
{
|
||||
var cleanedProperty = property.ToLowerInvariant().Replace(" ", "").Replace("-", "").Replace("_", "");
|
||||
|
||||
if (cleanedProperty == cleanedInput)
|
||||
{
|
||||
suggestions.Add(property);
|
||||
continue;
|
||||
}
|
||||
|
||||
var inputWords = userInput.ToLowerInvariant().Split(new[] { ' ', '-', '_' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (inputWords.All(word => cleanedProperty.Contains(word.ToLowerInvariant())))
|
||||
{
|
||||
suggestions.Add(property);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (LevenshteinDistance(cleanedInput, cleanedProperty) <= Math.Max(2, cleanedInput.Length / 4))
|
||||
{
|
||||
suggestions.Add(property);
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions.OrderBy(s => LevenshteinDistance(cleanedInput, s.ToLowerInvariant().Replace(" ", "")))
|
||||
.Take(3)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Levenshtein distance between two strings for similarity matching.
|
||||
/// </summary>
|
||||
private static int LevenshteinDistance(string s1, string s2)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s1)) return s2?.Length ?? 0;
|
||||
if (string.IsNullOrEmpty(s2)) return s1.Length;
|
||||
|
||||
var matrix = new int[s1.Length + 1, s2.Length + 1];
|
||||
|
||||
for (int i = 0; i <= s1.Length; i++) matrix[i, 0] = i;
|
||||
for (int j = 0; j <= s2.Length; j++) matrix[0, j] = j;
|
||||
|
||||
for (int i = 1; i <= s1.Length; i++)
|
||||
{
|
||||
for (int j = 1; j <= s2.Length; j++)
|
||||
{
|
||||
int cost = (s2[j - 1] == s1[i - 1]) ? 0 : 1;
|
||||
matrix[i, j] = Math.Min(Math.Min(
|
||||
matrix[i - 1, j] + 1,
|
||||
matrix[i, j - 1] + 1),
|
||||
matrix[i - 1, j - 1] + cost);
|
||||
}
|
||||
}
|
||||
|
||||
return matrix[s1.Length, s2.Length];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5e5a46bdebc040c68897fa4b5e689c7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,410 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using MCPForUnity.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.GameObjects
|
||||
{
|
||||
internal static class GameObjectComponentHelpers
|
||||
{
|
||||
internal static object AddComponentInternal(GameObject targetGo, string typeName, JObject properties)
|
||||
{
|
||||
Type componentType = FindType(typeName);
|
||||
if (componentType == null)
|
||||
{
|
||||
return new ErrorResponse($"Component type '{typeName}' not found or is not a valid Component.");
|
||||
}
|
||||
if (!typeof(Component).IsAssignableFrom(componentType))
|
||||
{
|
||||
return new ErrorResponse($"Type '{typeName}' is not a Component.");
|
||||
}
|
||||
|
||||
if (componentType == typeof(Transform))
|
||||
{
|
||||
return new ErrorResponse("Cannot add another Transform component.");
|
||||
}
|
||||
|
||||
bool isAdding2DPhysics = typeof(Rigidbody2D).IsAssignableFrom(componentType) || typeof(Collider2D).IsAssignableFrom(componentType);
|
||||
bool isAdding3DPhysics = typeof(Rigidbody).IsAssignableFrom(componentType) || typeof(Collider).IsAssignableFrom(componentType);
|
||||
|
||||
if (isAdding2DPhysics)
|
||||
{
|
||||
if (targetGo.GetComponent<Rigidbody>() != null || targetGo.GetComponent<Collider>() != null)
|
||||
{
|
||||
return new ErrorResponse($"Cannot add 2D physics component '{typeName}' because the GameObject '{targetGo.name}' already has a 3D Rigidbody or Collider.");
|
||||
}
|
||||
}
|
||||
else if (isAdding3DPhysics)
|
||||
{
|
||||
if (targetGo.GetComponent<Rigidbody2D>() != null || targetGo.GetComponent<Collider2D>() != null)
|
||||
{
|
||||
return new ErrorResponse($"Cannot add 3D physics component '{typeName}' because the GameObject '{targetGo.name}' already has a 2D Rigidbody or Collider.");
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Component newComponent = Undo.AddComponent(targetGo, componentType);
|
||||
if (newComponent == null)
|
||||
{
|
||||
return new ErrorResponse($"Failed to add component '{typeName}' to '{targetGo.name}'. It might be disallowed (e.g., adding script twice)."
|
||||
);
|
||||
}
|
||||
|
||||
if (newComponent is Light light)
|
||||
{
|
||||
light.type = LightType.Directional;
|
||||
}
|
||||
|
||||
if (properties != null)
|
||||
{
|
||||
var setResult = SetComponentPropertiesInternal(targetGo, typeName, properties, newComponent);
|
||||
if (setResult != null)
|
||||
{
|
||||
Undo.DestroyObjectImmediate(newComponent);
|
||||
return setResult;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Error adding component '{typeName}' to '{targetGo.name}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal static object RemoveComponentInternal(GameObject targetGo, string typeName)
|
||||
{
|
||||
if (targetGo == null)
|
||||
{
|
||||
return new ErrorResponse("Target GameObject is null.");
|
||||
}
|
||||
|
||||
Type componentType = FindType(typeName);
|
||||
if (componentType == null)
|
||||
{
|
||||
return new ErrorResponse($"Component type '{typeName}' not found for removal.");
|
||||
}
|
||||
|
||||
if (componentType == typeof(Transform))
|
||||
{
|
||||
return new ErrorResponse("Cannot remove the Transform component.");
|
||||
}
|
||||
|
||||
Component componentToRemove = targetGo.GetComponent(componentType);
|
||||
if (componentToRemove == null)
|
||||
{
|
||||
return new ErrorResponse($"Component '{typeName}' not found on '{targetGo.name}' to remove.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Undo.DestroyObjectImmediate(componentToRemove);
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Error removing component '{typeName}' from '{targetGo.name}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal static object SetComponentPropertiesInternal(GameObject targetGo, string componentTypeName, JObject properties, Component targetComponentInstance = null)
|
||||
{
|
||||
Component targetComponent = targetComponentInstance;
|
||||
if (targetComponent == null)
|
||||
{
|
||||
if (ComponentResolver.TryResolve(componentTypeName, out var compType, out var compError))
|
||||
{
|
||||
targetComponent = targetGo.GetComponent(compType);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetComponent = targetGo.GetComponent(componentTypeName);
|
||||
}
|
||||
}
|
||||
if (targetComponent == null)
|
||||
{
|
||||
return new ErrorResponse($"Component '{componentTypeName}' not found on '{targetGo.name}' to set properties.");
|
||||
}
|
||||
|
||||
Undo.RecordObject(targetComponent, "Set Component Properties");
|
||||
|
||||
var failures = new List<string>();
|
||||
foreach (var prop in properties.Properties())
|
||||
{
|
||||
string propName = prop.Name;
|
||||
JToken propValue = prop.Value;
|
||||
|
||||
try
|
||||
{
|
||||
bool setResult = SetProperty(targetComponent, propName, propValue);
|
||||
if (!setResult)
|
||||
{
|
||||
var availableProperties = ComponentResolver.GetAllComponentProperties(targetComponent.GetType());
|
||||
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions(propName, availableProperties);
|
||||
var msg = suggestions.Any()
|
||||
? $"Property '{propName}' not found. Did you mean: {string.Join(", ", suggestions)}? Available: [{string.Join(", ", availableProperties)}]"
|
||||
: $"Property '{propName}' not found. Available: [{string.Join(", ", availableProperties)}]";
|
||||
McpLog.Warn($"[ManageGameObject] {msg}");
|
||||
failures.Add(msg);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Error($"[ManageGameObject] Error setting property '{propName}' on '{componentTypeName}': {e.Message}");
|
||||
failures.Add($"Error setting '{propName}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(targetComponent);
|
||||
return failures.Count == 0
|
||||
? null
|
||||
: new ErrorResponse($"One or more properties failed on '{componentTypeName}'.", new { errors = failures });
|
||||
}
|
||||
|
||||
private static JsonSerializer InputSerializer => UnityJsonSerializer.Instance;
|
||||
|
||||
private static bool SetProperty(object target, string memberName, JToken value)
|
||||
{
|
||||
Type type = target.GetType();
|
||||
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
|
||||
|
||||
string normalizedName = Helpers.ParamCoercion.NormalizePropertyName(memberName);
|
||||
var inputSerializer = InputSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
if (memberName.Contains('.') || memberName.Contains('['))
|
||||
{
|
||||
return SetNestedProperty(target, memberName, value, inputSerializer);
|
||||
}
|
||||
|
||||
PropertyInfo propInfo = type.GetProperty(memberName, flags) ?? type.GetProperty(normalizedName, flags);
|
||||
if (propInfo != null && propInfo.CanWrite)
|
||||
{
|
||||
object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType, inputSerializer);
|
||||
if (convertedValue != null || value.Type == JTokenType.Null)
|
||||
{
|
||||
propInfo.SetValue(target, convertedValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FieldInfo fieldInfo = type.GetField(memberName, flags) ?? type.GetField(normalizedName, flags);
|
||||
if (fieldInfo != null)
|
||||
{
|
||||
object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType, inputSerializer);
|
||||
if (convertedValue != null || value.Type == JTokenType.Null)
|
||||
{
|
||||
fieldInfo.SetValue(target, convertedValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var npField = type.GetField(memberName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase)
|
||||
?? type.GetField(normalizedName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
if (npField != null && npField.GetCustomAttribute<SerializeField>() != null)
|
||||
{
|
||||
object convertedValue = ConvertJTokenToType(value, npField.FieldType, inputSerializer);
|
||||
if (convertedValue != null || value.Type == JTokenType.Null)
|
||||
{
|
||||
npField.SetValue(target, convertedValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"[SetProperty] Failed to set '{memberName}' on {type.Name}: {ex.Message}\nToken: {value.ToString(Formatting.None)}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool SetNestedProperty(object target, string path, JToken value, JsonSerializer inputSerializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
string[] pathParts = SplitPropertyPath(path);
|
||||
if (pathParts.Length == 0)
|
||||
return false;
|
||||
|
||||
object currentObject = target;
|
||||
Type currentType = currentObject.GetType();
|
||||
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
|
||||
|
||||
for (int i = 0; i < pathParts.Length - 1; i++)
|
||||
{
|
||||
string part = pathParts[i];
|
||||
bool isArray = false;
|
||||
int arrayIndex = -1;
|
||||
|
||||
if (part.Contains("["))
|
||||
{
|
||||
int startBracket = part.IndexOf('[');
|
||||
int endBracket = part.IndexOf(']');
|
||||
if (startBracket > 0 && endBracket > startBracket)
|
||||
{
|
||||
string indexStr = part.Substring(startBracket + 1, endBracket - startBracket - 1);
|
||||
if (int.TryParse(indexStr, out arrayIndex))
|
||||
{
|
||||
isArray = true;
|
||||
part = part.Substring(0, startBracket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PropertyInfo propInfo = currentType.GetProperty(part, flags);
|
||||
FieldInfo fieldInfo = null;
|
||||
if (propInfo == null)
|
||||
{
|
||||
fieldInfo = currentType.GetField(part, flags);
|
||||
if (fieldInfo == null)
|
||||
{
|
||||
McpLog.Warn($"[SetNestedProperty] Could not find property or field '{part}' on type '{currentType.Name}'");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
currentObject = propInfo != null ? propInfo.GetValue(currentObject) : fieldInfo.GetValue(currentObject);
|
||||
if (currentObject == null)
|
||||
{
|
||||
McpLog.Warn($"[SetNestedProperty] Property '{part}' is null, cannot access nested properties.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isArray)
|
||||
{
|
||||
if (currentObject is Material[])
|
||||
{
|
||||
var materials = currentObject as Material[];
|
||||
if (arrayIndex < 0 || arrayIndex >= materials.Length)
|
||||
{
|
||||
McpLog.Warn($"[SetNestedProperty] Material index {arrayIndex} out of range (0-{materials.Length - 1})");
|
||||
return false;
|
||||
}
|
||||
currentObject = materials[arrayIndex];
|
||||
}
|
||||
else if (currentObject is System.Collections.IList)
|
||||
{
|
||||
var list = currentObject as System.Collections.IList;
|
||||
if (arrayIndex < 0 || arrayIndex >= list.Count)
|
||||
{
|
||||
McpLog.Warn($"[SetNestedProperty] Index {arrayIndex} out of range (0-{list.Count - 1})");
|
||||
return false;
|
||||
}
|
||||
currentObject = list[arrayIndex];
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Warn($"[SetNestedProperty] Property '{part}' is not an array or list, cannot access by index.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
currentType = currentObject.GetType();
|
||||
}
|
||||
|
||||
string finalPart = pathParts[pathParts.Length - 1];
|
||||
|
||||
if (currentObject is Material material && finalPart.StartsWith("_"))
|
||||
{
|
||||
return MaterialOps.TrySetShaderProperty(material, finalPart, value, inputSerializer);
|
||||
}
|
||||
|
||||
PropertyInfo finalPropInfo = currentType.GetProperty(finalPart, flags);
|
||||
if (finalPropInfo != null && finalPropInfo.CanWrite)
|
||||
{
|
||||
object convertedValue = ConvertJTokenToType(value, finalPropInfo.PropertyType, inputSerializer);
|
||||
if (convertedValue != null || value.Type == JTokenType.Null)
|
||||
{
|
||||
finalPropInfo.SetValue(currentObject, convertedValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FieldInfo finalFieldInfo = currentType.GetField(finalPart, flags);
|
||||
if (finalFieldInfo != null)
|
||||
{
|
||||
object convertedValue = ConvertJTokenToType(value, finalFieldInfo.FieldType, inputSerializer);
|
||||
if (convertedValue != null || value.Type == JTokenType.Null)
|
||||
{
|
||||
finalFieldInfo.SetValue(currentObject, convertedValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"[SetNestedProperty] Error setting nested property '{path}': {ex.Message}\nToken: {value.ToString(Formatting.None)}");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string[] SplitPropertyPath(string path)
|
||||
{
|
||||
List<string> parts = new List<string>();
|
||||
int startIndex = 0;
|
||||
bool inBrackets = false;
|
||||
|
||||
for (int i = 0; i < path.Length; i++)
|
||||
{
|
||||
char c = path[i];
|
||||
|
||||
if (c == '[')
|
||||
{
|
||||
inBrackets = true;
|
||||
}
|
||||
else if (c == ']')
|
||||
{
|
||||
inBrackets = false;
|
||||
}
|
||||
else if (c == '.' && !inBrackets)
|
||||
{
|
||||
parts.Add(path.Substring(startIndex, i - startIndex));
|
||||
startIndex = i + 1;
|
||||
}
|
||||
}
|
||||
if (startIndex < path.Length)
|
||||
{
|
||||
parts.Add(path.Substring(startIndex));
|
||||
}
|
||||
return parts.ToArray();
|
||||
}
|
||||
|
||||
private static object ConvertJTokenToType(JToken token, Type targetType, JsonSerializer inputSerializer)
|
||||
{
|
||||
return PropertyConversion.ConvertToType(token, targetType);
|
||||
}
|
||||
|
||||
private static Type FindType(string typeName)
|
||||
{
|
||||
if (ComponentResolver.TryResolve(typeName, out Type resolvedType, out string error))
|
||||
{
|
||||
return resolvedType;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
McpLog.Warn($"[FindType] {error}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b580af06e2d3a4788960f3f779edac54
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,338 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.GameObjects
|
||||
{
|
||||
internal static class GameObjectCreate
|
||||
{
|
||||
internal static object Handle(JObject @params)
|
||||
{
|
||||
string name = @params["name"]?.ToString();
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return new ErrorResponse("'name' parameter is required for 'create' action.");
|
||||
}
|
||||
|
||||
// Get prefab creation parameters
|
||||
bool saveAsPrefab = @params["saveAsPrefab"]?.ToObject<bool>() ?? false;
|
||||
string prefabPath = @params["prefabPath"]?.ToString();
|
||||
string tag = @params["tag"]?.ToString();
|
||||
string primitiveType = @params["primitiveType"]?.ToString();
|
||||
GameObject newGo = null;
|
||||
|
||||
// --- Try Instantiating Prefab First ---
|
||||
string originalPrefabPath = prefabPath;
|
||||
if (!saveAsPrefab && !string.IsNullOrEmpty(prefabPath))
|
||||
{
|
||||
string extension = System.IO.Path.GetExtension(prefabPath);
|
||||
|
||||
if (!prefabPath.Contains("/") && (string.IsNullOrEmpty(extension) || extension.Equals(".prefab", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
string prefabNameOnly = prefabPath;
|
||||
McpLog.Info($"[ManageGameObject.Create] Searching for prefab named: '{prefabNameOnly}'");
|
||||
string[] guids = AssetDatabase.FindAssets($"t:Prefab {prefabNameOnly}");
|
||||
if (guids.Length == 0)
|
||||
{
|
||||
return new ErrorResponse($"Prefab named '{prefabNameOnly}' not found anywhere in the project.");
|
||||
}
|
||||
else if (guids.Length > 1)
|
||||
{
|
||||
string foundPaths = string.Join(", ", guids.Select(g => AssetDatabase.GUIDToAssetPath(g)));
|
||||
return new ErrorResponse($"Multiple prefabs found matching name '{prefabNameOnly}': {foundPaths}. Please provide a more specific path.");
|
||||
}
|
||||
else
|
||||
{
|
||||
prefabPath = AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
McpLog.Info($"[ManageGameObject.Create] Found unique prefab at path: '{prefabPath}'");
|
||||
}
|
||||
}
|
||||
else if (prefabPath.Contains("/") && string.IsNullOrEmpty(extension))
|
||||
{
|
||||
McpLog.Warn($"[ManageGameObject.Create] Provided prefabPath '{prefabPath}' has no extension. Assuming it's a prefab and appending .prefab.");
|
||||
prefabPath += ".prefab";
|
||||
}
|
||||
else if (!prefabPath.Contains("/") && !string.IsNullOrEmpty(extension) && !extension.Equals(".prefab", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string fileName = prefabPath;
|
||||
string fileNameWithoutExtension = System.IO.Path.GetFileNameWithoutExtension(fileName);
|
||||
McpLog.Info($"[ManageGameObject.Create] Searching for asset file named: '{fileName}'");
|
||||
|
||||
string[] guids = AssetDatabase.FindAssets(fileNameWithoutExtension);
|
||||
var matches = guids
|
||||
.Select(g => AssetDatabase.GUIDToAssetPath(g))
|
||||
.Where(p => p.EndsWith("/" + fileName, StringComparison.OrdinalIgnoreCase) || p.Equals(fileName, StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
|
||||
if (matches.Length == 0)
|
||||
{
|
||||
return new ErrorResponse($"Asset file '{fileName}' not found anywhere in the project.");
|
||||
}
|
||||
else if (matches.Length > 1)
|
||||
{
|
||||
string foundPaths = string.Join(", ", matches);
|
||||
return new ErrorResponse($"Multiple assets found matching file name '{fileName}': {foundPaths}. Please provide a more specific path.");
|
||||
}
|
||||
else
|
||||
{
|
||||
prefabPath = matches[0];
|
||||
McpLog.Info($"[ManageGameObject.Create] Found unique asset at path: '{prefabPath}'");
|
||||
}
|
||||
}
|
||||
|
||||
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
if (prefabAsset != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
newGo = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject;
|
||||
|
||||
if (newGo == null)
|
||||
{
|
||||
McpLog.Error($"[ManageGameObject.Create] Failed to instantiate prefab at '{prefabPath}', asset might be corrupted or not a GameObject.");
|
||||
return new ErrorResponse($"Failed to instantiate prefab at '{prefabPath}'.");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
newGo.name = name;
|
||||
}
|
||||
Undo.RegisterCreatedObjectUndo(newGo, $"Instantiate Prefab '{prefabAsset.name}' as '{newGo.name}'");
|
||||
McpLog.Info($"[ManageGameObject.Create] Instantiated prefab '{prefabAsset.name}' from path '{prefabPath}' as '{newGo.name}'.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Error instantiating prefab '{prefabPath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse($"Asset not found or not a GameObject at path: '{prefabPath}'.");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fallback: Create Primitive or Empty GameObject ---
|
||||
bool createdNewObject = false;
|
||||
if (newGo == null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(primitiveType))
|
||||
{
|
||||
try
|
||||
{
|
||||
PrimitiveType type = (PrimitiveType)Enum.Parse(typeof(PrimitiveType), primitiveType, true);
|
||||
newGo = GameObject.CreatePrimitive(type);
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
newGo.name = name;
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return new ErrorResponse("'name' parameter is required when creating a primitive.");
|
||||
}
|
||||
createdNewObject = true;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return new ErrorResponse($"Invalid primitive type: '{primitiveType}'. Valid types: {string.Join(", ", Enum.GetNames(typeof(PrimitiveType)))}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Failed to create primitive '{primitiveType}': {e.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return new ErrorResponse("'name' parameter is required for 'create' action when not instantiating a prefab or creating a primitive.");
|
||||
}
|
||||
newGo = new GameObject(name);
|
||||
createdNewObject = true;
|
||||
}
|
||||
|
||||
if (createdNewObject)
|
||||
{
|
||||
Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{newGo.name}'");
|
||||
}
|
||||
}
|
||||
|
||||
if (newGo == null)
|
||||
{
|
||||
return new ErrorResponse("Failed to create or instantiate the GameObject.");
|
||||
}
|
||||
|
||||
Undo.RecordObject(newGo.transform, "Set GameObject Transform");
|
||||
Undo.RecordObject(newGo, "Set GameObject Properties");
|
||||
|
||||
// Set Parent
|
||||
JToken parentToken = @params["parent"];
|
||||
if (parentToken != null)
|
||||
{
|
||||
GameObject parentGo = ManageGameObjectCommon.FindObjectInternal(parentToken, "by_id_or_name_or_path");
|
||||
if (parentGo == null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return new ErrorResponse($"Parent specified ('{parentToken}') but not found.");
|
||||
}
|
||||
newGo.transform.SetParent(parentGo.transform, true);
|
||||
}
|
||||
|
||||
// Set Transform
|
||||
Vector3? position = VectorParsing.ParseVector3(@params["position"]);
|
||||
Vector3? rotation = VectorParsing.ParseVector3(@params["rotation"]);
|
||||
Vector3? scale = VectorParsing.ParseVector3(@params["scale"]);
|
||||
|
||||
if (position.HasValue) newGo.transform.localPosition = position.Value;
|
||||
if (rotation.HasValue) newGo.transform.localEulerAngles = rotation.Value;
|
||||
if (scale.HasValue) newGo.transform.localScale = scale.Value;
|
||||
|
||||
// Set Tag
|
||||
if (!string.IsNullOrEmpty(tag))
|
||||
{
|
||||
if (tag != "Untagged" && !System.Linq.Enumerable.Contains(InternalEditorUtility.tags, tag))
|
||||
{
|
||||
McpLog.Info($"[ManageGameObject.Create] Tag '{tag}' not found. Creating it.");
|
||||
try
|
||||
{
|
||||
InternalEditorUtility.AddTag(tag);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return new ErrorResponse($"Failed to create tag '{tag}': {ex.Message}.");
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
newGo.tag = tag;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return new ErrorResponse($"Failed to set tag to '{tag}' during creation: {ex.Message}.");
|
||||
}
|
||||
}
|
||||
|
||||
// Set Layer
|
||||
string layerName = @params["layer"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(layerName))
|
||||
{
|
||||
int layerId = LayerMask.NameToLayer(layerName);
|
||||
if (layerId != -1)
|
||||
{
|
||||
newGo.layer = layerId;
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Warn($"[ManageGameObject.Create] Layer '{layerName}' not found. Using default layer.");
|
||||
}
|
||||
}
|
||||
|
||||
// Add Components
|
||||
if (@params["componentsToAdd"] is JArray componentsToAddArray)
|
||||
{
|
||||
foreach (var compToken in componentsToAddArray)
|
||||
{
|
||||
string typeName = null;
|
||||
JObject properties = null;
|
||||
|
||||
if (compToken.Type == JTokenType.String)
|
||||
{
|
||||
typeName = compToken.ToString();
|
||||
}
|
||||
else if (compToken is JObject compObj)
|
||||
{
|
||||
typeName = compObj["typeName"]?.ToString();
|
||||
properties = compObj["properties"] as JObject;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(typeName))
|
||||
{
|
||||
var addResult = GameObjectComponentHelpers.AddComponentInternal(newGo, typeName, properties);
|
||||
if (addResult != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return addResult;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Warn($"[ManageGameObject] Invalid component format in componentsToAdd: {compToken}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save as Prefab ONLY if we *created* a new object AND saveAsPrefab is true
|
||||
GameObject finalInstance = newGo;
|
||||
if (createdNewObject && saveAsPrefab)
|
||||
{
|
||||
string finalPrefabPath = prefabPath;
|
||||
if (string.IsNullOrEmpty(finalPrefabPath))
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return new ErrorResponse("'prefabPath' is required when 'saveAsPrefab' is true and creating a new object.");
|
||||
}
|
||||
if (!finalPrefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
McpLog.Info($"[ManageGameObject.Create] Appending .prefab extension to save path: '{finalPrefabPath}' -> '{finalPrefabPath}.prefab'");
|
||||
finalPrefabPath += ".prefab";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string directoryPath = System.IO.Path.GetDirectoryName(finalPrefabPath);
|
||||
if (!string.IsNullOrEmpty(directoryPath) && !System.IO.Directory.Exists(directoryPath))
|
||||
{
|
||||
System.IO.Directory.CreateDirectory(directoryPath);
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
||||
McpLog.Info($"[ManageGameObject.Create] Created directory for prefab: {directoryPath}");
|
||||
}
|
||||
|
||||
finalInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(newGo, finalPrefabPath, InteractionMode.UserAction);
|
||||
|
||||
if (finalInstance == null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return new ErrorResponse($"Failed to save GameObject '{name}' as prefab at '{finalPrefabPath}'. Check path and permissions.");
|
||||
}
|
||||
McpLog.Info($"[ManageGameObject.Create] GameObject '{name}' saved as prefab to '{finalPrefabPath}' and instance connected.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return new ErrorResponse($"Error saving prefab '{finalPrefabPath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
Selection.activeGameObject = finalInstance;
|
||||
|
||||
string messagePrefabPath =
|
||||
finalInstance == null
|
||||
? originalPrefabPath
|
||||
: AssetDatabase.GetAssetPath(PrefabUtility.GetCorrespondingObjectFromSource(finalInstance) ?? (UnityEngine.Object)finalInstance);
|
||||
|
||||
string successMessage;
|
||||
if (!createdNewObject && !string.IsNullOrEmpty(messagePrefabPath))
|
||||
{
|
||||
successMessage = $"Prefab '{messagePrefabPath}' instantiated successfully as '{finalInstance.name}'.";
|
||||
}
|
||||
else if (createdNewObject && saveAsPrefab && !string.IsNullOrEmpty(messagePrefabPath))
|
||||
{
|
||||
successMessage = $"GameObject '{finalInstance.name}' created and saved as prefab to '{messagePrefabPath}'.";
|
||||
}
|
||||
else
|
||||
{
|
||||
successMessage = $"GameObject '{finalInstance.name}' created successfully in scene.";
|
||||
}
|
||||
|
||||
return new SuccessResponse(successMessage, Helpers.GameObjectSerializer.GetGameObjectData(finalInstance));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0931774a07e4b4626b4261dd8d0974c2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,48 @@
|
||||
#nullable disable
|
||||
using System.Collections.Generic;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.GameObjects
|
||||
{
|
||||
internal static class GameObjectDelete
|
||||
{
|
||||
internal static object Handle(JToken targetToken, string searchMethod)
|
||||
{
|
||||
List<GameObject> targets = ManageGameObjectCommon.FindObjectsInternal(targetToken, searchMethod, true);
|
||||
|
||||
if (targets.Count == 0)
|
||||
{
|
||||
return new ErrorResponse($"Target GameObject(s) ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
|
||||
}
|
||||
|
||||
List<object> deletedObjects = new List<object>();
|
||||
foreach (var targetGo in targets)
|
||||
{
|
||||
if (targetGo != null)
|
||||
{
|
||||
string goName = targetGo.name;
|
||||
int goId = targetGo.GetInstanceID();
|
||||
// Note: Undo.DestroyObjectImmediate doesn't work reliably in test context,
|
||||
// so we use Object.DestroyImmediate. This means delete isn't undoable.
|
||||
// TODO: Investigate Undo.DestroyObjectImmediate behavior in Unity 2022+
|
||||
Object.DestroyImmediate(targetGo);
|
||||
deletedObjects.Add(new { name = goName, instanceID = goId });
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedObjects.Count > 0)
|
||||
{
|
||||
string message =
|
||||
targets.Count == 1
|
||||
? $"GameObject '{((dynamic)deletedObjects[0]).name}' deleted successfully."
|
||||
: $"{deletedObjects.Count} GameObjects deleted successfully.";
|
||||
return new SuccessResponse(message, deletedObjects);
|
||||
}
|
||||
|
||||
return new ErrorResponse("Failed to delete target GameObject(s).");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 505a482aaf60b415abd794737a630b10
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,86 @@
|
||||
#nullable disable
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.GameObjects
|
||||
{
|
||||
internal static class GameObjectDuplicate
|
||||
{
|
||||
internal static object Handle(JObject @params, JToken targetToken, string searchMethod)
|
||||
{
|
||||
GameObject sourceGo = ManageGameObjectCommon.FindObjectInternal(targetToken, searchMethod);
|
||||
if (sourceGo == null)
|
||||
{
|
||||
return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
|
||||
}
|
||||
|
||||
string newName = @params["new_name"]?.ToString();
|
||||
Vector3? position = VectorParsing.ParseVector3(@params["position"]);
|
||||
Vector3? offset = VectorParsing.ParseVector3(@params["offset"]);
|
||||
JToken parentToken = @params["parent"];
|
||||
|
||||
GameObject duplicatedGo = UnityEngine.Object.Instantiate(sourceGo);
|
||||
Undo.RegisterCreatedObjectUndo(duplicatedGo, $"Duplicate {sourceGo.name}");
|
||||
|
||||
if (!string.IsNullOrEmpty(newName))
|
||||
{
|
||||
duplicatedGo.name = newName;
|
||||
}
|
||||
else
|
||||
{
|
||||
duplicatedGo.name = sourceGo.name.Replace("(Clone)", "").Trim() + "_Copy";
|
||||
}
|
||||
|
||||
if (position.HasValue)
|
||||
{
|
||||
duplicatedGo.transform.position = position.Value;
|
||||
}
|
||||
else if (offset.HasValue)
|
||||
{
|
||||
duplicatedGo.transform.position = sourceGo.transform.position + offset.Value;
|
||||
}
|
||||
|
||||
if (parentToken != null)
|
||||
{
|
||||
if (parentToken.Type == JTokenType.Null || (parentToken.Type == JTokenType.String && string.IsNullOrEmpty(parentToken.ToString())))
|
||||
{
|
||||
duplicatedGo.transform.SetParent(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameObject newParent = ManageGameObjectCommon.FindObjectInternal(parentToken, "by_id_or_name_or_path");
|
||||
if (newParent != null)
|
||||
{
|
||||
duplicatedGo.transform.SetParent(newParent.transform, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Warn($"[ManageGameObject.Duplicate] Parent '{parentToken}' not found. Object will remain at root level.");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
duplicatedGo.transform.SetParent(sourceGo.transform.parent, true);
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(duplicatedGo);
|
||||
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
|
||||
|
||||
Selection.activeGameObject = duplicatedGo;
|
||||
|
||||
return new SuccessResponse(
|
||||
$"Duplicated '{sourceGo.name}' as '{duplicatedGo.name}'.",
|
||||
new
|
||||
{
|
||||
originalName = sourceGo.name,
|
||||
originalId = sourceGo.GetInstanceID(),
|
||||
duplicatedObject = Helpers.GameObjectSerializer.GetGameObjectData(duplicatedGo)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 698728d56425a47af92a45377031a48b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
#nullable disable
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.GameObjects
|
||||
{
|
||||
internal static class GameObjectHandlers
|
||||
{
|
||||
internal static object Create(JObject @params) => GameObjectCreate.Handle(@params);
|
||||
|
||||
internal static object Modify(JObject @params, JToken targetToken, string searchMethod)
|
||||
=> GameObjectModify.Handle(@params, targetToken, searchMethod);
|
||||
|
||||
internal static object Delete(JToken targetToken, string searchMethod)
|
||||
=> GameObjectDelete.Handle(targetToken, searchMethod);
|
||||
|
||||
internal static object Duplicate(JObject @params, JToken targetToken, string searchMethod)
|
||||
=> GameObjectDuplicate.Handle(@params, targetToken, searchMethod);
|
||||
|
||||
internal static object MoveRelative(JObject @params, JToken targetToken, string searchMethod)
|
||||
=> GameObjectMoveRelative.Handle(@params, targetToken, searchMethod);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f3cf2313460d44a09b258d2ee04c5ef0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,297 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.GameObjects
|
||||
{
|
||||
internal static class GameObjectModify
|
||||
{
|
||||
internal static object Handle(JObject @params, JToken targetToken, string searchMethod)
|
||||
{
|
||||
// When setActive=true is specified, we need to search for inactive objects
|
||||
// otherwise we can't find an inactive object to activate it
|
||||
JObject findParams = null;
|
||||
if (@params["setActive"]?.ToObject<bool?>() == true)
|
||||
{
|
||||
findParams = new JObject { ["searchInactive"] = true };
|
||||
}
|
||||
|
||||
GameObject targetGo = ManageGameObjectCommon.FindObjectInternal(targetToken, searchMethod, findParams);
|
||||
if (targetGo == null)
|
||||
{
|
||||
return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
|
||||
}
|
||||
|
||||
Undo.RecordObject(targetGo.transform, "Modify GameObject Transform");
|
||||
Undo.RecordObject(targetGo, "Modify GameObject Properties");
|
||||
|
||||
bool modified = false;
|
||||
|
||||
string name = @params["name"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(name) && targetGo.name != name)
|
||||
{
|
||||
// Check if we're renaming the root object of an open prefab stage
|
||||
var prefabStageForRename = PrefabStageUtility.GetCurrentPrefabStage();
|
||||
bool isRenamingPrefabRoot = prefabStageForRename != null &&
|
||||
prefabStageForRename.prefabContentsRoot == targetGo;
|
||||
|
||||
if (isRenamingPrefabRoot)
|
||||
{
|
||||
// Rename the prefab asset file to match the new name (avoids Unity dialog)
|
||||
string assetPath = prefabStageForRename.assetPath;
|
||||
string directory = System.IO.Path.GetDirectoryName(assetPath);
|
||||
string newAssetPath = AssetPathUtility.NormalizeSeparators(System.IO.Path.Combine(directory, name + ".prefab"));
|
||||
|
||||
// Only rename if the path actually changes
|
||||
if (newAssetPath != assetPath)
|
||||
{
|
||||
// Check for collision using GUID comparison
|
||||
string currentGuid = AssetDatabase.AssetPathToGUID(assetPath);
|
||||
string existingGuid = AssetDatabase.AssetPathToGUID(newAssetPath);
|
||||
|
||||
// Collision only if there's a different asset at the new path
|
||||
if (!string.IsNullOrEmpty(existingGuid) && existingGuid != currentGuid)
|
||||
{
|
||||
return new ErrorResponse($"Cannot rename prefab root to '{name}': a prefab already exists at '{newAssetPath}'.");
|
||||
}
|
||||
|
||||
// Rename the asset file
|
||||
string renameError = AssetDatabase.RenameAsset(assetPath, name);
|
||||
if (!string.IsNullOrEmpty(renameError))
|
||||
{
|
||||
return new ErrorResponse($"Failed to rename prefab asset: {renameError}");
|
||||
}
|
||||
|
||||
McpLog.Info($"[GameObjectModify] Renamed prefab asset from '{assetPath}' to '{newAssetPath}'");
|
||||
}
|
||||
}
|
||||
|
||||
targetGo.name = name;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
JToken parentToken = @params["parent"];
|
||||
if (parentToken != null)
|
||||
{
|
||||
GameObject newParentGo = ManageGameObjectCommon.FindObjectInternal(parentToken, "by_id_or_name_or_path");
|
||||
if (
|
||||
newParentGo == null
|
||||
&& !(parentToken.Type == JTokenType.Null
|
||||
|| (parentToken.Type == JTokenType.String && string.IsNullOrEmpty(parentToken.ToString())))
|
||||
)
|
||||
{
|
||||
return new ErrorResponse($"New parent ('{parentToken}') not found.");
|
||||
}
|
||||
if (newParentGo != null && newParentGo.transform.IsChildOf(targetGo.transform))
|
||||
{
|
||||
return new ErrorResponse($"Cannot parent '{targetGo.name}' to '{newParentGo.name}', as it would create a hierarchy loop.");
|
||||
}
|
||||
if (targetGo.transform.parent != (newParentGo?.transform))
|
||||
{
|
||||
targetGo.transform.SetParent(newParentGo?.transform, true);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool? setActive = @params["setActive"]?.ToObject<bool?>();
|
||||
if (setActive.HasValue && targetGo.activeSelf != setActive.Value)
|
||||
{
|
||||
targetGo.SetActive(setActive.Value);
|
||||
modified = true;
|
||||
}
|
||||
|
||||
string tag = @params["tag"]?.ToString();
|
||||
if (tag != null && targetGo.tag != tag)
|
||||
{
|
||||
string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag;
|
||||
|
||||
if (tagToSet != "Untagged" && !System.Linq.Enumerable.Contains(InternalEditorUtility.tags, tagToSet))
|
||||
{
|
||||
McpLog.Info($"[ManageGameObject] Tag '{tagToSet}' not found. Creating it.");
|
||||
try
|
||||
{
|
||||
InternalEditorUtility.AddTag(tagToSet);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ErrorResponse($"Failed to create tag '{tagToSet}': {ex.Message}.");
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
targetGo.tag = tagToSet;
|
||||
modified = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ErrorResponse($"Failed to set tag to '{tagToSet}': {ex.Message}.");
|
||||
}
|
||||
}
|
||||
|
||||
string layerName = @params["layer"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(layerName))
|
||||
{
|
||||
int layerId = LayerMask.NameToLayer(layerName);
|
||||
if (layerId == -1)
|
||||
{
|
||||
return new ErrorResponse($"Invalid layer specified: '{layerName}'. Use a valid layer name.");
|
||||
}
|
||||
if (layerId != -1 && targetGo.layer != layerId)
|
||||
{
|
||||
targetGo.layer = layerId;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
Vector3? position = VectorParsing.ParseVector3(@params["position"]);
|
||||
Vector3? rotation = VectorParsing.ParseVector3(@params["rotation"]);
|
||||
Vector3? scale = VectorParsing.ParseVector3(@params["scale"]);
|
||||
|
||||
if (position.HasValue && targetGo.transform.localPosition != position.Value)
|
||||
{
|
||||
targetGo.transform.localPosition = position.Value;
|
||||
modified = true;
|
||||
}
|
||||
if (rotation.HasValue && targetGo.transform.localEulerAngles != rotation.Value)
|
||||
{
|
||||
targetGo.transform.localEulerAngles = rotation.Value;
|
||||
modified = true;
|
||||
}
|
||||
if (scale.HasValue && targetGo.transform.localScale != scale.Value)
|
||||
{
|
||||
targetGo.transform.localScale = scale.Value;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (@params["componentsToRemove"] is JArray componentsToRemoveArray)
|
||||
{
|
||||
foreach (var compToken in componentsToRemoveArray)
|
||||
{
|
||||
string typeName = compToken.ToString();
|
||||
if (!string.IsNullOrEmpty(typeName))
|
||||
{
|
||||
var removeResult = GameObjectComponentHelpers.RemoveComponentInternal(targetGo, typeName);
|
||||
if (removeResult != null)
|
||||
return removeResult;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (@params["componentsToAdd"] is JArray componentsToAddArrayModify)
|
||||
{
|
||||
foreach (var compToken in componentsToAddArrayModify)
|
||||
{
|
||||
string typeName = null;
|
||||
JObject properties = null;
|
||||
if (compToken.Type == JTokenType.String)
|
||||
typeName = compToken.ToString();
|
||||
else if (compToken is JObject compObj)
|
||||
{
|
||||
typeName = compObj["typeName"]?.ToString();
|
||||
properties = compObj["properties"] as JObject;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(typeName))
|
||||
{
|
||||
var addResult = GameObjectComponentHelpers.AddComponentInternal(targetGo, typeName, properties);
|
||||
if (addResult != null)
|
||||
return addResult;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var componentErrors = new List<object>();
|
||||
if (@params["componentProperties"] is JObject componentPropertiesObj)
|
||||
{
|
||||
foreach (var prop in componentPropertiesObj.Properties())
|
||||
{
|
||||
string compName = prop.Name;
|
||||
JObject propertiesToSet = prop.Value as JObject;
|
||||
if (propertiesToSet != null)
|
||||
{
|
||||
var setResult = GameObjectComponentHelpers.SetComponentPropertiesInternal(targetGo, compName, propertiesToSet);
|
||||
if (setResult != null)
|
||||
{
|
||||
componentErrors.Add(setResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (componentErrors.Count > 0)
|
||||
{
|
||||
var aggregatedErrors = new List<string>();
|
||||
foreach (var errorObj in componentErrors)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dataProp = errorObj?.GetType().GetProperty("data");
|
||||
var dataVal = dataProp?.GetValue(errorObj);
|
||||
if (dataVal != null)
|
||||
{
|
||||
var errorsProp = dataVal.GetType().GetProperty("errors");
|
||||
var errorsEnum = errorsProp?.GetValue(dataVal) as System.Collections.IEnumerable;
|
||||
if (errorsEnum != null)
|
||||
{
|
||||
foreach (var item in errorsEnum)
|
||||
{
|
||||
var s = item?.ToString();
|
||||
if (!string.IsNullOrEmpty(s)) aggregatedErrors.Add(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"[GameObjectModify] Error aggregating component errors: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return new ErrorResponse(
|
||||
$"One or more component property operations failed on '{targetGo.name}'.",
|
||||
new { componentErrors = componentErrors, errors = aggregatedErrors }
|
||||
);
|
||||
}
|
||||
|
||||
if (!modified)
|
||||
{
|
||||
return new SuccessResponse(
|
||||
$"No modifications applied to GameObject '{targetGo.name}'.",
|
||||
Helpers.GameObjectSerializer.GetGameObjectData(targetGo)
|
||||
);
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(targetGo);
|
||||
|
||||
// Mark the appropriate scene as dirty (handles both regular scenes and prefab stages)
|
||||
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||
if (prefabStage != null)
|
||||
{
|
||||
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
|
||||
}
|
||||
|
||||
return new SuccessResponse(
|
||||
$"GameObject '{targetGo.name}' modified successfully.",
|
||||
Helpers.GameObjectSerializer.GetGameObjectData(targetGo)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec5e33513bd094257a26ef6f75ea4574
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,119 @@
|
||||
#nullable disable
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.GameObjects
|
||||
{
|
||||
internal static class GameObjectMoveRelative
|
||||
{
|
||||
internal static object Handle(JObject @params, JToken targetToken, string searchMethod)
|
||||
{
|
||||
GameObject targetGo = ManageGameObjectCommon.FindObjectInternal(targetToken, searchMethod);
|
||||
if (targetGo == null)
|
||||
{
|
||||
return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
|
||||
}
|
||||
|
||||
JToken referenceToken = @params["reference_object"];
|
||||
if (referenceToken == null)
|
||||
{
|
||||
return new ErrorResponse("'reference_object' parameter is required for 'move_relative' action.");
|
||||
}
|
||||
|
||||
GameObject referenceGo = ManageGameObjectCommon.FindObjectInternal(referenceToken, "by_id_or_name_or_path");
|
||||
if (referenceGo == null)
|
||||
{
|
||||
return new ErrorResponse($"Reference object '{referenceToken}' not found.");
|
||||
}
|
||||
|
||||
string direction = @params["direction"]?.ToString()?.ToLower();
|
||||
float distance = @params["distance"]?.ToObject<float>() ?? 1f;
|
||||
Vector3? customOffset = VectorParsing.ParseVector3(@params["offset"]);
|
||||
bool useWorldSpace = @params["world_space"]?.ToObject<bool>() ?? true;
|
||||
|
||||
Undo.RecordObject(targetGo.transform, $"Move {targetGo.name} relative to {referenceGo.name}");
|
||||
|
||||
Vector3 newPosition;
|
||||
|
||||
if (customOffset.HasValue)
|
||||
{
|
||||
if (useWorldSpace)
|
||||
{
|
||||
newPosition = referenceGo.transform.position + customOffset.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
newPosition = referenceGo.transform.TransformPoint(customOffset.Value);
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(direction))
|
||||
{
|
||||
Vector3 directionVector = GetDirectionVector(direction, referenceGo.transform, useWorldSpace);
|
||||
newPosition = referenceGo.transform.position + directionVector * distance;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse("Either 'direction' or 'offset' parameter is required for 'move_relative' action.");
|
||||
}
|
||||
|
||||
targetGo.transform.position = newPosition;
|
||||
|
||||
EditorUtility.SetDirty(targetGo);
|
||||
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
|
||||
|
||||
return new SuccessResponse(
|
||||
$"Moved '{targetGo.name}' relative to '{referenceGo.name}'.",
|
||||
new
|
||||
{
|
||||
movedObject = targetGo.name,
|
||||
referenceObject = referenceGo.name,
|
||||
newPosition = new[] { targetGo.transform.position.x, targetGo.transform.position.y, targetGo.transform.position.z },
|
||||
direction = direction,
|
||||
distance = distance,
|
||||
gameObject = Helpers.GameObjectSerializer.GetGameObjectData(targetGo)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static Vector3 GetDirectionVector(string direction, Transform referenceTransform, bool useWorldSpace)
|
||||
{
|
||||
if (useWorldSpace)
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case "right": return Vector3.right;
|
||||
case "left": return Vector3.left;
|
||||
case "up": return Vector3.up;
|
||||
case "down": return Vector3.down;
|
||||
case "forward":
|
||||
case "front": return Vector3.forward;
|
||||
case "back":
|
||||
case "backward":
|
||||
case "behind": return Vector3.back;
|
||||
default:
|
||||
McpLog.Warn($"[ManageGameObject.MoveRelative] Unknown direction '{direction}', defaulting to forward.");
|
||||
return Vector3.forward;
|
||||
}
|
||||
}
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case "right": return referenceTransform.right;
|
||||
case "left": return -referenceTransform.right;
|
||||
case "up": return referenceTransform.up;
|
||||
case "down": return -referenceTransform.up;
|
||||
case "forward":
|
||||
case "front": return referenceTransform.forward;
|
||||
case "back":
|
||||
case "backward":
|
||||
case "behind": return -referenceTransform.forward;
|
||||
default:
|
||||
McpLog.Warn($"[ManageGameObject.MoveRelative] Unknown direction '{direction}', defaulting to forward.");
|
||||
return referenceTransform.forward;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b19997a165de45c2af3ada79a6d3f08
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,115 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using MCPForUnity.Editor.Helpers; // For Response class
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles GameObject manipulation within the current scene (CRUD, find, components).
|
||||
/// </summary>
|
||||
[McpForUnityTool("manage_gameobject", AutoRegister = false)]
|
||||
public static class ManageGameObject
|
||||
{
|
||||
// --- Main Handler ---
|
||||
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
if (@params == null)
|
||||
{
|
||||
return new ErrorResponse("Parameters cannot be null.");
|
||||
}
|
||||
|
||||
string action = @params["action"]?.ToString().ToLower();
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return new ErrorResponse("Action parameter is required.");
|
||||
}
|
||||
|
||||
// Parameters used by various actions
|
||||
JToken targetToken = @params["target"]; // Can be string (name/path) or int (instanceID)
|
||||
string name = @params["name"]?.ToString();
|
||||
|
||||
// --- Usability Improvement: Alias 'name' to 'target' for modification actions ---
|
||||
// If 'target' is missing but 'name' is provided, and we aren't creating a new object,
|
||||
// assume the user meant "find object by name".
|
||||
if (targetToken == null && !string.IsNullOrEmpty(name) && action != "create")
|
||||
{
|
||||
targetToken = name;
|
||||
// We don't update @params["target"] because we use targetToken locally mostly,
|
||||
// but some downstream methods might parse @params directly. Let's update @params too for safety.
|
||||
@params["target"] = name;
|
||||
}
|
||||
// -------------------------------------------------------------------------------
|
||||
|
||||
string searchMethod = @params["searchMethod"]?.ToString().ToLower();
|
||||
string tag = @params["tag"]?.ToString();
|
||||
string layer = @params["layer"]?.ToString();
|
||||
JToken parentToken = @params["parent"];
|
||||
|
||||
// Coerce string JSON to JObject for 'componentProperties' if provided as a JSON string
|
||||
var componentPropsToken = @params["componentProperties"];
|
||||
if (componentPropsToken != null && componentPropsToken.Type == JTokenType.String)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parsed = JObject.Parse(componentPropsToken.ToString());
|
||||
@params["componentProperties"] = parsed;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Warn($"[ManageGameObject] Could not parse 'componentProperties' JSON string: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Prefab Asset Check ---
|
||||
// Prefab assets require different tools. Only 'create' (instantiation) is valid here.
|
||||
string targetPath =
|
||||
targetToken?.Type == JTokenType.String ? targetToken.ToString() : null;
|
||||
if (
|
||||
!string.IsNullOrEmpty(targetPath)
|
||||
&& targetPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)
|
||||
&& action != "create" // Allow prefab instantiation
|
||||
)
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Target '{targetPath}' is a prefab asset. " +
|
||||
$"Use 'manage_asset' with action='modify' for prefab asset modifications, " +
|
||||
$"or 'manage_prefabs' with action='open_stage' to edit the prefab in isolation mode."
|
||||
);
|
||||
}
|
||||
// --- End Prefab Asset Check ---
|
||||
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
// --- Primary lifecycle actions (kept in manage_gameobject) ---
|
||||
case "create":
|
||||
return GameObjectCreate.Handle(@params);
|
||||
case "modify":
|
||||
return GameObjectModify.Handle(@params, targetToken, searchMethod);
|
||||
case "delete":
|
||||
return GameObjectDelete.Handle(targetToken, searchMethod);
|
||||
case "duplicate":
|
||||
return GameObjectDuplicate.Handle(@params, targetToken, searchMethod);
|
||||
case "move_relative":
|
||||
return GameObjectMoveRelative.Handle(@params, targetToken, searchMethod);
|
||||
|
||||
default:
|
||||
return new ErrorResponse($"Unknown action: '{action}'.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Error($"[ManageGameObject] Action '{action}' failed: {e}");
|
||||
return new ErrorResponse($"Internal error processing action '{action}': {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7641d7388f0f6634b9d83d34de87b2ee
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,238 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.GameObjects
|
||||
{
|
||||
internal static class ManageGameObjectCommon
|
||||
{
|
||||
internal static GameObject FindObjectInternal(JToken targetToken, string searchMethod, JObject findParams = null)
|
||||
{
|
||||
bool findAll = findParams?["findAll"]?.ToObject<bool>() ?? false;
|
||||
|
||||
if (
|
||||
targetToken?.Type == JTokenType.Integer
|
||||
|| (searchMethod == "by_id" && int.TryParse(targetToken?.ToString(), out _))
|
||||
)
|
||||
{
|
||||
findAll = false;
|
||||
}
|
||||
|
||||
List<GameObject> results = FindObjectsInternal(targetToken, searchMethod, findAll, findParams);
|
||||
return results.Count > 0 ? results[0] : null;
|
||||
}
|
||||
|
||||
internal static List<GameObject> FindObjectsInternal(
|
||||
JToken targetToken,
|
||||
string searchMethod,
|
||||
bool findAll,
|
||||
JObject findParams = null
|
||||
)
|
||||
{
|
||||
List<GameObject> results = new List<GameObject>();
|
||||
string searchTerm = findParams?["searchTerm"]?.ToString() ?? targetToken?.ToString();
|
||||
bool searchInChildren = findParams?["searchInChildren"]?.ToObject<bool>() ?? false;
|
||||
bool searchInactive = findParams?["searchInactive"]?.ToObject<bool>() ?? false;
|
||||
|
||||
if (string.IsNullOrEmpty(searchMethod))
|
||||
{
|
||||
if (targetToken?.Type == JTokenType.Integer)
|
||||
searchMethod = "by_id";
|
||||
else if (!string.IsNullOrEmpty(searchTerm) && searchTerm.Contains('/'))
|
||||
searchMethod = "by_path";
|
||||
else
|
||||
searchMethod = "by_name";
|
||||
}
|
||||
|
||||
GameObject rootSearchObject = null;
|
||||
if (searchInChildren && targetToken != null)
|
||||
{
|
||||
rootSearchObject = FindObjectInternal(targetToken, "by_id_or_name_or_path");
|
||||
if (rootSearchObject == null)
|
||||
{
|
||||
McpLog.Warn($"[ManageGameObject.Find] Root object '{targetToken}' for child search not found.");
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
switch (searchMethod)
|
||||
{
|
||||
case "by_id":
|
||||
if (int.TryParse(searchTerm, out int instanceId))
|
||||
{
|
||||
var allObjects = GetAllSceneObjects(searchInactive);
|
||||
GameObject obj = allObjects.FirstOrDefault(go => go.GetInstanceID() == instanceId);
|
||||
if (obj != null)
|
||||
results.Add(obj);
|
||||
}
|
||||
break;
|
||||
|
||||
case "by_name":
|
||||
var searchPoolName = rootSearchObject
|
||||
? rootSearchObject
|
||||
.GetComponentsInChildren<Transform>(searchInactive)
|
||||
.Select(t => t.gameObject)
|
||||
: GetAllSceneObjects(searchInactive);
|
||||
results.AddRange(searchPoolName.Where(go => go.name == searchTerm));
|
||||
break;
|
||||
|
||||
case "by_path":
|
||||
if (rootSearchObject != null)
|
||||
{
|
||||
Transform foundTransform = rootSearchObject.transform.Find(searchTerm);
|
||||
if (foundTransform != null)
|
||||
results.Add(foundTransform.gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||
if (prefabStage != null || searchInactive)
|
||||
{
|
||||
// In Prefab Stage, GameObject.Find() doesn't work, need to search manually
|
||||
var allObjects = GetAllSceneObjects(searchInactive);
|
||||
foreach (var go in allObjects)
|
||||
{
|
||||
if (GameObjectLookup.MatchesPath(go, searchTerm))
|
||||
{
|
||||
results.Add(go);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var found = GameObject.Find(searchTerm);
|
||||
if (found != null)
|
||||
results.Add(found);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "by_tag":
|
||||
var searchPoolTag = rootSearchObject
|
||||
? rootSearchObject
|
||||
.GetComponentsInChildren<Transform>(searchInactive)
|
||||
.Select(t => t.gameObject)
|
||||
: GetAllSceneObjects(searchInactive);
|
||||
results.AddRange(searchPoolTag.Where(go => go.CompareTag(searchTerm)));
|
||||
break;
|
||||
|
||||
case "by_layer":
|
||||
var searchPoolLayer = rootSearchObject
|
||||
? rootSearchObject
|
||||
.GetComponentsInChildren<Transform>(searchInactive)
|
||||
.Select(t => t.gameObject)
|
||||
: GetAllSceneObjects(searchInactive);
|
||||
if (int.TryParse(searchTerm, out int layerIndex))
|
||||
{
|
||||
results.AddRange(searchPoolLayer.Where(go => go.layer == layerIndex));
|
||||
}
|
||||
else
|
||||
{
|
||||
int namedLayer = LayerMask.NameToLayer(searchTerm);
|
||||
if (namedLayer != -1)
|
||||
results.AddRange(searchPoolLayer.Where(go => go.layer == namedLayer));
|
||||
}
|
||||
break;
|
||||
|
||||
case "by_component":
|
||||
Type componentType = FindType(searchTerm);
|
||||
if (componentType != null)
|
||||
{
|
||||
IEnumerable<GameObject> searchPoolComp;
|
||||
if (rootSearchObject)
|
||||
{
|
||||
searchPoolComp = rootSearchObject
|
||||
.GetComponentsInChildren(componentType, searchInactive)
|
||||
.Select(c => (c as Component).gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if UNITY_2023_1_OR_NEWER
|
||||
var inactive = searchInactive ? FindObjectsInactive.Include : FindObjectsInactive.Exclude;
|
||||
searchPoolComp = UnityEngine.Object.FindObjectsByType(componentType, inactive, FindObjectsSortMode.None)
|
||||
.Cast<Component>()
|
||||
.Select(c => c.gameObject);
|
||||
#else
|
||||
searchPoolComp = UnityEngine.Object.FindObjectsOfType(componentType, searchInactive)
|
||||
.Cast<Component>()
|
||||
.Select(c => c.gameObject);
|
||||
#endif
|
||||
}
|
||||
results.AddRange(searchPoolComp.Where(go => go != null));
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Warn($"[ManageGameObject.Find] Component type not found: {searchTerm}");
|
||||
}
|
||||
break;
|
||||
|
||||
case "by_id_or_name_or_path":
|
||||
if (int.TryParse(searchTerm, out int id))
|
||||
{
|
||||
var allObjectsId = GetAllSceneObjects(true);
|
||||
GameObject objById = allObjectsId.FirstOrDefault(go => go.GetInstanceID() == id);
|
||||
if (objById != null)
|
||||
{
|
||||
results.Add(objById);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Try path search - in Prefab Stage, GameObject.Find() doesn't work
|
||||
var allObjectsForPath = GetAllSceneObjects(true);
|
||||
GameObject objByPath = allObjectsForPath.FirstOrDefault(go =>
|
||||
{
|
||||
return GameObjectLookup.MatchesPath(go, searchTerm);
|
||||
});
|
||||
if (objByPath != null)
|
||||
{
|
||||
results.Add(objByPath);
|
||||
break;
|
||||
}
|
||||
|
||||
var allObjectsName = GetAllSceneObjects(true);
|
||||
results.AddRange(allObjectsName.Where(go => go.name == searchTerm));
|
||||
break;
|
||||
|
||||
default:
|
||||
McpLog.Warn($"[ManageGameObject.Find] Unknown search method: {searchMethod}");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!findAll && results.Count > 1)
|
||||
{
|
||||
return new List<GameObject> { results[0] };
|
||||
}
|
||||
|
||||
return results.Distinct().ToList();
|
||||
}
|
||||
|
||||
private static IEnumerable<GameObject> GetAllSceneObjects(bool includeInactive)
|
||||
{
|
||||
// Delegate to GameObjectLookup to avoid code duplication and ensure consistent behavior
|
||||
return GameObjectLookup.GetAllSceneObjects(includeInactive);
|
||||
}
|
||||
|
||||
private static Type FindType(string typeName)
|
||||
{
|
||||
if (ComponentResolver.TryResolve(typeName, out Type resolvedType, out string error))
|
||||
{
|
||||
return resolvedType;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
McpLog.Warn($"[FindType] {error}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6bf0edf3cd2af46729294682cee3bee4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
35
Packages/MCPForUnity/Editor/Tools/GetTestJob.cs
Normal file
35
Packages/MCPForUnity/Editor/Tools/GetTestJob.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Services;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Poll a previously started async test job by job_id.
|
||||
/// </summary>
|
||||
[McpForUnityTool("get_test_job", AutoRegister = false)]
|
||||
public static class GetTestJob
|
||||
{
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
string jobId = @params?["job_id"]?.ToString() ?? @params?["jobId"]?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(jobId))
|
||||
{
|
||||
return new ErrorResponse("Missing required parameter 'job_id'.");
|
||||
}
|
||||
|
||||
bool includeDetails = ParamCoercion.CoerceBool(@params?["includeDetails"], false);
|
||||
bool includeFailedTests = ParamCoercion.CoerceBool(@params?["includeFailedTests"], false);
|
||||
|
||||
var job = TestJobManager.GetJob(jobId);
|
||||
if (job == null)
|
||||
{
|
||||
return new ErrorResponse("Unknown job_id.");
|
||||
}
|
||||
|
||||
var payload = TestJobManager.ToSerializable(job, includeDetails, includeFailedTests);
|
||||
return new SuccessResponse("Test job status retrieved.", payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Packages/MCPForUnity/Editor/Tools/GetTestJob.cs.meta
Normal file
13
Packages/MCPForUnity/Editor/Tools/GetTestJob.cs.meta
Normal file
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f92c2b67a2c4b5c9d1a3c0e6f9b2d10
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
|
||||
31
Packages/MCPForUnity/Editor/Tools/JsonUtil.cs
Normal file
31
Packages/MCPForUnity/Editor/Tools/JsonUtil.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
internal static class JsonUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// If @params[paramName] is a JSON string, parse it to a JObject in-place.
|
||||
/// Logs a warning on parse failure and leaves the original value.
|
||||
/// </summary>
|
||||
internal static void CoerceJsonStringParameter(JObject @params, string paramName)
|
||||
{
|
||||
if (@params == null || string.IsNullOrEmpty(paramName)) return;
|
||||
var token = @params[paramName];
|
||||
if (token != null && token.Type == JTokenType.String)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parsed = JObject.Parse(token.ToString());
|
||||
@params[paramName] = parsed;
|
||||
}
|
||||
catch (Newtonsoft.Json.JsonReaderException e)
|
||||
{
|
||||
McpLog.Warn($"[MCP] Could not parse '{paramName}' JSON string: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/JsonUtil.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/JsonUtil.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d4b3b6009d53e4b8f97fe7ab57888c65
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1120
Packages/MCPForUnity/Editor/Tools/ManageAsset.cs
Normal file
1120
Packages/MCPForUnity/Editor/Tools/ManageAsset.cs
Normal file
File diff suppressed because it is too large
Load Diff
11
Packages/MCPForUnity/Editor/Tools/ManageAsset.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/ManageAsset.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de90a1d9743a2874cb235cf0b83444b1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
351
Packages/MCPForUnity/Editor/Tools/ManageComponents.cs
Normal file
351
Packages/MCPForUnity/Editor/Tools/ManageComponents.cs
Normal file
@@ -0,0 +1,351 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Tool for managing components on GameObjects.
|
||||
/// Actions: add, remove, set_property
|
||||
///
|
||||
/// This is a focused tool for component lifecycle operations.
|
||||
/// For reading component data, use the unity://scene/gameobject/{id}/components resource.
|
||||
/// </summary>
|
||||
[McpForUnityTool("manage_components")]
|
||||
public static class ManageComponents
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the manage_components command.
|
||||
/// </summary>
|
||||
/// <param name="params">Command parameters</param>
|
||||
/// <returns>Result of the component operation</returns>
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
if (@params == null)
|
||||
{
|
||||
return new ErrorResponse("Parameters cannot be null.");
|
||||
}
|
||||
|
||||
string action = ParamCoercion.CoerceString(@params["action"], null)?.ToLowerInvariant();
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return new ErrorResponse("'action' parameter is required (add, remove, set_property).");
|
||||
}
|
||||
|
||||
// Target resolution
|
||||
JToken targetToken = @params["target"];
|
||||
string searchMethod = ParamCoercion.CoerceString(@params["searchMethod"] ?? @params["search_method"], null);
|
||||
|
||||
if (targetToken == null)
|
||||
{
|
||||
return new ErrorResponse("'target' parameter is required.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return action switch
|
||||
{
|
||||
"add" => AddComponent(@params, targetToken, searchMethod),
|
||||
"remove" => RemoveComponent(@params, targetToken, searchMethod),
|
||||
"set_property" => SetProperty(@params, targetToken, searchMethod),
|
||||
_ => new ErrorResponse($"Unknown action: '{action}'. Supported actions: add, remove, set_property")
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Error($"[ManageComponents] Action '{action}' failed: {e}");
|
||||
return new ErrorResponse($"Internal error processing action '{action}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
#region Action Implementations
|
||||
|
||||
private static object AddComponent(JObject @params, JToken targetToken, string searchMethod)
|
||||
{
|
||||
GameObject targetGo = FindTarget(targetToken, searchMethod);
|
||||
if (targetGo == null)
|
||||
{
|
||||
return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
|
||||
}
|
||||
|
||||
string componentTypeName = ParamCoercion.CoerceString(@params["componentType"] ?? @params["component_type"], null);
|
||||
if (string.IsNullOrEmpty(componentTypeName))
|
||||
{
|
||||
return new ErrorResponse("'componentType' parameter is required for 'add' action.");
|
||||
}
|
||||
|
||||
// Resolve component type using unified type resolver
|
||||
Type type = UnityTypeResolver.ResolveComponent(componentTypeName);
|
||||
if (type == null)
|
||||
{
|
||||
return new ErrorResponse($"Component type '{componentTypeName}' not found. Use a fully-qualified name if needed.");
|
||||
}
|
||||
|
||||
// Use ComponentOps for the actual operation
|
||||
Component newComponent = ComponentOps.AddComponent(targetGo, type, out string error);
|
||||
if (newComponent == null)
|
||||
{
|
||||
return new ErrorResponse(error ?? $"Failed to add component '{componentTypeName}'.");
|
||||
}
|
||||
|
||||
// Set properties if provided
|
||||
JObject properties = @params["properties"] as JObject ?? @params["componentProperties"] as JObject;
|
||||
if (properties != null && properties.HasValues)
|
||||
{
|
||||
// Record for undo before modifying properties
|
||||
Undo.RecordObject(newComponent, "Modify Component Properties");
|
||||
SetPropertiesOnComponent(newComponent, properties);
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(targetGo);
|
||||
MarkOwningSceneDirty(targetGo);
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Component '{componentTypeName}' added to '{targetGo.name}'.",
|
||||
data = new
|
||||
{
|
||||
instanceID = targetGo.GetInstanceID(),
|
||||
componentType = type.FullName,
|
||||
componentInstanceID = newComponent.GetInstanceID()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static object RemoveComponent(JObject @params, JToken targetToken, string searchMethod)
|
||||
{
|
||||
GameObject targetGo = FindTarget(targetToken, searchMethod);
|
||||
if (targetGo == null)
|
||||
{
|
||||
return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
|
||||
}
|
||||
|
||||
string componentTypeName = ParamCoercion.CoerceString(@params["componentType"] ?? @params["component_type"], null);
|
||||
if (string.IsNullOrEmpty(componentTypeName))
|
||||
{
|
||||
return new ErrorResponse("'componentType' parameter is required for 'remove' action.");
|
||||
}
|
||||
|
||||
// Resolve component type using unified type resolver
|
||||
Type type = UnityTypeResolver.ResolveComponent(componentTypeName);
|
||||
if (type == null)
|
||||
{
|
||||
return new ErrorResponse($"Component type '{componentTypeName}' not found.");
|
||||
}
|
||||
|
||||
// Use ComponentOps for the actual operation
|
||||
bool removed = ComponentOps.RemoveComponent(targetGo, type, out string error);
|
||||
if (!removed)
|
||||
{
|
||||
return new ErrorResponse(error ?? $"Failed to remove component '{componentTypeName}'.");
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(targetGo);
|
||||
MarkOwningSceneDirty(targetGo);
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Component '{componentTypeName}' removed from '{targetGo.name}'.",
|
||||
data = new
|
||||
{
|
||||
instanceID = targetGo.GetInstanceID()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static object SetProperty(JObject @params, JToken targetToken, string searchMethod)
|
||||
{
|
||||
GameObject targetGo = FindTarget(targetToken, searchMethod);
|
||||
if (targetGo == null)
|
||||
{
|
||||
return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
|
||||
}
|
||||
|
||||
string componentType = ParamCoercion.CoerceString(@params["componentType"] ?? @params["component_type"], null);
|
||||
if (string.IsNullOrEmpty(componentType))
|
||||
{
|
||||
return new ErrorResponse("'componentType' parameter is required for 'set_property' action.");
|
||||
}
|
||||
|
||||
// Resolve component type using unified type resolver
|
||||
Type type = UnityTypeResolver.ResolveComponent(componentType);
|
||||
if (type == null)
|
||||
{
|
||||
return new ErrorResponse($"Component type '{componentType}' not found.");
|
||||
}
|
||||
|
||||
Component component = targetGo.GetComponent(type);
|
||||
if (component == null)
|
||||
{
|
||||
return new ErrorResponse($"Component '{componentType}' not found on '{targetGo.name}'.");
|
||||
}
|
||||
|
||||
// Get property and value
|
||||
string propertyName = ParamCoercion.CoerceString(@params["property"], null);
|
||||
JToken valueToken = @params["value"];
|
||||
|
||||
// Support both single property or properties object
|
||||
JObject properties = @params["properties"] as JObject;
|
||||
|
||||
if (string.IsNullOrEmpty(propertyName) && (properties == null || !properties.HasValues))
|
||||
{
|
||||
return new ErrorResponse("Either 'property'+'value' or 'properties' object is required for 'set_property' action.");
|
||||
}
|
||||
|
||||
var errors = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
Undo.RecordObject(component, $"Set property on {componentType}");
|
||||
|
||||
if (!string.IsNullOrEmpty(propertyName) && valueToken != null)
|
||||
{
|
||||
// Single property mode
|
||||
var error = TrySetProperty(component, propertyName, valueToken);
|
||||
if (error != null)
|
||||
{
|
||||
errors.Add(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (properties != null && properties.HasValues)
|
||||
{
|
||||
// Multiple properties mode
|
||||
foreach (var prop in properties.Properties())
|
||||
{
|
||||
var error = TrySetProperty(component, prop.Name, prop.Value);
|
||||
if (error != null)
|
||||
{
|
||||
errors.Add(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(component);
|
||||
MarkOwningSceneDirty(targetGo);
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
return new
|
||||
{
|
||||
success = false,
|
||||
message = $"Some properties failed to set on '{componentType}'.",
|
||||
data = new
|
||||
{
|
||||
instanceID = targetGo.GetInstanceID(),
|
||||
errors = errors
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Properties set on component '{componentType}' on '{targetGo.name}'.",
|
||||
data = new
|
||||
{
|
||||
instanceID = targetGo.GetInstanceID()
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Error setting properties on component '{componentType}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Marks the appropriate scene as dirty for the given GameObject.
|
||||
/// Handles both regular scenes and prefab stages.
|
||||
/// </summary>
|
||||
private static void MarkOwningSceneDirty(GameObject targetGo)
|
||||
{
|
||||
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||
if (prefabStage != null)
|
||||
{
|
||||
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorSceneManager.MarkSceneDirty(targetGo.scene);
|
||||
}
|
||||
}
|
||||
|
||||
private static GameObject FindTarget(JToken targetToken, string searchMethod)
|
||||
{
|
||||
if (targetToken == null)
|
||||
return null;
|
||||
|
||||
// Try instance ID first
|
||||
if (targetToken.Type == JTokenType.Integer)
|
||||
{
|
||||
int instanceId = targetToken.Value<int>();
|
||||
return GameObjectLookup.FindById(instanceId);
|
||||
}
|
||||
|
||||
string targetStr = targetToken.ToString();
|
||||
|
||||
// Try parsing as instance ID
|
||||
if (int.TryParse(targetStr, out int parsedId))
|
||||
{
|
||||
var byId = GameObjectLookup.FindById(parsedId);
|
||||
if (byId != null)
|
||||
return byId;
|
||||
}
|
||||
|
||||
// Use GameObjectLookup for search
|
||||
return GameObjectLookup.FindByTarget(targetToken, searchMethod ?? "by_name", true);
|
||||
}
|
||||
|
||||
private static void SetPropertiesOnComponent(Component component, JObject properties)
|
||||
{
|
||||
if (component == null || properties == null)
|
||||
return;
|
||||
|
||||
var errors = new List<string>();
|
||||
foreach (var prop in properties.Properties())
|
||||
{
|
||||
var error = TrySetProperty(component, prop.Name, prop.Value);
|
||||
if (error != null)
|
||||
errors.Add(error);
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
McpLog.Warn($"[ManageComponents] Some properties failed to set on {component.GetType().Name}: {string.Join(", ", errors)}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to set a property or field on a component.
|
||||
/// Delegates to ComponentOps.SetProperty for unified implementation.
|
||||
/// </summary>
|
||||
private static string TrySetProperty(Component component, string propertyName, JToken value)
|
||||
{
|
||||
if (component == null || string.IsNullOrEmpty(propertyName))
|
||||
return "Invalid component or property name";
|
||||
|
||||
if (ComponentOps.SetProperty(component, propertyName, value, out string error))
|
||||
{
|
||||
return null; // Success
|
||||
}
|
||||
|
||||
McpLog.Warn($"[ManageComponents] {error}");
|
||||
return error;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/ManageComponents.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/ManageComponents.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6f476359563842c79eda2c180566c98
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
393
Packages/MCPForUnity/Editor/Tools/ManageEditor.cs
Normal file
393
Packages/MCPForUnity/Editor/Tools/ManageEditor.cs
Normal file
@@ -0,0 +1,393 @@
|
||||
using System;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal; // Required for tag management
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles editor control actions including play mode control, tool selection,
|
||||
/// and tag/layer management. For reading editor state, use MCP resources instead.
|
||||
/// </summary>
|
||||
[McpForUnityTool("manage_editor", AutoRegister = false)]
|
||||
public static class ManageEditor
|
||||
{
|
||||
// Constant for starting user layer index
|
||||
private const int FirstUserLayerIndex = 8;
|
||||
|
||||
// Constant for total layer count
|
||||
private const int TotalLayerCount = 32;
|
||||
|
||||
/// <summary>
|
||||
/// Main handler for editor management actions.
|
||||
/// </summary>
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
// Step 1: Null parameter guard (consistent across all tools)
|
||||
if (@params == null)
|
||||
{
|
||||
return new ErrorResponse("Parameters cannot be null.");
|
||||
}
|
||||
|
||||
// Step 2: Wrap parameters
|
||||
var p = new ToolParams(@params);
|
||||
|
||||
// Step 3: Extract and validate required parameters
|
||||
var actionResult = p.GetRequired("action");
|
||||
if (!actionResult.IsSuccess)
|
||||
{
|
||||
return new ErrorResponse(actionResult.ErrorMessage);
|
||||
}
|
||||
string action = actionResult.Value.ToLowerInvariant();
|
||||
|
||||
// Parameters for specific actions
|
||||
string tagName = p.Get("tagName");
|
||||
string layerName = p.Get("layerName");
|
||||
bool waitForCompletion = p.GetBool("waitForCompletion", false);
|
||||
|
||||
// Route action
|
||||
switch (action)
|
||||
{
|
||||
// Play Mode Control
|
||||
case "play":
|
||||
try
|
||||
{
|
||||
if (!EditorApplication.isPlaying)
|
||||
{
|
||||
EditorApplication.isPlaying = true;
|
||||
return new SuccessResponse("Entered play mode.");
|
||||
}
|
||||
return new SuccessResponse("Already in play mode.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Error entering play mode: {e.Message}");
|
||||
}
|
||||
case "pause":
|
||||
try
|
||||
{
|
||||
if (EditorApplication.isPlaying)
|
||||
{
|
||||
EditorApplication.isPaused = !EditorApplication.isPaused;
|
||||
return new SuccessResponse(
|
||||
EditorApplication.isPaused ? "Game paused." : "Game resumed."
|
||||
);
|
||||
}
|
||||
return new ErrorResponse("Cannot pause/resume: Not in play mode.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Error pausing/resuming game: {e.Message}");
|
||||
}
|
||||
case "stop":
|
||||
try
|
||||
{
|
||||
if (EditorApplication.isPlaying)
|
||||
{
|
||||
EditorApplication.isPlaying = false;
|
||||
return new SuccessResponse("Exited play mode.");
|
||||
}
|
||||
return new SuccessResponse("Already stopped (not in play mode).");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Error stopping play mode: {e.Message}");
|
||||
}
|
||||
|
||||
// Tool Control
|
||||
case "set_active_tool":
|
||||
var toolNameResult = p.GetRequired("toolName", "'toolName' parameter required for set_active_tool.");
|
||||
if (!toolNameResult.IsSuccess)
|
||||
return new ErrorResponse(toolNameResult.ErrorMessage);
|
||||
return SetActiveTool(toolNameResult.Value);
|
||||
|
||||
// Tag Management
|
||||
case "add_tag":
|
||||
var addTagResult = p.GetRequired("tagName", "'tagName' parameter required for add_tag.");
|
||||
if (!addTagResult.IsSuccess)
|
||||
return new ErrorResponse(addTagResult.ErrorMessage);
|
||||
return AddTag(addTagResult.Value);
|
||||
case "remove_tag":
|
||||
var removeTagResult = p.GetRequired("tagName", "'tagName' parameter required for remove_tag.");
|
||||
if (!removeTagResult.IsSuccess)
|
||||
return new ErrorResponse(removeTagResult.ErrorMessage);
|
||||
return RemoveTag(removeTagResult.Value);
|
||||
// Layer Management
|
||||
case "add_layer":
|
||||
var addLayerResult = p.GetRequired("layerName", "'layerName' parameter required for add_layer.");
|
||||
if (!addLayerResult.IsSuccess)
|
||||
return new ErrorResponse(addLayerResult.ErrorMessage);
|
||||
return AddLayer(addLayerResult.Value);
|
||||
case "remove_layer":
|
||||
var removeLayerResult = p.GetRequired("layerName", "'layerName' parameter required for remove_layer.");
|
||||
if (!removeLayerResult.IsSuccess)
|
||||
return new ErrorResponse(removeLayerResult.ErrorMessage);
|
||||
return RemoveLayer(removeLayerResult.Value);
|
||||
// --- Settings (Example) ---
|
||||
// case "set_resolution":
|
||||
// int? width = @params["width"]?.ToObject<int?>();
|
||||
// int? height = @params["height"]?.ToObject<int?>();
|
||||
// if (!width.HasValue || !height.HasValue) return new ErrorResponse("'width' and 'height' parameters required.");
|
||||
// return SetGameViewResolution(width.Value, height.Value);
|
||||
// case "set_quality":
|
||||
// // Handle string name or int index
|
||||
// return SetQualityLevel(@params["qualityLevel"]);
|
||||
|
||||
default:
|
||||
return new ErrorResponse(
|
||||
$"Unknown action: '{action}'. Supported actions: play, pause, stop, set_active_tool, add_tag, remove_tag, add_layer, remove_layer. Use MCP resources for reading editor state, project info, tags, layers, selection, windows, prefab stage, and active tool."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tool Control Methods ---
|
||||
|
||||
private static object SetActiveTool(string toolName)
|
||||
{
|
||||
try
|
||||
{
|
||||
Tool targetTool;
|
||||
if (Enum.TryParse<Tool>(toolName, true, out targetTool)) // Case-insensitive parse
|
||||
{
|
||||
// Check if it's a valid built-in tool
|
||||
if (targetTool != Tool.None && targetTool <= Tool.Custom) // Tool.Custom is the last standard tool
|
||||
{
|
||||
UnityEditor.Tools.current = targetTool;
|
||||
return new SuccessResponse($"Set active tool to '{targetTool}'.");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Cannot directly set tool to '{toolName}'. It might be None, Custom, or invalid."
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Potentially try activating a custom tool by name here if needed
|
||||
// This often requires specific editor scripting knowledge for that tool.
|
||||
return new ErrorResponse(
|
||||
$"Could not parse '{toolName}' as a standard Unity Tool (View, Move, Rotate, Scale, Rect, Transform, Custom)."
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Error setting active tool: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tag Management Methods ---
|
||||
|
||||
private static object AddTag(string tagName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagName))
|
||||
return new ErrorResponse("Tag name cannot be empty or whitespace.");
|
||||
|
||||
// Check if tag already exists
|
||||
if (System.Linq.Enumerable.Contains(InternalEditorUtility.tags, tagName))
|
||||
{
|
||||
return new ErrorResponse($"Tag '{tagName}' already exists.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Add the tag using the internal utility
|
||||
InternalEditorUtility.AddTag(tagName);
|
||||
// Force save assets to ensure the change persists in the TagManager asset
|
||||
AssetDatabase.SaveAssets();
|
||||
return new SuccessResponse($"Tag '{tagName}' added successfully.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Failed to add tag '{tagName}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object RemoveTag(string tagName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagName))
|
||||
return new ErrorResponse("Tag name cannot be empty or whitespace.");
|
||||
if (tagName.Equals("Untagged", StringComparison.OrdinalIgnoreCase))
|
||||
return new ErrorResponse("Cannot remove the built-in 'Untagged' tag.");
|
||||
|
||||
// Check if tag exists before attempting removal
|
||||
if (!System.Linq.Enumerable.Contains(InternalEditorUtility.tags, tagName))
|
||||
{
|
||||
return new ErrorResponse($"Tag '{tagName}' does not exist.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Remove the tag using the internal utility
|
||||
InternalEditorUtility.RemoveTag(tagName);
|
||||
// Force save assets
|
||||
AssetDatabase.SaveAssets();
|
||||
return new SuccessResponse($"Tag '{tagName}' removed successfully.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Catch potential issues if the tag is somehow in use or removal fails
|
||||
return new ErrorResponse($"Failed to remove tag '{tagName}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Layer Management Methods ---
|
||||
|
||||
private static object AddLayer(string layerName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layerName))
|
||||
return new ErrorResponse("Layer name cannot be empty or whitespace.");
|
||||
|
||||
// Access the TagManager asset
|
||||
SerializedObject tagManager = GetTagManager();
|
||||
if (tagManager == null)
|
||||
return new ErrorResponse("Could not access TagManager asset.");
|
||||
|
||||
SerializedProperty layersProp = tagManager.FindProperty("layers");
|
||||
if (layersProp == null || !layersProp.isArray)
|
||||
return new ErrorResponse("Could not find 'layers' property in TagManager.");
|
||||
|
||||
// Check if layer name already exists (case-insensitive check recommended)
|
||||
for (int i = 0; i < TotalLayerCount; i++)
|
||||
{
|
||||
SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i);
|
||||
if (
|
||||
layerSP != null
|
||||
&& layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
{
|
||||
return new ErrorResponse($"Layer '{layerName}' already exists at index {i}.");
|
||||
}
|
||||
}
|
||||
|
||||
// Find the first empty user layer slot (indices 8 to 31)
|
||||
int firstEmptyUserLayer = -1;
|
||||
for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++)
|
||||
{
|
||||
SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i);
|
||||
if (layerSP != null && string.IsNullOrEmpty(layerSP.stringValue))
|
||||
{
|
||||
firstEmptyUserLayer = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstEmptyUserLayer == -1)
|
||||
{
|
||||
return new ErrorResponse("No empty User Layer slots available (8-31 are full).");
|
||||
}
|
||||
|
||||
// Assign the name to the found slot
|
||||
try
|
||||
{
|
||||
SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex(
|
||||
firstEmptyUserLayer
|
||||
);
|
||||
targetLayerSP.stringValue = layerName;
|
||||
// Apply the changes to the TagManager asset
|
||||
tagManager.ApplyModifiedProperties();
|
||||
// Save assets to make sure it's written to disk
|
||||
AssetDatabase.SaveAssets();
|
||||
return new SuccessResponse(
|
||||
$"Layer '{layerName}' added successfully to slot {firstEmptyUserLayer}."
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Failed to add layer '{layerName}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object RemoveLayer(string layerName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layerName))
|
||||
return new ErrorResponse("Layer name cannot be empty or whitespace.");
|
||||
|
||||
// Access the TagManager asset
|
||||
SerializedObject tagManager = GetTagManager();
|
||||
if (tagManager == null)
|
||||
return new ErrorResponse("Could not access TagManager asset.");
|
||||
|
||||
SerializedProperty layersProp = tagManager.FindProperty("layers");
|
||||
if (layersProp == null || !layersProp.isArray)
|
||||
return new ErrorResponse("Could not find 'layers' property in TagManager.");
|
||||
|
||||
// Find the layer by name (must be user layer)
|
||||
int layerIndexToRemove = -1;
|
||||
for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++) // Start from user layers
|
||||
{
|
||||
SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i);
|
||||
// Case-insensitive comparison is safer
|
||||
if (
|
||||
layerSP != null
|
||||
&& layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
{
|
||||
layerIndexToRemove = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (layerIndexToRemove == -1)
|
||||
{
|
||||
return new ErrorResponse($"User layer '{layerName}' not found.");
|
||||
}
|
||||
|
||||
// Clear the name for that index
|
||||
try
|
||||
{
|
||||
SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex(
|
||||
layerIndexToRemove
|
||||
);
|
||||
targetLayerSP.stringValue = string.Empty; // Set to empty string to remove
|
||||
// Apply the changes
|
||||
tagManager.ApplyModifiedProperties();
|
||||
// Save assets
|
||||
AssetDatabase.SaveAssets();
|
||||
return new SuccessResponse(
|
||||
$"Layer '{layerName}' (slot {layerIndexToRemove}) removed successfully."
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Failed to remove layer '{layerName}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helper Methods ---
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SerializedObject for the TagManager asset.
|
||||
/// </summary>
|
||||
private static SerializedObject GetTagManager()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Load the TagManager asset from the ProjectSettings folder
|
||||
UnityEngine.Object[] tagManagerAssets = AssetDatabase.LoadAllAssetsAtPath(
|
||||
"ProjectSettings/TagManager.asset"
|
||||
);
|
||||
if (tagManagerAssets == null || tagManagerAssets.Length == 0)
|
||||
{
|
||||
McpLog.Error("[ManageEditor] TagManager.asset not found in ProjectSettings.");
|
||||
return null;
|
||||
}
|
||||
// The first object in the asset file should be the TagManager
|
||||
return new SerializedObject(tagManagerAssets[0]);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Error($"[ManageEditor] Error accessing TagManager.asset: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Example Implementations for Settings ---
|
||||
/*
|
||||
private static object SetGameViewResolution(int width, int height) { ... }
|
||||
private static object SetQualityLevel(JToken qualityLevelToken) { ... }
|
||||
*/
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/ManageEditor.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/ManageEditor.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43ac60aa36b361b4dbe4a038ae9f35c8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
596
Packages/MCPForUnity/Editor/Tools/ManageMaterial.cs
Normal file
596
Packages/MCPForUnity/Editor/Tools/ManageMaterial.cs
Normal file
@@ -0,0 +1,596 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
[McpForUnityTool("manage_material", AutoRegister = false)]
|
||||
public static class ManageMaterial
|
||||
{
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
string action = @params["action"]?.ToString()?.ToLowerInvariant();
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return new ErrorResponse("Action is required");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "ping":
|
||||
return new SuccessResponse("pong", new { tool = "manage_material" });
|
||||
|
||||
case "create":
|
||||
return CreateMaterial(@params);
|
||||
|
||||
case "set_material_shader_property":
|
||||
return SetMaterialShaderProperty(@params);
|
||||
|
||||
case "set_material_color":
|
||||
return SetMaterialColor(@params);
|
||||
|
||||
case "assign_material_to_renderer":
|
||||
return AssignMaterialToRenderer(@params);
|
||||
|
||||
case "set_renderer_color":
|
||||
return SetRendererColor(@params);
|
||||
|
||||
case "get_material_info":
|
||||
return GetMaterialInfo(@params);
|
||||
|
||||
default:
|
||||
return new ErrorResponse($"Unknown action: {action}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ErrorResponse(ex.Message, new { stackTrace = ex.StackTrace });
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizePath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return path;
|
||||
|
||||
// Normalize separators and ensure Assets/ root
|
||||
path = AssetPathUtility.SanitizeAssetPath(path);
|
||||
|
||||
// Ensure .mat extension
|
||||
if (!path.EndsWith(".mat", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
path += ".mat";
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private static object SetMaterialShaderProperty(JObject @params)
|
||||
{
|
||||
string materialPath = NormalizePath(@params["materialPath"]?.ToString());
|
||||
string property = @params["property"]?.ToString();
|
||||
JToken value = @params["value"];
|
||||
|
||||
if (string.IsNullOrEmpty(materialPath) || string.IsNullOrEmpty(property) || value == null)
|
||||
{
|
||||
return new ErrorResponse("materialPath, property, and value are required");
|
||||
}
|
||||
|
||||
// Find material
|
||||
var findInstruction = new JObject { ["find"] = materialPath };
|
||||
Material mat = ObjectResolver.Resolve(findInstruction, typeof(Material)) as Material;
|
||||
|
||||
if (mat == null)
|
||||
{
|
||||
return new ErrorResponse($"Could not find material at path: {materialPath}");
|
||||
}
|
||||
|
||||
Undo.RecordObject(mat, "Set Material Property");
|
||||
|
||||
// Normalize alias/casing once for all code paths
|
||||
property = MaterialOps.ResolvePropertyName(mat, property);
|
||||
|
||||
// 1. Try handling Texture instruction explicitly (ManageMaterial special feature)
|
||||
if (value.Type == JTokenType.Object)
|
||||
{
|
||||
// Check if it looks like an instruction
|
||||
if (value is JObject obj && (obj.ContainsKey("find") || obj.ContainsKey("method")))
|
||||
{
|
||||
Texture tex = ObjectResolver.Resolve(obj, typeof(Texture)) as Texture;
|
||||
if (tex != null && mat.HasProperty(property))
|
||||
{
|
||||
mat.SetTexture(property, tex);
|
||||
EditorUtility.SetDirty(mat);
|
||||
return new SuccessResponse($"Set texture property {property} on {mat.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback to standard logic via MaterialOps (handles Colors, Floats, Strings->Path)
|
||||
bool success = MaterialOps.TrySetShaderProperty(mat, property, value, UnityJsonSerializer.Instance);
|
||||
|
||||
if (success)
|
||||
{
|
||||
EditorUtility.SetDirty(mat);
|
||||
return new SuccessResponse($"Set property {property} on {mat.name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse($"Failed to set property {property}. Value format might be unsupported or texture not found.");
|
||||
}
|
||||
}
|
||||
|
||||
private static object SetMaterialColor(JObject @params)
|
||||
{
|
||||
string materialPath = NormalizePath(@params["materialPath"]?.ToString());
|
||||
JToken colorToken = @params["color"];
|
||||
string property = @params["property"]?.ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(materialPath) || colorToken == null)
|
||||
{
|
||||
return new ErrorResponse("materialPath and color are required");
|
||||
}
|
||||
|
||||
var findInstruction = new JObject { ["find"] = materialPath };
|
||||
Material mat = ObjectResolver.Resolve(findInstruction, typeof(Material)) as Material;
|
||||
|
||||
if (mat == null)
|
||||
{
|
||||
return new ErrorResponse($"Could not find material at path: {materialPath}");
|
||||
}
|
||||
|
||||
Color color;
|
||||
try
|
||||
{
|
||||
color = MaterialOps.ParseColor(colorToken, UnityJsonSerializer.Instance);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Invalid color format: {e.Message}");
|
||||
}
|
||||
|
||||
Undo.RecordObject(mat, "Set Material Color");
|
||||
|
||||
bool foundProp = false;
|
||||
if (!string.IsNullOrEmpty(property))
|
||||
{
|
||||
if (mat.HasProperty(property))
|
||||
{
|
||||
mat.SetColor(property, color);
|
||||
foundProp = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback logic: _BaseColor (URP/HDRP) then _Color (Built-in)
|
||||
if (mat.HasProperty("_BaseColor"))
|
||||
{
|
||||
mat.SetColor("_BaseColor", color);
|
||||
foundProp = true;
|
||||
property = "_BaseColor";
|
||||
}
|
||||
else if (mat.HasProperty("_Color"))
|
||||
{
|
||||
mat.SetColor("_Color", color);
|
||||
foundProp = true;
|
||||
property = "_Color";
|
||||
}
|
||||
}
|
||||
|
||||
if (foundProp)
|
||||
{
|
||||
EditorUtility.SetDirty(mat);
|
||||
return new SuccessResponse($"Set color on {property}");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse("Could not find suitable color property (_BaseColor or _Color) or specified property does not exist.");
|
||||
}
|
||||
}
|
||||
|
||||
private static object AssignMaterialToRenderer(JObject @params)
|
||||
{
|
||||
string target = @params["target"]?.ToString();
|
||||
string searchMethod = @params["searchMethod"]?.ToString();
|
||||
string materialPath = NormalizePath(@params["materialPath"]?.ToString());
|
||||
int slot = @params["slot"]?.ToObject<int>() ?? 0;
|
||||
|
||||
if (string.IsNullOrEmpty(target) || string.IsNullOrEmpty(materialPath))
|
||||
{
|
||||
return new ErrorResponse("target and materialPath are required");
|
||||
}
|
||||
|
||||
var goInstruction = new JObject { ["find"] = target };
|
||||
if (!string.IsNullOrEmpty(searchMethod)) goInstruction["method"] = searchMethod;
|
||||
|
||||
GameObject go = ObjectResolver.Resolve(goInstruction, typeof(GameObject)) as GameObject;
|
||||
if (go == null)
|
||||
{
|
||||
return new ErrorResponse($"Could not find target GameObject: {target}");
|
||||
}
|
||||
|
||||
Renderer renderer = go.GetComponent<Renderer>();
|
||||
if (renderer == null)
|
||||
{
|
||||
return new ErrorResponse($"GameObject {go.name} has no Renderer component");
|
||||
}
|
||||
|
||||
var matInstruction = new JObject { ["find"] = materialPath };
|
||||
Material mat = ObjectResolver.Resolve(matInstruction, typeof(Material)) as Material;
|
||||
if (mat == null)
|
||||
{
|
||||
return new ErrorResponse($"Could not find material: {materialPath}");
|
||||
}
|
||||
|
||||
Undo.RecordObject(renderer, "Assign Material");
|
||||
|
||||
Material[] sharedMats = renderer.sharedMaterials;
|
||||
if (slot < 0 || slot >= sharedMats.Length)
|
||||
{
|
||||
return new ErrorResponse($"Slot {slot} out of bounds (count: {sharedMats.Length})");
|
||||
}
|
||||
|
||||
sharedMats[slot] = mat;
|
||||
renderer.sharedMaterials = sharedMats;
|
||||
|
||||
EditorUtility.SetDirty(renderer);
|
||||
return new SuccessResponse($"Assigned material {mat.name} to {go.name} slot {slot}");
|
||||
}
|
||||
|
||||
private static object SetRendererColor(JObject @params)
|
||||
{
|
||||
string target = @params["target"]?.ToString();
|
||||
string searchMethod = @params["searchMethod"]?.ToString();
|
||||
JToken colorToken = @params["color"];
|
||||
int slot = @params["slot"]?.ToObject<int>() ?? 0;
|
||||
string mode = @params["mode"]?.ToString() ?? "property_block";
|
||||
|
||||
if (string.IsNullOrEmpty(target) || colorToken == null)
|
||||
{
|
||||
return new ErrorResponse("target and color are required");
|
||||
}
|
||||
|
||||
Color color;
|
||||
try
|
||||
{
|
||||
color = MaterialOps.ParseColor(colorToken, UnityJsonSerializer.Instance);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Invalid color format: {e.Message}");
|
||||
}
|
||||
|
||||
var goInstruction = new JObject { ["find"] = target };
|
||||
if (!string.IsNullOrEmpty(searchMethod)) goInstruction["method"] = searchMethod;
|
||||
|
||||
GameObject go = ObjectResolver.Resolve(goInstruction, typeof(GameObject)) as GameObject;
|
||||
if (go == null)
|
||||
{
|
||||
return new ErrorResponse($"Could not find target GameObject: {target}");
|
||||
}
|
||||
|
||||
Renderer renderer = go.GetComponent<Renderer>();
|
||||
if (renderer == null)
|
||||
{
|
||||
return new ErrorResponse($"GameObject {go.name} has no Renderer component");
|
||||
}
|
||||
|
||||
if (mode == "property_block")
|
||||
{
|
||||
if (slot < 0 || slot >= renderer.sharedMaterials.Length)
|
||||
{
|
||||
return new ErrorResponse($"Slot {slot} out of bounds (count: {renderer.sharedMaterials.Length})");
|
||||
}
|
||||
|
||||
MaterialPropertyBlock block = new MaterialPropertyBlock();
|
||||
renderer.GetPropertyBlock(block, slot);
|
||||
|
||||
if (renderer.sharedMaterials[slot] != null)
|
||||
{
|
||||
Material mat = renderer.sharedMaterials[slot];
|
||||
if (mat.HasProperty("_BaseColor")) block.SetColor("_BaseColor", color);
|
||||
else if (mat.HasProperty("_Color")) block.SetColor("_Color", color);
|
||||
else block.SetColor("_Color", color);
|
||||
}
|
||||
else
|
||||
{
|
||||
block.SetColor("_Color", color);
|
||||
}
|
||||
|
||||
renderer.SetPropertyBlock(block, slot);
|
||||
EditorUtility.SetDirty(renderer);
|
||||
return new SuccessResponse($"Set renderer color (PropertyBlock) on slot {slot}");
|
||||
}
|
||||
else if (mode == "shared")
|
||||
{
|
||||
if (slot >= 0 && slot < renderer.sharedMaterials.Length)
|
||||
{
|
||||
Material mat = renderer.sharedMaterials[slot];
|
||||
if (mat == null)
|
||||
{
|
||||
return new ErrorResponse($"No material in slot {slot}");
|
||||
}
|
||||
Undo.RecordObject(mat, "Set Material Color");
|
||||
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
|
||||
else mat.SetColor("_Color", color);
|
||||
EditorUtility.SetDirty(mat);
|
||||
return new SuccessResponse("Set shared material color");
|
||||
}
|
||||
return new ErrorResponse("Invalid slot");
|
||||
}
|
||||
else if (mode == "instance")
|
||||
{
|
||||
if (slot >= 0 && slot < renderer.materials.Length)
|
||||
{
|
||||
Material mat = renderer.materials[slot];
|
||||
if (mat == null)
|
||||
{
|
||||
return new ErrorResponse($"No material in slot {slot}");
|
||||
}
|
||||
// Note: Undo cannot fully revert material instantiation
|
||||
Undo.RecordObject(mat, "Set Instance Material Color");
|
||||
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
|
||||
else mat.SetColor("_Color", color);
|
||||
return new SuccessResponse("Set instance material color", new { warning = "Material instance created; Undo cannot fully revert instantiation." });
|
||||
}
|
||||
return new ErrorResponse("Invalid slot");
|
||||
}
|
||||
|
||||
return new ErrorResponse($"Unknown mode: {mode}");
|
||||
}
|
||||
|
||||
private static object GetMaterialInfo(JObject @params)
|
||||
{
|
||||
string materialPath = NormalizePath(@params["materialPath"]?.ToString());
|
||||
if (string.IsNullOrEmpty(materialPath))
|
||||
{
|
||||
return new ErrorResponse("materialPath is required");
|
||||
}
|
||||
|
||||
var findInstruction = new JObject { ["find"] = materialPath };
|
||||
Material mat = ObjectResolver.Resolve(findInstruction, typeof(Material)) as Material;
|
||||
|
||||
if (mat == null)
|
||||
{
|
||||
return new ErrorResponse($"Could not find material at path: {materialPath}");
|
||||
}
|
||||
|
||||
Shader shader = mat.shader;
|
||||
var properties = new List<object>();
|
||||
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
int propertyCount = shader.GetPropertyCount();
|
||||
for (int i = 0; i < propertyCount; i++)
|
||||
{
|
||||
string name = shader.GetPropertyName(i);
|
||||
var type = shader.GetPropertyType(i);
|
||||
string description = shader.GetPropertyDescription(i);
|
||||
|
||||
object currentValue = null;
|
||||
try
|
||||
{
|
||||
if (mat.HasProperty(name))
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case UnityEngine.Rendering.ShaderPropertyType.Color:
|
||||
var c = mat.GetColor(name);
|
||||
currentValue = new { r = c.r, g = c.g, b = c.b, a = c.a };
|
||||
break;
|
||||
case UnityEngine.Rendering.ShaderPropertyType.Vector:
|
||||
var v = mat.GetVector(name);
|
||||
currentValue = new { x = v.x, y = v.y, z = v.z, w = v.w };
|
||||
break;
|
||||
case UnityEngine.Rendering.ShaderPropertyType.Float:
|
||||
case UnityEngine.Rendering.ShaderPropertyType.Range:
|
||||
currentValue = mat.GetFloat(name);
|
||||
break;
|
||||
case UnityEngine.Rendering.ShaderPropertyType.Texture:
|
||||
currentValue = mat.GetTexture(name)?.name ?? "null";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
currentValue = $"<error: {ex.Message}>";
|
||||
}
|
||||
|
||||
properties.Add(new
|
||||
{
|
||||
name = name,
|
||||
type = type.ToString(),
|
||||
description = description,
|
||||
value = currentValue
|
||||
});
|
||||
}
|
||||
#else
|
||||
int propertyCount = ShaderUtil.GetPropertyCount(shader);
|
||||
for (int i = 0; i < propertyCount; i++)
|
||||
{
|
||||
string name = ShaderUtil.GetPropertyName(shader, i);
|
||||
ShaderUtil.ShaderPropertyType type = ShaderUtil.GetPropertyType(shader, i);
|
||||
string description = ShaderUtil.GetPropertyDescription(shader, i);
|
||||
|
||||
object currentValue = null;
|
||||
try
|
||||
{
|
||||
if (mat.HasProperty(name))
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ShaderUtil.ShaderPropertyType.Color:
|
||||
var c = mat.GetColor(name);
|
||||
currentValue = new { r = c.r, g = c.g, b = c.b, a = c.a };
|
||||
break;
|
||||
case ShaderUtil.ShaderPropertyType.Vector:
|
||||
var v = mat.GetVector(name);
|
||||
currentValue = new { x = v.x, y = v.y, z = v.z, w = v.w };
|
||||
break;
|
||||
case ShaderUtil.ShaderPropertyType.Float: currentValue = mat.GetFloat(name); break;
|
||||
case ShaderUtil.ShaderPropertyType.Range: currentValue = mat.GetFloat(name); break;
|
||||
case ShaderUtil.ShaderPropertyType.TexEnv: currentValue = mat.GetTexture(name)?.name ?? "null"; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
currentValue = $"<error: {ex.Message}>";
|
||||
}
|
||||
|
||||
properties.Add(new
|
||||
{
|
||||
name = name,
|
||||
type = type.ToString(),
|
||||
description = description,
|
||||
value = currentValue
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
return new SuccessResponse($"Retrieved material info for {mat.name}", new
|
||||
{
|
||||
material = mat.name,
|
||||
shader = shader.name,
|
||||
properties = properties
|
||||
});
|
||||
}
|
||||
|
||||
private static object CreateMaterial(JObject @params)
|
||||
{
|
||||
string materialPath = NormalizePath(@params["materialPath"]?.ToString());
|
||||
string shaderName = @params["shader"]?.ToString() ?? "Standard";
|
||||
JToken colorToken = @params["color"];
|
||||
string colorProperty = @params["property"]?.ToString();
|
||||
|
||||
JObject properties = null;
|
||||
JToken propsToken = @params["properties"];
|
||||
if (propsToken != null)
|
||||
{
|
||||
if (propsToken.Type == JTokenType.String)
|
||||
{
|
||||
try { properties = JObject.Parse(propsToken.ToString()); }
|
||||
catch (Exception ex) { return new ErrorResponse($"Invalid JSON in properties: {ex.Message}"); }
|
||||
}
|
||||
else if (propsToken is JObject obj)
|
||||
{
|
||||
properties = obj;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(materialPath))
|
||||
{
|
||||
return new ErrorResponse("materialPath is required");
|
||||
}
|
||||
|
||||
// Safety check: SanitizeAssetPath should guarantee Assets/ prefix
|
||||
// This check catches edge cases where normalization might fail
|
||||
if (!materialPath.StartsWith("Assets/"))
|
||||
{
|
||||
return new ErrorResponse($"Invalid path '{materialPath}'. Path must be within Assets/ folder.");
|
||||
}
|
||||
|
||||
Shader shader = RenderPipelineUtility.ResolveShader(shaderName);
|
||||
if (shader == null)
|
||||
{
|
||||
return new ErrorResponse($"Could not find shader: {shaderName}");
|
||||
}
|
||||
|
||||
// Check for existing asset to avoid silent overwrite
|
||||
if (AssetDatabase.LoadAssetAtPath<Material>(materialPath) != null)
|
||||
{
|
||||
return new ErrorResponse($"Material already exists at {materialPath}");
|
||||
}
|
||||
|
||||
Material material = null;
|
||||
var shouldDestroyMaterial = true;
|
||||
try
|
||||
{
|
||||
material = new Material(shader);
|
||||
|
||||
// Apply color param during creation (keeps Python tool signature and C# implementation consistent).
|
||||
// If "properties" already contains a color property, let properties win.
|
||||
bool shouldApplyColor = false;
|
||||
if (colorToken != null)
|
||||
{
|
||||
if (properties == null)
|
||||
{
|
||||
shouldApplyColor = true;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(colorProperty))
|
||||
{
|
||||
// If colorProperty is specified, only check that specific property.
|
||||
shouldApplyColor = !properties.ContainsKey(colorProperty);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If colorProperty is not specified, check fallback properties.
|
||||
shouldApplyColor = !properties.ContainsKey("_BaseColor") && !properties.ContainsKey("_Color");
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldApplyColor)
|
||||
{
|
||||
Color color;
|
||||
try
|
||||
{
|
||||
color = MaterialOps.ParseColor(colorToken, UnityJsonSerializer.Instance);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Invalid color format: {e.Message}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(colorProperty))
|
||||
{
|
||||
if (material.HasProperty(colorProperty))
|
||||
{
|
||||
material.SetColor(colorProperty, color);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse($"Specified color property '{colorProperty}' does not exist on this material.");
|
||||
}
|
||||
}
|
||||
else if (material.HasProperty("_BaseColor"))
|
||||
{
|
||||
material.SetColor("_BaseColor", color);
|
||||
}
|
||||
else if (material.HasProperty("_Color"))
|
||||
{
|
||||
material.SetColor("_Color", color);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse("Could not find suitable color property (_BaseColor or _Color) on this material's shader.");
|
||||
}
|
||||
}
|
||||
|
||||
AssetDatabase.CreateAsset(material, materialPath);
|
||||
shouldDestroyMaterial = false; // material is now owned by the AssetDatabase
|
||||
|
||||
if (properties != null)
|
||||
{
|
||||
MaterialOps.ApplyProperties(material, properties, UnityJsonSerializer.Instance);
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(material);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new SuccessResponse($"Created material at {materialPath} with shader {shaderName}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (shouldDestroyMaterial && material != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(material);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/ManageMaterial.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/ManageMaterial.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e55741e2b00794a049a0ed5e63278a56
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
838
Packages/MCPForUnity/Editor/Tools/ManageScene.cs
Normal file
838
Packages/MCPForUnity/Editor/Tools/ManageScene.cs
Normal file
@@ -0,0 +1,838 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MCPForUnity.Editor.Helpers; // For Response class
|
||||
using MCPForUnity.Runtime.Helpers; // For ScreenshotUtility
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles scene management operations like loading, saving, creating, and querying hierarchy.
|
||||
/// </summary>
|
||||
[McpForUnityTool("manage_scene", AutoRegister = false)]
|
||||
public static class ManageScene
|
||||
{
|
||||
private sealed class SceneCommand
|
||||
{
|
||||
public string action { get; set; } = string.Empty;
|
||||
public string name { get; set; } = string.Empty;
|
||||
public string path { get; set; } = string.Empty;
|
||||
public int? buildIndex { get; set; }
|
||||
public string fileName { get; set; } = string.Empty;
|
||||
public int? superSize { get; set; }
|
||||
|
||||
// get_hierarchy paging + safety (summary-first)
|
||||
public JToken parent { get; set; }
|
||||
public int? pageSize { get; set; }
|
||||
public int? cursor { get; set; }
|
||||
public int? maxNodes { get; set; }
|
||||
public int? maxDepth { get; set; }
|
||||
public int? maxChildrenPerNode { get; set; }
|
||||
public bool? includeTransform { get; set; }
|
||||
}
|
||||
|
||||
private static SceneCommand ToSceneCommand(JObject p)
|
||||
{
|
||||
if (p == null) return new SceneCommand();
|
||||
return new SceneCommand
|
||||
{
|
||||
action = (p["action"]?.ToString() ?? string.Empty).Trim().ToLowerInvariant(),
|
||||
name = p["name"]?.ToString() ?? string.Empty,
|
||||
path = p["path"]?.ToString() ?? string.Empty,
|
||||
buildIndex = ParamCoercion.CoerceIntNullable(p["buildIndex"] ?? p["build_index"]),
|
||||
fileName = (p["fileName"] ?? p["filename"])?.ToString() ?? string.Empty,
|
||||
superSize = ParamCoercion.CoerceIntNullable(p["superSize"] ?? p["super_size"] ?? p["supersize"]),
|
||||
|
||||
// get_hierarchy paging + safety
|
||||
parent = p["parent"],
|
||||
pageSize = ParamCoercion.CoerceIntNullable(p["pageSize"] ?? p["page_size"]),
|
||||
cursor = ParamCoercion.CoerceIntNullable(p["cursor"]),
|
||||
maxNodes = ParamCoercion.CoerceIntNullable(p["maxNodes"] ?? p["max_nodes"]),
|
||||
maxDepth = ParamCoercion.CoerceIntNullable(p["maxDepth"] ?? p["max_depth"]),
|
||||
maxChildrenPerNode = ParamCoercion.CoerceIntNullable(p["maxChildrenPerNode"] ?? p["max_children_per_node"]),
|
||||
includeTransform = ParamCoercion.CoerceBoolNullable(p["includeTransform"] ?? p["include_transform"]),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main handler for scene management actions.
|
||||
/// </summary>
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
try { McpLog.Info("[ManageScene] HandleCommand: start", always: false); } catch { }
|
||||
var cmd = ToSceneCommand(@params);
|
||||
string action = cmd.action;
|
||||
string name = string.IsNullOrEmpty(cmd.name) ? null : cmd.name;
|
||||
string path = string.IsNullOrEmpty(cmd.path) ? null : cmd.path; // Relative to Assets/
|
||||
int? buildIndex = cmd.buildIndex;
|
||||
// bool loadAdditive = @params["loadAdditive"]?.ToObject<bool>() ?? false; // Example for future extension
|
||||
|
||||
// Ensure path is relative to Assets/, removing any leading "Assets/"
|
||||
string relativeDir = path ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(relativeDir))
|
||||
{
|
||||
relativeDir = AssetPathUtility.NormalizeSeparators(relativeDir).Trim('/');
|
||||
if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/');
|
||||
}
|
||||
}
|
||||
|
||||
// Apply default *after* sanitizing, using the original path variable for the check
|
||||
if (string.IsNullOrEmpty(path) && action == "create") // Check original path for emptiness
|
||||
{
|
||||
relativeDir = "Scenes"; // Default relative directory
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return new ErrorResponse("Action parameter is required.");
|
||||
}
|
||||
|
||||
string sceneFileName = string.IsNullOrEmpty(name) ? null : $"{name}.unity";
|
||||
// Construct full system path correctly: ProjectRoot/Assets/relativeDir/sceneFileName
|
||||
string fullPathDir = Path.Combine(Application.dataPath, relativeDir); // Combine with Assets path (Application.dataPath ends in Assets)
|
||||
string fullPath = string.IsNullOrEmpty(sceneFileName)
|
||||
? null
|
||||
: Path.Combine(fullPathDir, sceneFileName);
|
||||
// Ensure relativePath always starts with "Assets/" and uses forward slashes
|
||||
string relativePath = string.IsNullOrEmpty(sceneFileName)
|
||||
? null
|
||||
: AssetPathUtility.NormalizeSeparators(Path.Combine("Assets", relativeDir, sceneFileName));
|
||||
|
||||
// Ensure directory exists for 'create'
|
||||
if (action == "create" && !string.IsNullOrEmpty(fullPathDir))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(fullPathDir);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Could not create directory '{fullPathDir}': {e.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Route action
|
||||
try { McpLog.Info($"[ManageScene] Route action='{action}' name='{name}' path='{path}' buildIndex={(buildIndex.HasValue ? buildIndex.Value.ToString() : "null")}", always: false); } catch { }
|
||||
switch (action)
|
||||
{
|
||||
case "create":
|
||||
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(relativePath))
|
||||
return new ErrorResponse(
|
||||
"'name' and 'path' parameters are required for 'create' action."
|
||||
);
|
||||
return CreateScene(fullPath, relativePath);
|
||||
case "load":
|
||||
// Loading can be done by path/name or build index
|
||||
if (!string.IsNullOrEmpty(relativePath))
|
||||
return LoadScene(relativePath);
|
||||
else if (buildIndex.HasValue)
|
||||
return LoadScene(buildIndex.Value);
|
||||
else
|
||||
return new ErrorResponse(
|
||||
"Either 'name'/'path' or 'buildIndex' must be provided for 'load' action."
|
||||
);
|
||||
case "save":
|
||||
// Save current scene, optionally to a new path
|
||||
return SaveScene(fullPath, relativePath);
|
||||
case "get_hierarchy":
|
||||
try { McpLog.Info("[ManageScene] get_hierarchy: entering", always: false); } catch { }
|
||||
var gh = GetSceneHierarchyPaged(cmd);
|
||||
try { McpLog.Info("[ManageScene] get_hierarchy: exiting", always: false); } catch { }
|
||||
return gh;
|
||||
case "get_active":
|
||||
try { McpLog.Info("[ManageScene] get_active: entering", always: false); } catch { }
|
||||
var ga = GetActiveSceneInfo();
|
||||
try { McpLog.Info("[ManageScene] get_active: exiting", always: false); } catch { }
|
||||
return ga;
|
||||
case "get_build_settings":
|
||||
return GetBuildSettingsScenes();
|
||||
case "screenshot":
|
||||
return CaptureScreenshot(cmd.fileName, cmd.superSize);
|
||||
// Add cases for modifying build settings, additive loading, unloading etc.
|
||||
default:
|
||||
return new ErrorResponse(
|
||||
$"Unknown action: '{action}'. Valid actions: create, load, save, get_hierarchy, get_active, get_build_settings, screenshot."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures a screenshot to Assets/Screenshots and returns a response payload.
|
||||
/// Public so the tools UI can reuse the same logic without duplicating parameters.
|
||||
/// Available in both Edit Mode and Play Mode.
|
||||
/// </summary>
|
||||
public static object ExecuteScreenshot(string fileName = null, int? superSize = null)
|
||||
{
|
||||
return CaptureScreenshot(fileName, superSize);
|
||||
}
|
||||
|
||||
private static object CreateScene(string fullPath, string relativePath)
|
||||
{
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
return new ErrorResponse($"Scene already exists at '{relativePath}'.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Create a new empty scene
|
||||
Scene newScene = EditorSceneManager.NewScene(
|
||||
NewSceneSetup.EmptyScene,
|
||||
NewSceneMode.Single
|
||||
);
|
||||
// Save it to the specified path
|
||||
bool saved = EditorSceneManager.SaveScene(newScene, relativePath);
|
||||
|
||||
if (saved)
|
||||
{
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); // Ensure Unity sees the new scene file
|
||||
return new SuccessResponse(
|
||||
$"Scene '{Path.GetFileName(relativePath)}' created successfully at '{relativePath}'.",
|
||||
new { path = relativePath }
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If SaveScene fails, it might leave an untitled scene open.
|
||||
// Optionally try to close it, but be cautious.
|
||||
return new ErrorResponse($"Failed to save new scene to '{relativePath}'.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Error creating scene '{relativePath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object LoadScene(string relativePath)
|
||||
{
|
||||
if (
|
||||
!File.Exists(
|
||||
Path.Combine(
|
||||
Application.dataPath.Substring(
|
||||
0,
|
||||
Application.dataPath.Length - "Assets".Length
|
||||
),
|
||||
relativePath
|
||||
)
|
||||
)
|
||||
)
|
||||
{
|
||||
return new ErrorResponse($"Scene file not found at '{relativePath}'.");
|
||||
}
|
||||
|
||||
// Check for unsaved changes in the current scene
|
||||
if (EditorSceneManager.GetActiveScene().isDirty)
|
||||
{
|
||||
// Optionally prompt the user or save automatically before loading
|
||||
return new ErrorResponse(
|
||||
"Current scene has unsaved changes. Please save or discard changes before loading a new scene."
|
||||
);
|
||||
// Example: bool saveOK = EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
|
||||
// if (!saveOK) return new ErrorResponse("Load cancelled by user.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
EditorSceneManager.OpenScene(relativePath, OpenSceneMode.Single);
|
||||
return new SuccessResponse(
|
||||
$"Scene '{relativePath}' loaded successfully.",
|
||||
new
|
||||
{
|
||||
path = relativePath,
|
||||
name = Path.GetFileNameWithoutExtension(relativePath),
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Error loading scene '{relativePath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object LoadScene(int buildIndex)
|
||||
{
|
||||
if (buildIndex < 0 || buildIndex >= SceneManager.sceneCountInBuildSettings)
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Invalid build index: {buildIndex}. Must be between 0 and {SceneManager.sceneCountInBuildSettings - 1}."
|
||||
);
|
||||
}
|
||||
|
||||
// Check for unsaved changes
|
||||
if (EditorSceneManager.GetActiveScene().isDirty)
|
||||
{
|
||||
return new ErrorResponse(
|
||||
"Current scene has unsaved changes. Please save or discard changes before loading a new scene."
|
||||
);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string scenePath = SceneUtility.GetScenePathByBuildIndex(buildIndex);
|
||||
EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
|
||||
return new SuccessResponse(
|
||||
$"Scene at build index {buildIndex} ('{scenePath}') loaded successfully.",
|
||||
new
|
||||
{
|
||||
path = scenePath,
|
||||
name = Path.GetFileNameWithoutExtension(scenePath),
|
||||
buildIndex = buildIndex,
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Error loading scene with build index {buildIndex}: {e.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static object SaveScene(string fullPath, string relativePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
Scene currentScene = EditorSceneManager.GetActiveScene();
|
||||
if (!currentScene.IsValid())
|
||||
{
|
||||
return new ErrorResponse("No valid scene is currently active to save.");
|
||||
}
|
||||
|
||||
bool saved;
|
||||
string finalPath = currentScene.path; // Path where it was last saved or will be saved
|
||||
|
||||
if (!string.IsNullOrEmpty(relativePath) && currentScene.path != relativePath)
|
||||
{
|
||||
// Save As...
|
||||
// Ensure directory exists
|
||||
string dir = Path.GetDirectoryName(fullPath);
|
||||
if (!Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
saved = EditorSceneManager.SaveScene(currentScene, relativePath);
|
||||
finalPath = relativePath;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Save (overwrite existing or save untitled)
|
||||
if (string.IsNullOrEmpty(currentScene.path))
|
||||
{
|
||||
// Scene is untitled, needs a path
|
||||
return new ErrorResponse(
|
||||
"Cannot save an untitled scene without providing a 'name' and 'path'. Use Save As functionality."
|
||||
);
|
||||
}
|
||||
saved = EditorSceneManager.SaveScene(currentScene);
|
||||
}
|
||||
|
||||
if (saved)
|
||||
{
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
||||
return new SuccessResponse(
|
||||
$"Scene '{currentScene.name}' saved successfully to '{finalPath}'.",
|
||||
new { path = finalPath, name = currentScene.name }
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse($"Failed to save scene '{currentScene.name}'.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Error saving scene: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object CaptureScreenshot(string fileName, int? superSize)
|
||||
{
|
||||
try
|
||||
{
|
||||
int resolvedSuperSize = (superSize.HasValue && superSize.Value > 0) ? superSize.Value : 1;
|
||||
|
||||
// Batch mode warning
|
||||
if (Application.isBatchMode)
|
||||
{
|
||||
McpLog.Warn("[ManageScene] Screenshot capture in batch mode uses camera-based fallback. Results may vary.");
|
||||
}
|
||||
|
||||
// Check Screen Capture module availability and warn if not available
|
||||
bool screenCaptureAvailable = ScreenshotUtility.IsScreenCaptureModuleAvailable;
|
||||
bool hasCameraFallback = Camera.main != null || UnityEngine.Object.FindObjectsOfType<Camera>().Length > 0;
|
||||
|
||||
#if UNITY_2022_1_OR_NEWER
|
||||
if (!screenCaptureAvailable && !hasCameraFallback)
|
||||
{
|
||||
return new ErrorResponse(
|
||||
"Cannot capture screenshot. The Screen Capture module is not enabled and no Camera was found in the scene. " +
|
||||
"Please either: (1) Enable the Screen Capture module: Window > Package Manager > Built-in > Screen Capture > Enable, " +
|
||||
"or (2) Add a Camera to your scene for camera-based fallback capture."
|
||||
);
|
||||
}
|
||||
|
||||
if (!screenCaptureAvailable)
|
||||
{
|
||||
McpLog.Warn("[ManageScene] Screen Capture module not enabled. Using camera-based fallback. " +
|
||||
"For best results, enable it: Window > Package Manager > Built-in > Screen Capture > Enable.");
|
||||
}
|
||||
#else
|
||||
if (!hasCameraFallback)
|
||||
{
|
||||
return new ErrorResponse(
|
||||
"No camera found in the scene. Screenshot capture on Unity versions before 2022.1 requires a Camera in the scene. " +
|
||||
"Please add a Camera to your scene or upgrade to Unity 2022.1+ for ScreenCapture API support."
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Best-effort: ensure Game View exists and repaints before capture.
|
||||
if (!Application.isBatchMode)
|
||||
{
|
||||
EnsureGameView();
|
||||
}
|
||||
|
||||
ScreenshotCaptureResult result = ScreenshotUtility.CaptureToAssetsFolder(fileName, resolvedSuperSize, ensureUniqueFileName: true);
|
||||
|
||||
// ScreenCapture.CaptureScreenshot is async. Import after the file actually hits disk.
|
||||
if (result.IsAsync)
|
||||
{
|
||||
ScheduleAssetImportWhenFileExists(result.AssetsRelativePath, result.FullPath, timeoutSeconds: 30.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
AssetDatabase.ImportAsset(result.AssetsRelativePath, ImportAssetOptions.ForceSynchronousImport);
|
||||
}
|
||||
|
||||
string verb = result.IsAsync ? "Screenshot requested" : "Screenshot captured";
|
||||
string message = $"{verb} to '{result.AssetsRelativePath}' (full: {result.FullPath}).";
|
||||
|
||||
return new SuccessResponse(
|
||||
message,
|
||||
new
|
||||
{
|
||||
path = result.AssetsRelativePath,
|
||||
fullPath = result.FullPath,
|
||||
superSize = result.SuperSize,
|
||||
isAsync = result.IsAsync,
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Error capturing screenshot: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureGameView()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Ensure a Game View exists and has a chance to repaint before capture.
|
||||
try
|
||||
{
|
||||
if (!EditorApplication.ExecuteMenuItem("Window/General/Game"))
|
||||
{
|
||||
// Some Unity versions expose hotkey suffixes in menu paths.
|
||||
EditorApplication.ExecuteMenuItem("Window/General/Game %2");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try { McpLog.Debug($"[ManageScene] screenshot: failed to open Game View via menu item: {e.Message}"); } catch { }
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var gameViewType = Type.GetType("UnityEditor.GameView,UnityEditor");
|
||||
if (gameViewType != null)
|
||||
{
|
||||
var window = EditorWindow.GetWindow(gameViewType);
|
||||
window?.Repaint();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try { McpLog.Debug($"[ManageScene] screenshot: failed to repaint Game View: {e.Message}"); } catch { }
|
||||
}
|
||||
|
||||
try { SceneView.RepaintAll(); }
|
||||
catch (Exception e)
|
||||
{
|
||||
try { McpLog.Debug($"[ManageScene] screenshot: failed to repaint Scene View: {e.Message}"); } catch { }
|
||||
}
|
||||
|
||||
try { EditorApplication.QueuePlayerLoopUpdate(); }
|
||||
catch (Exception e)
|
||||
{
|
||||
try { McpLog.Debug($"[ManageScene] screenshot: failed to queue player loop update: {e.Message}"); } catch { }
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try { McpLog.Debug($"[ManageScene] screenshot: EnsureGameView failed: {e.Message}"); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private static void ScheduleAssetImportWhenFileExists(string assetsRelativePath, string fullPath, double timeoutSeconds)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(assetsRelativePath) || string.IsNullOrWhiteSpace(fullPath))
|
||||
{
|
||||
McpLog.Warn("[ManageScene] ScheduleAssetImportWhenFileExists: invalid paths provided, skipping import scheduling.");
|
||||
return;
|
||||
}
|
||||
|
||||
double start = EditorApplication.timeSinceStartup;
|
||||
int failureCount = 0;
|
||||
bool hasSeenFile = false;
|
||||
const int maxLoggedFailures = 3;
|
||||
EditorApplication.CallbackFunction tick = null;
|
||||
tick = () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
hasSeenFile = true;
|
||||
|
||||
AssetDatabase.ImportAsset(assetsRelativePath, ImportAssetOptions.ForceSynchronousImport);
|
||||
McpLog.Debug($"[ManageScene] Imported asset at '{assetsRelativePath}'.");
|
||||
EditorApplication.update -= tick;
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
failureCount++;
|
||||
|
||||
if (failureCount <= maxLoggedFailures)
|
||||
{
|
||||
McpLog.Warn($"[ManageScene] Exception while importing asset '{assetsRelativePath}' from '{fullPath}' (attempt {failureCount}): {e}");
|
||||
}
|
||||
}
|
||||
|
||||
if (EditorApplication.timeSinceStartup - start > timeoutSeconds)
|
||||
{
|
||||
if (!hasSeenFile)
|
||||
{
|
||||
McpLog.Warn($"[ManageScene] Timed out waiting for file '{fullPath}' (asset: '{assetsRelativePath}') after {timeoutSeconds:F1} seconds. The asset was not imported.");
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Warn($"[ManageScene] Timed out importing asset '{assetsRelativePath}' from '{fullPath}' after {timeoutSeconds:F1} seconds. The file existed but the asset was not imported.");
|
||||
}
|
||||
|
||||
EditorApplication.update -= tick;
|
||||
}
|
||||
};
|
||||
|
||||
EditorApplication.update += tick;
|
||||
}
|
||||
|
||||
private static object GetActiveSceneInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
try { McpLog.Info("[ManageScene] get_active: querying EditorSceneManager.GetActiveScene", always: false); } catch { }
|
||||
Scene activeScene = EditorSceneManager.GetActiveScene();
|
||||
try { McpLog.Info($"[ManageScene] get_active: got scene valid={activeScene.IsValid()} loaded={activeScene.isLoaded} name='{activeScene.name}'", always: false); } catch { }
|
||||
if (!activeScene.IsValid())
|
||||
{
|
||||
return new ErrorResponse("No active scene found.");
|
||||
}
|
||||
|
||||
var sceneInfo = new
|
||||
{
|
||||
name = activeScene.name,
|
||||
path = activeScene.path,
|
||||
buildIndex = activeScene.buildIndex, // -1 if not in build settings
|
||||
isDirty = activeScene.isDirty,
|
||||
isLoaded = activeScene.isLoaded,
|
||||
rootCount = activeScene.rootCount,
|
||||
};
|
||||
|
||||
return new SuccessResponse("Retrieved active scene information.", sceneInfo);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try { McpLog.Error($"[ManageScene] get_active: exception {e.Message}"); } catch { }
|
||||
return new ErrorResponse($"Error getting active scene info: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object GetBuildSettingsScenes()
|
||||
{
|
||||
try
|
||||
{
|
||||
var scenes = new List<object>();
|
||||
for (int i = 0; i < EditorBuildSettings.scenes.Length; i++)
|
||||
{
|
||||
var scene = EditorBuildSettings.scenes[i];
|
||||
scenes.Add(
|
||||
new
|
||||
{
|
||||
path = scene.path,
|
||||
guid = scene.guid.ToString(),
|
||||
enabled = scene.enabled,
|
||||
buildIndex = i, // Actual build index considering only enabled scenes might differ
|
||||
}
|
||||
);
|
||||
}
|
||||
return new SuccessResponse("Retrieved scenes from Build Settings.", scenes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Error getting scenes from Build Settings: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object GetSceneHierarchyPaged(SceneCommand cmd)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check Prefab Stage first
|
||||
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||
Scene activeScene;
|
||||
|
||||
if (prefabStage != null)
|
||||
{
|
||||
activeScene = prefabStage.scene;
|
||||
try { McpLog.Info("[ManageScene] get_hierarchy: using Prefab Stage scene", always: false); } catch { }
|
||||
}
|
||||
else
|
||||
{
|
||||
try { McpLog.Info("[ManageScene] get_hierarchy: querying EditorSceneManager.GetActiveScene", always: false); } catch { }
|
||||
activeScene = EditorSceneManager.GetActiveScene();
|
||||
}
|
||||
|
||||
try { McpLog.Info($"[ManageScene] get_hierarchy: got scene valid={activeScene.IsValid()} loaded={activeScene.isLoaded} name='{activeScene.name}'", always: false); } catch { }
|
||||
if (!activeScene.IsValid() || !activeScene.isLoaded)
|
||||
{
|
||||
return new ErrorResponse(
|
||||
"No valid and loaded scene is active to get hierarchy from."
|
||||
);
|
||||
}
|
||||
|
||||
// Defaults tuned for safety; callers can override but we clamp to sane maxes.
|
||||
// NOTE: pageSize is "items per page", not "number of pages".
|
||||
// Keep this conservative to reduce peak response sizes when callers omit page_size.
|
||||
int resolvedPageSize = Mathf.Clamp(cmd.pageSize ?? 50, 1, 500);
|
||||
int resolvedCursor = Mathf.Max(0, cmd.cursor ?? 0);
|
||||
int resolvedMaxNodes = Mathf.Clamp(cmd.maxNodes ?? 1000, 1, 5000);
|
||||
int effectiveTake = Mathf.Min(resolvedPageSize, resolvedMaxNodes);
|
||||
int resolvedMaxChildrenPerNode = Mathf.Clamp(cmd.maxChildrenPerNode ?? 200, 0, 2000);
|
||||
bool includeTransform = cmd.includeTransform ?? false;
|
||||
|
||||
// NOTE: maxDepth is accepted for forward-compatibility, but current paging mode
|
||||
// returns a single level (roots or direct children). This keeps payloads bounded.
|
||||
|
||||
List<GameObject> nodes;
|
||||
string scope;
|
||||
|
||||
GameObject parentGo = ResolveGameObject(cmd.parent, activeScene);
|
||||
if (cmd.parent == null || cmd.parent.Type == JTokenType.Null)
|
||||
{
|
||||
try { McpLog.Info("[ManageScene] get_hierarchy: listing root objects (paged summary)", always: false); } catch { }
|
||||
nodes = activeScene.GetRootGameObjects().Where(go => go != null).ToList();
|
||||
scope = "roots";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (parentGo == null)
|
||||
{
|
||||
return new ErrorResponse($"Parent GameObject ('{cmd.parent}') not found.");
|
||||
}
|
||||
try { McpLog.Info($"[ManageScene] get_hierarchy: listing children of '{parentGo.name}' (paged summary)", always: false); } catch { }
|
||||
nodes = new List<GameObject>(parentGo.transform.childCount);
|
||||
foreach (Transform child in parentGo.transform)
|
||||
{
|
||||
if (child != null) nodes.Add(child.gameObject);
|
||||
}
|
||||
scope = "children";
|
||||
}
|
||||
|
||||
int total = nodes.Count;
|
||||
if (resolvedCursor > total) resolvedCursor = total;
|
||||
int end = Mathf.Min(total, resolvedCursor + effectiveTake);
|
||||
|
||||
var items = new List<object>(Mathf.Max(0, end - resolvedCursor));
|
||||
for (int i = resolvedCursor; i < end; i++)
|
||||
{
|
||||
var go = nodes[i];
|
||||
if (go == null) continue;
|
||||
items.Add(BuildGameObjectSummary(go, includeTransform, resolvedMaxChildrenPerNode));
|
||||
}
|
||||
|
||||
bool truncated = end < total;
|
||||
string nextCursor = truncated ? end.ToString() : null;
|
||||
|
||||
var payload = new
|
||||
{
|
||||
scope = scope,
|
||||
cursor = resolvedCursor,
|
||||
pageSize = effectiveTake,
|
||||
next_cursor = nextCursor,
|
||||
truncated = truncated,
|
||||
total = total,
|
||||
items = items,
|
||||
};
|
||||
|
||||
var resp = new SuccessResponse($"Retrieved hierarchy page for scene '{activeScene.name}'.", payload);
|
||||
try { McpLog.Info("[ManageScene] get_hierarchy: success", always: false); } catch { }
|
||||
return resp;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try { McpLog.Error($"[ManageScene] get_hierarchy: exception {e.Message}"); } catch { }
|
||||
return new ErrorResponse($"Error getting scene hierarchy: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static GameObject ResolveGameObject(JToken targetToken, Scene activeScene)
|
||||
{
|
||||
if (targetToken == null || targetToken.Type == JTokenType.Null) return null;
|
||||
|
||||
try
|
||||
{
|
||||
if (targetToken.Type == JTokenType.Integer || int.TryParse(targetToken.ToString(), out _))
|
||||
{
|
||||
if (int.TryParse(targetToken.ToString(), out int id))
|
||||
{
|
||||
var obj = EditorUtility.InstanceIDToObject(id);
|
||||
if (obj is GameObject go) return go;
|
||||
if (obj is Component c) return c.gameObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
string s = targetToken.ToString();
|
||||
if (string.IsNullOrEmpty(s)) return null;
|
||||
|
||||
// Path-based find (e.g., "Root/Child/GrandChild")
|
||||
if (s.Contains("/"))
|
||||
{
|
||||
try
|
||||
{
|
||||
var ids = GameObjectLookup.SearchGameObjects("by_path", s, includeInactive: true, maxResults: 1);
|
||||
if (ids.Count > 0)
|
||||
{
|
||||
var byPath = GameObjectLookup.FindById(ids[0]);
|
||||
if (byPath != null) return byPath;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// Name-based find (first match, includes inactive)
|
||||
try
|
||||
{
|
||||
var all = activeScene.GetRootGameObjects();
|
||||
foreach (var root in all)
|
||||
{
|
||||
if (root == null) continue;
|
||||
if (root.name == s) return root;
|
||||
var trs = root.GetComponentsInChildren<Transform>(includeInactive: true);
|
||||
foreach (var t in trs)
|
||||
{
|
||||
if (t != null && t.gameObject != null && t.gameObject.name == s) return t.gameObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static object BuildGameObjectSummary(GameObject go, bool includeTransform, int maxChildrenPerNode)
|
||||
{
|
||||
if (go == null) return null;
|
||||
|
||||
int childCount = 0;
|
||||
try { childCount = go.transform != null ? go.transform.childCount : 0; } catch { }
|
||||
bool childrenTruncated = childCount > 0; // We do not inline children in summary mode.
|
||||
|
||||
// Get component type names (lightweight - no full serialization)
|
||||
var componentTypes = new List<string>();
|
||||
try
|
||||
{
|
||||
var components = go.GetComponents<Component>();
|
||||
if (components != null)
|
||||
{
|
||||
foreach (var c in components)
|
||||
{
|
||||
if (c != null)
|
||||
{
|
||||
componentTypes.Add(c.GetType().Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Debug($"[ManageScene] Failed to enumerate components for '{go.name}': {ex.Message}");
|
||||
}
|
||||
|
||||
var d = new Dictionary<string, object>
|
||||
{
|
||||
{ "name", go.name },
|
||||
{ "instanceID", go.GetInstanceID() },
|
||||
{ "activeSelf", go.activeSelf },
|
||||
{ "activeInHierarchy", go.activeInHierarchy },
|
||||
{ "tag", go.tag },
|
||||
{ "layer", go.layer },
|
||||
{ "isStatic", go.isStatic },
|
||||
{ "path", GetGameObjectPath(go) },
|
||||
{ "childCount", childCount },
|
||||
{ "childrenTruncated", childrenTruncated },
|
||||
{ "childrenCursor", childCount > 0 ? "0" : null },
|
||||
{ "childrenPageSizeDefault", maxChildrenPerNode },
|
||||
{ "componentTypes", componentTypes },
|
||||
};
|
||||
|
||||
if (includeTransform && go.transform != null)
|
||||
{
|
||||
var t = go.transform;
|
||||
d["transform"] = new
|
||||
{
|
||||
position = new[] { t.localPosition.x, t.localPosition.y, t.localPosition.z },
|
||||
rotation = new[] { t.localRotation.eulerAngles.x, t.localRotation.eulerAngles.y, t.localRotation.eulerAngles.z },
|
||||
scale = new[] { t.localScale.x, t.localScale.y, t.localScale.z },
|
||||
};
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
private static string GetGameObjectPath(GameObject go)
|
||||
{
|
||||
if (go == null) return string.Empty;
|
||||
try
|
||||
{
|
||||
var names = new Stack<string>();
|
||||
Transform t = go.transform;
|
||||
while (t != null)
|
||||
{
|
||||
names.Push(t.name);
|
||||
t = t.parent;
|
||||
}
|
||||
return string.Join("/", names);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return go.name;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/ManageScene.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/ManageScene.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6ddda47f4077e74fbb5092388cefcc2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
2672
Packages/MCPForUnity/Editor/Tools/ManageScript.cs
Normal file
2672
Packages/MCPForUnity/Editor/Tools/ManageScript.cs
Normal file
File diff suppressed because it is too large
Load Diff
11
Packages/MCPForUnity/Editor/Tools/ManageScript.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/ManageScript.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 626d2d44668019a45ae52e9ee066b7ec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1522
Packages/MCPForUnity/Editor/Tools/ManageScriptableObject.cs
Normal file
1522
Packages/MCPForUnity/Editor/Tools/ManageScriptableObject.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e0bb5a8c1b24b7ea8bce09ce0a1f234
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
|
||||
|
||||
344
Packages/MCPForUnity/Editor/Tools/ManageShader.cs
Normal file
344
Packages/MCPForUnity/Editor/Tools/ManageShader.cs
Normal file
@@ -0,0 +1,344 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles CRUD operations for shader files within the Unity project.
|
||||
/// </summary>
|
||||
[McpForUnityTool("manage_shader", AutoRegister = false)]
|
||||
public static class ManageShader
|
||||
{
|
||||
/// <summary>
|
||||
/// Main handler for shader management actions.
|
||||
/// </summary>
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
// Extract parameters
|
||||
string action = @params["action"]?.ToString()?.ToLowerInvariant();
|
||||
string name = @params["name"]?.ToString();
|
||||
string path = @params["path"]?.ToString(); // Relative to Assets/
|
||||
string contents = null;
|
||||
|
||||
// Check if we have base64 encoded contents
|
||||
bool contentsEncoded = @params["contentsEncoded"]?.ToObject<bool>() ?? false;
|
||||
if (contentsEncoded && @params["encodedContents"] != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
contents = DecodeBase64(@params["encodedContents"].ToString());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Failed to decode shader contents: {e.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
contents = @params["contents"]?.ToString();
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return new ErrorResponse("Action parameter is required.");
|
||||
}
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return new ErrorResponse("Name parameter is required.");
|
||||
}
|
||||
// Basic name validation (alphanumeric, underscores, cannot start with number)
|
||||
if (!Regex.IsMatch(name, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Invalid shader name: '{name}'. Use only letters, numbers, underscores, and don't start with a number."
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure path is relative to Assets/, removing any leading "Assets/"
|
||||
// Set default directory to "Shaders" if path is not provided
|
||||
string relativeDir = path ?? "Shaders"; // Default to "Shaders" if path is null
|
||||
if (!string.IsNullOrEmpty(relativeDir))
|
||||
{
|
||||
relativeDir = AssetPathUtility.NormalizeSeparators(relativeDir).Trim('/');
|
||||
if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/');
|
||||
}
|
||||
}
|
||||
// Handle empty string case explicitly after processing
|
||||
if (string.IsNullOrEmpty(relativeDir))
|
||||
{
|
||||
relativeDir = "Shaders"; // Ensure default if path was provided as "" or only "/" or "Assets/"
|
||||
}
|
||||
|
||||
// Construct paths
|
||||
string shaderFileName = $"{name}.shader";
|
||||
string fullPathDir = Path.Combine(Application.dataPath, relativeDir);
|
||||
string fullPath = Path.Combine(fullPathDir, shaderFileName);
|
||||
string relativePath = AssetPathUtility.NormalizeSeparators(
|
||||
Path.Combine("Assets", relativeDir, shaderFileName)
|
||||
); // Ensure "Assets/" prefix and forward slashes
|
||||
|
||||
// Ensure the target directory exists for create/update
|
||||
if (action == "create" || action == "update")
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(fullPathDir))
|
||||
{
|
||||
Directory.CreateDirectory(fullPathDir);
|
||||
// Refresh AssetDatabase to recognize new folders
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Could not create directory '{fullPathDir}': {e.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Route to specific action handlers
|
||||
switch (action)
|
||||
{
|
||||
case "create":
|
||||
return CreateShader(fullPath, relativePath, name, contents);
|
||||
case "read":
|
||||
return ReadShader(fullPath, relativePath);
|
||||
case "update":
|
||||
return UpdateShader(fullPath, relativePath, name, contents);
|
||||
case "delete":
|
||||
return DeleteShader(fullPath, relativePath);
|
||||
default:
|
||||
return new ErrorResponse(
|
||||
$"Unknown action: '{action}'. Valid actions are: create, read, update, delete."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode base64 string to normal text
|
||||
/// </summary>
|
||||
private static string DecodeBase64(string encoded)
|
||||
{
|
||||
byte[] data = Convert.FromBase64String(encoded);
|
||||
return System.Text.Encoding.UTF8.GetString(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encode text to base64 string
|
||||
/// </summary>
|
||||
private static string EncodeBase64(string text)
|
||||
{
|
||||
byte[] data = System.Text.Encoding.UTF8.GetBytes(text);
|
||||
return Convert.ToBase64String(data);
|
||||
}
|
||||
|
||||
private static object CreateShader(
|
||||
string fullPath,
|
||||
string relativePath,
|
||||
string name,
|
||||
string contents
|
||||
)
|
||||
{
|
||||
// Check if shader already exists
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Shader already exists at '{relativePath}'. Use 'update' action to modify."
|
||||
);
|
||||
}
|
||||
|
||||
// Add validation for shader name conflicts in Unity
|
||||
if (Shader.Find(name) != null)
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"A shader with name '{name}' already exists in the project. Choose a different name."
|
||||
);
|
||||
}
|
||||
|
||||
// Generate default content if none provided
|
||||
if (string.IsNullOrEmpty(contents))
|
||||
{
|
||||
contents = GenerateDefaultShaderContent(name);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false));
|
||||
AssetDatabase.ImportAsset(relativePath);
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); // Ensure Unity recognizes the new shader
|
||||
return new SuccessResponse(
|
||||
$"Shader '{name}.shader' created successfully at '{relativePath}'.",
|
||||
new { path = relativePath }
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Failed to create shader '{relativePath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object ReadShader(string fullPath, string relativePath)
|
||||
{
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
return new ErrorResponse($"Shader not found at '{relativePath}'.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string contents = File.ReadAllText(fullPath);
|
||||
|
||||
// Return both normal and encoded contents for larger files
|
||||
//TODO: Consider a threshold for large files
|
||||
bool isLarge = contents.Length > 10000; // If content is large, include encoded version
|
||||
var responseData = new
|
||||
{
|
||||
path = relativePath,
|
||||
contents = contents,
|
||||
// For large files, also include base64-encoded version
|
||||
encodedContents = isLarge ? EncodeBase64(contents) : null,
|
||||
contentsEncoded = isLarge,
|
||||
};
|
||||
|
||||
return new SuccessResponse(
|
||||
$"Shader '{Path.GetFileName(relativePath)}' read successfully.",
|
||||
responseData
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Failed to read shader '{relativePath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object UpdateShader(
|
||||
string fullPath,
|
||||
string relativePath,
|
||||
string name,
|
||||
string contents
|
||||
)
|
||||
{
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Shader not found at '{relativePath}'. Use 'create' action to add a new shader."
|
||||
);
|
||||
}
|
||||
if (string.IsNullOrEmpty(contents))
|
||||
{
|
||||
return new ErrorResponse("Content is required for the 'update' action.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false));
|
||||
AssetDatabase.ImportAsset(relativePath);
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
||||
return new SuccessResponse(
|
||||
$"Shader '{Path.GetFileName(relativePath)}' updated successfully.",
|
||||
new { path = relativePath }
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Failed to update shader '{relativePath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object DeleteShader(string fullPath, string relativePath)
|
||||
{
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
return new ErrorResponse($"Shader not found at '{relativePath}'.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Delete the asset through Unity's AssetDatabase first
|
||||
bool success = AssetDatabase.DeleteAsset(relativePath);
|
||||
if (!success)
|
||||
{
|
||||
return new ErrorResponse($"Failed to delete shader through Unity's AssetDatabase: '{relativePath}'");
|
||||
}
|
||||
|
||||
// If the file still exists (rare case), try direct deletion
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
File.Delete(fullPath);
|
||||
}
|
||||
|
||||
return new SuccessResponse($"Shader '{Path.GetFileName(relativePath)}' deleted successfully.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Failed to delete shader '{relativePath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
//This is a CGProgram template
|
||||
//TODO: making a HLSL template as well?
|
||||
private static string GenerateDefaultShaderContent(string name)
|
||||
{
|
||||
return @"Shader """ + name + @"""
|
||||
{
|
||||
Properties
|
||||
{
|
||||
_MainTex (""Texture"", 2D) = ""white"" {}
|
||||
}
|
||||
SubShader
|
||||
{
|
||||
Tags { ""RenderType""=""Opaque"" }
|
||||
LOD 100
|
||||
|
||||
Pass
|
||||
{
|
||||
CGPROGRAM
|
||||
#pragma vertex vert
|
||||
#pragma fragment frag
|
||||
#include ""UnityCG.cginc""
|
||||
|
||||
struct appdata
|
||||
{
|
||||
float4 vertex : POSITION;
|
||||
float2 uv : TEXCOORD0;
|
||||
};
|
||||
|
||||
struct v2f
|
||||
{
|
||||
float2 uv : TEXCOORD0;
|
||||
float4 vertex : SV_POSITION;
|
||||
};
|
||||
|
||||
sampler2D _MainTex;
|
||||
float4 _MainTex_ST;
|
||||
|
||||
v2f vert (appdata v)
|
||||
{
|
||||
v2f o;
|
||||
o.vertex = UnityObjectToClipPos(v.vertex);
|
||||
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
|
||||
return o;
|
||||
}
|
||||
|
||||
fixed4 frag (v2f i) : SV_Target
|
||||
{
|
||||
fixed4 col = tex2D(_MainTex, i.uv);
|
||||
return col;
|
||||
}
|
||||
ENDCG
|
||||
}
|
||||
}
|
||||
}";
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/ManageShader.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/ManageShader.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bcf4f1f3110494344b2af9324cf5c571
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1025
Packages/MCPForUnity/Editor/Tools/ManageTexture.cs
Normal file
1025
Packages/MCPForUnity/Editor/Tools/ManageTexture.cs
Normal file
File diff suppressed because it is too large
Load Diff
11
Packages/MCPForUnity/Editor/Tools/ManageTexture.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/ManageTexture.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8028b64102744ea5aad53a762d48079a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
107
Packages/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs
Normal file
107
Packages/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks a class as an MCP tool handler
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||
public class McpForUnityToolAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Tool name (if null, derived from class name)
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tool description for LLM
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this tool returns structured output
|
||||
/// </summary>
|
||||
public bool StructuredOutput { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether this tool is automatically registered with FastMCP.
|
||||
/// Defaults to true so most tools opt-in automatically. Set to false
|
||||
/// for legacy/built-in tools that already exist server-side.
|
||||
/// </summary>
|
||||
public bool AutoRegister { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enables the polling middleware for long-running tools. When true, Unity
|
||||
/// should return a PendingResponse and the Python side will poll using
|
||||
/// <see cref="PollAction"/> until completion.
|
||||
/// </summary>
|
||||
public bool RequiresPolling { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// The action name to use when polling for status. Defaults to "status".
|
||||
/// </summary>
|
||||
public string PollAction { get; set; } = "status";
|
||||
|
||||
/// <summary>
|
||||
/// The command name used to route requests to this tool.
|
||||
/// If not specified, defaults to the PascalCase class name converted to snake_case.
|
||||
/// Kept for backward compatibility.
|
||||
/// </summary>
|
||||
public string CommandName
|
||||
{
|
||||
get => Name;
|
||||
set => Name = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an MCP tool attribute with auto-generated command name.
|
||||
/// The command name will be derived from the class name (PascalCase → snake_case).
|
||||
/// Example: ManageAsset → manage_asset
|
||||
/// </summary>
|
||||
public McpForUnityToolAttribute()
|
||||
{
|
||||
Name = null; // Will be auto-generated
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an MCP tool attribute with explicit command name.
|
||||
/// </summary>
|
||||
/// <param name="name">The command name (e.g., "manage_asset")</param>
|
||||
public McpForUnityToolAttribute(string name = null)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a tool parameter
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
|
||||
public class ToolParameterAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameter name (if null, derived from property/field name)
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Parameter description for LLM
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this parameter is required
|
||||
/// </summary>
|
||||
public bool Required { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Default value (as string)
|
||||
/// </summary>
|
||||
public string DefaultValue { get; set; }
|
||||
|
||||
public ToolParameterAttribute(string description)
|
||||
{
|
||||
Description = description;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 804d07b886f4e4eb39316bbef34687c7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/MCPForUnity/Editor/Tools/Prefabs.meta
Normal file
8
Packages/MCPForUnity/Editor/Tools/Prefabs.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1bd48a1b7555c46bba168078ce0291cc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
982
Packages/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs
Normal file
982
Packages/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs
Normal file
@@ -0,0 +1,982 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Prefabs
|
||||
{
|
||||
[McpForUnityTool("manage_prefabs", AutoRegister = false)]
|
||||
/// <summary>
|
||||
/// Tool to manage Unity Prefabs: create, inspect, and modify prefab assets.
|
||||
/// Uses headless editing (no UI, no dialogs) for reliable automated workflows.
|
||||
/// </summary>
|
||||
public static class ManagePrefabs
|
||||
{
|
||||
// Action constants
|
||||
private const string ACTION_CREATE_FROM_GAMEOBJECT = "create_from_gameobject";
|
||||
private const string ACTION_GET_INFO = "get_info";
|
||||
private const string ACTION_GET_HIERARCHY = "get_hierarchy";
|
||||
private const string ACTION_MODIFY_CONTENTS = "modify_contents";
|
||||
private const string SupportedActions = ACTION_CREATE_FROM_GAMEOBJECT + ", " + ACTION_GET_INFO + ", " + ACTION_GET_HIERARCHY + ", " + ACTION_MODIFY_CONTENTS;
|
||||
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
if (@params == null)
|
||||
{
|
||||
return new ErrorResponse("Parameters cannot be null.");
|
||||
}
|
||||
|
||||
string action = @params["action"]?.ToString()?.ToLowerInvariant();
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return new ErrorResponse($"Action parameter is required. Valid actions are: {SupportedActions}.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case ACTION_CREATE_FROM_GAMEOBJECT:
|
||||
return CreatePrefabFromGameObject(@params);
|
||||
case ACTION_GET_INFO:
|
||||
return GetInfo(@params);
|
||||
case ACTION_GET_HIERARCHY:
|
||||
return GetHierarchy(@params);
|
||||
case ACTION_MODIFY_CONTENTS:
|
||||
return ModifyContents(@params);
|
||||
default:
|
||||
return new ErrorResponse($"Unknown action: '{action}'. Valid actions are: {SupportedActions}.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Error($"[ManagePrefabs] Action '{action}' failed: {e}");
|
||||
return new ErrorResponse($"Internal error: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
#region Create Prefab from GameObject
|
||||
|
||||
/// <summary>
|
||||
/// Creates a prefab asset from a GameObject in the scene.
|
||||
/// </summary>
|
||||
private static object CreatePrefabFromGameObject(JObject @params)
|
||||
{
|
||||
// 1. Validate and parse parameters
|
||||
var validation = ValidateCreatePrefabParams(@params);
|
||||
if (!validation.isValid)
|
||||
{
|
||||
return new ErrorResponse(validation.errorMessage);
|
||||
}
|
||||
|
||||
string targetName = validation.targetName;
|
||||
string finalPath = validation.finalPath;
|
||||
bool includeInactive = validation.includeInactive;
|
||||
bool replaceExisting = validation.replaceExisting;
|
||||
bool unlinkIfInstance = validation.unlinkIfInstance;
|
||||
|
||||
// 2. Find the source object
|
||||
GameObject sourceObject = FindSceneObjectByName(targetName, includeInactive);
|
||||
if (sourceObject == null)
|
||||
{
|
||||
return new ErrorResponse($"GameObject '{targetName}' not found in the active scene or prefab stage{(includeInactive ? " (including inactive objects)" : "")}.");
|
||||
}
|
||||
|
||||
// 3. Validate source object state
|
||||
var objectValidation = ValidateSourceObjectForPrefab(sourceObject, unlinkIfInstance);
|
||||
if (!objectValidation.isValid)
|
||||
{
|
||||
return new ErrorResponse(objectValidation.errorMessage);
|
||||
}
|
||||
|
||||
// 4. Check for path conflicts and track if file will be replaced
|
||||
bool fileExistedAtPath = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(finalPath) != null;
|
||||
|
||||
if (!replaceExisting && fileExistedAtPath)
|
||||
{
|
||||
finalPath = AssetDatabase.GenerateUniqueAssetPath(finalPath);
|
||||
McpLog.Info($"[ManagePrefabs] Generated unique path: {finalPath}");
|
||||
}
|
||||
|
||||
// 5. Ensure directory exists
|
||||
EnsureAssetDirectoryExists(finalPath);
|
||||
|
||||
// 6. Unlink from existing prefab if needed
|
||||
if (unlinkIfInstance && objectValidation.shouldUnlink)
|
||||
{
|
||||
try
|
||||
{
|
||||
// UnpackPrefabInstance requires the prefab instance root, not a child object
|
||||
GameObject rootToUnlink = PrefabUtility.GetOutermostPrefabInstanceRoot(sourceObject);
|
||||
if (rootToUnlink != null)
|
||||
{
|
||||
PrefabUtility.UnpackPrefabInstance(rootToUnlink, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
|
||||
McpLog.Info($"[ManagePrefabs] Unpacked prefab instance '{rootToUnlink.name}' before creating new prefab.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Failed to unlink prefab instance: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Create the prefab
|
||||
try
|
||||
{
|
||||
GameObject result = CreatePrefabAsset(sourceObject, finalPath, replaceExisting);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return new ErrorResponse($"Failed to create prefab asset at '{finalPath}'.");
|
||||
}
|
||||
|
||||
// 8. Select the newly created instance
|
||||
Selection.activeGameObject = result;
|
||||
|
||||
return new SuccessResponse(
|
||||
$"Prefab created at '{finalPath}' and instance linked.",
|
||||
new
|
||||
{
|
||||
prefabPath = finalPath,
|
||||
instanceId = result.GetInstanceID(),
|
||||
instanceName = result.name,
|
||||
wasUnlinked = unlinkIfInstance && objectValidation.shouldUnlink,
|
||||
wasReplaced = replaceExisting && fileExistedAtPath,
|
||||
componentCount = result.GetComponents<Component>().Length,
|
||||
childCount = result.transform.childCount
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Error($"[ManagePrefabs] Error creating prefab at '{finalPath}': {e}");
|
||||
return new ErrorResponse($"Error saving prefab asset: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates parameters for creating a prefab from GameObject.
|
||||
/// </summary>
|
||||
private static (bool isValid, string errorMessage, string targetName, string finalPath, bool includeInactive, bool replaceExisting, bool unlinkIfInstance)
|
||||
ValidateCreatePrefabParams(JObject @params)
|
||||
{
|
||||
string targetName = @params["target"]?.ToString() ?? @params["name"]?.ToString();
|
||||
if (string.IsNullOrEmpty(targetName))
|
||||
{
|
||||
return (false, "'target' parameter is required for create_from_gameobject.", null, null, false, false, false);
|
||||
}
|
||||
|
||||
string requestedPath = @params["prefabPath"]?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(requestedPath))
|
||||
{
|
||||
return (false, "'prefabPath' parameter is required for create_from_gameobject.", targetName, null, false, false, false);
|
||||
}
|
||||
|
||||
string sanitizedPath = AssetPathUtility.SanitizeAssetPath(requestedPath);
|
||||
if (sanitizedPath == null)
|
||||
{
|
||||
return (false, $"Invalid prefab path (path traversal detected): '{requestedPath}'", targetName, null, false, false, false);
|
||||
}
|
||||
if (string.IsNullOrEmpty(sanitizedPath))
|
||||
{
|
||||
return (false, $"Invalid prefab path '{requestedPath}'. Path cannot be empty.", targetName, null, false, false, false);
|
||||
}
|
||||
if (!sanitizedPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
sanitizedPath += ".prefab";
|
||||
}
|
||||
|
||||
// Validate path is within Assets folder
|
||||
if (!sanitizedPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (false, $"Prefab path must be within the Assets folder. Got: '{sanitizedPath}'", targetName, null, false, false, false);
|
||||
}
|
||||
|
||||
bool includeInactive = @params["searchInactive"]?.ToObject<bool>() ?? false;
|
||||
bool replaceExisting = @params["allowOverwrite"]?.ToObject<bool>() ?? false;
|
||||
bool unlinkIfInstance = @params["unlinkIfInstance"]?.ToObject<bool>() ?? false;
|
||||
|
||||
return (true, null, targetName, sanitizedPath, includeInactive, replaceExisting, unlinkIfInstance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates source object can be converted to prefab.
|
||||
/// </summary>
|
||||
private static (bool isValid, string errorMessage, bool shouldUnlink, string existingPrefabPath)
|
||||
ValidateSourceObjectForPrefab(GameObject sourceObject, bool unlinkIfInstance)
|
||||
{
|
||||
// Check if this is a Prefab Asset (the .prefab file itself in the editor)
|
||||
if (PrefabUtility.IsPartOfPrefabAsset(sourceObject))
|
||||
{
|
||||
return (false,
|
||||
$"GameObject '{sourceObject.name}' is part of a prefab asset. " +
|
||||
"Open the prefab stage to save changes instead.",
|
||||
false, null);
|
||||
}
|
||||
|
||||
// Check if this is already a Prefab Instance
|
||||
PrefabInstanceStatus status = PrefabUtility.GetPrefabInstanceStatus(sourceObject);
|
||||
if (status != PrefabInstanceStatus.NotAPrefab)
|
||||
{
|
||||
string existingPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(sourceObject);
|
||||
|
||||
if (!unlinkIfInstance)
|
||||
{
|
||||
return (false,
|
||||
$"GameObject '{sourceObject.name}' is already linked to prefab '{existingPath}'. " +
|
||||
"Set 'unlinkIfInstance' to true to unlink it first, or modify the existing prefab instead.",
|
||||
false, existingPath);
|
||||
}
|
||||
|
||||
// Needs to be unlinked
|
||||
return (true, null, true, existingPath);
|
||||
}
|
||||
|
||||
return (true, null, false, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a prefab asset from a GameObject.
|
||||
/// </summary>
|
||||
private static GameObject CreatePrefabAsset(GameObject sourceObject, string path, bool replaceExisting)
|
||||
{
|
||||
GameObject result = PrefabUtility.SaveAsPrefabAssetAndConnect(
|
||||
sourceObject,
|
||||
path,
|
||||
InteractionMode.AutomatedAction
|
||||
);
|
||||
|
||||
string action = replaceExisting ? "Replaced existing" : "Created new";
|
||||
McpLog.Info($"[ManagePrefabs] {action} prefab at '{path}'.");
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the directory for an asset path exists, creating it if necessary.
|
||||
/// </summary>
|
||||
private static void EnsureAssetDirectoryExists(string assetPath)
|
||||
{
|
||||
string directory = Path.GetDirectoryName(assetPath);
|
||||
if (string.IsNullOrEmpty(directory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Use Application.dataPath for more reliable path resolution
|
||||
// Application.dataPath points to the Assets folder (e.g., ".../ProjectName/Assets")
|
||||
string assetsPath = Application.dataPath;
|
||||
string projectRoot = Path.GetDirectoryName(assetsPath);
|
||||
string fullDirectory = Path.Combine(projectRoot, directory);
|
||||
|
||||
if (!Directory.Exists(fullDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(fullDirectory);
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
||||
McpLog.Info($"[ManagePrefabs] Created directory: {directory}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a GameObject by name in the active scene or current prefab stage.
|
||||
/// </summary>
|
||||
private static GameObject FindSceneObjectByName(string name, bool includeInactive)
|
||||
{
|
||||
// First check if we're in Prefab Stage
|
||||
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||
if (stage?.prefabContentsRoot != null)
|
||||
{
|
||||
foreach (Transform transform in stage.prefabContentsRoot.GetComponentsInChildren<Transform>(includeInactive))
|
||||
{
|
||||
if (transform.name == name && (includeInactive || transform.gameObject.activeSelf))
|
||||
{
|
||||
return transform.gameObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search in the active scene
|
||||
Scene activeScene = SceneManager.GetActiveScene();
|
||||
foreach (GameObject root in activeScene.GetRootGameObjects())
|
||||
{
|
||||
// Check the root object itself
|
||||
if (root.name == name && (includeInactive || root.activeSelf))
|
||||
{
|
||||
return root;
|
||||
}
|
||||
|
||||
// Check children
|
||||
foreach (Transform transform in root.GetComponentsInChildren<Transform>(includeInactive))
|
||||
{
|
||||
if (transform.name == name && (includeInactive || transform.gameObject.activeSelf))
|
||||
{
|
||||
return transform.gameObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#region Read Operations
|
||||
|
||||
/// <summary>
|
||||
/// Gets basic metadata information about a prefab asset.
|
||||
/// </summary>
|
||||
private static object GetInfo(JObject @params)
|
||||
{
|
||||
string prefabPath = @params["prefabPath"]?.ToString() ?? @params["path"]?.ToString();
|
||||
if (string.IsNullOrEmpty(prefabPath))
|
||||
{
|
||||
return new ErrorResponse("'prefabPath' parameter is required for get_info.");
|
||||
}
|
||||
|
||||
string sanitizedPath = AssetPathUtility.SanitizeAssetPath(prefabPath);
|
||||
if (string.IsNullOrEmpty(sanitizedPath))
|
||||
{
|
||||
return new ErrorResponse($"Invalid prefab path: '{prefabPath}'.");
|
||||
}
|
||||
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(sanitizedPath);
|
||||
if (prefabAsset == null)
|
||||
{
|
||||
return new ErrorResponse($"No prefab asset found at path '{sanitizedPath}'.");
|
||||
}
|
||||
|
||||
string guid = PrefabUtilityHelper.GetPrefabGUID(sanitizedPath);
|
||||
PrefabAssetType assetType = PrefabUtility.GetPrefabAssetType(prefabAsset);
|
||||
string prefabTypeString = assetType.ToString();
|
||||
var componentTypes = PrefabUtilityHelper.GetComponentTypeNames(prefabAsset);
|
||||
int childCount = PrefabUtilityHelper.CountChildrenRecursive(prefabAsset.transform);
|
||||
var (isVariant, parentPrefab, _) = PrefabUtilityHelper.GetVariantInfo(prefabAsset);
|
||||
|
||||
return new SuccessResponse(
|
||||
$"Successfully retrieved prefab info.",
|
||||
new
|
||||
{
|
||||
assetPath = sanitizedPath,
|
||||
guid = guid,
|
||||
prefabType = prefabTypeString,
|
||||
rootObjectName = prefabAsset.name,
|
||||
rootComponentTypes = componentTypes,
|
||||
childCount = childCount,
|
||||
isVariant = isVariant,
|
||||
parentPrefab = parentPrefab
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the hierarchical structure of a prefab asset.
|
||||
/// Returns all objects in the prefab for full client-side filtering and search.
|
||||
/// </summary>
|
||||
private static object GetHierarchy(JObject @params)
|
||||
{
|
||||
string prefabPath = @params["prefabPath"]?.ToString() ?? @params["path"]?.ToString();
|
||||
if (string.IsNullOrEmpty(prefabPath))
|
||||
{
|
||||
return new ErrorResponse("'prefabPath' parameter is required for get_hierarchy.");
|
||||
}
|
||||
|
||||
string sanitizedPath = AssetPathUtility.SanitizeAssetPath(prefabPath);
|
||||
if (string.IsNullOrEmpty(sanitizedPath))
|
||||
{
|
||||
return new ErrorResponse($"Invalid prefab path '{prefabPath}'. Path traversal sequences are not allowed.");
|
||||
}
|
||||
|
||||
// Load prefab contents in background (without opening stage UI)
|
||||
GameObject prefabContents = PrefabUtility.LoadPrefabContents(sanitizedPath);
|
||||
if (prefabContents == null)
|
||||
{
|
||||
return new ErrorResponse($"Failed to load prefab contents from '{sanitizedPath}'.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Build complete hierarchy items (no pagination)
|
||||
var allItems = BuildHierarchyItems(prefabContents.transform, sanitizedPath);
|
||||
|
||||
return new SuccessResponse(
|
||||
$"Successfully retrieved prefab hierarchy. Found {allItems.Count} objects.",
|
||||
new
|
||||
{
|
||||
prefabPath = sanitizedPath,
|
||||
total = allItems.Count,
|
||||
items = allItems
|
||||
}
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Always unload prefab contents to free memory
|
||||
PrefabUtility.UnloadPrefabContents(prefabContents);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Headless Prefab Editing
|
||||
|
||||
/// <summary>
|
||||
/// Modifies a prefab's contents directly without opening the prefab stage.
|
||||
/// This is ideal for automated/agentic workflows as it avoids UI, dirty flags, and dialogs.
|
||||
/// </summary>
|
||||
private static object ModifyContents(JObject @params)
|
||||
{
|
||||
string prefabPath = @params["prefabPath"]?.ToString() ?? @params["path"]?.ToString();
|
||||
if (string.IsNullOrEmpty(prefabPath))
|
||||
{
|
||||
return new ErrorResponse("'prefabPath' parameter is required for modify_contents.");
|
||||
}
|
||||
|
||||
string sanitizedPath = AssetPathUtility.SanitizeAssetPath(prefabPath);
|
||||
if (string.IsNullOrEmpty(sanitizedPath))
|
||||
{
|
||||
return new ErrorResponse($"Invalid prefab path '{prefabPath}'. Path traversal sequences are not allowed.");
|
||||
}
|
||||
|
||||
// Load prefab contents in isolated context (no UI)
|
||||
GameObject prefabContents = PrefabUtility.LoadPrefabContents(sanitizedPath);
|
||||
if (prefabContents == null)
|
||||
{
|
||||
return new ErrorResponse($"Failed to load prefab contents from '{sanitizedPath}'.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Find target object within the prefab (defaults to root)
|
||||
string targetName = @params["target"]?.ToString();
|
||||
GameObject targetGo = FindInPrefabContents(prefabContents, targetName);
|
||||
|
||||
if (targetGo == null)
|
||||
{
|
||||
string searchedFor = string.IsNullOrEmpty(targetName) ? "root" : $"'{targetName}'";
|
||||
return new ErrorResponse($"Target {searchedFor} not found in prefab '{sanitizedPath}'.");
|
||||
}
|
||||
|
||||
// Apply modifications
|
||||
var modifyResult = ApplyModificationsToPrefabObject(targetGo, @params, prefabContents);
|
||||
if (modifyResult.error != null)
|
||||
{
|
||||
return modifyResult.error;
|
||||
}
|
||||
|
||||
// Skip saving when no modifications were made to avoid unnecessary asset writes
|
||||
if (!modifyResult.modified)
|
||||
{
|
||||
return new SuccessResponse(
|
||||
$"Prefab '{sanitizedPath}' is already up to date; no changes were applied.",
|
||||
new
|
||||
{
|
||||
prefabPath = sanitizedPath,
|
||||
targetName = targetGo.name,
|
||||
modified = false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Save the prefab
|
||||
bool success;
|
||||
PrefabUtility.SaveAsPrefabAsset(prefabContents, sanitizedPath, out success);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
return new ErrorResponse($"Failed to save prefab asset at '{sanitizedPath}'.");
|
||||
}
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
McpLog.Info($"[ManagePrefabs] Successfully modified and saved prefab '{sanitizedPath}' (headless).");
|
||||
|
||||
return new SuccessResponse(
|
||||
$"Prefab '{sanitizedPath}' modified and saved successfully.",
|
||||
new
|
||||
{
|
||||
prefabPath = sanitizedPath,
|
||||
targetName = targetGo.name,
|
||||
modified = modifyResult.modified,
|
||||
transform = new
|
||||
{
|
||||
position = new { x = targetGo.transform.localPosition.x, y = targetGo.transform.localPosition.y, z = targetGo.transform.localPosition.z },
|
||||
rotation = new { x = targetGo.transform.localEulerAngles.x, y = targetGo.transform.localEulerAngles.y, z = targetGo.transform.localEulerAngles.z },
|
||||
scale = new { x = targetGo.transform.localScale.x, y = targetGo.transform.localScale.y, z = targetGo.transform.localScale.z }
|
||||
},
|
||||
componentTypes = PrefabUtilityHelper.GetComponentTypeNames(targetGo)
|
||||
}
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Always unload prefab contents to free memory
|
||||
PrefabUtility.UnloadPrefabContents(prefabContents);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a GameObject within loaded prefab contents by name or path.
|
||||
/// </summary>
|
||||
private static GameObject FindInPrefabContents(GameObject prefabContents, string target)
|
||||
{
|
||||
if (string.IsNullOrEmpty(target))
|
||||
{
|
||||
// Return root if no target specified
|
||||
return prefabContents;
|
||||
}
|
||||
|
||||
// Try to find by path first (e.g., "Parent/Child/Target")
|
||||
if (target.Contains("/"))
|
||||
{
|
||||
Transform found = prefabContents.transform.Find(target);
|
||||
if (found != null)
|
||||
{
|
||||
return found.gameObject;
|
||||
}
|
||||
|
||||
// If path starts with root name, try without it
|
||||
if (target.StartsWith(prefabContents.name + "/"))
|
||||
{
|
||||
string relativePath = target.Substring(prefabContents.name.Length + 1);
|
||||
found = prefabContents.transform.Find(relativePath);
|
||||
if (found != null)
|
||||
{
|
||||
return found.gameObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if target matches root name
|
||||
if (prefabContents.name == target)
|
||||
{
|
||||
return prefabContents;
|
||||
}
|
||||
|
||||
// Search by name in hierarchy
|
||||
foreach (Transform t in prefabContents.GetComponentsInChildren<Transform>(true))
|
||||
{
|
||||
if (t.gameObject.name == target)
|
||||
{
|
||||
return t.gameObject;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies modifications to a GameObject within loaded prefab contents.
|
||||
/// Returns (modified: bool, error: ErrorResponse or null).
|
||||
/// </summary>
|
||||
private static (bool modified, ErrorResponse error) ApplyModificationsToPrefabObject(GameObject targetGo, JObject @params, GameObject prefabRoot)
|
||||
{
|
||||
bool modified = false;
|
||||
|
||||
// Name change
|
||||
string newName = @params["name"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(newName) && targetGo.name != newName)
|
||||
{
|
||||
// If renaming the root, this will affect the prefab asset name on save
|
||||
targetGo.name = newName;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Active state
|
||||
bool? setActive = @params["setActive"]?.ToObject<bool?>();
|
||||
if (setActive.HasValue && targetGo.activeSelf != setActive.Value)
|
||||
{
|
||||
targetGo.SetActive(setActive.Value);
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Tag
|
||||
string tag = @params["tag"]?.ToString();
|
||||
if (tag != null && targetGo.tag != tag)
|
||||
{
|
||||
string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag;
|
||||
try
|
||||
{
|
||||
targetGo.tag = tagToSet;
|
||||
modified = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, new ErrorResponse($"Failed to set tag to '{tagToSet}': {ex.Message}"));
|
||||
}
|
||||
}
|
||||
|
||||
// Layer
|
||||
string layerName = @params["layer"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(layerName))
|
||||
{
|
||||
int layerId = LayerMask.NameToLayer(layerName);
|
||||
if (layerId == -1)
|
||||
{
|
||||
return (false, new ErrorResponse($"Invalid layer specified: '{layerName}'. Use a valid layer name."));
|
||||
}
|
||||
if (targetGo.layer != layerId)
|
||||
{
|
||||
targetGo.layer = layerId;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Transform: position, rotation, scale
|
||||
Vector3? position = VectorParsing.ParseVector3(@params["position"]);
|
||||
Vector3? rotation = VectorParsing.ParseVector3(@params["rotation"]);
|
||||
Vector3? scale = VectorParsing.ParseVector3(@params["scale"]);
|
||||
|
||||
if (position.HasValue && targetGo.transform.localPosition != position.Value)
|
||||
{
|
||||
targetGo.transform.localPosition = position.Value;
|
||||
modified = true;
|
||||
}
|
||||
if (rotation.HasValue && targetGo.transform.localEulerAngles != rotation.Value)
|
||||
{
|
||||
targetGo.transform.localEulerAngles = rotation.Value;
|
||||
modified = true;
|
||||
}
|
||||
if (scale.HasValue && targetGo.transform.localScale != scale.Value)
|
||||
{
|
||||
targetGo.transform.localScale = scale.Value;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Parent change (within prefab hierarchy)
|
||||
JToken parentToken = @params["parent"];
|
||||
if (parentToken != null)
|
||||
{
|
||||
string parentTarget = parentToken.ToString();
|
||||
Transform newParent = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(parentTarget))
|
||||
{
|
||||
GameObject parentGo = FindInPrefabContents(prefabRoot, parentTarget);
|
||||
if (parentGo == null)
|
||||
{
|
||||
return (false, new ErrorResponse($"Parent '{parentTarget}' not found in prefab."));
|
||||
}
|
||||
if (parentGo.transform.IsChildOf(targetGo.transform))
|
||||
{
|
||||
return (false, new ErrorResponse($"Cannot parent '{targetGo.name}' to '{parentGo.name}' as it would create a hierarchy loop."));
|
||||
}
|
||||
newParent = parentGo.transform;
|
||||
}
|
||||
|
||||
if (targetGo.transform.parent != newParent)
|
||||
{
|
||||
targetGo.transform.SetParent(newParent, true);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Components to add
|
||||
if (@params["componentsToAdd"] is JArray componentsToAdd)
|
||||
{
|
||||
foreach (var compToken in componentsToAdd)
|
||||
{
|
||||
string typeName = compToken.Type == JTokenType.String
|
||||
? compToken.ToString()
|
||||
: (compToken as JObject)?["typeName"]?.ToString();
|
||||
|
||||
if (!string.IsNullOrEmpty(typeName))
|
||||
{
|
||||
if (!ComponentResolver.TryResolve(typeName, out Type componentType, out string error))
|
||||
{
|
||||
return (false, new ErrorResponse($"Component type '{typeName}' not found: {error}"));
|
||||
}
|
||||
targetGo.AddComponent(componentType);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Components to remove
|
||||
if (@params["componentsToRemove"] is JArray componentsToRemove)
|
||||
{
|
||||
foreach (var compToken in componentsToRemove)
|
||||
{
|
||||
string typeName = compToken.ToString();
|
||||
if (!string.IsNullOrEmpty(typeName))
|
||||
{
|
||||
if (!ComponentResolver.TryResolve(typeName, out Type componentType, out string error))
|
||||
{
|
||||
return (false, new ErrorResponse($"Component type '{typeName}' not found: {error}"));
|
||||
}
|
||||
Component comp = targetGo.GetComponent(componentType);
|
||||
if (comp != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(comp);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create child GameObjects (supports single object or array)
|
||||
JToken createChildToken = @params["createChild"] ?? @params["create_child"];
|
||||
if (createChildToken != null)
|
||||
{
|
||||
// Handle array of children
|
||||
if (createChildToken is JArray childArray)
|
||||
{
|
||||
foreach (var childToken in childArray)
|
||||
{
|
||||
var childResult = CreateSingleChildInPrefab(childToken, targetGo, prefabRoot);
|
||||
if (childResult.error != null)
|
||||
{
|
||||
return (false, childResult.error);
|
||||
}
|
||||
if (childResult.created)
|
||||
{
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle single child object
|
||||
var childResult = CreateSingleChildInPrefab(createChildToken, targetGo, prefabRoot);
|
||||
if (childResult.error != null)
|
||||
{
|
||||
return (false, childResult.error);
|
||||
}
|
||||
if (childResult.created)
|
||||
{
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (modified, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a single child GameObject within the prefab contents.
|
||||
/// </summary>
|
||||
private static (bool created, ErrorResponse error) CreateSingleChildInPrefab(JToken createChildToken, GameObject defaultParent, GameObject prefabRoot)
|
||||
{
|
||||
JObject childParams;
|
||||
if (createChildToken is JObject obj)
|
||||
{
|
||||
childParams = obj;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (false, new ErrorResponse("'create_child' must be an object with child properties."));
|
||||
}
|
||||
|
||||
// Required: name
|
||||
string childName = childParams["name"]?.ToString();
|
||||
if (string.IsNullOrEmpty(childName))
|
||||
{
|
||||
return (false, new ErrorResponse("'create_child.name' is required."));
|
||||
}
|
||||
|
||||
// Optional: parent (defaults to the target object)
|
||||
string parentName = childParams["parent"]?.ToString();
|
||||
Transform parentTransform = defaultParent.transform;
|
||||
if (!string.IsNullOrEmpty(parentName))
|
||||
{
|
||||
GameObject parentGo = FindInPrefabContents(prefabRoot, parentName);
|
||||
if (parentGo == null)
|
||||
{
|
||||
return (false, new ErrorResponse($"Parent '{parentName}' not found in prefab for create_child."));
|
||||
}
|
||||
parentTransform = parentGo.transform;
|
||||
}
|
||||
|
||||
// Create the GameObject
|
||||
GameObject newChild;
|
||||
string primitiveType = childParams["primitiveType"]?.ToString() ?? childParams["primitive_type"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(primitiveType))
|
||||
{
|
||||
try
|
||||
{
|
||||
PrimitiveType type = (PrimitiveType)Enum.Parse(typeof(PrimitiveType), primitiveType, true);
|
||||
newChild = GameObject.CreatePrimitive(type);
|
||||
newChild.name = childName;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return (false, new ErrorResponse($"Invalid primitive type: '{primitiveType}'. Valid types: {string.Join(", ", Enum.GetNames(typeof(PrimitiveType)))}"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newChild = new GameObject(childName);
|
||||
}
|
||||
|
||||
// Set parent
|
||||
newChild.transform.SetParent(parentTransform, false);
|
||||
|
||||
// Apply transform properties
|
||||
Vector3? position = VectorParsing.ParseVector3(childParams["position"]);
|
||||
Vector3? rotation = VectorParsing.ParseVector3(childParams["rotation"]);
|
||||
Vector3? scale = VectorParsing.ParseVector3(childParams["scale"]);
|
||||
|
||||
if (position.HasValue)
|
||||
{
|
||||
newChild.transform.localPosition = position.Value;
|
||||
}
|
||||
if (rotation.HasValue)
|
||||
{
|
||||
newChild.transform.localEulerAngles = rotation.Value;
|
||||
}
|
||||
if (scale.HasValue)
|
||||
{
|
||||
newChild.transform.localScale = scale.Value;
|
||||
}
|
||||
|
||||
// Add components
|
||||
JArray componentsToAdd = childParams["componentsToAdd"] as JArray ?? childParams["components_to_add"] as JArray;
|
||||
if (componentsToAdd != null)
|
||||
{
|
||||
for (int i = 0; i < componentsToAdd.Count; i++)
|
||||
{
|
||||
var compToken = componentsToAdd[i];
|
||||
string typeName = compToken.Type == JTokenType.String
|
||||
? compToken.ToString()
|
||||
: (compToken as JObject)?["typeName"]?.ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(typeName))
|
||||
{
|
||||
// Clean up partially created child
|
||||
UnityEngine.Object.DestroyImmediate(newChild);
|
||||
return (false, new ErrorResponse($"create_child.components_to_add[{i}] must be a string or object with 'typeName' field, got {compToken.Type}"));
|
||||
}
|
||||
|
||||
if (!ComponentResolver.TryResolve(typeName, out Type componentType, out string error))
|
||||
{
|
||||
// Clean up partially created child
|
||||
UnityEngine.Object.DestroyImmediate(newChild);
|
||||
return (false, new ErrorResponse($"Component type '{typeName}' not found for create_child: {error}"));
|
||||
}
|
||||
newChild.AddComponent(componentType);
|
||||
}
|
||||
}
|
||||
|
||||
// Set tag if specified
|
||||
string tag = childParams["tag"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(tag))
|
||||
{
|
||||
try
|
||||
{
|
||||
newChild.tag = tag;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newChild);
|
||||
return (false, new ErrorResponse($"Failed to set tag '{tag}' on child '{childName}': {ex.Message}"));
|
||||
}
|
||||
}
|
||||
|
||||
// Set layer if specified
|
||||
string layerName = childParams["layer"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(layerName))
|
||||
{
|
||||
int layerId = LayerMask.NameToLayer(layerName);
|
||||
if (layerId == -1)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newChild);
|
||||
return (false, new ErrorResponse($"Invalid layer '{layerName}' for child '{childName}'. Use a valid layer name."));
|
||||
}
|
||||
newChild.layer = layerId;
|
||||
}
|
||||
|
||||
// Set active state
|
||||
bool? setActive = childParams["setActive"]?.ToObject<bool?>() ?? childParams["set_active"]?.ToObject<bool?>();
|
||||
if (setActive.HasValue)
|
||||
{
|
||||
newChild.SetActive(setActive.Value);
|
||||
}
|
||||
|
||||
McpLog.Info($"[ManagePrefabs] Created child '{childName}' under '{parentTransform.name}' in prefab.");
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Hierarchy Builder
|
||||
|
||||
/// <summary>
|
||||
/// Builds a flat list of hierarchy items from a transform root.
|
||||
/// </summary>
|
||||
/// <param name="root">The root transform of the prefab.</param>
|
||||
/// <param name="mainPrefabPath">Asset path of the main prefab.</param>
|
||||
/// <returns>List of hierarchy items with prefab information.</returns>
|
||||
private static List<object> BuildHierarchyItems(Transform root, string mainPrefabPath)
|
||||
{
|
||||
var items = new List<object>();
|
||||
BuildHierarchyItemsRecursive(root, root, mainPrefabPath, "", items);
|
||||
return items;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively builds hierarchy items.
|
||||
/// </summary>
|
||||
/// <param name="transform">Current transform being processed.</param>
|
||||
/// <param name="mainPrefabRoot">Root transform of the main prefab asset.</param>
|
||||
/// <param name="mainPrefabPath">Asset path of the main prefab.</param>
|
||||
/// <param name="parentPath">Parent path for building full hierarchy path.</param>
|
||||
/// <param name="items">List to accumulate hierarchy items.</param>
|
||||
private static void BuildHierarchyItemsRecursive(Transform transform, Transform mainPrefabRoot, string mainPrefabPath, string parentPath, List<object> items)
|
||||
{
|
||||
if (transform == null) return;
|
||||
|
||||
string name = transform.gameObject.name;
|
||||
string path = string.IsNullOrEmpty(parentPath) ? name : $"{parentPath}/{name}";
|
||||
int instanceId = transform.gameObject.GetInstanceID();
|
||||
bool activeSelf = transform.gameObject.activeSelf;
|
||||
int childCount = transform.childCount;
|
||||
var componentTypes = PrefabUtilityHelper.GetComponentTypeNames(transform.gameObject);
|
||||
|
||||
// Prefab information
|
||||
bool isNestedPrefab = PrefabUtility.IsAnyPrefabInstanceRoot(transform.gameObject);
|
||||
bool isPrefabRoot = transform == mainPrefabRoot;
|
||||
int nestingDepth = isPrefabRoot ? 0 : PrefabUtilityHelper.GetPrefabNestingDepth(transform.gameObject, mainPrefabRoot);
|
||||
string parentPrefabPath = isNestedPrefab && !isPrefabRoot
|
||||
? PrefabUtilityHelper.GetParentPrefabPath(transform.gameObject, mainPrefabRoot)
|
||||
: null;
|
||||
string nestedPrefabPath = isNestedPrefab ? PrefabUtilityHelper.GetNestedPrefabPath(transform.gameObject) : null;
|
||||
|
||||
var item = new
|
||||
{
|
||||
name = name,
|
||||
instanceId = instanceId,
|
||||
path = path,
|
||||
activeSelf = activeSelf,
|
||||
childCount = childCount,
|
||||
componentTypes = componentTypes,
|
||||
prefab = new
|
||||
{
|
||||
isRoot = isPrefabRoot,
|
||||
isNestedRoot = isNestedPrefab,
|
||||
nestingDepth = nestingDepth,
|
||||
assetPath = isNestedPrefab ? nestedPrefabPath : mainPrefabPath,
|
||||
parentPath = parentPrefabPath
|
||||
}
|
||||
};
|
||||
|
||||
items.Add(item);
|
||||
|
||||
// Recursively process children
|
||||
foreach (Transform child in transform)
|
||||
{
|
||||
BuildHierarchyItemsRecursive(child, mainPrefabRoot, mainPrefabPath, path, items);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c14e76b2aa7bb4570a88903b061e946e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
641
Packages/MCPForUnity/Editor/Tools/ReadConsole.cs
Normal file
641
Packages/MCPForUnity/Editor/Tools/ReadConsole.cs
Normal file
@@ -0,0 +1,641 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using MCPForUnity.Editor.Helpers; // For Response class
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles reading and clearing Unity Editor console log entries.
|
||||
/// Uses reflection to access internal LogEntry methods/properties.
|
||||
/// </summary>
|
||||
[McpForUnityTool("read_console", AutoRegister = false)]
|
||||
public static class ReadConsole
|
||||
{
|
||||
// (Calibration removed)
|
||||
|
||||
// Reflection members for accessing internal LogEntry data
|
||||
// private static MethodInfo _getEntriesMethod; // Removed as it's unused and fails reflection
|
||||
private static MethodInfo _startGettingEntriesMethod;
|
||||
private static MethodInfo _endGettingEntriesMethod; // Renamed from _stopGettingEntriesMethod, trying End...
|
||||
private static MethodInfo _clearMethod;
|
||||
private static MethodInfo _getCountMethod;
|
||||
private static MethodInfo _getEntryMethod;
|
||||
private static FieldInfo _modeField;
|
||||
private static FieldInfo _messageField;
|
||||
private static FieldInfo _fileField;
|
||||
private static FieldInfo _lineField;
|
||||
private static FieldInfo _instanceIdField;
|
||||
|
||||
// Note: Timestamp is not directly available in LogEntry; need to parse message or find alternative?
|
||||
|
||||
// Static constructor for reflection setup
|
||||
static ReadConsole()
|
||||
{
|
||||
try
|
||||
{
|
||||
Type logEntriesType = typeof(EditorApplication).Assembly.GetType(
|
||||
"UnityEditor.LogEntries"
|
||||
);
|
||||
if (logEntriesType == null)
|
||||
throw new Exception("Could not find internal type UnityEditor.LogEntries");
|
||||
|
||||
|
||||
|
||||
// Include NonPublic binding flags as internal APIs might change accessibility
|
||||
BindingFlags staticFlags =
|
||||
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
BindingFlags instanceFlags =
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
|
||||
_startGettingEntriesMethod = logEntriesType.GetMethod(
|
||||
"StartGettingEntries",
|
||||
staticFlags
|
||||
);
|
||||
if (_startGettingEntriesMethod == null)
|
||||
throw new Exception("Failed to reflect LogEntries.StartGettingEntries");
|
||||
|
||||
// Try reflecting EndGettingEntries based on warning message
|
||||
_endGettingEntriesMethod = logEntriesType.GetMethod(
|
||||
"EndGettingEntries",
|
||||
staticFlags
|
||||
);
|
||||
if (_endGettingEntriesMethod == null)
|
||||
throw new Exception("Failed to reflect LogEntries.EndGettingEntries");
|
||||
|
||||
_clearMethod = logEntriesType.GetMethod("Clear", staticFlags);
|
||||
if (_clearMethod == null)
|
||||
throw new Exception("Failed to reflect LogEntries.Clear");
|
||||
|
||||
_getCountMethod = logEntriesType.GetMethod("GetCount", staticFlags);
|
||||
if (_getCountMethod == null)
|
||||
throw new Exception("Failed to reflect LogEntries.GetCount");
|
||||
|
||||
_getEntryMethod = logEntriesType.GetMethod("GetEntryInternal", staticFlags);
|
||||
if (_getEntryMethod == null)
|
||||
throw new Exception("Failed to reflect LogEntries.GetEntryInternal");
|
||||
|
||||
Type logEntryType = typeof(EditorApplication).Assembly.GetType(
|
||||
"UnityEditor.LogEntry"
|
||||
);
|
||||
if (logEntryType == null)
|
||||
throw new Exception("Could not find internal type UnityEditor.LogEntry");
|
||||
|
||||
_modeField = logEntryType.GetField("mode", instanceFlags);
|
||||
if (_modeField == null)
|
||||
throw new Exception("Failed to reflect LogEntry.mode");
|
||||
|
||||
_messageField = logEntryType.GetField("message", instanceFlags);
|
||||
if (_messageField == null)
|
||||
throw new Exception("Failed to reflect LogEntry.message");
|
||||
|
||||
_fileField = logEntryType.GetField("file", instanceFlags);
|
||||
if (_fileField == null)
|
||||
throw new Exception("Failed to reflect LogEntry.file");
|
||||
|
||||
_lineField = logEntryType.GetField("line", instanceFlags);
|
||||
if (_lineField == null)
|
||||
throw new Exception("Failed to reflect LogEntry.line");
|
||||
|
||||
_instanceIdField = logEntryType.GetField("instanceID", instanceFlags);
|
||||
if (_instanceIdField == null)
|
||||
throw new Exception("Failed to reflect LogEntry.instanceID");
|
||||
|
||||
// (Calibration removed)
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Error(
|
||||
$"[ReadConsole] Static Initialization Failed: Could not setup reflection for LogEntries/LogEntry. Console reading/clearing will likely fail. Specific Error: {e.Message}"
|
||||
);
|
||||
// Set members to null to prevent NullReferenceExceptions later, HandleCommand should check this.
|
||||
_startGettingEntriesMethod =
|
||||
_endGettingEntriesMethod =
|
||||
_clearMethod =
|
||||
_getCountMethod =
|
||||
_getEntryMethod =
|
||||
null;
|
||||
_modeField = _messageField = _fileField = _lineField = _instanceIdField = null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main Handler ---
|
||||
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
// Check if ALL required reflection members were successfully initialized.
|
||||
if (
|
||||
_startGettingEntriesMethod == null
|
||||
|| _endGettingEntriesMethod == null
|
||||
|| _clearMethod == null
|
||||
|| _getCountMethod == null
|
||||
|| _getEntryMethod == null
|
||||
|| _modeField == null
|
||||
|| _messageField == null
|
||||
|| _fileField == null
|
||||
|| _lineField == null
|
||||
|| _instanceIdField == null
|
||||
)
|
||||
{
|
||||
// Log the error here as well for easier debugging in Unity Console
|
||||
McpLog.Error(
|
||||
"[ReadConsole] HandleCommand called but reflection members are not initialized. Static constructor might have failed silently or there's an issue."
|
||||
);
|
||||
return new ErrorResponse(
|
||||
"ReadConsole handler failed to initialize due to reflection errors. Cannot access console logs."
|
||||
);
|
||||
}
|
||||
|
||||
if (@params == null)
|
||||
{
|
||||
return new ErrorResponse("Parameters cannot be null.");
|
||||
}
|
||||
|
||||
var p = new ToolParams(@params);
|
||||
string action = p.Get("action", "get").ToLower();
|
||||
|
||||
try
|
||||
{
|
||||
if (action == "clear")
|
||||
{
|
||||
return ClearConsole();
|
||||
}
|
||||
else if (action == "get")
|
||||
{
|
||||
// Extract parameters for 'get'
|
||||
var types =
|
||||
(p.GetRaw("types") as JArray)?.Select(t => t.ToString().ToLower()).ToList()
|
||||
?? new List<string> { "error", "warning" };
|
||||
int? count = p.GetInt("count");
|
||||
int? pageSize = p.GetInt("pageSize");
|
||||
int? cursor = p.GetInt("cursor");
|
||||
string filterText = p.Get("filterText");
|
||||
string sinceTimestampStr = p.Get("sinceTimestamp"); // TODO: Implement timestamp filtering
|
||||
string format = p.Get("format", "plain").ToLower();
|
||||
bool includeStacktrace = p.GetBool("includeStacktrace", false);
|
||||
|
||||
if (types.Contains("all"))
|
||||
{
|
||||
types = new List<string> { "error", "warning", "log" }; // Expand 'all'
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(sinceTimestampStr))
|
||||
{
|
||||
McpLog.Warn(
|
||||
"[ReadConsole] Filtering by 'since_timestamp' is not currently implemented."
|
||||
);
|
||||
// Need a way to get timestamp per log entry.
|
||||
}
|
||||
|
||||
return GetConsoleEntries(
|
||||
types,
|
||||
count,
|
||||
pageSize,
|
||||
cursor,
|
||||
filterText,
|
||||
format,
|
||||
includeStacktrace
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Unknown action: '{action}'. Valid actions are 'get' or 'clear'."
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Error($"[ReadConsole] Action '{action}' failed: {e}");
|
||||
return new ErrorResponse($"Internal error processing action '{action}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Action Implementations ---
|
||||
|
||||
private static object ClearConsole()
|
||||
{
|
||||
try
|
||||
{
|
||||
_clearMethod.Invoke(null, null); // Static method, no instance, no parameters
|
||||
return new SuccessResponse("Console cleared successfully.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Error($"[ReadConsole] Failed to clear console: {e}");
|
||||
return new ErrorResponse($"Failed to clear console: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves console log entries with optional filtering and paging.
|
||||
/// </summary>
|
||||
/// <param name="types">Log types to include (e.g., "error", "warning", "log").</param>
|
||||
/// <param name="count">Maximum entries to return in non-paging mode. Ignored when paging is active.</param>
|
||||
/// <param name="pageSize">Number of entries per page. Defaults to 50 when omitted.</param>
|
||||
/// <param name="cursor">Starting index for paging (0-based). Defaults to 0.</param>
|
||||
/// <param name="filterText">Optional text filter (case-insensitive substring match).</param>
|
||||
/// <param name="format">Output format: "plain", "detailed", or "json".</param>
|
||||
/// <param name="includeStacktrace">Whether to include stack traces in the output.</param>
|
||||
/// <returns>A success response with entries, or an error response.</returns>
|
||||
private static object GetConsoleEntries(
|
||||
List<string> types,
|
||||
int? count,
|
||||
int? pageSize,
|
||||
int? cursor,
|
||||
string filterText,
|
||||
string format,
|
||||
bool includeStacktrace
|
||||
)
|
||||
{
|
||||
List<object> formattedEntries = new List<object>();
|
||||
int retrievedCount = 0;
|
||||
int totalMatches = 0;
|
||||
bool usePaging = pageSize.HasValue || cursor.HasValue;
|
||||
// pageSize defaults to 50 when omitted; count is the overall non-paging limit only
|
||||
int resolvedPageSize = Mathf.Clamp(pageSize ?? 50, 1, 500);
|
||||
int resolvedCursor = Mathf.Max(0, cursor ?? 0);
|
||||
int pageEndExclusive = resolvedCursor + resolvedPageSize;
|
||||
|
||||
try
|
||||
{
|
||||
// LogEntries requires calling Start/Stop around GetEntries/GetEntryInternal
|
||||
_startGettingEntriesMethod.Invoke(null, null);
|
||||
|
||||
int totalEntries = (int)_getCountMethod.Invoke(null, null);
|
||||
// Create instance to pass to GetEntryInternal - Ensure the type is correct
|
||||
Type logEntryType = typeof(EditorApplication).Assembly.GetType(
|
||||
"UnityEditor.LogEntry"
|
||||
);
|
||||
if (logEntryType == null)
|
||||
throw new Exception(
|
||||
"Could not find internal type UnityEditor.LogEntry during GetConsoleEntries."
|
||||
);
|
||||
object logEntryInstance = Activator.CreateInstance(logEntryType);
|
||||
|
||||
for (int i = 0; i < totalEntries; i++)
|
||||
{
|
||||
// Get the entry data into our instance using reflection
|
||||
_getEntryMethod.Invoke(null, new object[] { i, logEntryInstance });
|
||||
|
||||
// Extract data using reflection
|
||||
int mode = (int)_modeField.GetValue(logEntryInstance);
|
||||
string message = (string)_messageField.GetValue(logEntryInstance);
|
||||
string file = (string)_fileField.GetValue(logEntryInstance);
|
||||
|
||||
int line = (int)_lineField.GetValue(logEntryInstance);
|
||||
// int instanceId = (int)_instanceIdField.GetValue(logEntryInstance);
|
||||
|
||||
if (string.IsNullOrEmpty(message))
|
||||
{
|
||||
continue; // Skip empty messages
|
||||
}
|
||||
|
||||
// (Calibration removed)
|
||||
|
||||
// --- Filtering ---
|
||||
// Prefer classifying severity from message/stacktrace; fallback to mode bits if needed
|
||||
LogType unityType = InferTypeFromMessage(message);
|
||||
bool isExplicitDebug = IsExplicitDebugLog(message);
|
||||
if (!isExplicitDebug && unityType == LogType.Log)
|
||||
{
|
||||
unityType = GetLogTypeFromMode(mode);
|
||||
}
|
||||
|
||||
bool want;
|
||||
// Treat Exception/Assert as errors for filtering convenience
|
||||
if (unityType == LogType.Exception)
|
||||
{
|
||||
want = types.Contains("error") || types.Contains("exception");
|
||||
}
|
||||
else if (unityType == LogType.Assert)
|
||||
{
|
||||
want = types.Contains("error") || types.Contains("assert");
|
||||
}
|
||||
else
|
||||
{
|
||||
want = types.Contains(unityType.ToString().ToLowerInvariant());
|
||||
}
|
||||
|
||||
if (!want) continue;
|
||||
|
||||
// Filter by text (case-insensitive)
|
||||
if (
|
||||
!string.IsNullOrEmpty(filterText)
|
||||
&& message.IndexOf(filterText, StringComparison.OrdinalIgnoreCase) < 0
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Filter by timestamp (requires timestamp data)
|
||||
|
||||
// --- Formatting ---
|
||||
string stackTrace = includeStacktrace ? ExtractStackTrace(message) : null;
|
||||
// Always get first line for the message, use full message only if no stack trace exists
|
||||
string[] messageLines = message.Split(
|
||||
new[] { '\n', '\r' },
|
||||
StringSplitOptions.RemoveEmptyEntries
|
||||
);
|
||||
string messageOnly = messageLines.Length > 0 ? messageLines[0] : message;
|
||||
|
||||
// If not including stacktrace, ensure we only show the first line
|
||||
if (!includeStacktrace)
|
||||
{
|
||||
stackTrace = null;
|
||||
}
|
||||
|
||||
object formattedEntry = null;
|
||||
switch (format)
|
||||
{
|
||||
case "plain":
|
||||
formattedEntry = messageOnly;
|
||||
break;
|
||||
case "json":
|
||||
case "detailed": // Treat detailed as json for structured return
|
||||
default:
|
||||
formattedEntry = new
|
||||
{
|
||||
type = unityType.ToString(),
|
||||
message = messageOnly,
|
||||
file = file,
|
||||
line = line,
|
||||
// timestamp = "", // TODO
|
||||
stackTrace = stackTrace, // Will be null if includeStacktrace is false or no stack found
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
totalMatches++;
|
||||
|
||||
if (usePaging)
|
||||
{
|
||||
if (totalMatches > resolvedCursor && totalMatches <= pageEndExclusive)
|
||||
{
|
||||
formattedEntries.Add(formattedEntry);
|
||||
retrievedCount++;
|
||||
}
|
||||
// Early exit: we've filled the page and only need to check if more exist
|
||||
else if (totalMatches > pageEndExclusive)
|
||||
{
|
||||
// We've passed the page; totalMatches now indicates truncation
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
formattedEntries.Add(formattedEntry);
|
||||
retrievedCount++;
|
||||
|
||||
// Apply count limit (after filtering)
|
||||
if (count.HasValue && retrievedCount >= count.Value)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Error($"[ReadConsole] Error while retrieving log entries: {e}");
|
||||
// EndGettingEntries will be called in the finally block
|
||||
return new ErrorResponse($"Error retrieving log entries: {e.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Ensure we always call EndGettingEntries
|
||||
try
|
||||
{
|
||||
_endGettingEntriesMethod.Invoke(null, null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Error($"[ReadConsole] Failed to call EndGettingEntries: {e}");
|
||||
// Don't return error here as we might have valid data, but log it.
|
||||
}
|
||||
}
|
||||
|
||||
if (usePaging)
|
||||
{
|
||||
bool truncated = totalMatches > pageEndExclusive;
|
||||
string nextCursor = truncated ? pageEndExclusive.ToString() : null;
|
||||
var payload = new
|
||||
{
|
||||
cursor = resolvedCursor,
|
||||
pageSize = resolvedPageSize,
|
||||
nextCursor = nextCursor,
|
||||
truncated = truncated,
|
||||
total = totalMatches,
|
||||
items = formattedEntries,
|
||||
};
|
||||
|
||||
return new SuccessResponse(
|
||||
$"Retrieved {formattedEntries.Count} log entries.",
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
||||
// Return the filtered and formatted list (might be empty)
|
||||
return new SuccessResponse(
|
||||
$"Retrieved {formattedEntries.Count} log entries.",
|
||||
formattedEntries
|
||||
);
|
||||
}
|
||||
|
||||
// --- Internal Helpers ---
|
||||
|
||||
// Mapping bits from LogEntry.mode. These may vary by Unity version.
|
||||
private const int ModeBitError = 1 << 0;
|
||||
private const int ModeBitAssert = 1 << 1;
|
||||
private const int ModeBitWarning = 1 << 2;
|
||||
private const int ModeBitLog = 1 << 3;
|
||||
private const int ModeBitException = 1 << 4; // often combined with Error bits
|
||||
private const int ModeBitScriptingError = 1 << 9;
|
||||
private const int ModeBitScriptingWarning = 1 << 10;
|
||||
private const int ModeBitScriptingLog = 1 << 11;
|
||||
private const int ModeBitScriptingException = 1 << 18;
|
||||
private const int ModeBitScriptingAssertion = 1 << 22;
|
||||
|
||||
private static LogType GetLogTypeFromMode(int mode)
|
||||
{
|
||||
// Preserve Unity's real type (no remapping); bits may vary by version
|
||||
if ((mode & (ModeBitException | ModeBitScriptingException)) != 0) return LogType.Exception;
|
||||
if ((mode & (ModeBitError | ModeBitScriptingError)) != 0) return LogType.Error;
|
||||
if ((mode & (ModeBitAssert | ModeBitScriptingAssertion)) != 0) return LogType.Assert;
|
||||
if ((mode & (ModeBitWarning | ModeBitScriptingWarning)) != 0) return LogType.Warning;
|
||||
return LogType.Log;
|
||||
}
|
||||
|
||||
// (Calibration helpers removed)
|
||||
|
||||
/// <summary>
|
||||
/// Classifies severity using message/stacktrace content. Works across Unity versions.
|
||||
/// </summary>
|
||||
private static LogType InferTypeFromMessage(string fullMessage)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fullMessage)) return LogType.Log;
|
||||
|
||||
// Fast path: look for explicit Debug API names in the appended stack trace
|
||||
// e.g., "UnityEngine.Debug:LogError (object)" or "LogWarning"
|
||||
if (fullMessage.IndexOf("LogError", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return LogType.Error;
|
||||
if (fullMessage.IndexOf("LogWarning", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return LogType.Warning;
|
||||
|
||||
// Compiler diagnostics (C#): "warning CSxxxx" / "error CSxxxx"
|
||||
if (fullMessage.IndexOf(" warning CS", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| fullMessage.IndexOf(": warning CS", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return LogType.Warning;
|
||||
if (fullMessage.IndexOf(" error CS", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| fullMessage.IndexOf(": error CS", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return LogType.Error;
|
||||
|
||||
// Exceptions (avoid misclassifying compiler diagnostics)
|
||||
if (fullMessage.IndexOf("Exception", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return LogType.Exception;
|
||||
|
||||
// Unity assertions
|
||||
if (fullMessage.IndexOf("Assertion", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return LogType.Assert;
|
||||
|
||||
return LogType.Log;
|
||||
}
|
||||
|
||||
private static bool IsExplicitDebugLog(string fullMessage)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fullMessage)) return false;
|
||||
if (fullMessage.IndexOf("Debug:Log (", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
if (fullMessage.IndexOf("UnityEngine.Debug:Log (", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the "one level lower" remapping for filtering, like the old version.
|
||||
/// This ensures compatibility with the filtering logic that expects remapped types.
|
||||
/// </summary>
|
||||
private static LogType GetRemappedTypeForFiltering(LogType unityType)
|
||||
{
|
||||
switch (unityType)
|
||||
{
|
||||
case LogType.Error:
|
||||
return LogType.Warning; // Error becomes Warning
|
||||
case LogType.Warning:
|
||||
return LogType.Log; // Warning becomes Log
|
||||
case LogType.Assert:
|
||||
return LogType.Assert; // Assert remains Assert
|
||||
case LogType.Log:
|
||||
return LogType.Log; // Log remains Log
|
||||
case LogType.Exception:
|
||||
return LogType.Warning; // Exception becomes Warning
|
||||
default:
|
||||
return LogType.Log; // Default fallback
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to extract the stack trace part from a log message.
|
||||
/// Unity log messages often have the stack trace appended after the main message,
|
||||
/// starting on a new line and typically indented or beginning with "at ".
|
||||
/// </summary>
|
||||
/// <param name="fullMessage">The complete log message including potential stack trace.</param>
|
||||
/// <returns>The extracted stack trace string, or null if none is found.</returns>
|
||||
private static string ExtractStackTrace(string fullMessage)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fullMessage))
|
||||
return null;
|
||||
|
||||
// Split into lines, removing empty ones to handle different line endings gracefully.
|
||||
// Using StringSplitOptions.None might be better if empty lines matter within stack trace, but RemoveEmptyEntries is usually safer here.
|
||||
string[] lines = fullMessage.Split(
|
||||
new[] { '\r', '\n' },
|
||||
StringSplitOptions.RemoveEmptyEntries
|
||||
);
|
||||
|
||||
// If there's only one line or less, there's no separate stack trace.
|
||||
if (lines.Length <= 1)
|
||||
return null;
|
||||
|
||||
int stackStartIndex = -1;
|
||||
|
||||
// Start checking from the second line onwards.
|
||||
for (int i = 1; i < lines.Length; ++i)
|
||||
{
|
||||
// Performance: TrimStart creates a new string. Consider using IsWhiteSpace check if performance critical.
|
||||
string trimmedLine = lines[i].TrimStart();
|
||||
|
||||
// Check for common stack trace patterns.
|
||||
if (
|
||||
trimmedLine.StartsWith("at ")
|
||||
|| trimmedLine.StartsWith("UnityEngine.")
|
||||
|| trimmedLine.StartsWith("UnityEditor.")
|
||||
|| trimmedLine.Contains("(at ")
|
||||
|| // Covers "(at Assets/..." pattern
|
||||
// Heuristic: Check if line starts with likely namespace/class pattern (Uppercase.Something)
|
||||
(
|
||||
trimmedLine.Length > 0
|
||||
&& char.IsUpper(trimmedLine[0])
|
||||
&& trimmedLine.Contains('.')
|
||||
)
|
||||
)
|
||||
{
|
||||
stackStartIndex = i;
|
||||
break; // Found the likely start of the stack trace
|
||||
}
|
||||
}
|
||||
|
||||
// If a potential start index was found...
|
||||
if (stackStartIndex > 0)
|
||||
{
|
||||
// Join the lines from the stack start index onwards using standard newline characters.
|
||||
// This reconstructs the stack trace part of the message.
|
||||
return string.Join("\n", lines.Skip(stackStartIndex));
|
||||
}
|
||||
|
||||
// No clear stack trace found based on the patterns.
|
||||
return null;
|
||||
}
|
||||
|
||||
/* LogEntry.mode bits exploration (based on Unity decompilation/observation):
|
||||
May change between versions.
|
||||
|
||||
Basic Types:
|
||||
kError = 1 << 0 (1)
|
||||
kAssert = 1 << 1 (2)
|
||||
kWarning = 1 << 2 (4)
|
||||
kLog = 1 << 3 (8)
|
||||
kFatal = 1 << 4 (16) - Often treated as Exception/Error
|
||||
|
||||
Modifiers/Context:
|
||||
kAssetImportError = 1 << 7 (128)
|
||||
kAssetImportWarning = 1 << 8 (256)
|
||||
kScriptingError = 1 << 9 (512)
|
||||
kScriptingWarning = 1 << 10 (1024)
|
||||
kScriptingLog = 1 << 11 (2048)
|
||||
kScriptCompileError = 1 << 12 (4096)
|
||||
kScriptCompileWarning = 1 << 13 (8192)
|
||||
kStickyError = 1 << 14 (16384) - Stays visible even after Clear On Play
|
||||
kMayIgnoreLineNumber = 1 << 15 (32768)
|
||||
kReportBug = 1 << 16 (65536) - Shows the "Report Bug" button
|
||||
kDisplayPreviousErrorInStatusBar = 1 << 17 (131072)
|
||||
kScriptingException = 1 << 18 (262144)
|
||||
kDontExtractStacktrace = 1 << 19 (524288) - Hint to the console UI
|
||||
kShouldClearOnPlay = 1 << 20 (1048576) - Default behavior
|
||||
kGraphCompileError = 1 << 21 (2097152)
|
||||
kScriptingAssertion = 1 << 22 (4194304)
|
||||
kVisualScriptingError = 1 << 23 (8388608)
|
||||
|
||||
Example observed values:
|
||||
Log: 2048 (ScriptingLog) or 8 (Log)
|
||||
Warning: 1028 (ScriptingWarning | Warning) or 4 (Warning)
|
||||
Error: 513 (ScriptingError | Error) or 1 (Error)
|
||||
Exception: 262161 (ScriptingException | Error | kFatal?) - Complex combination
|
||||
Assertion: 4194306 (ScriptingAssertion | Assert) or 2 (Assert)
|
||||
*/
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/ReadConsole.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/ReadConsole.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46c4f3614ed61f547ba823f0b2790267
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
171
Packages/MCPForUnity/Editor/Tools/RefreshUnity.cs
Normal file
171
Packages/MCPForUnity/Editor/Tools/RefreshUnity.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Services;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Compilation;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Explicitly refreshes Unity's asset database and optionally requests a script compilation.
|
||||
/// This is side-effectful and should be treated as a tool.
|
||||
/// </summary>
|
||||
[McpForUnityTool("refresh_unity", AutoRegister = false)]
|
||||
public static class RefreshUnity
|
||||
{
|
||||
private const int DefaultWaitTimeoutSeconds = 60;
|
||||
|
||||
public static async Task<object> HandleCommand(JObject @params)
|
||||
{
|
||||
string mode = @params?["mode"]?.ToString() ?? "if_dirty";
|
||||
string scope = @params?["scope"]?.ToString() ?? "all";
|
||||
string compile = @params?["compile"]?.ToString() ?? "none";
|
||||
bool waitForReady = ParamCoercion.CoerceBool(@params?["wait_for_ready"], false);
|
||||
|
||||
if (TestRunStatus.IsRunning)
|
||||
{
|
||||
return new ErrorResponse("tests_running", new
|
||||
{
|
||||
reason = "tests_running",
|
||||
retry_after_ms = 5000
|
||||
});
|
||||
}
|
||||
|
||||
bool refreshTriggered = false;
|
||||
bool compileRequested = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Best-effort semantics: if_dirty currently behaves like force unless future dirty signals are added.
|
||||
bool shouldRefresh = string.Equals(mode, "force", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(mode, "if_dirty", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (shouldRefresh)
|
||||
{
|
||||
if (string.Equals(scope, "scripts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// For scripts, requesting compilation is usually the meaningful action.
|
||||
// We avoid a heavyweight full refresh by default.
|
||||
}
|
||||
else
|
||||
{
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
|
||||
refreshTriggered = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.Equals(compile, "request", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
CompilationPipeline.RequestScriptCompilation();
|
||||
compileRequested = true;
|
||||
}
|
||||
|
||||
if (string.Equals(scope, "all", StringComparison.OrdinalIgnoreCase) && !refreshTriggered)
|
||||
{
|
||||
// If the caller asked for "all" and we skipped refresh above (e.g., scripts-only path),
|
||||
// do a lightweight refresh now. Use ForceSynchronousImport to ensure the refresh
|
||||
// completes before returning, preventing stalls when Unity is backgrounded.
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
||||
refreshTriggered = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ErrorResponse($"refresh_failed: {ex.Message}");
|
||||
}
|
||||
|
||||
// Unity 6+ fix: Skip wait_for_ready when compile was requested.
|
||||
// The EditorApplication.update polling in WaitForUnityReadyAsync doesn't survive
|
||||
// domain reloads properly in Unity 6+, causing infinite compilation loops.
|
||||
// When compilation is requested, return immediately and let client poll editor_state.
|
||||
// Earlier Unity versions retain the original behavior.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
bool shouldWaitForReady = waitForReady && !compileRequested;
|
||||
#else
|
||||
bool shouldWaitForReady = waitForReady;
|
||||
#endif
|
||||
if (shouldWaitForReady)
|
||||
{
|
||||
try
|
||||
{
|
||||
await WaitForUnityReadyAsync(
|
||||
TimeSpan.FromSeconds(DefaultWaitTimeoutSeconds)).ConfigureAwait(true);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
return new ErrorResponse("refresh_timeout_waiting_for_ready", new
|
||||
{
|
||||
refresh_triggered = refreshTriggered,
|
||||
compile_requested = compileRequested,
|
||||
resulting_state = "unknown",
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ErrorResponse($"refresh_wait_failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
string resultingState = EditorApplication.isCompiling
|
||||
? "compiling"
|
||||
: (EditorApplication.isUpdating ? "asset_import" : "idle");
|
||||
|
||||
return new SuccessResponse("Refresh requested.", new
|
||||
{
|
||||
refresh_triggered = refreshTriggered,
|
||||
compile_requested = compileRequested,
|
||||
resulting_state = resultingState,
|
||||
hint = shouldWaitForReady
|
||||
? "Unity refresh completed; editor should be ready."
|
||||
: "If Unity enters compilation/domain reload, poll editor_state until ready_for_tools is true."
|
||||
});
|
||||
}
|
||||
|
||||
private static Task WaitForUnityReadyAsync(TimeSpan timeout)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var start = DateTime.UtcNow;
|
||||
|
||||
void Tick()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (tcs.Task.IsCompleted)
|
||||
{
|
||||
EditorApplication.update -= Tick;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((DateTime.UtcNow - start) > timeout)
|
||||
{
|
||||
EditorApplication.update -= Tick;
|
||||
tcs.TrySetException(new TimeoutException());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EditorApplication.isCompiling
|
||||
&& !EditorApplication.isUpdating
|
||||
&& !TestRunStatus.IsRunning
|
||||
&& !EditorApplication.isPlayingOrWillChangePlaymode)
|
||||
{
|
||||
EditorApplication.update -= Tick;
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EditorApplication.update -= Tick;
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
EditorApplication.update += Tick;
|
||||
// Nudge Unity to pump once in case update is throttled.
|
||||
try { EditorApplication.QueuePlayerLoopUpdate(); } catch { }
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/RefreshUnity.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/RefreshUnity.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2c02170faca940d09c813706493ecb3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
118
Packages/MCPForUnity/Editor/Tools/RunTests.cs
Normal file
118
Packages/MCPForUnity/Editor/Tools/RunTests.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Resources.Tests;
|
||||
using MCPForUnity.Editor.Services;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor.TestTools.TestRunner.Api;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts a Unity Test Runner run asynchronously and returns a job id immediately.
|
||||
/// Use get_test_job(job_id) to poll status/results.
|
||||
/// </summary>
|
||||
[McpForUnityTool("run_tests", AutoRegister = false)]
|
||||
public static class RunTests
|
||||
{
|
||||
public static Task<object> HandleCommand(JObject @params)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check for clear_stuck action first
|
||||
if (ParamCoercion.CoerceBool(@params?["clear_stuck"], false))
|
||||
{
|
||||
bool wasCleared = TestJobManager.ClearStuckJob();
|
||||
return Task.FromResult<object>(new SuccessResponse(
|
||||
wasCleared ? "Stuck job cleared." : "No running job to clear.",
|
||||
new { cleared = wasCleared }
|
||||
));
|
||||
}
|
||||
|
||||
string modeStr = @params?["mode"]?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(modeStr))
|
||||
{
|
||||
modeStr = "EditMode";
|
||||
}
|
||||
|
||||
if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError))
|
||||
{
|
||||
return Task.FromResult<object>(new ErrorResponse(parseError));
|
||||
}
|
||||
|
||||
bool includeDetails = ParamCoercion.CoerceBool(@params?["includeDetails"], false);
|
||||
bool includeFailedTests = ParamCoercion.CoerceBool(@params?["includeFailedTests"], false);
|
||||
|
||||
var filterOptions = GetFilterOptions(@params);
|
||||
string jobId = TestJobManager.StartJob(parsedMode.Value, filterOptions);
|
||||
|
||||
return Task.FromResult<object>(new SuccessResponse("Test job started.", new
|
||||
{
|
||||
job_id = jobId,
|
||||
status = "running",
|
||||
mode = parsedMode.Value.ToString(),
|
||||
include_details = includeDetails,
|
||||
include_failed_tests = includeFailedTests
|
||||
}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Normalize the already-running case to a stable error token.
|
||||
if (ex.Message != null && ex.Message.IndexOf("already in progress", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return Task.FromResult<object>(new ErrorResponse("tests_running", new { reason = "tests_running", retry_after_ms = 5000 }));
|
||||
}
|
||||
return Task.FromResult<object>(new ErrorResponse($"Failed to start test job: {ex.Message}"));
|
||||
}
|
||||
}
|
||||
|
||||
private static TestFilterOptions GetFilterOptions(JObject @params)
|
||||
{
|
||||
if (@params == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string[] ParseStringArray(string key)
|
||||
{
|
||||
var token = @params[key];
|
||||
if (token == null) return null;
|
||||
if (token.Type == JTokenType.String)
|
||||
{
|
||||
var value = token.ToString();
|
||||
return string.IsNullOrWhiteSpace(value) ? null : new[] { value };
|
||||
}
|
||||
if (token.Type == JTokenType.Array)
|
||||
{
|
||||
var array = token as JArray;
|
||||
if (array == null || array.Count == 0) return null;
|
||||
var values = array
|
||||
.Values<string>()
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||
.ToArray();
|
||||
return values.Length > 0 ? values : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var testNames = ParseStringArray("testNames");
|
||||
var groupNames = ParseStringArray("groupNames");
|
||||
var categoryNames = ParseStringArray("categoryNames");
|
||||
var assemblyNames = ParseStringArray("assemblyNames");
|
||||
|
||||
if (testNames == null && groupNames == null && categoryNames == null && assemblyNames == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new TestFilterOptions
|
||||
{
|
||||
TestNames = testNames,
|
||||
GroupNames = groupNames,
|
||||
CategoryNames = categoryNames,
|
||||
AssemblyNames = assemblyNames
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/RunTests.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/RunTests.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cc0c41b1a8b4e0e9d0f1f8b1d7d2a9c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/MCPForUnity/Editor/Tools/Vfx.meta
Normal file
8
Packages/MCPForUnity/Editor/Tools/Vfx.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1805768600c6a4228bae31231f2a4a9f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
220
Packages/MCPForUnity/Editor/Tools/Vfx/LineCreate.cs
Normal file
220
Packages/MCPForUnity/Editor/Tools/Vfx/LineCreate.cs
Normal file
@@ -0,0 +1,220 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
internal static class LineCreate
|
||||
{
|
||||
public static object CreateLine(JObject @params)
|
||||
{
|
||||
LineRenderer lr = LineRead.FindLineRenderer(@params);
|
||||
if (lr == null) return new { success = false, message = "LineRenderer not found" };
|
||||
|
||||
Vector3 start = ManageVfxCommon.ParseVector3(@params["start"]);
|
||||
Vector3 end = ManageVfxCommon.ParseVector3(@params["end"]);
|
||||
|
||||
Undo.RecordObject(lr, "Create Line");
|
||||
lr.positionCount = 2;
|
||||
lr.SetPosition(0, start);
|
||||
lr.SetPosition(1, end);
|
||||
|
||||
RendererHelpers.EnsureMaterial(lr);
|
||||
|
||||
// Apply optional width
|
||||
if (@params["width"] != null)
|
||||
{
|
||||
float w = @params["width"].ToObject<float>();
|
||||
lr.startWidth = w;
|
||||
lr.endWidth = w;
|
||||
}
|
||||
if (@params["startWidth"] != null) lr.startWidth = @params["startWidth"].ToObject<float>();
|
||||
if (@params["endWidth"] != null) lr.endWidth = @params["endWidth"].ToObject<float>();
|
||||
|
||||
// Apply optional color
|
||||
if (@params["color"] != null)
|
||||
{
|
||||
Color c = ManageVfxCommon.ParseColor(@params["color"]);
|
||||
lr.startColor = c;
|
||||
lr.endColor = c;
|
||||
}
|
||||
if (@params["startColor"] != null) lr.startColor = ManageVfxCommon.ParseColor(@params["startColor"]);
|
||||
if (@params["endColor"] != null) lr.endColor = ManageVfxCommon.ParseColor(@params["endColor"]);
|
||||
|
||||
EditorUtility.SetDirty(lr);
|
||||
|
||||
return new { success = true, message = "Created line" };
|
||||
}
|
||||
|
||||
public static object CreateCircle(JObject @params)
|
||||
{
|
||||
LineRenderer lr = LineRead.FindLineRenderer(@params);
|
||||
if (lr == null) return new { success = false, message = "LineRenderer not found" };
|
||||
|
||||
Vector3 center = ManageVfxCommon.ParseVector3(@params["center"]);
|
||||
float radius = @params["radius"]?.ToObject<float>() ?? 1f;
|
||||
int segments = @params["segments"]?.ToObject<int>() ?? 32;
|
||||
Vector3 normal = @params["normal"] != null ? ManageVfxCommon.ParseVector3(@params["normal"]).normalized : Vector3.up;
|
||||
|
||||
Vector3 right = Vector3.Cross(normal, Vector3.forward);
|
||||
if (right.sqrMagnitude < 0.001f) right = Vector3.Cross(normal, Vector3.up);
|
||||
right = right.normalized;
|
||||
Vector3 forward = Vector3.Cross(right, normal).normalized;
|
||||
|
||||
Undo.RecordObject(lr, "Create Circle");
|
||||
lr.positionCount = segments;
|
||||
lr.loop = true;
|
||||
|
||||
for (int i = 0; i < segments; i++)
|
||||
{
|
||||
float angle = (float)i / segments * Mathf.PI * 2f;
|
||||
Vector3 point = center + (right * Mathf.Cos(angle) + forward * Mathf.Sin(angle)) * radius;
|
||||
lr.SetPosition(i, point);
|
||||
}
|
||||
|
||||
RendererHelpers.EnsureMaterial(lr);
|
||||
|
||||
// Apply optional width
|
||||
if (@params["width"] != null)
|
||||
{
|
||||
float w = @params["width"].ToObject<float>();
|
||||
lr.startWidth = w;
|
||||
lr.endWidth = w;
|
||||
}
|
||||
if (@params["startWidth"] != null) lr.startWidth = @params["startWidth"].ToObject<float>();
|
||||
if (@params["endWidth"] != null) lr.endWidth = @params["endWidth"].ToObject<float>();
|
||||
|
||||
// Apply optional color
|
||||
if (@params["color"] != null)
|
||||
{
|
||||
Color c = ManageVfxCommon.ParseColor(@params["color"]);
|
||||
lr.startColor = c;
|
||||
lr.endColor = c;
|
||||
}
|
||||
if (@params["startColor"] != null) lr.startColor = ManageVfxCommon.ParseColor(@params["startColor"]);
|
||||
if (@params["endColor"] != null) lr.endColor = ManageVfxCommon.ParseColor(@params["endColor"]);
|
||||
|
||||
EditorUtility.SetDirty(lr);
|
||||
return new { success = true, message = $"Created circle with {segments} segments" };
|
||||
}
|
||||
|
||||
public static object CreateArc(JObject @params)
|
||||
{
|
||||
LineRenderer lr = LineRead.FindLineRenderer(@params);
|
||||
if (lr == null) return new { success = false, message = "LineRenderer not found" };
|
||||
|
||||
Vector3 center = ManageVfxCommon.ParseVector3(@params["center"]);
|
||||
float radius = @params["radius"]?.ToObject<float>() ?? 1f;
|
||||
float startAngle = (@params["startAngle"]?.ToObject<float>() ?? 0f) * Mathf.Deg2Rad;
|
||||
float endAngle = (@params["endAngle"]?.ToObject<float>() ?? 180f) * Mathf.Deg2Rad;
|
||||
int segments = @params["segments"]?.ToObject<int>() ?? 16;
|
||||
Vector3 normal = @params["normal"] != null ? ManageVfxCommon.ParseVector3(@params["normal"]).normalized : Vector3.up;
|
||||
|
||||
Vector3 right = Vector3.Cross(normal, Vector3.forward);
|
||||
if (right.sqrMagnitude < 0.001f) right = Vector3.Cross(normal, Vector3.up);
|
||||
right = right.normalized;
|
||||
Vector3 forward = Vector3.Cross(right, normal).normalized;
|
||||
|
||||
Undo.RecordObject(lr, "Create Arc");
|
||||
lr.positionCount = segments + 1;
|
||||
lr.loop = false;
|
||||
|
||||
for (int i = 0; i <= segments; i++)
|
||||
{
|
||||
float t = (float)i / segments;
|
||||
float angle = Mathf.Lerp(startAngle, endAngle, t);
|
||||
Vector3 point = center + (right * Mathf.Cos(angle) + forward * Mathf.Sin(angle)) * radius;
|
||||
lr.SetPosition(i, point);
|
||||
}
|
||||
|
||||
RendererHelpers.EnsureMaterial(lr);
|
||||
|
||||
// Apply optional width
|
||||
if (@params["width"] != null)
|
||||
{
|
||||
float w = @params["width"].ToObject<float>();
|
||||
lr.startWidth = w;
|
||||
lr.endWidth = w;
|
||||
}
|
||||
if (@params["startWidth"] != null) lr.startWidth = @params["startWidth"].ToObject<float>();
|
||||
if (@params["endWidth"] != null) lr.endWidth = @params["endWidth"].ToObject<float>();
|
||||
|
||||
// Apply optional color
|
||||
if (@params["color"] != null)
|
||||
{
|
||||
Color c = ManageVfxCommon.ParseColor(@params["color"]);
|
||||
lr.startColor = c;
|
||||
lr.endColor = c;
|
||||
}
|
||||
if (@params["startColor"] != null) lr.startColor = ManageVfxCommon.ParseColor(@params["startColor"]);
|
||||
if (@params["endColor"] != null) lr.endColor = ManageVfxCommon.ParseColor(@params["endColor"]);
|
||||
|
||||
EditorUtility.SetDirty(lr);
|
||||
return new { success = true, message = $"Created arc with {segments} segments" };
|
||||
}
|
||||
|
||||
public static object CreateBezier(JObject @params)
|
||||
{
|
||||
LineRenderer lr = LineRead.FindLineRenderer(@params);
|
||||
if (lr == null) return new { success = false, message = "LineRenderer not found" };
|
||||
|
||||
Vector3 start = ManageVfxCommon.ParseVector3(@params["start"]);
|
||||
Vector3 end = ManageVfxCommon.ParseVector3(@params["end"]);
|
||||
Vector3 cp1 = ManageVfxCommon.ParseVector3(@params["controlPoint1"] ?? @params["control1"]);
|
||||
Vector3 cp2 = @params["controlPoint2"] != null || @params["control2"] != null
|
||||
? ManageVfxCommon.ParseVector3(@params["controlPoint2"] ?? @params["control2"])
|
||||
: cp1;
|
||||
int segments = @params["segments"]?.ToObject<int>() ?? 32;
|
||||
bool isQuadratic = @params["controlPoint2"] == null && @params["control2"] == null;
|
||||
|
||||
Undo.RecordObject(lr, "Create Bezier");
|
||||
lr.positionCount = segments + 1;
|
||||
lr.loop = false;
|
||||
|
||||
for (int i = 0; i <= segments; i++)
|
||||
{
|
||||
float t = (float)i / segments;
|
||||
Vector3 point;
|
||||
|
||||
if (isQuadratic)
|
||||
{
|
||||
float u = 1 - t;
|
||||
point = u * u * start + 2 * u * t * cp1 + t * t * end;
|
||||
}
|
||||
else
|
||||
{
|
||||
float u = 1 - t;
|
||||
point = u * u * u * start + 3 * u * u * t * cp1 + 3 * u * t * t * cp2 + t * t * t * end;
|
||||
}
|
||||
|
||||
lr.SetPosition(i, point);
|
||||
}
|
||||
|
||||
RendererHelpers.EnsureMaterial(lr);
|
||||
|
||||
// Apply optional width
|
||||
if (@params["width"] != null)
|
||||
{
|
||||
float w = @params["width"].ToObject<float>();
|
||||
lr.startWidth = w;
|
||||
lr.endWidth = w;
|
||||
}
|
||||
if (@params["startWidth"] != null) lr.startWidth = @params["startWidth"].ToObject<float>();
|
||||
if (@params["endWidth"] != null) lr.endWidth = @params["endWidth"].ToObject<float>();
|
||||
|
||||
// Apply optional color
|
||||
if (@params["color"] != null)
|
||||
{
|
||||
Color c = ManageVfxCommon.ParseColor(@params["color"]);
|
||||
lr.startColor = c;
|
||||
lr.endColor = c;
|
||||
}
|
||||
if (@params["startColor"] != null) lr.startColor = ManageVfxCommon.ParseColor(@params["startColor"]);
|
||||
if (@params["endColor"] != null) lr.endColor = ManageVfxCommon.ParseColor(@params["endColor"]);
|
||||
|
||||
EditorUtility.SetDirty(lr);
|
||||
return new { success = true, message = $"Created {(isQuadratic ? "quadratic" : "cubic")} Bezier" };
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/Vfx/LineCreate.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/Vfx/LineCreate.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d553d3837ecc4d999225bc9b3160a26
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
52
Packages/MCPForUnity/Editor/Tools/Vfx/LineRead.cs
Normal file
52
Packages/MCPForUnity/Editor/Tools/Vfx/LineRead.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
internal static class LineRead
|
||||
{
|
||||
public static LineRenderer FindLineRenderer(JObject @params)
|
||||
{
|
||||
GameObject go = ManageVfxCommon.FindTargetGameObject(@params);
|
||||
return go?.GetComponent<LineRenderer>();
|
||||
}
|
||||
|
||||
public static object GetInfo(JObject @params)
|
||||
{
|
||||
LineRenderer lr = FindLineRenderer(@params);
|
||||
if (lr == null) return new { success = false, message = "LineRenderer not found" };
|
||||
|
||||
var positions = new Vector3[lr.positionCount];
|
||||
lr.GetPositions(positions);
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
data = new
|
||||
{
|
||||
gameObject = lr.gameObject.name,
|
||||
positionCount = lr.positionCount,
|
||||
positions = positions.Select(p => new { x = p.x, y = p.y, z = p.z }).ToArray(),
|
||||
startWidth = lr.startWidth,
|
||||
endWidth = lr.endWidth,
|
||||
loop = lr.loop,
|
||||
useWorldSpace = lr.useWorldSpace,
|
||||
alignment = lr.alignment.ToString(),
|
||||
textureMode = lr.textureMode.ToString(),
|
||||
numCornerVertices = lr.numCornerVertices,
|
||||
numCapVertices = lr.numCapVertices,
|
||||
generateLightingData = lr.generateLightingData,
|
||||
material = lr.sharedMaterial?.name,
|
||||
shadowCastingMode = lr.shadowCastingMode.ToString(),
|
||||
receiveShadows = lr.receiveShadows,
|
||||
lightProbeUsage = lr.lightProbeUsage.ToString(),
|
||||
reflectionProbeUsage = lr.reflectionProbeUsage.ToString(),
|
||||
sortingOrder = lr.sortingOrder,
|
||||
sortingLayerName = lr.sortingLayerName,
|
||||
renderingLayerMask = lr.renderingLayerMask
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/Vfx/LineRead.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/Vfx/LineRead.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df77cf0ca14344b0cb2f1b84c5eb15e7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
189
Packages/MCPForUnity/Editor/Tools/Vfx/LineWrite.cs
Normal file
189
Packages/MCPForUnity/Editor/Tools/Vfx/LineWrite.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using System.Collections.Generic;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
internal static class LineWrite
|
||||
{
|
||||
public static object SetPositions(JObject @params)
|
||||
{
|
||||
LineRenderer lr = LineRead.FindLineRenderer(@params);
|
||||
if (lr == null) return new { success = false, message = "LineRenderer not found" };
|
||||
|
||||
RendererHelpers.EnsureMaterial(lr);
|
||||
|
||||
JArray posArr = @params["positions"] as JArray;
|
||||
if (posArr == null) return new { success = false, message = "Positions array required" };
|
||||
|
||||
var positions = new Vector3[posArr.Count];
|
||||
for (int i = 0; i < posArr.Count; i++)
|
||||
{
|
||||
positions[i] = ManageVfxCommon.ParseVector3(posArr[i]);
|
||||
}
|
||||
|
||||
Undo.RecordObject(lr, "Set Line Positions");
|
||||
lr.positionCount = positions.Length;
|
||||
lr.SetPositions(positions);
|
||||
EditorUtility.SetDirty(lr);
|
||||
|
||||
return new { success = true, message = $"Set {positions.Length} positions" };
|
||||
}
|
||||
|
||||
public static object AddPosition(JObject @params)
|
||||
{
|
||||
LineRenderer lr = LineRead.FindLineRenderer(@params);
|
||||
if (lr == null) return new { success = false, message = "LineRenderer not found" };
|
||||
|
||||
RendererHelpers.EnsureMaterial(lr);
|
||||
|
||||
Vector3 pos = ManageVfxCommon.ParseVector3(@params["position"]);
|
||||
|
||||
Undo.RecordObject(lr, "Add Line Position");
|
||||
int idx = lr.positionCount;
|
||||
lr.positionCount = idx + 1;
|
||||
lr.SetPosition(idx, pos);
|
||||
EditorUtility.SetDirty(lr);
|
||||
|
||||
return new { success = true, message = $"Added position at index {idx}", index = idx };
|
||||
}
|
||||
|
||||
public static object SetPosition(JObject @params)
|
||||
{
|
||||
LineRenderer lr = LineRead.FindLineRenderer(@params);
|
||||
if (lr == null) return new { success = false, message = "LineRenderer not found" };
|
||||
|
||||
RendererHelpers.EnsureMaterial(lr);
|
||||
|
||||
int index = @params["index"]?.ToObject<int>() ?? -1;
|
||||
if (index < 0 || index >= lr.positionCount) return new { success = false, message = $"Invalid index {index}" };
|
||||
|
||||
Vector3 pos = ManageVfxCommon.ParseVector3(@params["position"]);
|
||||
|
||||
Undo.RecordObject(lr, "Set Line Position");
|
||||
lr.SetPosition(index, pos);
|
||||
EditorUtility.SetDirty(lr);
|
||||
|
||||
return new { success = true, message = $"Set position at index {index}" };
|
||||
}
|
||||
|
||||
public static object SetWidth(JObject @params)
|
||||
{
|
||||
LineRenderer lr = LineRead.FindLineRenderer(@params);
|
||||
if (lr == null) return new { success = false, message = "LineRenderer not found" };
|
||||
|
||||
RendererHelpers.EnsureMaterial(lr);
|
||||
|
||||
Undo.RecordObject(lr, "Set Line Width");
|
||||
var changes = new List<string>();
|
||||
|
||||
RendererHelpers.ApplyWidthProperties(@params, changes,
|
||||
v => lr.startWidth = v, v => lr.endWidth = v,
|
||||
v => lr.widthCurve = v, v => lr.widthMultiplier = v,
|
||||
ManageVfxCommon.ParseAnimationCurve);
|
||||
|
||||
EditorUtility.SetDirty(lr);
|
||||
return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
|
||||
}
|
||||
|
||||
public static object SetColor(JObject @params)
|
||||
{
|
||||
LineRenderer lr = LineRead.FindLineRenderer(@params);
|
||||
if (lr == null) return new { success = false, message = "LineRenderer not found" };
|
||||
|
||||
RendererHelpers.EnsureMaterial(lr);
|
||||
|
||||
Undo.RecordObject(lr, "Set Line Color");
|
||||
var changes = new List<string>();
|
||||
|
||||
RendererHelpers.ApplyColorProperties(@params, changes,
|
||||
v => lr.startColor = v, v => lr.endColor = v,
|
||||
v => lr.colorGradient = v,
|
||||
ManageVfxCommon.ParseColor, ManageVfxCommon.ParseGradient, fadeEndAlpha: false);
|
||||
|
||||
EditorUtility.SetDirty(lr);
|
||||
return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
|
||||
}
|
||||
|
||||
public static object SetMaterial(JObject @params)
|
||||
{
|
||||
LineRenderer lr = LineRead.FindLineRenderer(@params);
|
||||
return RendererHelpers.SetRendererMaterial(lr, @params, "Set Line Material", ManageVfxCommon.FindMaterialByPath);
|
||||
}
|
||||
|
||||
public static object SetProperties(JObject @params)
|
||||
{
|
||||
LineRenderer lr = LineRead.FindLineRenderer(@params);
|
||||
if (lr == null) return new { success = false, message = "LineRenderer not found" };
|
||||
|
||||
RendererHelpers.EnsureMaterial(lr);
|
||||
|
||||
Undo.RecordObject(lr, "Set Line Properties");
|
||||
var changes = new List<string>();
|
||||
|
||||
// Handle material if provided
|
||||
if (@params["materialPath"] != null)
|
||||
{
|
||||
Material mat = ManageVfxCommon.FindMaterialByPath(@params["materialPath"].ToString());
|
||||
if (mat != null)
|
||||
{
|
||||
lr.sharedMaterial = mat;
|
||||
changes.Add($"material={mat.name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Warn($"Material not found: {@params["materialPath"]}");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle positions if provided
|
||||
if (@params["positions"] != null)
|
||||
{
|
||||
JArray posArr = @params["positions"] as JArray;
|
||||
if (posArr != null && posArr.Count > 0)
|
||||
{
|
||||
var positions = new Vector3[posArr.Count];
|
||||
for (int i = 0; i < posArr.Count; i++)
|
||||
{
|
||||
positions[i] = ManageVfxCommon.ParseVector3(posArr[i]);
|
||||
}
|
||||
lr.positionCount = positions.Length;
|
||||
lr.SetPositions(positions);
|
||||
changes.Add($"positions({positions.Length})");
|
||||
}
|
||||
}
|
||||
else if (@params["positionCount"] != null)
|
||||
{
|
||||
int count = @params["positionCount"].ToObject<int>();
|
||||
lr.positionCount = count;
|
||||
changes.Add("positionCount");
|
||||
}
|
||||
|
||||
RendererHelpers.ApplyLineTrailProperties(@params, changes,
|
||||
v => lr.loop = v, v => lr.useWorldSpace = v,
|
||||
v => lr.numCornerVertices = v, v => lr.numCapVertices = v,
|
||||
v => lr.alignment = v, v => lr.textureMode = v,
|
||||
v => lr.generateLightingData = v);
|
||||
|
||||
RendererHelpers.ApplyCommonRendererProperties(lr, @params, changes);
|
||||
|
||||
EditorUtility.SetDirty(lr);
|
||||
return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
|
||||
}
|
||||
|
||||
public static object Clear(JObject @params)
|
||||
{
|
||||
LineRenderer lr = LineRead.FindLineRenderer(@params);
|
||||
if (lr == null) return new { success = false, message = "LineRenderer not found" };
|
||||
|
||||
int count = lr.positionCount;
|
||||
Undo.RecordObject(lr, "Clear Line");
|
||||
lr.positionCount = 0;
|
||||
EditorUtility.SetDirty(lr);
|
||||
|
||||
return new { success = true, message = $"Cleared {count} positions" };
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/Vfx/LineWrite.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/Vfx/LineWrite.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3911acc5a6a6a494cb88a647e0426d67
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
412
Packages/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs
Normal file
412
Packages/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs
Normal file
@@ -0,0 +1,412 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
|
||||
#if UNITY_VFX_GRAPH //Please enable the symbol in the project settings for VisualEffectGraph to work
|
||||
using UnityEngine.VFX;
|
||||
#endif
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
/// <summary>
|
||||
/// Tool for managing Unity VFX components:
|
||||
/// - ParticleSystem (legacy particle effects)
|
||||
/// - Visual Effect Graph (modern GPU particles, currently only support HDRP, other SRPs may not work)
|
||||
/// - LineRenderer (lines, bezier curves, shapes)
|
||||
/// - TrailRenderer (motion trails)
|
||||
///
|
||||
/// COMPONENT REQUIREMENTS:
|
||||
/// - particle_* actions require ParticleSystem component on target GameObject
|
||||
/// - vfx_* actions require VisualEffect component (+ com.unity.visualeffectgraph package)
|
||||
/// - line_* actions require LineRenderer component
|
||||
/// - trail_* actions require TrailRenderer component
|
||||
///
|
||||
/// TARGETING:
|
||||
/// Use 'target' parameter with optional 'searchMethod':
|
||||
/// - by_name (default): "Fire" finds first GameObject named "Fire"
|
||||
/// - by_path: "Effects/Fire" finds GameObject at hierarchy path
|
||||
/// - by_id: "12345" finds GameObject by instance ID (most reliable)
|
||||
/// - by_tag: "Enemy" finds first GameObject with tag
|
||||
///
|
||||
/// AUTOMATIC MATERIAL ASSIGNMENT:
|
||||
/// VFX components (ParticleSystem, LineRenderer, TrailRenderer) automatically receive
|
||||
/// appropriate default materials based on the active rendering pipeline when no material
|
||||
/// is explicitly specified:
|
||||
/// - Built-in Pipeline: Uses Unity's built-in Default-Particle.mat and Default-Line.mat
|
||||
/// - URP/HDRP: Creates materials with pipeline-appropriate unlit shaders
|
||||
/// - Materials are cached to avoid recreation
|
||||
/// - Explicit materialPath parameter always overrides auto-assignment
|
||||
/// - Auto-assigned materials are logged for transparency
|
||||
///
|
||||
/// AVAILABLE ACTIONS:
|
||||
///
|
||||
/// ParticleSystem (particle_*):
|
||||
/// - particle_get_info: Get system info and current state
|
||||
/// - particle_set_main: Set main module (duration, looping, startLifetime, startSpeed, startSize, startColor, gravityModifier, maxParticles, simulationSpace, playOnAwake, etc.)
|
||||
/// - particle_set_emission: Set emission module (rateOverTime, rateOverDistance)
|
||||
/// - particle_set_shape: Set shape module (shapeType, radius, angle, arc, position, rotation, scale)
|
||||
/// - particle_set_color_over_lifetime: Set color gradient over particle lifetime
|
||||
/// - particle_set_size_over_lifetime: Set size curve over particle lifetime
|
||||
/// - particle_set_velocity_over_lifetime: Set velocity (x, y, z, speedModifier, space)
|
||||
/// - particle_set_noise: Set noise turbulence (strength, frequency, scrollSpeed, damping, octaveCount, quality)
|
||||
/// - particle_set_renderer: Set renderer (renderMode, material, sortMode, minParticleSize, maxParticleSize, etc.)
|
||||
/// - particle_enable_module: Enable/disable modules by name
|
||||
/// - particle_play/stop/pause/restart/clear: Playback control (withChildren optional)
|
||||
/// - particle_add_burst: Add emission burst (time, count, cycles, interval, probability)
|
||||
/// - particle_clear_bursts: Clear all bursts
|
||||
///
|
||||
/// Visual Effect Graph (vfx_*):
|
||||
/// Asset Management:
|
||||
/// - vfx_create_asset: Create new VFX asset file (assetName, folderPath, template, overwrite)
|
||||
/// - vfx_assign_asset: Assign VFX asset to VisualEffect component (target, assetPath)
|
||||
/// - vfx_list_templates: List available VFX templates in project and packages
|
||||
/// - vfx_list_assets: List all VFX assets (folder, search filters)
|
||||
/// Runtime Control:
|
||||
/// - vfx_get_info: Get VFX info including exposed parameters
|
||||
/// - vfx_set_float/int/bool: Set exposed scalar parameters (parameter, value)
|
||||
/// - vfx_set_vector2/vector3/vector4: Set exposed vector parameters (parameter, value as array)
|
||||
/// - vfx_set_color: Set exposed color (parameter, color as [r,g,b,a])
|
||||
/// - vfx_set_gradient: Set exposed gradient (parameter, gradient)
|
||||
/// - vfx_set_texture: Set exposed texture (parameter, texturePath)
|
||||
/// - vfx_set_mesh: Set exposed mesh (parameter, meshPath)
|
||||
/// - vfx_set_curve: Set exposed animation curve (parameter, curve)
|
||||
/// - vfx_send_event: Send event with attributes (eventName, position, velocity, color, size, lifetime)
|
||||
/// - vfx_play/stop/pause/reinit: Playback control
|
||||
/// - vfx_set_playback_speed: Set playback speed multiplier (playRate)
|
||||
/// - vfx_set_seed: Set random seed (seed, resetSeedOnPlay)
|
||||
///
|
||||
/// LineRenderer (line_*):
|
||||
/// - line_get_info: Get line info (position count, width, color, etc.)
|
||||
/// - line_set_positions: Set all positions (positions as [[x,y,z], ...])
|
||||
/// - line_add_position: Add position at end (position as [x,y,z])
|
||||
/// - line_set_position: Set specific position (index, position)
|
||||
/// - line_set_width: Set width (width, startWidth, endWidth, widthCurve, widthMultiplier)
|
||||
/// - line_set_color: Set color (color, gradient, startColor, endColor)
|
||||
/// - line_set_material: Set material (materialPath)
|
||||
/// - line_set_properties: Set renderer properties (loop, useWorldSpace, alignment, textureMode, numCornerVertices, numCapVertices, etc.)
|
||||
/// - line_clear: Clear all positions
|
||||
/// Shape Creation:
|
||||
/// - line_create_line: Create simple line (start, end, segments)
|
||||
/// - line_create_circle: Create circle (center, radius, segments, normal)
|
||||
/// - line_create_arc: Create arc (center, radius, startAngle, endAngle, segments, normal)
|
||||
/// - line_create_bezier: Create Bezier curve (start, end, controlPoint1, controlPoint2, segments)
|
||||
///
|
||||
/// TrailRenderer (trail_*):
|
||||
/// - trail_get_info: Get trail info
|
||||
/// - trail_set_time: Set trail duration (time)
|
||||
/// - trail_set_width: Set width (width, startWidth, endWidth, widthCurve, widthMultiplier)
|
||||
/// - trail_set_color: Set color (color, gradient, startColor, endColor)
|
||||
/// - trail_set_material: Set material (materialPath)
|
||||
/// - trail_set_properties: Set properties (minVertexDistance, autodestruct, emitting, alignment, textureMode, etc.)
|
||||
/// - trail_clear: Clear trail
|
||||
/// - trail_emit: Emit point at current position (Unity 2021.1+)
|
||||
///
|
||||
/// COMMON PARAMETERS:
|
||||
/// - target (string): GameObject identifier
|
||||
/// - searchMethod (string): "by_id" | "by_name" | "by_path" | "by_tag" | "by_layer"
|
||||
/// - materialPath (string): Asset path to material (e.g., "Assets/Materials/Fire.mat")
|
||||
/// - color (array): Color as [r, g, b, a] with values 0-1
|
||||
/// - position (array): 3D position as [x, y, z]
|
||||
/// - gradient (object): {colorKeys: [{color: [r,g,b,a], time: 0-1}], alphaKeys: [{alpha: 0-1, time: 0-1}]}
|
||||
/// - curve (object): {keys: [{time: 0-1, value: number, inTangent: number, outTangent: number}]}
|
||||
///
|
||||
/// For full parameter details, refer to Unity documentation for each component type.
|
||||
/// </summary>
|
||||
[McpForUnityTool("manage_vfx", AutoRegister = false)]
|
||||
public static class ManageVFX
|
||||
{
|
||||
private static readonly Dictionary<string, string> ParamAliases = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "size_over_lifetime", "size" },
|
||||
{ "start_color_line", "startColor" },
|
||||
{ "sorting_layer_id", "sortingLayerID" },
|
||||
{ "material", "materialPath" },
|
||||
};
|
||||
|
||||
private static JObject NormalizeParams(JObject source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return new JObject();
|
||||
}
|
||||
|
||||
var normalized = new JObject();
|
||||
var properties = ExtractProperties(source);
|
||||
if (properties != null)
|
||||
{
|
||||
foreach (var prop in properties.Properties())
|
||||
{
|
||||
normalized[NormalizeKey(prop.Name, true)] = NormalizeToken(prop.Value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in source.Properties())
|
||||
{
|
||||
if (string.Equals(prop.Name, "properties", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
normalized[NormalizeKey(prop.Name, true)] = NormalizeToken(prop.Value);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static JObject ExtractProperties(JObject source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!source.TryGetValue("properties", StringComparison.OrdinalIgnoreCase, out var token))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (token == null || token.Type == JTokenType.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (token is JObject obj)
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (token.Type == JTokenType.String)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JToken.Parse(token.ToString()) as JObject;
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
throw new JsonException(
|
||||
$"Failed to parse 'properties' JSON string. Raw value: {token}",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string NormalizeKey(string key, bool allowAliases)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
return key;
|
||||
}
|
||||
if (string.Equals(key, "action", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "action";
|
||||
}
|
||||
if (allowAliases && ParamAliases.TryGetValue(key, out var alias))
|
||||
{
|
||||
return alias;
|
||||
}
|
||||
if (key.IndexOf('_') >= 0)
|
||||
{
|
||||
return ToCamelCase(key);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
private static JToken NormalizeToken(JToken token)
|
||||
{
|
||||
if (token == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (token is JObject obj)
|
||||
{
|
||||
var normalized = new JObject();
|
||||
foreach (var prop in obj.Properties())
|
||||
{
|
||||
normalized[NormalizeKey(prop.Name, false)] = NormalizeToken(prop.Value);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (token is JArray array)
|
||||
{
|
||||
var normalized = new JArray();
|
||||
foreach (var item in array)
|
||||
{
|
||||
normalized.Add(NormalizeToken(item));
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
private static string ToCamelCase(string key) => StringCaseUtility.ToCamelCase(key);
|
||||
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
JObject normalizedParams = NormalizeParams(@params);
|
||||
string action = normalizedParams["action"]?.ToString();
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return new { success = false, message = "Action is required" };
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string actionLower = action.ToLowerInvariant();
|
||||
|
||||
// Route to appropriate handler based on action prefix
|
||||
if (actionLower == "ping")
|
||||
{
|
||||
return new { success = true, tool = "manage_vfx", components = new[] { "ParticleSystem", "VisualEffect", "LineRenderer", "TrailRenderer" } };
|
||||
}
|
||||
|
||||
// ParticleSystem actions (particle_*)
|
||||
if (actionLower.StartsWith("particle_"))
|
||||
{
|
||||
return HandleParticleSystemAction(normalizedParams, actionLower.Substring(9));
|
||||
}
|
||||
|
||||
// VFX Graph actions (vfx_*)
|
||||
if (actionLower.StartsWith("vfx_"))
|
||||
{
|
||||
return HandleVFXGraphAction(normalizedParams, actionLower.Substring(4));
|
||||
}
|
||||
|
||||
// LineRenderer actions (line_*)
|
||||
if (actionLower.StartsWith("line_"))
|
||||
{
|
||||
return HandleLineRendererAction(normalizedParams, actionLower.Substring(5));
|
||||
}
|
||||
|
||||
// TrailRenderer actions (trail_*)
|
||||
if (actionLower.StartsWith("trail_"))
|
||||
{
|
||||
return HandleTrailRendererAction(normalizedParams, actionLower.Substring(6));
|
||||
}
|
||||
|
||||
return new { success = false, message = $"Unknown action: {action}. Actions must be prefixed with: particle_, vfx_, line_, or trail_" };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new { success = false, message = ex.Message, stackTrace = ex.StackTrace };
|
||||
}
|
||||
}
|
||||
|
||||
private static object HandleParticleSystemAction(JObject @params, string action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "get_info": return ParticleRead.GetInfo(@params);
|
||||
case "set_main": return ParticleWrite.SetMain(@params);
|
||||
case "set_emission": return ParticleWrite.SetEmission(@params);
|
||||
case "set_shape": return ParticleWrite.SetShape(@params);
|
||||
case "set_color_over_lifetime": return ParticleWrite.SetColorOverLifetime(@params);
|
||||
case "set_size_over_lifetime": return ParticleWrite.SetSizeOverLifetime(@params);
|
||||
case "set_velocity_over_lifetime": return ParticleWrite.SetVelocityOverLifetime(@params);
|
||||
case "set_noise": return ParticleWrite.SetNoise(@params);
|
||||
case "set_renderer": return ParticleWrite.SetRenderer(@params);
|
||||
case "enable_module": return ParticleControl.EnableModule(@params);
|
||||
case "play": return ParticleControl.Control(@params, "play");
|
||||
case "stop": return ParticleControl.Control(@params, "stop");
|
||||
case "pause": return ParticleControl.Control(@params, "pause");
|
||||
case "restart": return ParticleControl.Control(@params, "restart");
|
||||
case "clear": return ParticleControl.Control(@params, "clear");
|
||||
case "add_burst": return ParticleControl.AddBurst(@params);
|
||||
case "clear_bursts": return ParticleControl.ClearBursts(@params);
|
||||
default:
|
||||
return new { success = false, message = $"Unknown particle action: {action}. Valid: get_info, set_main, set_emission, set_shape, set_color_over_lifetime, set_size_over_lifetime, set_velocity_over_lifetime, set_noise, set_renderer, enable_module, play, stop, pause, restart, clear, add_burst, clear_bursts" };
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== VFX GRAPH ====================
|
||||
#region VFX Graph
|
||||
|
||||
private static object HandleVFXGraphAction(JObject @params, string action)
|
||||
{
|
||||
#if !UNITY_VFX_GRAPH
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
#else
|
||||
switch (action)
|
||||
{
|
||||
// Asset management
|
||||
case "create_asset": return VfxGraphAssets.CreateAsset(@params);
|
||||
case "assign_asset": return VfxGraphAssets.AssignAsset(@params);
|
||||
case "list_templates": return VfxGraphAssets.ListTemplates(@params);
|
||||
case "list_assets": return VfxGraphAssets.ListAssets(@params);
|
||||
|
||||
// Runtime parameter control
|
||||
case "get_info": return VfxGraphRead.GetInfo(@params);
|
||||
case "set_float": return VfxGraphWrite.SetParameter<float>(@params, (vfx, n, v) => vfx.SetFloat(n, v));
|
||||
case "set_int": return VfxGraphWrite.SetParameter<int>(@params, (vfx, n, v) => vfx.SetInt(n, v));
|
||||
case "set_bool": return VfxGraphWrite.SetParameter<bool>(@params, (vfx, n, v) => vfx.SetBool(n, v));
|
||||
case "set_vector2": return VfxGraphWrite.SetVector(@params, 2);
|
||||
case "set_vector3": return VfxGraphWrite.SetVector(@params, 3);
|
||||
case "set_vector4": return VfxGraphWrite.SetVector(@params, 4);
|
||||
case "set_color": return VfxGraphWrite.SetColor(@params);
|
||||
case "set_gradient": return VfxGraphWrite.SetGradient(@params);
|
||||
case "set_texture": return VfxGraphWrite.SetTexture(@params);
|
||||
case "set_mesh": return VfxGraphWrite.SetMesh(@params);
|
||||
case "set_curve": return VfxGraphWrite.SetCurve(@params);
|
||||
case "send_event": return VfxGraphWrite.SendEvent(@params);
|
||||
case "play": return VfxGraphControl.Control(@params, "play");
|
||||
case "stop": return VfxGraphControl.Control(@params, "stop");
|
||||
case "pause": return VfxGraphControl.Control(@params, "pause");
|
||||
case "reinit": return VfxGraphControl.Control(@params, "reinit");
|
||||
case "set_playback_speed": return VfxGraphControl.SetPlaybackSpeed(@params);
|
||||
case "set_seed": return VfxGraphControl.SetSeed(@params);
|
||||
default:
|
||||
return new { success = false, message = $"Unknown vfx action: {action}. Valid: create_asset, assign_asset, list_templates, list_assets, get_info, set_float, set_int, set_bool, set_vector2/3/4, set_color, set_gradient, set_texture, set_mesh, set_curve, send_event, play, stop, pause, reinit, set_playback_speed, set_seed" };
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
private static object HandleLineRendererAction(JObject @params, string action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "get_info": return LineRead.GetInfo(@params);
|
||||
case "set_positions": return LineWrite.SetPositions(@params);
|
||||
case "add_position": return LineWrite.AddPosition(@params);
|
||||
case "set_position": return LineWrite.SetPosition(@params);
|
||||
case "set_width": return LineWrite.SetWidth(@params);
|
||||
case "set_color": return LineWrite.SetColor(@params);
|
||||
case "set_material": return LineWrite.SetMaterial(@params);
|
||||
case "set_properties": return LineWrite.SetProperties(@params);
|
||||
case "clear": return LineWrite.Clear(@params);
|
||||
case "create_line": return LineCreate.CreateLine(@params);
|
||||
case "create_circle": return LineCreate.CreateCircle(@params);
|
||||
case "create_arc": return LineCreate.CreateArc(@params);
|
||||
case "create_bezier": return LineCreate.CreateBezier(@params);
|
||||
default:
|
||||
return new { success = false, message = $"Unknown line action: {action}. Valid: get_info, set_positions, add_position, set_position, set_width, set_color, set_material, set_properties, clear, create_line, create_circle, create_arc, create_bezier" };
|
||||
}
|
||||
}
|
||||
|
||||
private static object HandleTrailRendererAction(JObject @params, string action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "get_info": return TrailRead.GetInfo(@params);
|
||||
case "set_time": return TrailWrite.SetTime(@params);
|
||||
case "set_width": return TrailWrite.SetWidth(@params);
|
||||
case "set_color": return TrailWrite.SetColor(@params);
|
||||
case "set_material": return TrailWrite.SetMaterial(@params);
|
||||
case "set_properties": return TrailWrite.SetProperties(@params);
|
||||
case "clear": return TrailControl.Clear(@params);
|
||||
case "emit": return TrailControl.Emit(@params);
|
||||
default:
|
||||
return new { success = false, message = $"Unknown trail action: {action}. Valid: get_info, set_time, set_width, set_color, set_material, set_properties, clear, emit" };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Packages/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs.meta
Normal file
13
Packages/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs.meta
Normal file
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8f3d2c1e9b74f6a8c5d0e2f1a3b4c5d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
|
||||
22
Packages/MCPForUnity/Editor/Tools/Vfx/ManageVfxCommon.cs
Normal file
22
Packages/MCPForUnity/Editor/Tools/Vfx/ManageVfxCommon.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
internal static class ManageVfxCommon
|
||||
{
|
||||
public static Color ParseColor(JToken token) => VectorParsing.ParseColorOrDefault(token);
|
||||
public static Vector3 ParseVector3(JToken token) => VectorParsing.ParseVector3OrDefault(token);
|
||||
public static Vector4 ParseVector4(JToken token) => VectorParsing.ParseVector4OrDefault(token);
|
||||
public static Gradient ParseGradient(JToken token) => VectorParsing.ParseGradientOrDefault(token);
|
||||
public static AnimationCurve ParseAnimationCurve(JToken token, float defaultValue = 1f)
|
||||
=> VectorParsing.ParseAnimationCurveOrDefault(token, defaultValue);
|
||||
|
||||
public static GameObject FindTargetGameObject(JObject @params)
|
||||
=> ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
|
||||
|
||||
public static Material FindMaterialByPath(string path)
|
||||
=> ObjectResolver.ResolveMaterial(path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c5e603b26d2f47529394c1ec6b8ed79
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
87
Packages/MCPForUnity/Editor/Tools/Vfx/ParticleCommon.cs
Normal file
87
Packages/MCPForUnity/Editor/Tools/Vfx/ParticleCommon.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
internal static class ParticleCommon
|
||||
{
|
||||
public static ParticleSystem FindParticleSystem(JObject @params)
|
||||
{
|
||||
GameObject go = ManageVfxCommon.FindTargetGameObject(@params);
|
||||
return go?.GetComponent<ParticleSystem>();
|
||||
}
|
||||
|
||||
public static ParticleSystem.MinMaxCurve ParseMinMaxCurve(JToken token, float defaultValue = 1f)
|
||||
{
|
||||
if (token == null)
|
||||
return new ParticleSystem.MinMaxCurve(defaultValue);
|
||||
|
||||
if (token.Type == JTokenType.Float || token.Type == JTokenType.Integer)
|
||||
{
|
||||
return new ParticleSystem.MinMaxCurve(token.ToObject<float>());
|
||||
}
|
||||
|
||||
if (token is JObject obj)
|
||||
{
|
||||
string mode = obj["mode"]?.ToString()?.ToLowerInvariant() ?? "constant";
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case "constant":
|
||||
float constant = obj["value"]?.ToObject<float>() ?? defaultValue;
|
||||
return new ParticleSystem.MinMaxCurve(constant);
|
||||
|
||||
case "random_between_constants":
|
||||
case "two_constants":
|
||||
float min = obj["min"]?.ToObject<float>() ?? 0f;
|
||||
float max = obj["max"]?.ToObject<float>() ?? 1f;
|
||||
return new ParticleSystem.MinMaxCurve(min, max);
|
||||
|
||||
case "curve":
|
||||
AnimationCurve curve = ManageVfxCommon.ParseAnimationCurve(obj, defaultValue);
|
||||
return new ParticleSystem.MinMaxCurve(obj["multiplier"]?.ToObject<float>() ?? 1f, curve);
|
||||
|
||||
default:
|
||||
return new ParticleSystem.MinMaxCurve(defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
return new ParticleSystem.MinMaxCurve(defaultValue);
|
||||
}
|
||||
|
||||
public static ParticleSystem.MinMaxGradient ParseMinMaxGradient(JToken token)
|
||||
{
|
||||
if (token == null)
|
||||
return new ParticleSystem.MinMaxGradient(Color.white);
|
||||
|
||||
if (token is JArray arr && arr.Count >= 3)
|
||||
{
|
||||
return new ParticleSystem.MinMaxGradient(ManageVfxCommon.ParseColor(arr));
|
||||
}
|
||||
|
||||
if (token is JObject obj)
|
||||
{
|
||||
string mode = obj["mode"]?.ToString()?.ToLowerInvariant() ?? "color";
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case "color":
|
||||
return new ParticleSystem.MinMaxGradient(ManageVfxCommon.ParseColor(obj["color"]));
|
||||
|
||||
case "two_colors":
|
||||
Color colorMin = ManageVfxCommon.ParseColor(obj["colorMin"]);
|
||||
Color colorMax = ManageVfxCommon.ParseColor(obj["colorMax"]);
|
||||
return new ParticleSystem.MinMaxGradient(colorMin, colorMax);
|
||||
|
||||
case "gradient":
|
||||
return new ParticleSystem.MinMaxGradient(ManageVfxCommon.ParseGradient(obj));
|
||||
|
||||
default:
|
||||
return new ParticleSystem.MinMaxGradient(Color.white);
|
||||
}
|
||||
}
|
||||
|
||||
return new ParticleSystem.MinMaxGradient(Color.white);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/Vfx/ParticleCommon.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/Vfx/ParticleCommon.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3a91aa6f6b9c4121a2ccc1a8147bbf9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
121
Packages/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs
Normal file
121
Packages/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
internal static class ParticleControl
|
||||
{
|
||||
public static object EnableModule(JObject @params)
|
||||
{
|
||||
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
|
||||
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
|
||||
|
||||
string moduleName = @params["module"]?.ToString()?.ToLowerInvariant();
|
||||
bool enabled = @params["enabled"]?.ToObject<bool>() ?? true;
|
||||
|
||||
if (string.IsNullOrEmpty(moduleName)) return new { success = false, message = "Module name required" };
|
||||
|
||||
Undo.RecordObject(ps, $"Toggle {moduleName}");
|
||||
|
||||
switch (moduleName.Replace("_", ""))
|
||||
{
|
||||
case "emission": var em = ps.emission; em.enabled = enabled; break;
|
||||
case "shape": var sh = ps.shape; sh.enabled = enabled; break;
|
||||
case "coloroverlifetime": var col = ps.colorOverLifetime; col.enabled = enabled; break;
|
||||
case "sizeoverlifetime": var sol = ps.sizeOverLifetime; sol.enabled = enabled; break;
|
||||
case "velocityoverlifetime": var vol = ps.velocityOverLifetime; vol.enabled = enabled; break;
|
||||
case "noise": var n = ps.noise; n.enabled = enabled; break;
|
||||
case "collision": var coll = ps.collision; coll.enabled = enabled; break;
|
||||
case "trails": var tr = ps.trails; tr.enabled = enabled; break;
|
||||
case "lights": var li = ps.lights; li.enabled = enabled; break;
|
||||
default: return new { success = false, message = $"Unknown module: {moduleName}" };
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(ps);
|
||||
return new { success = true, message = $"Module '{moduleName}' {(enabled ? "enabled" : "disabled")}" };
|
||||
}
|
||||
|
||||
public static object Control(JObject @params, string action)
|
||||
{
|
||||
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
|
||||
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
|
||||
|
||||
// Ensure material is assigned before playing
|
||||
if (action == "play" || action == "restart")
|
||||
{
|
||||
var renderer = ps.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
RendererHelpers.EnsureMaterial(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
bool withChildren = @params["withChildren"]?.ToObject<bool>() ?? true;
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case "play": ps.Play(withChildren); break;
|
||||
case "stop": ps.Stop(withChildren, ParticleSystemStopBehavior.StopEmitting); break;
|
||||
case "pause": ps.Pause(withChildren); break;
|
||||
case "restart": ps.Stop(withChildren, ParticleSystemStopBehavior.StopEmittingAndClear); ps.Play(withChildren); break;
|
||||
case "clear": ps.Clear(withChildren); break;
|
||||
default: return new { success = false, message = $"Unknown action: {action}" };
|
||||
}
|
||||
|
||||
return new { success = true, message = $"ParticleSystem {action}" };
|
||||
}
|
||||
|
||||
public static object AddBurst(JObject @params)
|
||||
{
|
||||
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
|
||||
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
|
||||
|
||||
// Ensure material is assigned
|
||||
var renderer = ps.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
RendererHelpers.EnsureMaterial(renderer);
|
||||
}
|
||||
|
||||
Undo.RecordObject(ps, "Add Burst");
|
||||
var emission = ps.emission;
|
||||
|
||||
float time = @params["time"]?.ToObject<float>() ?? 0f;
|
||||
int minCountRaw = @params["minCount"]?.ToObject<int>() ?? @params["count"]?.ToObject<int>() ?? 30;
|
||||
int maxCountRaw = @params["maxCount"]?.ToObject<int>() ?? @params["count"]?.ToObject<int>() ?? 30;
|
||||
short minCount = (short)Math.Clamp(minCountRaw, 0, short.MaxValue);
|
||||
short maxCount = (short)Math.Clamp(maxCountRaw, 0, short.MaxValue);
|
||||
int cycles = @params["cycles"]?.ToObject<int>() ?? 1;
|
||||
float interval = @params["interval"]?.ToObject<float>() ?? 0.01f;
|
||||
|
||||
var burst = new ParticleSystem.Burst(time, minCount, maxCount, cycles, interval);
|
||||
burst.probability = @params["probability"]?.ToObject<float>() ?? 1f;
|
||||
|
||||
int idx = emission.burstCount;
|
||||
var bursts = new ParticleSystem.Burst[idx + 1];
|
||||
emission.GetBursts(bursts);
|
||||
bursts[idx] = burst;
|
||||
emission.SetBursts(bursts);
|
||||
|
||||
EditorUtility.SetDirty(ps);
|
||||
return new { success = true, message = $"Added burst at t={time}", burstIndex = idx };
|
||||
}
|
||||
|
||||
public static object ClearBursts(JObject @params)
|
||||
{
|
||||
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
|
||||
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
|
||||
|
||||
Undo.RecordObject(ps, "Clear Bursts");
|
||||
var emission = ps.emission;
|
||||
int count = emission.burstCount;
|
||||
emission.SetBursts(new ParticleSystem.Burst[0]);
|
||||
|
||||
EditorUtility.SetDirty(ps);
|
||||
return new { success = true, message = $"Cleared {count} bursts" };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 04e1bfb655f184337943edd5a3fbbcdb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
153
Packages/MCPForUnity/Editor/Tools/Vfx/ParticleRead.cs
Normal file
153
Packages/MCPForUnity/Editor/Tools/Vfx/ParticleRead.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
internal static class ParticleRead
|
||||
{
|
||||
private static object SerializeAnimationCurve(AnimationCurve curve)
|
||||
{
|
||||
if (curve == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
keys = curve.keys.Select(k => new
|
||||
{
|
||||
time = k.time,
|
||||
value = k.value,
|
||||
inTangent = k.inTangent,
|
||||
outTangent = k.outTangent
|
||||
}).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private static object SerializeMinMaxCurve(ParticleSystem.MinMaxCurve curve)
|
||||
{
|
||||
switch (curve.mode)
|
||||
{
|
||||
case ParticleSystemCurveMode.Constant:
|
||||
return new
|
||||
{
|
||||
mode = "constant",
|
||||
value = curve.constant
|
||||
};
|
||||
|
||||
case ParticleSystemCurveMode.TwoConstants:
|
||||
return new
|
||||
{
|
||||
mode = "two_constants",
|
||||
min = curve.constantMin,
|
||||
max = curve.constantMax
|
||||
};
|
||||
|
||||
case ParticleSystemCurveMode.Curve:
|
||||
return new
|
||||
{
|
||||
mode = "curve",
|
||||
multiplier = curve.curveMultiplier,
|
||||
keys = curve.curve.keys.Select(k => new
|
||||
{
|
||||
time = k.time,
|
||||
value = k.value,
|
||||
inTangent = k.inTangent,
|
||||
outTangent = k.outTangent
|
||||
}).ToArray()
|
||||
};
|
||||
|
||||
case ParticleSystemCurveMode.TwoCurves:
|
||||
return new
|
||||
{
|
||||
mode = "curve",
|
||||
multiplier = curve.curveMultiplier,
|
||||
keys = curve.curveMax.keys.Select(k => new
|
||||
{
|
||||
time = k.time,
|
||||
value = k.value,
|
||||
inTangent = k.inTangent,
|
||||
outTangent = k.outTangent
|
||||
}).ToArray(),
|
||||
originalMode = "two_curves",
|
||||
curveMin = SerializeAnimationCurve(curve.curveMin),
|
||||
curveMax = SerializeAnimationCurve(curve.curveMax)
|
||||
};
|
||||
|
||||
default:
|
||||
return new
|
||||
{
|
||||
mode = "constant",
|
||||
value = curve.constant
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static object GetInfo(JObject @params)
|
||||
{
|
||||
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
|
||||
if (ps == null)
|
||||
{
|
||||
return new { success = false, message = "ParticleSystem not found" };
|
||||
}
|
||||
|
||||
var main = ps.main;
|
||||
var emission = ps.emission;
|
||||
var shape = ps.shape;
|
||||
var renderer = ps.GetComponent<ParticleSystemRenderer>();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
data = new
|
||||
{
|
||||
gameObject = ps.gameObject.name,
|
||||
isPlaying = ps.isPlaying,
|
||||
isPaused = ps.isPaused,
|
||||
particleCount = ps.particleCount,
|
||||
main = new
|
||||
{
|
||||
duration = main.duration,
|
||||
looping = main.loop,
|
||||
startLifetime = SerializeMinMaxCurve(main.startLifetime),
|
||||
startSpeed = SerializeMinMaxCurve(main.startSpeed),
|
||||
startSize = SerializeMinMaxCurve(main.startSize),
|
||||
gravityModifier = SerializeMinMaxCurve(main.gravityModifier),
|
||||
simulationSpace = main.simulationSpace.ToString(),
|
||||
maxParticles = main.maxParticles
|
||||
},
|
||||
emission = new
|
||||
{
|
||||
enabled = emission.enabled,
|
||||
rateOverTime = SerializeMinMaxCurve(emission.rateOverTime),
|
||||
burstCount = emission.burstCount
|
||||
},
|
||||
shape = new
|
||||
{
|
||||
enabled = shape.enabled,
|
||||
shapeType = shape.shapeType.ToString(),
|
||||
radius = shape.radius,
|
||||
angle = shape.angle
|
||||
},
|
||||
renderer = renderer != null ? new
|
||||
{
|
||||
renderMode = renderer.renderMode.ToString(),
|
||||
sortMode = renderer.sortMode.ToString(),
|
||||
material = renderer.sharedMaterial?.name,
|
||||
trailMaterial = renderer.trailMaterial?.name,
|
||||
minParticleSize = renderer.minParticleSize,
|
||||
maxParticleSize = renderer.maxParticleSize,
|
||||
shadowCastingMode = renderer.shadowCastingMode.ToString(),
|
||||
receiveShadows = renderer.receiveShadows,
|
||||
lightProbeUsage = renderer.lightProbeUsage.ToString(),
|
||||
reflectionProbeUsage = renderer.reflectionProbeUsage.ToString(),
|
||||
sortingOrder = renderer.sortingOrder,
|
||||
sortingLayerName = renderer.sortingLayerName,
|
||||
renderingLayerMask = renderer.renderingLayerMask
|
||||
} : null
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/Vfx/ParticleRead.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/Vfx/ParticleRead.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74bb7c48a4e1944bcba43b3619653cb9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
295
Packages/MCPForUnity/Editor/Tools/Vfx/ParticleWrite.cs
Normal file
295
Packages/MCPForUnity/Editor/Tools/Vfx/ParticleWrite.cs
Normal file
@@ -0,0 +1,295 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
internal static class ParticleWrite
|
||||
{
|
||||
public static object SetMain(JObject @params)
|
||||
{
|
||||
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
|
||||
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
|
||||
|
||||
// Ensure material is assigned before any configuration
|
||||
var renderer = ps.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
RendererHelpers.EnsureMaterial(renderer);
|
||||
}
|
||||
|
||||
// Stop particle system if it's playing and duration needs to be changed
|
||||
bool wasPlaying = ps.isPlaying;
|
||||
bool needsStop = @params["duration"] != null && wasPlaying;
|
||||
if (needsStop)
|
||||
{
|
||||
ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
|
||||
}
|
||||
|
||||
Undo.RecordObject(ps, "Set ParticleSystem Main");
|
||||
var main = ps.main;
|
||||
var changes = new List<string>();
|
||||
|
||||
if (@params["duration"] != null) { main.duration = @params["duration"].ToObject<float>(); changes.Add("duration"); }
|
||||
if (@params["looping"] != null) { main.loop = @params["looping"].ToObject<bool>(); changes.Add("looping"); }
|
||||
if (@params["prewarm"] != null) { main.prewarm = @params["prewarm"].ToObject<bool>(); changes.Add("prewarm"); }
|
||||
if (@params["startDelay"] != null) { main.startDelay = ParticleCommon.ParseMinMaxCurve(@params["startDelay"], 0f); changes.Add("startDelay"); }
|
||||
if (@params["startLifetime"] != null) { main.startLifetime = ParticleCommon.ParseMinMaxCurve(@params["startLifetime"], 5f); changes.Add("startLifetime"); }
|
||||
if (@params["startSpeed"] != null) { main.startSpeed = ParticleCommon.ParseMinMaxCurve(@params["startSpeed"], 5f); changes.Add("startSpeed"); }
|
||||
if (@params["startSize"] != null) { main.startSize = ParticleCommon.ParseMinMaxCurve(@params["startSize"], 1f); changes.Add("startSize"); }
|
||||
if (@params["startRotation"] != null) { main.startRotation = ParticleCommon.ParseMinMaxCurve(@params["startRotation"], 0f); changes.Add("startRotation"); }
|
||||
if (@params["startColor"] != null) { main.startColor = ParticleCommon.ParseMinMaxGradient(@params["startColor"]); changes.Add("startColor"); }
|
||||
if (@params["gravityModifier"] != null) { main.gravityModifier = ParticleCommon.ParseMinMaxCurve(@params["gravityModifier"], 0f); changes.Add("gravityModifier"); }
|
||||
if (@params["simulationSpace"] != null && Enum.TryParse<ParticleSystemSimulationSpace>(@params["simulationSpace"].ToString(), true, out var simSpace)) { main.simulationSpace = simSpace; changes.Add("simulationSpace"); }
|
||||
if (@params["scalingMode"] != null && Enum.TryParse<ParticleSystemScalingMode>(@params["scalingMode"].ToString(), true, out var scaleMode)) { main.scalingMode = scaleMode; changes.Add("scalingMode"); }
|
||||
if (@params["playOnAwake"] != null) { main.playOnAwake = @params["playOnAwake"].ToObject<bool>(); changes.Add("playOnAwake"); }
|
||||
if (@params["maxParticles"] != null) { main.maxParticles = @params["maxParticles"].ToObject<int>(); changes.Add("maxParticles"); }
|
||||
|
||||
EditorUtility.SetDirty(ps);
|
||||
|
||||
// Restart particle system if it was playing
|
||||
if (needsStop && wasPlaying)
|
||||
{
|
||||
ps.Play(true);
|
||||
changes.Add("(restarted after duration change)");
|
||||
}
|
||||
|
||||
return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
|
||||
}
|
||||
|
||||
public static object SetEmission(JObject @params)
|
||||
{
|
||||
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
|
||||
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
|
||||
|
||||
// Ensure material is assigned
|
||||
var renderer = ps.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
RendererHelpers.EnsureMaterial(renderer);
|
||||
}
|
||||
|
||||
Undo.RecordObject(ps, "Set ParticleSystem Emission");
|
||||
var emission = ps.emission;
|
||||
var changes = new List<string>();
|
||||
|
||||
if (@params["enabled"] != null) { emission.enabled = @params["enabled"].ToObject<bool>(); changes.Add("enabled"); }
|
||||
if (@params["rateOverTime"] != null) { emission.rateOverTime = ParticleCommon.ParseMinMaxCurve(@params["rateOverTime"], 10f); changes.Add("rateOverTime"); }
|
||||
if (@params["rateOverDistance"] != null) { emission.rateOverDistance = ParticleCommon.ParseMinMaxCurve(@params["rateOverDistance"], 0f); changes.Add("rateOverDistance"); }
|
||||
|
||||
EditorUtility.SetDirty(ps);
|
||||
return new { success = true, message = $"Updated emission: {string.Join(", ", changes)}" };
|
||||
}
|
||||
|
||||
public static object SetShape(JObject @params)
|
||||
{
|
||||
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
|
||||
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
|
||||
|
||||
// Ensure material is assigned
|
||||
var renderer = ps.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
RendererHelpers.EnsureMaterial(renderer);
|
||||
}
|
||||
|
||||
Undo.RecordObject(ps, "Set ParticleSystem Shape");
|
||||
var shape = ps.shape;
|
||||
var changes = new List<string>();
|
||||
|
||||
if (@params["enabled"] != null) { shape.enabled = @params["enabled"].ToObject<bool>(); changes.Add("enabled"); }
|
||||
if (@params["shapeType"] != null && Enum.TryParse<ParticleSystemShapeType>(@params["shapeType"].ToString(), true, out var shapeType)) { shape.shapeType = shapeType; changes.Add("shapeType"); }
|
||||
if (@params["radius"] != null) { shape.radius = @params["radius"].ToObject<float>(); changes.Add("radius"); }
|
||||
if (@params["radiusThickness"] != null) { shape.radiusThickness = @params["radiusThickness"].ToObject<float>(); changes.Add("radiusThickness"); }
|
||||
if (@params["angle"] != null) { shape.angle = @params["angle"].ToObject<float>(); changes.Add("angle"); }
|
||||
if (@params["arc"] != null) { shape.arc = @params["arc"].ToObject<float>(); changes.Add("arc"); }
|
||||
if (@params["position"] != null) { shape.position = ManageVfxCommon.ParseVector3(@params["position"]); changes.Add("position"); }
|
||||
if (@params["rotation"] != null) { shape.rotation = ManageVfxCommon.ParseVector3(@params["rotation"]); changes.Add("rotation"); }
|
||||
if (@params["scale"] != null) { shape.scale = ManageVfxCommon.ParseVector3(@params["scale"]); changes.Add("scale"); }
|
||||
|
||||
EditorUtility.SetDirty(ps);
|
||||
return new { success = true, message = $"Updated shape: {string.Join(", ", changes)}" };
|
||||
}
|
||||
|
||||
public static object SetColorOverLifetime(JObject @params)
|
||||
{
|
||||
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
|
||||
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
|
||||
|
||||
// Ensure material is assigned
|
||||
var renderer = ps.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
RendererHelpers.EnsureMaterial(renderer);
|
||||
}
|
||||
|
||||
Undo.RecordObject(ps, "Set ParticleSystem Color Over Lifetime");
|
||||
var col = ps.colorOverLifetime;
|
||||
var changes = new List<string>();
|
||||
|
||||
if (@params["enabled"] != null) { col.enabled = @params["enabled"].ToObject<bool>(); changes.Add("enabled"); }
|
||||
if (@params["color"] != null) { col.color = ParticleCommon.ParseMinMaxGradient(@params["color"]); changes.Add("color"); }
|
||||
|
||||
EditorUtility.SetDirty(ps);
|
||||
return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
|
||||
}
|
||||
|
||||
public static object SetSizeOverLifetime(JObject @params)
|
||||
{
|
||||
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
|
||||
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
|
||||
|
||||
// Ensure material is assigned
|
||||
var renderer = ps.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
RendererHelpers.EnsureMaterial(renderer);
|
||||
}
|
||||
|
||||
Undo.RecordObject(ps, "Set ParticleSystem Size Over Lifetime");
|
||||
var sol = ps.sizeOverLifetime;
|
||||
var changes = new List<string>();
|
||||
|
||||
bool hasSizeProperty = @params["size"] != null || @params["sizeX"] != null ||
|
||||
@params["sizeY"] != null || @params["sizeZ"] != null;
|
||||
if (hasSizeProperty && @params["enabled"] == null && !sol.enabled)
|
||||
{
|
||||
sol.enabled = true;
|
||||
changes.Add("enabled");
|
||||
}
|
||||
else if (@params["enabled"] != null)
|
||||
{
|
||||
sol.enabled = @params["enabled"].ToObject<bool>();
|
||||
changes.Add("enabled");
|
||||
}
|
||||
|
||||
if (@params["separateAxes"] != null) { sol.separateAxes = @params["separateAxes"].ToObject<bool>(); changes.Add("separateAxes"); }
|
||||
if (@params["size"] != null) { sol.size = ParticleCommon.ParseMinMaxCurve(@params["size"], 1f); changes.Add("size"); }
|
||||
if (@params["sizeX"] != null) { sol.x = ParticleCommon.ParseMinMaxCurve(@params["sizeX"], 1f); changes.Add("sizeX"); }
|
||||
if (@params["sizeY"] != null) { sol.y = ParticleCommon.ParseMinMaxCurve(@params["sizeY"], 1f); changes.Add("sizeY"); }
|
||||
if (@params["sizeZ"] != null) { sol.z = ParticleCommon.ParseMinMaxCurve(@params["sizeZ"], 1f); changes.Add("sizeZ"); }
|
||||
|
||||
EditorUtility.SetDirty(ps);
|
||||
return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
|
||||
}
|
||||
|
||||
public static object SetVelocityOverLifetime(JObject @params)
|
||||
{
|
||||
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
|
||||
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
|
||||
|
||||
// Ensure material is assigned
|
||||
var renderer = ps.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
RendererHelpers.EnsureMaterial(renderer);
|
||||
}
|
||||
|
||||
Undo.RecordObject(ps, "Set ParticleSystem Velocity Over Lifetime");
|
||||
var vol = ps.velocityOverLifetime;
|
||||
var changes = new List<string>();
|
||||
|
||||
if (@params["enabled"] != null) { vol.enabled = @params["enabled"].ToObject<bool>(); changes.Add("enabled"); }
|
||||
if (@params["space"] != null && Enum.TryParse<ParticleSystemSimulationSpace>(@params["space"].ToString(), true, out var space)) { vol.space = space; changes.Add("space"); }
|
||||
if (@params["x"] != null) { vol.x = ParticleCommon.ParseMinMaxCurve(@params["x"], 0f); changes.Add("x"); }
|
||||
if (@params["y"] != null) { vol.y = ParticleCommon.ParseMinMaxCurve(@params["y"], 0f); changes.Add("y"); }
|
||||
if (@params["z"] != null) { vol.z = ParticleCommon.ParseMinMaxCurve(@params["z"], 0f); changes.Add("z"); }
|
||||
if (@params["speedModifier"] != null) { vol.speedModifier = ParticleCommon.ParseMinMaxCurve(@params["speedModifier"], 1f); changes.Add("speedModifier"); }
|
||||
|
||||
EditorUtility.SetDirty(ps);
|
||||
return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
|
||||
}
|
||||
|
||||
public static object SetNoise(JObject @params)
|
||||
{
|
||||
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
|
||||
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
|
||||
|
||||
// Ensure material is assigned
|
||||
var renderer = ps.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
RendererHelpers.EnsureMaterial(renderer);
|
||||
}
|
||||
|
||||
Undo.RecordObject(ps, "Set ParticleSystem Noise");
|
||||
var noise = ps.noise;
|
||||
var changes = new List<string>();
|
||||
|
||||
if (@params["enabled"] != null) { noise.enabled = @params["enabled"].ToObject<bool>(); changes.Add("enabled"); }
|
||||
if (@params["strength"] != null) { noise.strength = ParticleCommon.ParseMinMaxCurve(@params["strength"], 1f); changes.Add("strength"); }
|
||||
if (@params["frequency"] != null) { noise.frequency = @params["frequency"].ToObject<float>(); changes.Add("frequency"); }
|
||||
if (@params["scrollSpeed"] != null) { noise.scrollSpeed = ParticleCommon.ParseMinMaxCurve(@params["scrollSpeed"], 0f); changes.Add("scrollSpeed"); }
|
||||
if (@params["damping"] != null) { noise.damping = @params["damping"].ToObject<bool>(); changes.Add("damping"); }
|
||||
if (@params["octaveCount"] != null) { noise.octaveCount = @params["octaveCount"].ToObject<int>(); changes.Add("octaveCount"); }
|
||||
if (@params["quality"] != null && Enum.TryParse<ParticleSystemNoiseQuality>(@params["quality"].ToString(), true, out var quality)) { noise.quality = quality; changes.Add("quality"); }
|
||||
|
||||
EditorUtility.SetDirty(ps);
|
||||
return new { success = true, message = $"Updated noise: {string.Join(", ", changes)}" };
|
||||
}
|
||||
|
||||
public static object SetRenderer(JObject @params)
|
||||
{
|
||||
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
|
||||
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
|
||||
|
||||
var renderer = ps.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer == null) return new { success = false, message = "ParticleSystemRenderer not found" };
|
||||
|
||||
// Ensure material is set before any other operations
|
||||
RendererHelpers.EnsureMaterial(renderer);
|
||||
|
||||
Undo.RecordObject(renderer, "Set ParticleSystem Renderer");
|
||||
var changes = new List<string>();
|
||||
|
||||
if (@params["renderMode"] != null && Enum.TryParse<ParticleSystemRenderMode>(@params["renderMode"].ToString(), true, out var renderMode)) { renderer.renderMode = renderMode; changes.Add("renderMode"); }
|
||||
if (@params["sortMode"] != null && Enum.TryParse<ParticleSystemSortMode>(@params["sortMode"].ToString(), true, out var sortMode)) { renderer.sortMode = sortMode; changes.Add("sortMode"); }
|
||||
|
||||
if (@params["minParticleSize"] != null) { renderer.minParticleSize = @params["minParticleSize"].ToObject<float>(); changes.Add("minParticleSize"); }
|
||||
if (@params["maxParticleSize"] != null) { renderer.maxParticleSize = @params["maxParticleSize"].ToObject<float>(); changes.Add("maxParticleSize"); }
|
||||
|
||||
if (@params["lengthScale"] != null) { renderer.lengthScale = @params["lengthScale"].ToObject<float>(); changes.Add("lengthScale"); }
|
||||
if (@params["velocityScale"] != null) { renderer.velocityScale = @params["velocityScale"].ToObject<float>(); changes.Add("velocityScale"); }
|
||||
if (@params["cameraVelocityScale"] != null) { renderer.cameraVelocityScale = @params["cameraVelocityScale"].ToObject<float>(); changes.Add("cameraVelocityScale"); }
|
||||
if (@params["normalDirection"] != null) { renderer.normalDirection = @params["normalDirection"].ToObject<float>(); changes.Add("normalDirection"); }
|
||||
|
||||
if (@params["alignment"] != null && Enum.TryParse<ParticleSystemRenderSpace>(@params["alignment"].ToString(), true, out var alignment)) { renderer.alignment = alignment; changes.Add("alignment"); }
|
||||
if (@params["pivot"] != null) { renderer.pivot = ManageVfxCommon.ParseVector3(@params["pivot"]); changes.Add("pivot"); }
|
||||
if (@params["flip"] != null) { renderer.flip = ManageVfxCommon.ParseVector3(@params["flip"]); changes.Add("flip"); }
|
||||
if (@params["allowRoll"] != null) { renderer.allowRoll = @params["allowRoll"].ToObject<bool>(); changes.Add("allowRoll"); }
|
||||
|
||||
if (@params["shadowBias"] != null) { renderer.shadowBias = @params["shadowBias"].ToObject<float>(); changes.Add("shadowBias"); }
|
||||
|
||||
RendererHelpers.ApplyCommonRendererProperties(renderer, @params, changes);
|
||||
|
||||
if (@params["materialPath"] != null)
|
||||
{
|
||||
string matPath = @params["materialPath"].ToString();
|
||||
var findInst = new JObject { ["find"] = matPath };
|
||||
Material mat = ObjectResolver.Resolve(findInst, typeof(Material)) as Material;
|
||||
if (mat != null)
|
||||
{
|
||||
renderer.sharedMaterial = mat;
|
||||
changes.Add($"material={mat.name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Warn($"Material not found at path: {matPath}. Keeping existing material.");
|
||||
}
|
||||
}
|
||||
|
||||
if (@params["trailMaterialPath"] != null)
|
||||
{
|
||||
var findInst = new JObject { ["find"] = @params["trailMaterialPath"].ToString() };
|
||||
Material mat = ObjectResolver.Resolve(findInst, typeof(Material)) as Material;
|
||||
if (mat != null) { renderer.trailMaterial = mat; changes.Add("trailMaterial"); }
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(renderer);
|
||||
return new { success = true, message = $"Updated renderer: {string.Join(", ", changes)}" };
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/Vfx/ParticleWrite.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/Vfx/ParticleWrite.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a68818a59fac4e2c83ad23433ddc9c1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
36
Packages/MCPForUnity/Editor/Tools/Vfx/TrailControl.cs
Normal file
36
Packages/MCPForUnity/Editor/Tools/Vfx/TrailControl.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
internal static class TrailControl
|
||||
{
|
||||
public static object Clear(JObject @params)
|
||||
{
|
||||
TrailRenderer tr = TrailRead.FindTrailRenderer(@params);
|
||||
if (tr == null) return new { success = false, message = "TrailRenderer not found" };
|
||||
|
||||
Undo.RecordObject(tr, "Clear Trail");
|
||||
tr.Clear();
|
||||
return new { success = true, message = "Trail cleared" };
|
||||
}
|
||||
|
||||
public static object Emit(JObject @params)
|
||||
{
|
||||
TrailRenderer tr = TrailRead.FindTrailRenderer(@params);
|
||||
if (tr == null) return new { success = false, message = "TrailRenderer not found" };
|
||||
|
||||
RendererHelpers.EnsureMaterial(tr);
|
||||
|
||||
#if UNITY_2021_1_OR_NEWER
|
||||
Vector3 pos = ManageVfxCommon.ParseVector3(@params["position"]);
|
||||
tr.AddPosition(pos);
|
||||
return new { success = true, message = $"Emitted at ({pos.x}, {pos.y}, {pos.z})" };
|
||||
#else
|
||||
return new { success = false, message = "AddPosition requires Unity 2021.1+" };
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/Vfx/TrailControl.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/Vfx/TrailControl.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: edebad99699494d5585418395a2bf518
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
49
Packages/MCPForUnity/Editor/Tools/Vfx/TrailRead.cs
Normal file
49
Packages/MCPForUnity/Editor/Tools/Vfx/TrailRead.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
internal static class TrailRead
|
||||
{
|
||||
public static TrailRenderer FindTrailRenderer(JObject @params)
|
||||
{
|
||||
GameObject go = ManageVfxCommon.FindTargetGameObject(@params);
|
||||
return go?.GetComponent<TrailRenderer>();
|
||||
}
|
||||
|
||||
public static object GetInfo(JObject @params)
|
||||
{
|
||||
TrailRenderer tr = FindTrailRenderer(@params);
|
||||
if (tr == null) return new { success = false, message = "TrailRenderer not found" };
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
data = new
|
||||
{
|
||||
gameObject = tr.gameObject.name,
|
||||
time = tr.time,
|
||||
startWidth = tr.startWidth,
|
||||
endWidth = tr.endWidth,
|
||||
minVertexDistance = tr.minVertexDistance,
|
||||
emitting = tr.emitting,
|
||||
autodestruct = tr.autodestruct,
|
||||
positionCount = tr.positionCount,
|
||||
alignment = tr.alignment.ToString(),
|
||||
textureMode = tr.textureMode.ToString(),
|
||||
numCornerVertices = tr.numCornerVertices,
|
||||
numCapVertices = tr.numCapVertices,
|
||||
generateLightingData = tr.generateLightingData,
|
||||
material = tr.sharedMaterial?.name,
|
||||
shadowCastingMode = tr.shadowCastingMode.ToString(),
|
||||
receiveShadows = tr.receiveShadows,
|
||||
lightProbeUsage = tr.lightProbeUsage.ToString(),
|
||||
reflectionProbeUsage = tr.reflectionProbeUsage.ToString(),
|
||||
sortingOrder = tr.sortingOrder,
|
||||
sortingLayerName = tr.sortingLayerName,
|
||||
renderingLayerMask = tr.renderingLayerMask
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/Vfx/TrailRead.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/Vfx/TrailRead.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2921f0042777b4ebbaec4c79c60908a1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
130
Packages/MCPForUnity/Editor/Tools/Vfx/TrailWrite.cs
Normal file
130
Packages/MCPForUnity/Editor/Tools/Vfx/TrailWrite.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
internal static class TrailWrite
|
||||
{
|
||||
public static object SetTime(JObject @params)
|
||||
{
|
||||
TrailRenderer tr = TrailRead.FindTrailRenderer(@params);
|
||||
if (tr == null) return new { success = false, message = "TrailRenderer not found" };
|
||||
|
||||
RendererHelpers.EnsureMaterial(tr);
|
||||
|
||||
float time = @params["time"]?.ToObject<float>() ?? 5f;
|
||||
|
||||
Undo.RecordObject(tr, "Set Trail Time");
|
||||
tr.time = time;
|
||||
EditorUtility.SetDirty(tr);
|
||||
|
||||
return new { success = true, message = $"Set trail time to {time}s" };
|
||||
}
|
||||
|
||||
public static object SetWidth(JObject @params)
|
||||
{
|
||||
TrailRenderer tr = TrailRead.FindTrailRenderer(@params);
|
||||
if (tr == null) return new { success = false, message = "TrailRenderer not found" };
|
||||
|
||||
RendererHelpers.EnsureMaterial(tr);
|
||||
|
||||
Undo.RecordObject(tr, "Set Trail Width");
|
||||
var changes = new List<string>();
|
||||
|
||||
RendererHelpers.ApplyWidthProperties(@params, changes,
|
||||
v => tr.startWidth = v, v => tr.endWidth = v,
|
||||
v => tr.widthCurve = v, v => tr.widthMultiplier = v,
|
||||
ManageVfxCommon.ParseAnimationCurve);
|
||||
|
||||
EditorUtility.SetDirty(tr);
|
||||
return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
|
||||
}
|
||||
|
||||
public static object SetColor(JObject @params)
|
||||
{
|
||||
TrailRenderer tr = TrailRead.FindTrailRenderer(@params);
|
||||
if (tr == null) return new { success = false, message = "TrailRenderer not found" };
|
||||
|
||||
RendererHelpers.EnsureMaterial(tr);
|
||||
|
||||
Undo.RecordObject(tr, "Set Trail Color");
|
||||
var changes = new List<string>();
|
||||
|
||||
RendererHelpers.ApplyColorProperties(@params, changes,
|
||||
v => tr.startColor = v, v => tr.endColor = v,
|
||||
v => tr.colorGradient = v,
|
||||
ManageVfxCommon.ParseColor, ManageVfxCommon.ParseGradient, fadeEndAlpha: true);
|
||||
|
||||
EditorUtility.SetDirty(tr);
|
||||
return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
|
||||
}
|
||||
|
||||
public static object SetMaterial(JObject @params)
|
||||
{
|
||||
TrailRenderer tr = TrailRead.FindTrailRenderer(@params);
|
||||
return RendererHelpers.SetRendererMaterial(tr, @params, "Set Trail Material", ManageVfxCommon.FindMaterialByPath);
|
||||
}
|
||||
|
||||
public static object SetProperties(JObject @params)
|
||||
{
|
||||
TrailRenderer tr = TrailRead.FindTrailRenderer(@params);
|
||||
if (tr == null) return new { success = false, message = "TrailRenderer not found" };
|
||||
|
||||
RendererHelpers.EnsureMaterial(tr);
|
||||
|
||||
Undo.RecordObject(tr, "Set Trail Properties");
|
||||
var changes = new List<string>();
|
||||
|
||||
// Handle material if provided
|
||||
if (@params["materialPath"] != null)
|
||||
{
|
||||
Material mat = ManageVfxCommon.FindMaterialByPath(@params["materialPath"].ToString());
|
||||
if (mat != null)
|
||||
{
|
||||
tr.sharedMaterial = mat;
|
||||
changes.Add($"material={mat.name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Warn($"Material not found: {@params["materialPath"]}");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle time if provided
|
||||
if (@params["time"] != null) { tr.time = @params["time"].ToObject<float>(); changes.Add("time"); }
|
||||
|
||||
// Handle width properties if provided
|
||||
if (@params["width"] != null || @params["startWidth"] != null || @params["endWidth"] != null)
|
||||
{
|
||||
if (@params["width"] != null)
|
||||
{
|
||||
float w = @params["width"].ToObject<float>();
|
||||
tr.startWidth = w;
|
||||
tr.endWidth = w;
|
||||
changes.Add("width");
|
||||
}
|
||||
if (@params["startWidth"] != null) { tr.startWidth = @params["startWidth"].ToObject<float>(); changes.Add("startWidth"); }
|
||||
if (@params["endWidth"] != null) { tr.endWidth = @params["endWidth"].ToObject<float>(); changes.Add("endWidth"); }
|
||||
}
|
||||
|
||||
if (@params["minVertexDistance"] != null) { tr.minVertexDistance = @params["minVertexDistance"].ToObject<float>(); changes.Add("minVertexDistance"); }
|
||||
if (@params["autodestruct"] != null) { tr.autodestruct = @params["autodestruct"].ToObject<bool>(); changes.Add("autodestruct"); }
|
||||
if (@params["emitting"] != null) { tr.emitting = @params["emitting"].ToObject<bool>(); changes.Add("emitting"); }
|
||||
|
||||
RendererHelpers.ApplyLineTrailProperties(@params, changes,
|
||||
null, null,
|
||||
v => tr.numCornerVertices = v, v => tr.numCapVertices = v,
|
||||
v => tr.alignment = v, v => tr.textureMode = v,
|
||||
v => tr.generateLightingData = v);
|
||||
|
||||
RendererHelpers.ApplyCommonRendererProperties(tr, @params, changes);
|
||||
|
||||
EditorUtility.SetDirty(tr);
|
||||
return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/Vfx/TrailWrite.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/Vfx/TrailWrite.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 33ba432240c134206a4f71ab24f0fb3a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
568
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphAssets.cs
Normal file
568
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphAssets.cs
Normal file
@@ -0,0 +1,568 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_VFX_GRAPH
|
||||
using UnityEngine.VFX;
|
||||
#endif
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
/// <summary>
|
||||
/// Asset management operations for VFX Graph.
|
||||
/// Handles creating, assigning, and listing VFX assets.
|
||||
/// Requires com.unity.visualeffectgraph package and UNITY_VFX_GRAPH symbol.
|
||||
/// </summary>
|
||||
internal static class VfxGraphAssets
|
||||
{
|
||||
#if !UNITY_VFX_GRAPH
|
||||
public static object CreateAsset(JObject @params)
|
||||
{
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
}
|
||||
|
||||
public static object AssignAsset(JObject @params)
|
||||
{
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
}
|
||||
|
||||
public static object ListTemplates(JObject @params)
|
||||
{
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
}
|
||||
|
||||
public static object ListAssets(JObject @params)
|
||||
{
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
}
|
||||
#else
|
||||
private static readonly string[] SupportedVfxGraphVersions = { "12.1" };
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new VFX Graph asset file from a template.
|
||||
/// </summary>
|
||||
public static object CreateAsset(JObject @params)
|
||||
{
|
||||
string assetName = @params["assetName"]?.ToString();
|
||||
string folderPath = @params["folderPath"]?.ToString() ?? "Assets/VFX";
|
||||
string template = @params["template"]?.ToString() ?? "empty";
|
||||
|
||||
if (string.IsNullOrEmpty(assetName))
|
||||
{
|
||||
return new { success = false, message = "assetName is required" };
|
||||
}
|
||||
|
||||
string versionError = ValidateVfxGraphVersion();
|
||||
if (!string.IsNullOrEmpty(versionError))
|
||||
{
|
||||
return new { success = false, message = versionError };
|
||||
}
|
||||
|
||||
// Ensure folder exists
|
||||
if (!AssetDatabase.IsValidFolder(folderPath))
|
||||
{
|
||||
string[] folders = folderPath.Split('/');
|
||||
string currentPath = folders[0];
|
||||
for (int i = 1; i < folders.Length; i++)
|
||||
{
|
||||
string newPath = currentPath + "/" + folders[i];
|
||||
if (!AssetDatabase.IsValidFolder(newPath))
|
||||
{
|
||||
AssetDatabase.CreateFolder(currentPath, folders[i]);
|
||||
}
|
||||
currentPath = newPath;
|
||||
}
|
||||
}
|
||||
|
||||
string assetPath = $"{folderPath}/{assetName}.vfx";
|
||||
|
||||
// Check if asset already exists
|
||||
if (AssetDatabase.LoadAssetAtPath<VisualEffectAsset>(assetPath) != null)
|
||||
{
|
||||
bool overwrite = @params["overwrite"]?.ToObject<bool>() ?? false;
|
||||
if (!overwrite)
|
||||
{
|
||||
return new { success = false, message = $"Asset already exists at {assetPath}. Set overwrite=true to replace." };
|
||||
}
|
||||
AssetDatabase.DeleteAsset(assetPath);
|
||||
}
|
||||
|
||||
// Find template asset and copy it
|
||||
string templatePath = FindTemplate(template);
|
||||
string templateAssetPath = TryGetAssetPathFromFileSystem(templatePath);
|
||||
VisualEffectAsset newAsset = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(templateAssetPath))
|
||||
{
|
||||
// Copy the asset to create a new VFX Graph asset
|
||||
if (!AssetDatabase.CopyAsset(templateAssetPath, assetPath))
|
||||
{
|
||||
return new { success = false, message = $"Failed to copy VFX template from {templateAssetPath}" };
|
||||
}
|
||||
AssetDatabase.Refresh();
|
||||
newAsset = AssetDatabase.LoadAssetAtPath<VisualEffectAsset>(assetPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new { success = false, message = "VFX template not found. Add a .vfx template asset or install VFX Graph templates." };
|
||||
}
|
||||
|
||||
if (newAsset == null)
|
||||
{
|
||||
return new { success = false, message = "Failed to create VFX asset. Try using a template from list_templates." };
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Created VFX asset: {assetPath}",
|
||||
data = new
|
||||
{
|
||||
assetPath = assetPath,
|
||||
assetName = newAsset.name,
|
||||
template = template
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds VFX template path by name.
|
||||
/// </summary>
|
||||
private static string FindTemplate(string templateName)
|
||||
{
|
||||
// Get the actual filesystem path for the VFX Graph package using PackageManager API
|
||||
var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph");
|
||||
|
||||
var searchPaths = new List<string>();
|
||||
|
||||
if (packageInfo != null)
|
||||
{
|
||||
// Use the resolved path from PackageManager (handles Library/PackageCache paths)
|
||||
searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Editor/Templates"));
|
||||
searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Samples"));
|
||||
}
|
||||
|
||||
// Also search project-local paths
|
||||
searchPaths.Add("Assets/VFX/Templates");
|
||||
|
||||
string[] templatePatterns = new[]
|
||||
{
|
||||
$"{templateName}.vfx",
|
||||
$"VFX{templateName}.vfx",
|
||||
$"Simple{templateName}.vfx",
|
||||
$"{templateName}VFX.vfx"
|
||||
};
|
||||
|
||||
foreach (string basePath in searchPaths)
|
||||
{
|
||||
string searchRoot = basePath;
|
||||
if (basePath.StartsWith("Assets/"))
|
||||
{
|
||||
searchRoot = System.IO.Path.Combine(UnityEngine.Application.dataPath, basePath.Substring("Assets/".Length));
|
||||
}
|
||||
|
||||
if (!System.IO.Directory.Exists(searchRoot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (string pattern in templatePatterns)
|
||||
{
|
||||
string[] files = System.IO.Directory.GetFiles(searchRoot, pattern, System.IO.SearchOption.AllDirectories);
|
||||
if (files.Length > 0)
|
||||
{
|
||||
return files[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Also search by partial match
|
||||
try
|
||||
{
|
||||
string[] allVfxFiles = System.IO.Directory.GetFiles(searchRoot, "*.vfx", System.IO.SearchOption.AllDirectories);
|
||||
foreach (string file in allVfxFiles)
|
||||
{
|
||||
if (System.IO.Path.GetFileNameWithoutExtension(file).ToLower().Contains(templateName.ToLower()))
|
||||
{
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Failed to search VFX templates under '{searchRoot}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Search in project assets
|
||||
string[] guids = AssetDatabase.FindAssets("t:VisualEffectAsset " + templateName);
|
||||
if (guids.Length > 0)
|
||||
{
|
||||
string assetPath = AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
// Convert asset path (e.g., "Assets/...") to absolute filesystem path
|
||||
if (!string.IsNullOrEmpty(assetPath) && assetPath.StartsWith("Assets/"))
|
||||
{
|
||||
return System.IO.Path.Combine(UnityEngine.Application.dataPath, assetPath.Substring("Assets/".Length));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(assetPath) && assetPath.StartsWith("Packages/"))
|
||||
{
|
||||
var info = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(assetPath);
|
||||
if (info != null)
|
||||
{
|
||||
string relPath = assetPath.Substring(("Packages/" + info.name + "/").Length);
|
||||
return System.IO.Path.Combine(info.resolvedPath, relPath);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns a VFX asset to a VisualEffect component.
|
||||
/// </summary>
|
||||
public static object AssignAsset(JObject @params)
|
||||
{
|
||||
VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params);
|
||||
if (vfx == null)
|
||||
{
|
||||
return new { success = false, message = "VisualEffect component not found" };
|
||||
}
|
||||
|
||||
string assetPath = @params["assetPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(assetPath))
|
||||
{
|
||||
return new { success = false, message = "assetPath is required" };
|
||||
}
|
||||
|
||||
// Validate and normalize path
|
||||
// Reject absolute paths, parent directory traversal, and backslashes
|
||||
if (assetPath.Contains("\\") || assetPath.Contains("..") || System.IO.Path.IsPathRooted(assetPath))
|
||||
{
|
||||
return new { success = false, message = "Invalid assetPath: traversal and absolute paths are not allowed" };
|
||||
}
|
||||
|
||||
if (assetPath.StartsWith("Packages/"))
|
||||
{
|
||||
return new { success = false, message = "Invalid assetPath: VFX assets must live under Assets/." };
|
||||
}
|
||||
|
||||
if (!assetPath.StartsWith("Assets/"))
|
||||
{
|
||||
assetPath = "Assets/" + assetPath;
|
||||
}
|
||||
if (!assetPath.EndsWith(".vfx"))
|
||||
{
|
||||
assetPath += ".vfx";
|
||||
}
|
||||
|
||||
// Verify the normalized path doesn't escape the project
|
||||
string fullPath = System.IO.Path.Combine(UnityEngine.Application.dataPath, assetPath.Substring("Assets/".Length));
|
||||
string canonicalProjectRoot = System.IO.Path.GetFullPath(UnityEngine.Application.dataPath);
|
||||
string canonicalAssetPath = System.IO.Path.GetFullPath(fullPath);
|
||||
if (!canonicalAssetPath.StartsWith(canonicalProjectRoot + System.IO.Path.DirectorySeparatorChar) &&
|
||||
canonicalAssetPath != canonicalProjectRoot)
|
||||
{
|
||||
return new { success = false, message = "Invalid assetPath: would escape project directory" };
|
||||
}
|
||||
|
||||
var asset = AssetDatabase.LoadAssetAtPath<VisualEffectAsset>(assetPath);
|
||||
if (asset == null)
|
||||
{
|
||||
// Try searching by name
|
||||
string searchName = System.IO.Path.GetFileNameWithoutExtension(assetPath);
|
||||
string[] guids = AssetDatabase.FindAssets($"t:VisualEffectAsset {searchName}");
|
||||
if (guids.Length > 0)
|
||||
{
|
||||
assetPath = AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
asset = AssetDatabase.LoadAssetAtPath<VisualEffectAsset>(assetPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (asset == null)
|
||||
{
|
||||
return new { success = false, message = $"VFX asset not found: {assetPath}" };
|
||||
}
|
||||
|
||||
Undo.RecordObject(vfx, "Assign VFX Asset");
|
||||
vfx.visualEffectAsset = asset;
|
||||
EditorUtility.SetDirty(vfx);
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Assigned VFX asset '{asset.name}' to {vfx.gameObject.name}",
|
||||
data = new
|
||||
{
|
||||
gameObject = vfx.gameObject.name,
|
||||
assetName = asset.name,
|
||||
assetPath = assetPath
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists available VFX templates.
|
||||
/// </summary>
|
||||
public static object ListTemplates(JObject @params)
|
||||
{
|
||||
var templates = new List<object>();
|
||||
var seenPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Get the actual filesystem path for the VFX Graph package using PackageManager API
|
||||
var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph");
|
||||
|
||||
var searchPaths = new List<string>();
|
||||
|
||||
if (packageInfo != null)
|
||||
{
|
||||
// Use the resolved path from PackageManager (handles Library/PackageCache paths)
|
||||
searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Editor/Templates"));
|
||||
searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Samples"));
|
||||
}
|
||||
|
||||
// Also search project-local paths
|
||||
searchPaths.Add("Assets/VFX/Templates");
|
||||
searchPaths.Add("Assets/VFX");
|
||||
|
||||
// Precompute normalized package path for comparison
|
||||
string normalizedPackagePath = null;
|
||||
if (packageInfo != null)
|
||||
{
|
||||
normalizedPackagePath = packageInfo.resolvedPath.Replace("\\", "/");
|
||||
}
|
||||
|
||||
// Precompute the Assets base path for converting absolute paths to project-relative
|
||||
string assetsBasePath = Application.dataPath.Replace("\\", "/");
|
||||
|
||||
foreach (string basePath in searchPaths)
|
||||
{
|
||||
if (!System.IO.Directory.Exists(basePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string[] vfxFiles = System.IO.Directory.GetFiles(basePath, "*.vfx", System.IO.SearchOption.AllDirectories);
|
||||
foreach (string file in vfxFiles)
|
||||
{
|
||||
string absolutePath = file.Replace("\\", "/");
|
||||
string name = System.IO.Path.GetFileNameWithoutExtension(file);
|
||||
bool isPackage = normalizedPackagePath != null && absolutePath.StartsWith(normalizedPackagePath);
|
||||
|
||||
// Convert absolute path to project-relative path
|
||||
string projectRelativePath;
|
||||
if (isPackage)
|
||||
{
|
||||
// For package paths, convert to Packages/... format
|
||||
projectRelativePath = "Packages/" + packageInfo.name + absolutePath.Substring(normalizedPackagePath.Length);
|
||||
}
|
||||
else if (absolutePath.StartsWith(assetsBasePath))
|
||||
{
|
||||
// For project assets, convert to Assets/... format
|
||||
projectRelativePath = "Assets" + absolutePath.Substring(assetsBasePath.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: use the absolute path if we can't determine the relative path
|
||||
projectRelativePath = absolutePath;
|
||||
}
|
||||
|
||||
string normalizedPath = projectRelativePath.Replace("\\", "/");
|
||||
if (seenPaths.Add(normalizedPath))
|
||||
{
|
||||
templates.Add(new { name = name, path = projectRelativePath, source = isPackage ? "package" : "project" });
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Failed to list VFX templates under '{basePath}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Also search project assets
|
||||
string[] guids = AssetDatabase.FindAssets("t:VisualEffectAsset");
|
||||
foreach (string guid in guids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
string normalizedPath = path.Replace("\\", "/");
|
||||
if (seenPaths.Add(normalizedPath))
|
||||
{
|
||||
string name = System.IO.Path.GetFileNameWithoutExtension(path);
|
||||
templates.Add(new { name = name, path = path, source = "project" });
|
||||
}
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
data = new
|
||||
{
|
||||
count = templates.Count,
|
||||
templates = templates
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists all VFX assets in the project.
|
||||
/// </summary>
|
||||
public static object ListAssets(JObject @params)
|
||||
{
|
||||
string searchFolder = @params["folder"]?.ToString();
|
||||
string searchPattern = @params["search"]?.ToString();
|
||||
|
||||
string filter = "t:VisualEffectAsset";
|
||||
if (!string.IsNullOrEmpty(searchPattern))
|
||||
{
|
||||
filter += " " + searchPattern;
|
||||
}
|
||||
|
||||
string[] guids;
|
||||
if (!string.IsNullOrEmpty(searchFolder))
|
||||
{
|
||||
if (searchFolder.Contains("\\") || searchFolder.Contains("..") || System.IO.Path.IsPathRooted(searchFolder))
|
||||
{
|
||||
return new { success = false, message = "Invalid folder: traversal and absolute paths are not allowed" };
|
||||
}
|
||||
|
||||
if (searchFolder.StartsWith("Packages/"))
|
||||
{
|
||||
return new { success = false, message = "Invalid folder: VFX assets must live under Assets/." };
|
||||
}
|
||||
|
||||
if (!searchFolder.StartsWith("Assets/"))
|
||||
{
|
||||
searchFolder = "Assets/" + searchFolder;
|
||||
}
|
||||
|
||||
string fullPath = System.IO.Path.Combine(UnityEngine.Application.dataPath, searchFolder.Substring("Assets/".Length));
|
||||
string canonicalProjectRoot = System.IO.Path.GetFullPath(UnityEngine.Application.dataPath);
|
||||
string canonicalSearchFolder = System.IO.Path.GetFullPath(fullPath);
|
||||
if (!canonicalSearchFolder.StartsWith(canonicalProjectRoot + System.IO.Path.DirectorySeparatorChar) &&
|
||||
canonicalSearchFolder != canonicalProjectRoot)
|
||||
{
|
||||
return new { success = false, message = "Invalid folder: would escape project directory" };
|
||||
}
|
||||
|
||||
guids = AssetDatabase.FindAssets(filter, new[] { searchFolder });
|
||||
}
|
||||
else
|
||||
{
|
||||
guids = AssetDatabase.FindAssets(filter);
|
||||
}
|
||||
|
||||
var assets = new List<object>();
|
||||
foreach (string guid in guids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<VisualEffectAsset>(path);
|
||||
if (asset != null)
|
||||
{
|
||||
assets.Add(new
|
||||
{
|
||||
name = asset.name,
|
||||
path = path,
|
||||
guid = guid
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
data = new
|
||||
{
|
||||
count = assets.Count,
|
||||
assets = assets
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string ValidateVfxGraphVersion()
|
||||
{
|
||||
var info = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph");
|
||||
if (info == null)
|
||||
{
|
||||
return "VFX Graph package (com.unity.visualeffectgraph) not installed";
|
||||
}
|
||||
|
||||
if (IsVersionSupported(info.version))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string supported = string.Join(", ", SupportedVfxGraphVersions.Select(version => $"{version}.x"));
|
||||
return $"Unsupported VFX Graph version {info.version}. Supported versions: {supported}.";
|
||||
}
|
||||
|
||||
private static bool IsVersionSupported(string installedVersion)
|
||||
{
|
||||
if (string.IsNullOrEmpty(installedVersion))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string normalized = installedVersion;
|
||||
int suffixIndex = normalized.IndexOfAny(new[] { '-', '+' });
|
||||
if (suffixIndex >= 0)
|
||||
{
|
||||
normalized = normalized.Substring(0, suffixIndex);
|
||||
}
|
||||
|
||||
if (!Version.TryParse(normalized, out Version installed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (string supported in SupportedVfxGraphVersions)
|
||||
{
|
||||
if (!Version.TryParse(supported, out Version target))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (installed.Major == target.Major && installed.Minor == target.Minor)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string TryGetAssetPathFromFileSystem(string templatePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(templatePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string normalized = templatePath.Replace("\\", "/");
|
||||
string assetsRoot = Application.dataPath.Replace("\\", "/");
|
||||
|
||||
if (normalized.StartsWith(assetsRoot + "/"))
|
||||
{
|
||||
return "Assets/" + normalized.Substring(assetsRoot.Length + 1);
|
||||
}
|
||||
|
||||
var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph");
|
||||
if (packageInfo != null)
|
||||
{
|
||||
string packageRoot = packageInfo.resolvedPath.Replace("\\", "/");
|
||||
if (normalized.StartsWith(packageRoot + "/"))
|
||||
{
|
||||
return "Packages/" + packageInfo.name + "/" + normalized.Substring(packageRoot.Length + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphAssets.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphAssets.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1dfb51f038764a6da23619cac60f299
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
29
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphCommon.cs
Normal file
29
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphCommon.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_VFX_GRAPH
|
||||
using UnityEngine.VFX;
|
||||
#endif
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
/// <summary>
|
||||
/// Common utilities for VFX Graph operations.
|
||||
/// </summary>
|
||||
internal static class VfxGraphCommon
|
||||
{
|
||||
#if UNITY_VFX_GRAPH
|
||||
/// <summary>
|
||||
/// Finds a VisualEffect component on the target GameObject.
|
||||
/// </summary>
|
||||
public static VisualEffect FindVisualEffect(JObject @params)
|
||||
{
|
||||
if (@params == null)
|
||||
return null;
|
||||
|
||||
GameObject go = ManageVfxCommon.FindTargetGameObject(@params);
|
||||
return go?.GetComponent<VisualEffect>();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphCommon.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphCommon.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a6dbf78125194cf29b98d658af1039a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
89
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphControl.cs
Normal file
89
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphControl.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
|
||||
#if UNITY_VFX_GRAPH
|
||||
using UnityEngine.VFX;
|
||||
#endif
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
/// <summary>
|
||||
/// Playback control operations for VFX Graph (VisualEffect component).
|
||||
/// Requires com.unity.visualeffectgraph package and UNITY_VFX_GRAPH symbol.
|
||||
/// </summary>
|
||||
internal static class VfxGraphControl
|
||||
{
|
||||
#if !UNITY_VFX_GRAPH
|
||||
public static object Control(JObject @params, string action)
|
||||
{
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
}
|
||||
|
||||
public static object SetPlaybackSpeed(JObject @params)
|
||||
{
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
}
|
||||
|
||||
public static object SetSeed(JObject @params)
|
||||
{
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
}
|
||||
#else
|
||||
public static object Control(JObject @params, string action)
|
||||
{
|
||||
VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params);
|
||||
if (vfx == null)
|
||||
{
|
||||
return new { success = false, message = "VisualEffect not found" };
|
||||
}
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case "play": vfx.Play(); break;
|
||||
case "stop": vfx.Stop(); break;
|
||||
case "pause": vfx.pause = !vfx.pause; break;
|
||||
case "reinit": vfx.Reinit(); break;
|
||||
default:
|
||||
return new { success = false, message = $"Unknown VFX action: {action}" };
|
||||
}
|
||||
|
||||
return new { success = true, message = $"VFX {action}", isPaused = vfx.pause };
|
||||
}
|
||||
|
||||
public static object SetPlaybackSpeed(JObject @params)
|
||||
{
|
||||
VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params);
|
||||
if (vfx == null)
|
||||
{
|
||||
return new { success = false, message = "VisualEffect not found" };
|
||||
}
|
||||
|
||||
float rate = @params["playRate"]?.ToObject<float>() ?? 1f;
|
||||
Undo.RecordObject(vfx, "Set VFX Play Rate");
|
||||
vfx.playRate = rate;
|
||||
EditorUtility.SetDirty(vfx);
|
||||
|
||||
return new { success = true, message = $"Set play rate = {rate}" };
|
||||
}
|
||||
|
||||
public static object SetSeed(JObject @params)
|
||||
{
|
||||
VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params);
|
||||
if (vfx == null)
|
||||
{
|
||||
return new { success = false, message = "VisualEffect not found" };
|
||||
}
|
||||
|
||||
uint seed = @params["seed"]?.ToObject<uint>() ?? 0;
|
||||
bool resetOnPlay = @params["resetSeedOnPlay"]?.ToObject<bool>() ?? true;
|
||||
|
||||
Undo.RecordObject(vfx, "Set VFX Seed");
|
||||
vfx.startSeed = seed;
|
||||
vfx.resetSeedOnPlay = resetOnPlay;
|
||||
EditorUtility.SetDirty(vfx);
|
||||
|
||||
return new { success = true, message = $"Set seed = {seed}" };
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4720d53b13bc14989803670a788a1eaa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
47
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphRead.cs
Normal file
47
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphRead.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_VFX_GRAPH
|
||||
using UnityEngine.VFX;
|
||||
#endif
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
/// <summary>
|
||||
/// Read operations for VFX Graph (VisualEffect component).
|
||||
/// Requires com.unity.visualeffectgraph package and UNITY_VFX_GRAPH symbol.
|
||||
/// </summary>
|
||||
internal static class VfxGraphRead
|
||||
{
|
||||
#if !UNITY_VFX_GRAPH
|
||||
public static object GetInfo(JObject @params)
|
||||
{
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
}
|
||||
#else
|
||||
public static object GetInfo(JObject @params)
|
||||
{
|
||||
VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params);
|
||||
if (vfx == null)
|
||||
{
|
||||
return new { success = false, message = "VisualEffect not found" };
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
data = new
|
||||
{
|
||||
gameObject = vfx.gameObject.name,
|
||||
assetName = vfx.visualEffectAsset?.name ?? "None",
|
||||
aliveParticleCount = vfx.aliveParticleCount,
|
||||
culled = vfx.culled,
|
||||
pause = vfx.pause,
|
||||
playRate = vfx.playRate,
|
||||
startSeed = vfx.startSeed
|
||||
}
|
||||
};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphRead.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphRead.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 419e293a95ea64af5ad6984b1d02b9b1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
310
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphWrite.cs
Normal file
310
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphWrite.cs
Normal file
@@ -0,0 +1,310 @@
|
||||
using System;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_VFX_GRAPH
|
||||
using UnityEngine.VFX;
|
||||
#endif
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Vfx
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameter setter operations for VFX Graph (VisualEffect component).
|
||||
/// Requires com.unity.visualeffectgraph package and UNITY_VFX_GRAPH symbol.
|
||||
/// </summary>
|
||||
internal static class VfxGraphWrite
|
||||
{
|
||||
#if !UNITY_VFX_GRAPH
|
||||
public static object SetParameter<T>(JObject @params, Action<object, string, T> setter)
|
||||
{
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
}
|
||||
|
||||
public static object SetVector(JObject @params, int dims)
|
||||
{
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
}
|
||||
|
||||
public static object SetColor(JObject @params)
|
||||
{
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
}
|
||||
|
||||
public static object SetGradient(JObject @params)
|
||||
{
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
}
|
||||
|
||||
public static object SetTexture(JObject @params)
|
||||
{
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
}
|
||||
|
||||
public static object SetMesh(JObject @params)
|
||||
{
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
}
|
||||
|
||||
public static object SetCurve(JObject @params)
|
||||
{
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
}
|
||||
|
||||
public static object SendEvent(JObject @params)
|
||||
{
|
||||
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
|
||||
}
|
||||
#else
|
||||
public static object SetParameter<T>(JObject @params, Action<VisualEffect, string, T> setter)
|
||||
{
|
||||
VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params);
|
||||
if (vfx == null)
|
||||
{
|
||||
return new { success = false, message = "VisualEffect not found" };
|
||||
}
|
||||
|
||||
string param = @params["parameter"]?.ToString();
|
||||
if (string.IsNullOrEmpty(param))
|
||||
{
|
||||
return new { success = false, message = "Parameter name required" };
|
||||
}
|
||||
|
||||
JToken valueToken = @params["value"];
|
||||
if (valueToken == null)
|
||||
{
|
||||
return new { success = false, message = "Value required" };
|
||||
}
|
||||
|
||||
// Safely deserialize the value
|
||||
T value;
|
||||
try
|
||||
{
|
||||
value = valueToken.ToObject<T>();
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return new { success = false, message = $"Invalid value for {param}: {ex.Message}" };
|
||||
}
|
||||
catch (InvalidCastException ex)
|
||||
{
|
||||
return new { success = false, message = $"Invalid value type for {param}: {ex.Message}" };
|
||||
}
|
||||
|
||||
Undo.RecordObject(vfx, $"Set VFX {param}");
|
||||
setter(vfx, param, value);
|
||||
EditorUtility.SetDirty(vfx);
|
||||
|
||||
return new { success = true, message = $"Set {param} = {value}" };
|
||||
}
|
||||
|
||||
public static object SetVector(JObject @params, int dims)
|
||||
{
|
||||
VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params);
|
||||
if (vfx == null)
|
||||
{
|
||||
return new { success = false, message = "VisualEffect not found" };
|
||||
}
|
||||
|
||||
string param = @params["parameter"]?.ToString();
|
||||
if (string.IsNullOrEmpty(param))
|
||||
{
|
||||
return new { success = false, message = "Parameter name required" };
|
||||
}
|
||||
|
||||
if (dims != 2 && dims != 3 && dims != 4)
|
||||
{
|
||||
return new { success = false, message = $"Unsupported vector dimension: {dims}. Expected 2, 3, or 4." };
|
||||
}
|
||||
|
||||
Vector4 vec = ManageVfxCommon.ParseVector4(@params["value"]);
|
||||
Undo.RecordObject(vfx, $"Set VFX {param}");
|
||||
|
||||
switch (dims)
|
||||
{
|
||||
case 2: vfx.SetVector2(param, new Vector2(vec.x, vec.y)); break;
|
||||
case 3: vfx.SetVector3(param, new Vector3(vec.x, vec.y, vec.z)); break;
|
||||
case 4: vfx.SetVector4(param, vec); break;
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(vfx);
|
||||
return new { success = true, message = $"Set {param}" };
|
||||
}
|
||||
|
||||
public static object SetColor(JObject @params)
|
||||
{
|
||||
VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params);
|
||||
if (vfx == null)
|
||||
{
|
||||
return new { success = false, message = "VisualEffect not found" };
|
||||
}
|
||||
|
||||
string param = @params["parameter"]?.ToString();
|
||||
if (string.IsNullOrEmpty(param))
|
||||
{
|
||||
return new { success = false, message = "Parameter name required" };
|
||||
}
|
||||
|
||||
Color color = ManageVfxCommon.ParseColor(@params["value"]);
|
||||
Undo.RecordObject(vfx, $"Set VFX Color {param}");
|
||||
vfx.SetVector4(param, new Vector4(color.r, color.g, color.b, color.a));
|
||||
EditorUtility.SetDirty(vfx);
|
||||
|
||||
return new { success = true, message = $"Set color {param}" };
|
||||
}
|
||||
|
||||
public static object SetGradient(JObject @params)
|
||||
{
|
||||
VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params);
|
||||
if (vfx == null)
|
||||
{
|
||||
return new { success = false, message = "VisualEffect not found" };
|
||||
}
|
||||
|
||||
string param = @params["parameter"]?.ToString();
|
||||
if (string.IsNullOrEmpty(param))
|
||||
{
|
||||
return new { success = false, message = "Parameter name required" };
|
||||
}
|
||||
|
||||
Gradient gradient = ManageVfxCommon.ParseGradient(@params["gradient"]);
|
||||
Undo.RecordObject(vfx, $"Set VFX Gradient {param}");
|
||||
vfx.SetGradient(param, gradient);
|
||||
EditorUtility.SetDirty(vfx);
|
||||
|
||||
return new { success = true, message = $"Set gradient {param}" };
|
||||
}
|
||||
|
||||
public static object SetTexture(JObject @params)
|
||||
{
|
||||
VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params);
|
||||
if (vfx == null)
|
||||
{
|
||||
return new { success = false, message = "VisualEffect not found" };
|
||||
}
|
||||
|
||||
string param = @params["parameter"]?.ToString();
|
||||
string path = @params["texturePath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(param) || string.IsNullOrEmpty(path))
|
||||
{
|
||||
return new { success = false, message = "Parameter and texturePath required" };
|
||||
}
|
||||
|
||||
var findInst = new JObject { ["find"] = path };
|
||||
Texture tex = ObjectResolver.Resolve(findInst, typeof(Texture)) as Texture;
|
||||
if (tex == null)
|
||||
{
|
||||
return new { success = false, message = $"Texture not found: {path}" };
|
||||
}
|
||||
|
||||
Undo.RecordObject(vfx, $"Set VFX Texture {param}");
|
||||
vfx.SetTexture(param, tex);
|
||||
EditorUtility.SetDirty(vfx);
|
||||
|
||||
return new { success = true, message = $"Set texture {param} = {tex.name}" };
|
||||
}
|
||||
|
||||
public static object SetMesh(JObject @params)
|
||||
{
|
||||
VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params);
|
||||
if (vfx == null)
|
||||
{
|
||||
return new { success = false, message = "VisualEffect not found" };
|
||||
}
|
||||
|
||||
string param = @params["parameter"]?.ToString();
|
||||
string path = @params["meshPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(param) || string.IsNullOrEmpty(path))
|
||||
{
|
||||
return new { success = false, message = "Parameter and meshPath required" };
|
||||
}
|
||||
|
||||
var findInst = new JObject { ["find"] = path };
|
||||
Mesh mesh = ObjectResolver.Resolve(findInst, typeof(Mesh)) as Mesh;
|
||||
if (mesh == null)
|
||||
{
|
||||
return new { success = false, message = $"Mesh not found: {path}" };
|
||||
}
|
||||
|
||||
Undo.RecordObject(vfx, $"Set VFX Mesh {param}");
|
||||
vfx.SetMesh(param, mesh);
|
||||
EditorUtility.SetDirty(vfx);
|
||||
|
||||
return new { success = true, message = $"Set mesh {param} = {mesh.name}" };
|
||||
}
|
||||
|
||||
public static object SetCurve(JObject @params)
|
||||
{
|
||||
VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params);
|
||||
if (vfx == null)
|
||||
{
|
||||
return new { success = false, message = "VisualEffect not found" };
|
||||
}
|
||||
|
||||
string param = @params["parameter"]?.ToString();
|
||||
if (string.IsNullOrEmpty(param))
|
||||
{
|
||||
return new { success = false, message = "Parameter name required" };
|
||||
}
|
||||
|
||||
AnimationCurve curve = ManageVfxCommon.ParseAnimationCurve(@params["curve"], 1f);
|
||||
Undo.RecordObject(vfx, $"Set VFX Curve {param}");
|
||||
vfx.SetAnimationCurve(param, curve);
|
||||
EditorUtility.SetDirty(vfx);
|
||||
|
||||
return new { success = true, message = $"Set curve {param}" };
|
||||
}
|
||||
|
||||
public static object SendEvent(JObject @params)
|
||||
{
|
||||
VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params);
|
||||
if (vfx == null)
|
||||
{
|
||||
return new { success = false, message = "VisualEffect not found" };
|
||||
}
|
||||
|
||||
string eventName = @params["eventName"]?.ToString();
|
||||
if (string.IsNullOrEmpty(eventName))
|
||||
{
|
||||
return new { success = false, message = "Event name required" };
|
||||
}
|
||||
|
||||
VFXEventAttribute attr = vfx.CreateVFXEventAttribute();
|
||||
if (@params["position"] != null)
|
||||
{
|
||||
attr.SetVector3("position", ManageVfxCommon.ParseVector3(@params["position"]));
|
||||
}
|
||||
if (@params["velocity"] != null)
|
||||
{
|
||||
attr.SetVector3("velocity", ManageVfxCommon.ParseVector3(@params["velocity"]));
|
||||
}
|
||||
if (@params["color"] != null)
|
||||
{
|
||||
var c = ManageVfxCommon.ParseColor(@params["color"]);
|
||||
attr.SetVector3("color", new Vector3(c.r, c.g, c.b));
|
||||
}
|
||||
if (@params["size"] != null)
|
||||
{
|
||||
float? sizeValue = @params["size"].Value<float?>();
|
||||
if (sizeValue.HasValue)
|
||||
{
|
||||
attr.SetFloat("size", sizeValue.Value);
|
||||
}
|
||||
}
|
||||
if (@params["lifetime"] != null)
|
||||
{
|
||||
float? lifetimeValue = @params["lifetime"].Value<float?>();
|
||||
if (lifetimeValue.HasValue)
|
||||
{
|
||||
attr.SetFloat("lifetime", lifetimeValue.Value);
|
||||
}
|
||||
}
|
||||
|
||||
vfx.SendEvent(eventName, attr);
|
||||
return new { success = true, message = $"Sent event '{eventName}'" };
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
11
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphWrite.cs.meta
Normal file
11
Packages/MCPForUnity/Editor/Tools/Vfx/VfxGraphWrite.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7516cdde6a4b648c9a2def6c26103cc4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user