7f8ca2a3bd
* Update .Bat file and Bug fix on ManageScript * Update the .Bat file to include runtime folder * Fix the inconsistent EditorPrefs variable so the GUI change on Script Validation could cause real change. * Further changes String to Int for consistency * [Custom Tool] Roslyn Runtime Compilation Allows users to generate/compile codes during Playmode * Fix based on CR * Create claude_skill_unity.zip Upload the unity_claude_skill that can be uploaded to Claude for a combo of unity-mcp-skill. * Update for Custom_Tool Fix and Detection 1. Fix Original Roslyn Compilation Custom Tool to fit the V8 standard 2. Add a new panel in the GUI to see and toggle/untoggle the tools. The toggle feature will be implemented in the future, right now its implemented here to discuss with the team if this is a good feature to add; 3. Add few missing summary in certain tools * Revert "Update for Custom_Tool Fix and Detection" This reverts commit ae8cfe5e256c70ac4a16c79d50341a39cbac18ba. * Update README.md * Reapply "Update for Custom_Tool Fix and Detection" This reverts commit f423c2f25e9ccff4f3b89d1d360ee9cf13143733. * Update ManageScript.cs Fix the layout problem of manage_script in the panel * Update To comply with the current server setting * Update on Batch Tested object generation/modification with batch and it works perfectly! We should push and let users test for a while and see PS: I tried both VS Copilot and Claude Desktop. Claude Desktop works but VS Copilot does not due to the nested structure of batch. Will look into it more. * Revert "Merge pull request #1 from Scriptwonder/batching" This reverts commit 55ee76810be161d414e1f5f5abaa5ee30ddd0052, reversing changes made to ae2eedd7fb2c6a66ff008bacac481aefb1b0d176.
529 lines
20 KiB
C#
529 lines
20 KiB
C#
#nullable disable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using Newtonsoft.Json.Linq;
|
|
using UnityEngine;
|
|
using UnityEditor;
|
|
using MCPForUnity.Editor.Helpers;
|
|
|
|
#if USE_ROSLYN
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.Emit;
|
|
#endif
|
|
|
|
namespace MCPForUnity.Editor.Tools
|
|
{
|
|
/// <summary>
|
|
/// Runtime compilation tool for MCP Unity.
|
|
/// Compiles and loads C# code at runtime without triggering domain reload via Roslyn Runtime Compilation, where in traditional Unity workflow it would take seconds to reload assets and reset script states for each script change.
|
|
/// </summary>
|
|
[McpForUnityTool(
|
|
name:"runtime_compilation",
|
|
Description = "Enable runtime compilation of C# code within Unity without domain reload via Roslyn.")]
|
|
public static class ManageRuntimeCompilation
|
|
{
|
|
private static readonly Dictionary<string, LoadedAssemblyInfo> LoadedAssemblies = new Dictionary<string, LoadedAssemblyInfo>();
|
|
private static string DynamicAssembliesPath => Path.Combine(Application.temporaryCachePath, "DynamicAssemblies");
|
|
|
|
private class LoadedAssemblyInfo
|
|
{
|
|
public string Name;
|
|
public Assembly Assembly;
|
|
public string DllPath;
|
|
public DateTime LoadedAt;
|
|
public List<string> TypeNames;
|
|
}
|
|
|
|
public static object HandleCommand(JObject @params)
|
|
{
|
|
string action = @params["action"]?.ToString()?.ToLower();
|
|
|
|
if (string.IsNullOrEmpty(action))
|
|
{
|
|
return new ErrorResponse("Action parameter is required. Valid actions: compile_and_load, list_loaded, get_types, execute_with_roslyn, get_history, save_history, clear_history");
|
|
}
|
|
|
|
switch (action)
|
|
{
|
|
case "compile_and_load":
|
|
return CompileAndLoad(@params);
|
|
|
|
case "list_loaded":
|
|
return ListLoadedAssemblies();
|
|
|
|
case "get_types":
|
|
return GetAssemblyTypes(@params);
|
|
|
|
case "execute_with_roslyn":
|
|
return ExecuteWithRoslyn(@params);
|
|
|
|
case "get_history":
|
|
return GetCompilationHistory();
|
|
|
|
case "save_history":
|
|
return SaveCompilationHistory();
|
|
|
|
case "clear_history":
|
|
return ClearCompilationHistory();
|
|
|
|
default:
|
|
return new ErrorResponse($"Unknown action '{action}'. Valid actions: compile_and_load, list_loaded, get_types, execute_with_roslyn, get_history, save_history, clear_history");
|
|
}
|
|
}
|
|
|
|
private static object CompileAndLoad(JObject @params)
|
|
{
|
|
#if !USE_ROSLYN
|
|
return new ErrorResponse(
|
|
"Runtime compilation requires Roslyn. Please install Microsoft.CodeAnalysis.CSharp NuGet package and add USE_ROSLYN to Scripting Define Symbols. " +
|
|
"See ManageScript.cs header for installation instructions."
|
|
);
|
|
#else
|
|
try
|
|
{
|
|
string code = @params["code"]?.ToString();
|
|
string assemblyName = @params["assembly_name"]?.ToString() ?? $"DynamicAssembly_{DateTime.Now.Ticks}";
|
|
string attachTo = @params["attach_to"]?.ToString();
|
|
bool loadImmediately = @params["load_immediately"]?.ToObject<bool>() ?? true;
|
|
|
|
if (string.IsNullOrEmpty(code))
|
|
{
|
|
return new ErrorResponse("'code' parameter is required");
|
|
}
|
|
|
|
// Ensure unique assembly name
|
|
if (LoadedAssemblies.ContainsKey(assemblyName))
|
|
{
|
|
assemblyName = $"{assemblyName}_{DateTime.Now.Ticks}";
|
|
}
|
|
|
|
// Create output directory
|
|
Directory.CreateDirectory(DynamicAssembliesPath);
|
|
string dllPath = Path.Combine(DynamicAssembliesPath, $"{assemblyName}.dll");
|
|
|
|
// Parse code
|
|
var syntaxTree = CSharpSyntaxTree.ParseText(code);
|
|
|
|
// Get references
|
|
var references = GetDefaultReferences();
|
|
|
|
// Create compilation
|
|
var compilation = CSharpCompilation.Create(
|
|
assemblyName,
|
|
new[] { syntaxTree },
|
|
references,
|
|
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
|
.WithOptimizationLevel(OptimizationLevel.Debug)
|
|
.WithPlatform(Platform.AnyCpu)
|
|
);
|
|
|
|
// Emit to file
|
|
EmitResult emitResult;
|
|
using (var stream = new FileStream(dllPath, FileMode.Create))
|
|
{
|
|
emitResult = compilation.Emit(stream);
|
|
}
|
|
|
|
// Check for compilation errors
|
|
if (!emitResult.Success)
|
|
{
|
|
var errors = emitResult.Diagnostics
|
|
.Where(d => d.Severity == DiagnosticSeverity.Error)
|
|
.Select(d => new
|
|
{
|
|
line = d.Location.GetLineSpan().StartLinePosition.Line + 1,
|
|
column = d.Location.GetLineSpan().StartLinePosition.Character + 1,
|
|
message = d.GetMessage(),
|
|
id = d.Id
|
|
})
|
|
.ToList();
|
|
|
|
return new ErrorResponse("Compilation failed", new
|
|
{
|
|
errors = errors,
|
|
error_count = errors.Count
|
|
});
|
|
}
|
|
|
|
// Load assembly if requested
|
|
Assembly loadedAssembly = null;
|
|
List<string> typeNames = new List<string>();
|
|
|
|
if (loadImmediately)
|
|
{
|
|
loadedAssembly = Assembly.LoadFrom(dllPath);
|
|
typeNames = loadedAssembly.GetTypes().Select(t => t.FullName).ToList();
|
|
|
|
// Store info
|
|
LoadedAssemblies[assemblyName] = new LoadedAssemblyInfo
|
|
{
|
|
Name = assemblyName,
|
|
Assembly = loadedAssembly,
|
|
DllPath = dllPath,
|
|
LoadedAt = DateTime.Now,
|
|
TypeNames = typeNames
|
|
};
|
|
|
|
Debug.Log($"[MCP] Runtime compilation successful: {assemblyName} ({typeNames.Count} types)");
|
|
}
|
|
|
|
// Optionally attach to GameObject
|
|
GameObject attachedTo = null;
|
|
Type attachedType = null;
|
|
|
|
if (!string.IsNullOrEmpty(attachTo) && loadedAssembly != null)
|
|
{
|
|
var go = GameObject.Find(attachTo);
|
|
if (go == null)
|
|
{
|
|
// Try hierarchical path search
|
|
go = FindGameObjectByPath(attachTo);
|
|
}
|
|
|
|
if (go != null)
|
|
{
|
|
// Find first MonoBehaviour type
|
|
var behaviourType = loadedAssembly.GetTypes()
|
|
.FirstOrDefault(t => t.IsSubclassOf(typeof(MonoBehaviour)) && !t.IsAbstract);
|
|
|
|
if (behaviourType != null)
|
|
{
|
|
go.AddComponent(behaviourType);
|
|
attachedTo = go;
|
|
attachedType = behaviourType;
|
|
Debug.Log($"[MCP] Attached {behaviourType.Name} to {go.name}");
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"[MCP] No MonoBehaviour types found in {assemblyName} to attach");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"[MCP] GameObject '{attachTo}' not found");
|
|
}
|
|
}
|
|
|
|
return new SuccessResponse("Runtime compilation completed successfully", new
|
|
{
|
|
assembly_name = assemblyName,
|
|
dll_path = dllPath,
|
|
loaded = loadImmediately,
|
|
type_count = typeNames.Count,
|
|
types = typeNames,
|
|
attached_to = attachedTo != null ? attachedTo.name : null,
|
|
attached_type = attachedType != null ? attachedType.FullName : null
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ErrorResponse($"Runtime compilation failed: {ex.Message}", new
|
|
{
|
|
exception = ex.GetType().Name,
|
|
stack_trace = ex.StackTrace
|
|
});
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private static object ListLoadedAssemblies()
|
|
{
|
|
var assemblies = LoadedAssemblies.Values.Select(info => new
|
|
{
|
|
name = info.Name,
|
|
dll_path = info.DllPath,
|
|
loaded_at = info.LoadedAt.ToString("o"),
|
|
type_count = info.TypeNames.Count,
|
|
types = info.TypeNames
|
|
}).ToList();
|
|
|
|
return new SuccessResponse($"Found {assemblies.Count} loaded dynamic assemblies", new
|
|
{
|
|
count = assemblies.Count,
|
|
assemblies = assemblies
|
|
});
|
|
}
|
|
|
|
private static object GetAssemblyTypes(JObject @params)
|
|
{
|
|
string assemblyName = @params["assembly_name"]?.ToString();
|
|
|
|
if (string.IsNullOrEmpty(assemblyName))
|
|
{
|
|
return new ErrorResponse("'assembly_name' parameter is required");
|
|
}
|
|
|
|
if (!LoadedAssemblies.TryGetValue(assemblyName, out var info))
|
|
{
|
|
return new ErrorResponse($"Assembly '{assemblyName}' not found in loaded assemblies");
|
|
}
|
|
|
|
var types = info.Assembly.GetTypes().Select(t => new
|
|
{
|
|
full_name = t.FullName,
|
|
name = t.Name,
|
|
@namespace = t.Namespace,
|
|
is_class = t.IsClass,
|
|
is_abstract = t.IsAbstract,
|
|
is_monobehaviour = t.IsSubclassOf(typeof(MonoBehaviour)),
|
|
base_type = t.BaseType?.FullName
|
|
}).ToList();
|
|
|
|
return new SuccessResponse($"Retrieved {types.Count} types from {assemblyName}", new
|
|
{
|
|
assembly_name = assemblyName,
|
|
type_count = types.Count,
|
|
types = types
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Execute code using RoslynRuntimeCompiler with full GUI tool integration
|
|
/// Supports MonoBehaviours, static methods, and coroutines
|
|
/// </summary>
|
|
private static object ExecuteWithRoslyn(JObject @params)
|
|
{
|
|
try
|
|
{
|
|
string code = @params["code"]?.ToString();
|
|
string className = @params["class_name"]?.ToString() ?? "AIGenerated";
|
|
string methodName = @params["method_name"]?.ToString() ?? "Run";
|
|
string targetObjectName = @params["target_object"]?.ToString();
|
|
bool attachAsComponent = @params["attach_as_component"]?.ToObject<bool>() ?? false;
|
|
|
|
if (string.IsNullOrEmpty(code))
|
|
{
|
|
return new ErrorResponse("'code' parameter is required");
|
|
}
|
|
|
|
// Get or create the RoslynRuntimeCompiler instance
|
|
var compiler = GetOrCreateRoslynCompiler();
|
|
|
|
// Find target GameObject if specified
|
|
GameObject targetObject = null;
|
|
if (!string.IsNullOrEmpty(targetObjectName))
|
|
{
|
|
targetObject = GameObject.Find(targetObjectName);
|
|
if (targetObject == null)
|
|
{
|
|
targetObject = FindGameObjectByPath(targetObjectName);
|
|
}
|
|
|
|
if (targetObject == null)
|
|
{
|
|
return new ErrorResponse($"Target GameObject '{targetObjectName}' not found");
|
|
}
|
|
}
|
|
|
|
// Use the RoslynRuntimeCompiler's CompileAndExecute method
|
|
bool success = compiler.CompileAndExecute(
|
|
code,
|
|
className,
|
|
methodName,
|
|
targetObject,
|
|
attachAsComponent,
|
|
out string errorMessage
|
|
);
|
|
|
|
if (success)
|
|
{
|
|
return new SuccessResponse($"Code compiled and executed successfully", new
|
|
{
|
|
class_name = className,
|
|
method_name = methodName,
|
|
target_object = targetObject != null ? targetObject.name : "compiler_host",
|
|
attached_as_component = attachAsComponent,
|
|
diagnostics = compiler.lastCompileDiagnostics
|
|
});
|
|
}
|
|
else
|
|
{
|
|
return new ErrorResponse($"Execution failed: {errorMessage}", new
|
|
{
|
|
diagnostics = compiler.lastCompileDiagnostics
|
|
});
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ErrorResponse($"Failed to execute with Roslyn: {ex.Message}", new
|
|
{
|
|
exception = ex.GetType().Name,
|
|
stack_trace = ex.StackTrace
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get compilation history from RoslynRuntimeCompiler
|
|
/// </summary>
|
|
private static object GetCompilationHistory()
|
|
{
|
|
try
|
|
{
|
|
var compiler = GetOrCreateRoslynCompiler();
|
|
var history = compiler.CompilationHistory;
|
|
|
|
var historyData = history.Select(entry => new
|
|
{
|
|
timestamp = entry.timestamp,
|
|
type_name = entry.typeName,
|
|
method_name = entry.methodName,
|
|
success = entry.success,
|
|
diagnostics = entry.diagnostics,
|
|
execution_target = entry.executionTarget,
|
|
source_code_preview = entry.sourceCode.Length > 200
|
|
? entry.sourceCode.Substring(0, 200) + "..."
|
|
: entry.sourceCode
|
|
}).ToList();
|
|
|
|
return new SuccessResponse($"Retrieved {historyData.Count} history entries", new
|
|
{
|
|
count = historyData.Count,
|
|
history = historyData
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ErrorResponse($"Failed to get history: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Save compilation history to JSON file
|
|
/// </summary>
|
|
private static object SaveCompilationHistory()
|
|
{
|
|
try
|
|
{
|
|
var compiler = GetOrCreateRoslynCompiler();
|
|
|
|
if (compiler.SaveHistoryToFile(out string savedPath, out string error))
|
|
{
|
|
return new SuccessResponse($"History saved successfully", new
|
|
{
|
|
path = savedPath,
|
|
entry_count = compiler.CompilationHistory.Count
|
|
});
|
|
}
|
|
else
|
|
{
|
|
return new ErrorResponse($"Failed to save history: {error}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ErrorResponse($"Failed to save history: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear compilation history
|
|
/// </summary>
|
|
private static object ClearCompilationHistory()
|
|
{
|
|
try
|
|
{
|
|
var compiler = GetOrCreateRoslynCompiler();
|
|
int count = compiler.CompilationHistory.Count;
|
|
compiler.ClearHistory();
|
|
|
|
return new SuccessResponse($"Cleared {count} history entries");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ErrorResponse($"Failed to clear history: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
#if USE_ROSLYN
|
|
private static List<MetadataReference> GetDefaultReferences()
|
|
{
|
|
var references = new List<MetadataReference>();
|
|
|
|
// Add core .NET references
|
|
references.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location));
|
|
references.Add(MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location));
|
|
|
|
// Add Unity references
|
|
var unityEngine = typeof(UnityEngine.Object).Assembly.Location;
|
|
references.Add(MetadataReference.CreateFromFile(unityEngine));
|
|
|
|
// Add UnityEditor if available
|
|
try
|
|
{
|
|
var unityEditor = typeof(UnityEditor.Editor).Assembly.Location;
|
|
references.Add(MetadataReference.CreateFromFile(unityEditor));
|
|
}
|
|
catch { /* Editor assembly not always needed */ }
|
|
|
|
// Add Assembly-CSharp (user scripts)
|
|
try
|
|
{
|
|
var assemblyCSharp = AppDomain.CurrentDomain.GetAssemblies()
|
|
.FirstOrDefault(a => a.GetName().Name == "Assembly-CSharp");
|
|
if (assemblyCSharp != null)
|
|
{
|
|
references.Add(MetadataReference.CreateFromFile(assemblyCSharp.Location));
|
|
}
|
|
}
|
|
catch { /* User assembly not always needed */ }
|
|
|
|
return references;
|
|
}
|
|
#endif
|
|
|
|
private static GameObject FindGameObjectByPath(string path)
|
|
{
|
|
// Handle hierarchical paths like "Canvas/Panel/Button"
|
|
var parts = path.Split('/');
|
|
GameObject current = null;
|
|
|
|
foreach (var part in parts)
|
|
{
|
|
if (current == null)
|
|
{
|
|
// Find root object
|
|
current = GameObject.Find(part);
|
|
}
|
|
else
|
|
{
|
|
// Find child
|
|
var transform = current.transform.Find(part);
|
|
if (transform == null)
|
|
return null;
|
|
current = transform.gameObject;
|
|
}
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get or create a RoslynRuntimeCompiler instance for GUI integration
|
|
/// This allows MCP commands to leverage the existing GUI tool
|
|
/// </summary>
|
|
private static RoslynRuntimeCompiler GetOrCreateRoslynCompiler()
|
|
{
|
|
var existing = UnityEngine.Object.FindFirstObjectByType<RoslynRuntimeCompiler>();
|
|
if (existing != null)
|
|
{
|
|
return existing;
|
|
}
|
|
|
|
var go = new GameObject("MCPRoslynCompiler");
|
|
var compiler = go.AddComponent<RoslynRuntimeCompiler>();
|
|
compiler.enableHistory = true; // Enable history tracking for MCP operations
|
|
if (!Application.isPlaying)
|
|
{
|
|
go.hideFlags = HideFlags.HideAndDontSave;
|
|
}
|
|
return compiler;
|
|
}
|
|
}
|
|
}
|