[CUSTOM TOOLS] Roslyn Runtime Compilation Feature (#371)
* 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 CRmain
parent
14b11ba073
commit
edd7817d40
|
|
@ -0,0 +1,549 @@
|
||||||
|
#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.
|
||||||
|
/// </summary>
|
||||||
|
[McpForUnityTool("runtime_compilation")]
|
||||||
|
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 Response.Error("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 Response.Error($"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 Response.Error(
|
||||||
|
"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();
|
||||||
|
var assemblyToken = @params["assembly_name"];
|
||||||
|
string assemblyName = assemblyToken == null || string.IsNullOrWhiteSpace(assemblyToken.ToString())
|
||||||
|
? $"DynamicAssembly_{DateTime.Now.Ticks}"
|
||||||
|
: assemblyToken.ToString().Trim();
|
||||||
|
string attachTo = @params["attach_to"]?.ToString();
|
||||||
|
bool loadImmediately = @params["load_immediately"]?.ToObject<bool>() ?? true;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(code))
|
||||||
|
{
|
||||||
|
return Response.Error("'code' parameter is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure unique assembly name
|
||||||
|
if (LoadedAssemblies.ContainsKey(assemblyName))
|
||||||
|
{
|
||||||
|
assemblyName = $"{assemblyName}_{DateTime.Now.Ticks}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create output directory
|
||||||
|
Directory.CreateDirectory(DynamicAssembliesPath);
|
||||||
|
string basePath = Path.GetFullPath(DynamicAssembliesPath);
|
||||||
|
Directory.CreateDirectory(basePath);
|
||||||
|
string safeFileName = SanitizeAssemblyFileName(assemblyName);
|
||||||
|
string dllPath = Path.GetFullPath(Path.Combine(basePath, $"{safeFileName}.dll"));
|
||||||
|
|
||||||
|
if (!dllPath.StartsWith(basePath, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return Response.Error("Assembly name must resolve inside the dynamic assemblies directory.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(dllPath))
|
||||||
|
{
|
||||||
|
dllPath = Path.GetFullPath(Path.Combine(basePath, $"{safeFileName}_{DateTime.Now.Ticks}.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, FileAccess.Write, FileShare.None))
|
||||||
|
{
|
||||||
|
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 Response.Error("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 Response.Success("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 Response.Error($"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 Response.Success($"Found {assemblies.Count} loaded dynamic assemblies", new
|
||||||
|
{
|
||||||
|
count = assemblies.Count,
|
||||||
|
assemblies = assemblies
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SanitizeAssemblyFileName(string assemblyName)
|
||||||
|
{
|
||||||
|
var invalidChars = Path.GetInvalidFileNameChars();
|
||||||
|
var sanitized = new string(assemblyName.Where(c => !invalidChars.Contains(c)).ToArray());
|
||||||
|
return string.IsNullOrWhiteSpace(sanitized) ? $"DynamicAssembly_{DateTime.Now.Ticks}" : sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object GetAssemblyTypes(JObject @params)
|
||||||
|
{
|
||||||
|
string assemblyName = @params["assembly_name"]?.ToString();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(assemblyName))
|
||||||
|
{
|
||||||
|
return Response.Error("'assembly_name' parameter is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!LoadedAssemblies.TryGetValue(assemblyName, out var info))
|
||||||
|
{
|
||||||
|
return Response.Error($"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 Response.Success($"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 Response.Error("'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 Response.Error($"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 Response.Success($"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 Response.Error($"Execution failed: {errorMessage}", new
|
||||||
|
{
|
||||||
|
diagnostics = compiler.lastCompileDiagnostics
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Response.Error($"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 Response.Success($"Retrieved {historyData.Count} history entries", new
|
||||||
|
{
|
||||||
|
count = historyData.Count,
|
||||||
|
history = historyData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Response.Error($"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 Response.Success($"History saved successfully", new
|
||||||
|
{
|
||||||
|
path = savedPath,
|
||||||
|
entry_count = compiler.CompilationHistory.Count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Response.Error($"Failed to save history: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Response.Error($"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 Response.Success($"Cleared {count} history entries");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Response.Error($"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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1c3b2419382faa04481f4a631c510ee6
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a3b463767742cdf43b366f68a656e42e
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Roslyn Runtime Compilation Tool
|
||||||
|
|
||||||
|
This custom tool uses Roslyn Runtime Compilation to have users run script generation and compilation during Playmode in realtime, where in traditional Unity workflow it would take seconds to reload assets and reset script states for each script change.
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 97f1198c66ce56043a3c8a5e05ba0150
|
||||||
|
|
@ -0,0 +1,269 @@
|
||||||
|
"""
|
||||||
|
Runtime compilation tool for MCP Unity.
|
||||||
|
Compiles and loads C# code at runtime without domain reload.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Annotated, Any
|
||||||
|
from fastmcp import Context
|
||||||
|
from registry import mcp_for_unity_tool
|
||||||
|
from unity_connection import send_command_with_retry
|
||||||
|
|
||||||
|
|
||||||
|
async def safe_info(ctx: Context, message: str) -> None:
|
||||||
|
"""Safely send info messages when a request context is available."""
|
||||||
|
try:
|
||||||
|
if ctx and hasattr(ctx, "info"):
|
||||||
|
await ctx.info(message)
|
||||||
|
except RuntimeError as ex:
|
||||||
|
# FastMCP raises this when called outside of an active request
|
||||||
|
if "outside of a request" not in str(ex):
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def handle_unity_command(command_name: str, params: dict) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Wrapper for Unity commands with better error handling.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = send_command_with_retry(command_name, params)
|
||||||
|
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
if "Context is not available" in error_msg or "not available outside of a request" in error_msg:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Unity is not connected. Please ensure Unity Editor is running and MCP bridge is active.",
|
||||||
|
"error": "connection_error",
|
||||||
|
"details": "This tool requires an active connection to Unity. Make sure the Unity project is open and the MCP bridge is initialized."
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Command failed: {error_msg}",
|
||||||
|
"error": "tool_error"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp_for_unity_tool(
|
||||||
|
description="Compile and load C# code at runtime without domain reload. Creates dynamic assemblies that can be attached to GameObjects during Play Mode. Requires Roslyn (Microsoft.CodeAnalysis.CSharp) to be installed in Unity."
|
||||||
|
)
|
||||||
|
async def compile_runtime_code(
|
||||||
|
ctx: Context,
|
||||||
|
code: Annotated[str, "Complete C# code including using statements, namespace, and class definition"],
|
||||||
|
assembly_name: Annotated[str, "Unique name for the dynamic assembly. If not provided, a timestamp-based name will be generated."] | None = None,
|
||||||
|
attach_to_gameobject: Annotated[str, "Name or hierarchy path of GameObject to attach the compiled script to (e.g., 'Player' or 'Canvas/Panel')"] | None = None,
|
||||||
|
load_immediately: Annotated[bool, "Whether to load the assembly immediately after compilation. Default is true."] = True
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Compile C# code at runtime and optionally attach it to a GameObject. Only enable it with Roslyn installed in Unity.
|
||||||
|
|
||||||
|
REQUIREMENTS:
|
||||||
|
- Unity must be running and connected
|
||||||
|
- Roslyn (Microsoft.CodeAnalysis.CSharp) must be installed via NuGet
|
||||||
|
- USE_ROSLYN scripting define symbol must be set
|
||||||
|
|
||||||
|
This tool allows you to:
|
||||||
|
- Compile new C# scripts without restarting Unity
|
||||||
|
- Load compiled assemblies into the running Unity instance
|
||||||
|
- Attach MonoBehaviour scripts to GameObjects dynamically
|
||||||
|
- Preserve game state during script additions
|
||||||
|
|
||||||
|
Example code:
|
||||||
|
```csharp
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DynamicScripts
|
||||||
|
{
|
||||||
|
public class MyDynamicBehavior : MonoBehaviour
|
||||||
|
{
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
Debug.Log("Dynamic script loaded!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
await safe_info(ctx, f"Compiling runtime code for assembly: {assembly_name or 'auto-generated'}")
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"action": "compile_and_load",
|
||||||
|
"code": code,
|
||||||
|
"assembly_name": assembly_name,
|
||||||
|
"attach_to": attach_to_gameobject,
|
||||||
|
"load_immediately": load_immediately,
|
||||||
|
}
|
||||||
|
params = {k: v for k, v in params.items() if v is not None}
|
||||||
|
|
||||||
|
return handle_unity_command("runtime_compilation", params)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp_for_unity_tool(
|
||||||
|
description="List all dynamically loaded assemblies in the current Unity session"
|
||||||
|
)
|
||||||
|
async def list_loaded_assemblies(
|
||||||
|
ctx: Context,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get a list of all dynamically loaded assemblies created during this session.
|
||||||
|
|
||||||
|
Returns information about:
|
||||||
|
- Assembly names
|
||||||
|
- Number of types in each assembly
|
||||||
|
- Load timestamps
|
||||||
|
- DLL file paths
|
||||||
|
"""
|
||||||
|
await safe_info(ctx, "Retrieving loaded dynamic assemblies...")
|
||||||
|
|
||||||
|
params = {"action": "list_loaded"}
|
||||||
|
return handle_unity_command("runtime_compilation", params)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp_for_unity_tool(
|
||||||
|
description="Get all types (classes) from a dynamically loaded assembly"
|
||||||
|
)
|
||||||
|
async def get_assembly_types(
|
||||||
|
ctx: Context,
|
||||||
|
assembly_name: Annotated[str, "Name of the assembly to query"],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Retrieve all types defined in a specific dynamic assembly.
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
- Inspecting what was compiled
|
||||||
|
- Finding MonoBehaviour classes to attach
|
||||||
|
- Debugging compilation results
|
||||||
|
"""
|
||||||
|
await safe_info(ctx, f"Getting types from assembly: {assembly_name}")
|
||||||
|
|
||||||
|
params = {"action": "get_types", "assembly_name": assembly_name}
|
||||||
|
return handle_unity_command("runtime_compilation", params)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp_for_unity_tool(
|
||||||
|
description="Execute C# code using the RoslynRuntimeCompiler with full GUI tool features including history tracking, MonoBehaviour support, and coroutines"
|
||||||
|
)
|
||||||
|
async def execute_with_roslyn(
|
||||||
|
ctx: Context,
|
||||||
|
code: Annotated[str, "Complete C# source code to compile and execute"],
|
||||||
|
class_name: Annotated[str, "Name of the class to instantiate/invoke (default: AIGenerated)"] = "AIGenerated",
|
||||||
|
method_name: Annotated[str, "Name of the static method to call (default: Run)"] = "Run",
|
||||||
|
target_object: Annotated[str, "Name or path of target GameObject (optional)"] | None = None,
|
||||||
|
attach_as_component: Annotated[bool, "If true and type is MonoBehaviour, attach as component (default: false)"] = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Execute C# code using Unity's RoslynRuntimeCompiler tool with advanced features:
|
||||||
|
|
||||||
|
- MonoBehaviour attachment: Set attach_as_component=true for classes inheriting MonoBehaviour
|
||||||
|
- Static method execution: Call public static methods (e.g., public static void Run(GameObject host))
|
||||||
|
- Coroutine support: Methods returning IEnumerator will be started as coroutines
|
||||||
|
- History tracking: All compilations are tracked in history for later review
|
||||||
|
|
||||||
|
Supported method signatures:
|
||||||
|
- public static void Run()
|
||||||
|
- public static void Run(GameObject host)
|
||||||
|
- public static void Run(MonoBehaviour host)
|
||||||
|
- public static IEnumerator RunCoroutine(MonoBehaviour host)
|
||||||
|
|
||||||
|
Example MonoBehaviour:
|
||||||
|
```csharp
|
||||||
|
using UnityEngine;
|
||||||
|
public class Rotator : MonoBehaviour {
|
||||||
|
void Update() {
|
||||||
|
transform.Rotate(Vector3.up * 30f * Time.deltaTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example Static Method:
|
||||||
|
```csharp
|
||||||
|
using UnityEngine;
|
||||||
|
public class AIGenerated {
|
||||||
|
public static void Run(GameObject host) {
|
||||||
|
Debug.Log($"Hello from {host.name}!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
await safe_info(ctx, f"Executing code with RoslynRuntimeCompiler: {class_name}.{method_name}")
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"action": "execute_with_roslyn",
|
||||||
|
"code": code,
|
||||||
|
"class_name": class_name,
|
||||||
|
"method_name": method_name,
|
||||||
|
"target_object": target_object,
|
||||||
|
"attach_as_component": attach_as_component,
|
||||||
|
}
|
||||||
|
params = {k: v for k, v in params.items() if v is not None}
|
||||||
|
|
||||||
|
return handle_unity_command("runtime_compilation", params)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp_for_unity_tool(
|
||||||
|
description="Get the compilation history from RoslynRuntimeCompiler showing all previous compilations and executions"
|
||||||
|
)
|
||||||
|
async def get_compilation_history(
|
||||||
|
ctx: Context,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Retrieve the compilation history from the RoslynRuntimeCompiler.
|
||||||
|
|
||||||
|
History includes:
|
||||||
|
- Timestamp of each compilation
|
||||||
|
- Class and method names
|
||||||
|
- Success/failure status
|
||||||
|
- Compilation diagnostics
|
||||||
|
- Target GameObject names
|
||||||
|
- Source code previews
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
- Reviewing what code has been compiled
|
||||||
|
- Debugging failed compilations
|
||||||
|
- Tracking execution flow
|
||||||
|
- Auditing dynamic code changes
|
||||||
|
"""
|
||||||
|
await safe_info(ctx, "Retrieving compilation history...")
|
||||||
|
|
||||||
|
params = {"action": "get_history"}
|
||||||
|
return handle_unity_command("runtime_compilation", params)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp_for_unity_tool(
|
||||||
|
description="Save the compilation history to a JSON file outside the Assets folder"
|
||||||
|
)
|
||||||
|
async def save_compilation_history(
|
||||||
|
ctx: Context,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Save all compilation history to a timestamped JSON file.
|
||||||
|
|
||||||
|
The file is saved to: ProjectRoot/RoslynHistory/RoslynHistory_TIMESTAMP.json
|
||||||
|
|
||||||
|
This allows you to:
|
||||||
|
- Keep a permanent record of dynamic compilations
|
||||||
|
- Review history after Unity restarts
|
||||||
|
- Share compilation sessions with team members
|
||||||
|
- Archive successful code patterns
|
||||||
|
"""
|
||||||
|
await safe_info(ctx, "Saving compilation history to file...")
|
||||||
|
|
||||||
|
params = {"action": "save_history"}
|
||||||
|
return handle_unity_command("runtime_compilation", params)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp_for_unity_tool(
|
||||||
|
description="Clear all compilation history from RoslynRuntimeCompiler"
|
||||||
|
)
|
||||||
|
async def clear_compilation_history(
|
||||||
|
ctx: Context,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Clear all compilation history entries.
|
||||||
|
|
||||||
|
This removes all tracked compilations from memory but does not delete
|
||||||
|
saved history files. Use this to start fresh or reduce memory usage.
|
||||||
|
"""
|
||||||
|
await safe_info(ctx, "Clearing compilation history...")
|
||||||
|
|
||||||
|
params = {"action": "clear_history"}
|
||||||
|
return handle_unity_command("runtime_compilation", params)
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3934c3a018e9eb540a1b39056c193f71
|
||||||
|
ScriptedImporter:
|
||||||
|
internalIDToNameTable: []
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
script: {fileID: 11500000, guid: d68ef794590944f1ea7ee102c91887c7, type: 3}
|
||||||
Loading…
Reference in New Issue