RoslynRuntimeCompiler.cs•46.8 kB
// RoslynRuntimeCompiler.cs
// Single-file Unity tool for Editor+PlayMode dynamic C# compilation using Roslyn.
// Features:
// - EditorWindow GUI with a large text area for LLM-generated code
// - Compile button (compiles in-memory using Roslyn)
// - Run button (invokes a well-known entry point in the compiled assembly)
// - Shows compile errors and runtime exceptions
// - Safe: Does NOT write .cs files to Assets (no Domain Reload)
//
// Requirements:
// 1) Add Microsoft.CodeAnalysis.CSharp.dll and Microsoft.CodeAnalysis.dll to your Unity project
// (place under Assets/Plugins or Packages and target the Editor). These come from the Roslyn nuget package.
// 2) This tool is designed to run in the Unity Editor (Play Mode or Edit Mode). It uses Assembly.Load(byte[]).
// 3) Generated code should expose a public type and a public static entry method matching one of the supported signatures:
// - public static void Run(UnityEngine.GameObject host)
// - public static void Run(UnityEngine.MonoBehaviour host)
// - public static System.Collections.IEnumerator RunCoroutine(UnityEngine.MonoBehaviour host) // if you want a coroutine
// By convention this demo looks for a type name you specify in the window (default: "AIGenerated").
//
// Usage:
// - Window -> Roslyn Runtime Compiler
// - Paste code into the big text area (or use LLM output pasted there)
// - Optionally set Entry Type (default AIGenerated) and Entry Method (default Run)
// - Press "Compile". Compiler diagnostics appear below.
// - In Play Mode, press "Run" to invoke the entry method. In Edit Mode it will attempt to run if valid.
//
// Security note: Any dynamically compiled code runs with the same permissions as the editor. Be careful when running untrusted code.
#if UNITY_EDITOR
using UnityEditor;
#endif
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
#endif
public class RoslynRuntimeCompiler : MonoBehaviour
{
[TextArea(8, 20)]
[Tooltip("Code to compile at runtime. Example class name: AIGenerated with public static void Run(GameObject host)")]
public string code = "using UnityEngine;\npublic class AIGenerated {\n public static void Run(GameObject host) {\n Debug.Log($\"Hello from AI - {host.name}\");\n host.transform.Rotate(Vector3.up * 45f * Time.deltaTime);\n }\n}";
[Tooltip("Fully qualified type name to invoke (default: AIGenerated)")]
public string entryTypeName = "AIGenerated";
[Tooltip("Method name to call on entry type (default: Run)")]
public string entryMethodName = "Run";
[Header("MonoBehaviour Support")]
[Tooltip("If true, attempts to attach generated MonoBehaviour to target GameObject")]
public bool attachAsComponent = false;
[Tooltip("Target GameObject to attach component to (if null, uses this.gameObject)")]
public GameObject targetGameObject;
[Header("History & Tracing")]
[Tooltip("Enable automatic history tracking of compiled scripts")]
public bool enableHistory = true;
[Tooltip("Maximum number of history entries to keep")]
public int maxHistoryEntries = 20;
// compiled assembly & method cache
private Assembly compiledAssembly;
private MethodInfo entryMethod;
private Type entryType;
private Component attachedComponent; // Track dynamically attached component
public bool HasCompiledAssembly => compiledAssembly != null;
public bool HasEntryMethod => entryMethod != null;
public bool HasEntryType => entryType != null;
public Type EntryType => entryType; // Public accessor for editor
// compile result diagnostics (string-friendly)
public string lastCompileDiagnostics = "";
// History tracking - SHARED across all instances
[System.Serializable]
public class CompilationHistoryEntry
{
public string timestamp;
public string sourceCode;
public string typeName;
public string methodName;
public bool success;
public string diagnostics;
public string executionTarget;
}
// Static shared history
private static System.Collections.Generic.List<CompilationHistoryEntry> _sharedHistory = new System.Collections.Generic.List<CompilationHistoryEntry>();
public System.Collections.Generic.List<CompilationHistoryEntry> CompilationHistory => _sharedHistory;
// public wrapper so EditorWindow or other runtime UI can call compile/run
public bool CompileInMemory(out string diagnostics)
{
#if UNITY_EDITOR
diagnostics = string.Empty;
lastCompileDiagnostics = string.Empty;
try
{
var syntaxTree = CSharpSyntaxTree.ParseText(code ?? string.Empty);
// collect references from loaded assemblies (Editor-safe)
var refs = new List<MetadataReference>();
// Always include mscorlib / system.runtime
refs.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location));
// Add all currently loaded assemblies' locations that are not dynamic and have a location
var assemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location))
.Distinct();
foreach (var a in assemblies)
{
try
{
refs.Add(MetadataReference.CreateFromFile(a.Location));
}
catch { }
}
var compilation = CSharpCompilation.Create(
assemblyName: "RoslynRuntimeAssembly_" + Guid.NewGuid().ToString("N"),
syntaxTrees: new[] { syntaxTree },
references: refs,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);
using (var ms = new MemoryStream())
{
var result = compilation.Emit(ms);
if (!result.Success)
{
var diagText = string.Join("\n", result.Diagnostics.Select(d => d.ToString()));
lastCompileDiagnostics = diagText;
diagnostics = diagText;
Debug.LogError("Roslyn compile failed:\n" + diagText);
return false;
}
ms.Seek(0, SeekOrigin.Begin);
var assemblyData = ms.ToArray();
compiledAssembly = Assembly.Load(assemblyData);
// find entry type
var type = compiledAssembly.GetType(entryTypeName);
if (type == null)
{
lastCompileDiagnostics = $"Type '{entryTypeName}' not found in compiled assembly.";
diagnostics = lastCompileDiagnostics;
return false;
}
entryType = type;
// Check if it's a MonoBehaviour
if (typeof(MonoBehaviour).IsAssignableFrom(type))
{
lastCompileDiagnostics = $"Compilation OK. Type '{entryTypeName}' is a MonoBehaviour and can be attached as a component.";
diagnostics = lastCompileDiagnostics;
Debug.Log(diagnostics);
return true;
}
// try various method signatures for non-MonoBehaviour types
entryMethod = type.GetMethod(entryMethodName, BindingFlags.Public | BindingFlags.Static);
if (entryMethod == null)
{
lastCompileDiagnostics = $"Static method '{entryMethodName}' not found on type '{entryTypeName}'.\n" +
$"For MonoBehaviour types, set 'attachAsComponent' to true instead.";
diagnostics = lastCompileDiagnostics;
return false;
}
lastCompileDiagnostics = "Compilation OK.";
diagnostics = lastCompileDiagnostics;
Debug.Log("Roslyn compilation successful.");
return true;
}
}
catch (Exception ex)
{
diagnostics = ex.ToString();
lastCompileDiagnostics = diagnostics;
Debug.LogError("Roslyn compile exception: " + diagnostics);
return false;
}
#else
diagnostics = "Roslyn compilation is only supported in the Unity Editor when referencing Roslyn assemblies.";
lastCompileDiagnostics = diagnostics;
Debug.LogError(diagnostics);
return false;
#endif
}
public bool InvokeEntry(GameObject host, out string runtimeError)
{
runtimeError = null;
if (compiledAssembly == null || entryType == null)
{
runtimeError = "No compiled assembly / entry type. Call CompileInMemory first.";
return false;
}
// Handle MonoBehaviour types
if (typeof(MonoBehaviour).IsAssignableFrom(entryType))
{
return AttachMonoBehaviour(host, out runtimeError);
}
// Handle static method invocation
if (entryMethod == null)
{
runtimeError = "No entry method found. For MonoBehaviour types, use attachAsComponent=true.";
return false;
}
try
{
var parameters = entryMethod.GetParameters();
if (parameters.Length == 0)
{
entryMethod.Invoke(null, null);
return true;
}
else if (parameters.Length == 1)
{
var pType = parameters[0].ParameterType;
if (pType == typeof(GameObject))
entryMethod.Invoke(null, new object[] { host });
else if (typeof(MonoBehaviour).IsAssignableFrom(pType))
{
var component = host.GetComponent(pType);
entryMethod.Invoke(null, new object[] { component != null ? component : (object)host });
}
else if (pType == typeof(Transform))
entryMethod.Invoke(null, new object[] { host.transform });
else if (pType == typeof(object))
entryMethod.Invoke(null, new object[] { host });
else
entryMethod.Invoke(null, new object[] { host }); // best effort
return true;
}
else
{
runtimeError = "Entry method has unsupported parameter signature.";
return false;
}
}
catch (TargetInvocationException tie)
{
runtimeError = tie.InnerException?.ToString() ?? tie.ToString();
Debug.LogError("Runtime invocation error: " + runtimeError);
return false;
}
catch (Exception ex)
{
runtimeError = ex.ToString();
Debug.LogError("Runtime invocation error: " + runtimeError);
return false;
}
}
/// <summary>
/// Attaches a dynamically compiled MonoBehaviour to a GameObject
/// </summary>
public bool AttachMonoBehaviour(GameObject host, out string runtimeError)
{
runtimeError = null;
if (host == null)
{
runtimeError = "Target GameObject is null.";
return false;
}
if (entryType == null || !typeof(MonoBehaviour).IsAssignableFrom(entryType))
{
runtimeError = $"Type '{entryTypeName}' is not a MonoBehaviour.";
return false;
}
try
{
// Check if component already exists
var existing = host.GetComponent(entryType);
if (existing != null)
{
Debug.LogWarning($"Component '{entryType.Name}' already exists on '{host.name}'. Removing old instance.");
if (Application.isPlaying)
Destroy(existing);
else
DestroyImmediate(existing);
}
// Add the component
attachedComponent = host.AddComponent(entryType);
if (attachedComponent == null)
{
runtimeError = "Failed to add component to GameObject.";
return false;
}
Debug.Log($"Successfully attached '{entryType.Name}' to '{host.name}'");
return true;
}
catch (Exception ex)
{
runtimeError = ex.ToString();
Debug.LogError("Failed to attach MonoBehaviour: " + runtimeError);
return false;
}
}
/// <summary>
/// Invokes a coroutine on the compiled type if it returns IEnumerator
/// </summary>
public bool InvokeCoroutine(MonoBehaviour host, out string runtimeError)
{
runtimeError = null;
if (entryMethod == null)
{
runtimeError = "No entry method found.";
return false;
}
if (!typeof(System.Collections.IEnumerator).IsAssignableFrom(entryMethod.ReturnType))
{
runtimeError = $"Method '{entryMethodName}' does not return IEnumerator.";
return false;
}
try
{
var parameters = entryMethod.GetParameters();
object result = null;
if (parameters.Length == 0)
{
result = entryMethod.Invoke(null, null);
}
else if (parameters.Length == 1)
{
var pType = parameters[0].ParameterType;
if (pType == typeof(GameObject))
result = entryMethod.Invoke(null, new object[] { host.gameObject });
else if (typeof(MonoBehaviour).IsAssignableFrom(pType))
result = entryMethod.Invoke(null, new object[] { host });
else
result = entryMethod.Invoke(null, new object[] { host });
}
if (result is System.Collections.IEnumerator coroutine)
{
host.StartCoroutine(coroutine);
Debug.Log($"Started coroutine '{entryMethodName}' on '{host.name}'");
return true;
}
else
{
runtimeError = "Method did not return a valid IEnumerator.";
return false;
}
}
catch (Exception ex)
{
runtimeError = ex.ToString();
Debug.LogError("Failed to start coroutine: " + runtimeError);
return false;
}
}
/// <summary>
/// MCP-callable function: Compiles code and optionally attaches to a GameObject
/// </summary>
/// <param name="sourceCode">C# source code to compile</param>
/// <param name="typeName">Type name to instantiate/invoke</param>
/// <param name="methodName">Method name to invoke (for static methods)</param>
/// <param name="targetObject">Target GameObject (null = this.gameObject)</param>
/// <param name="shouldAttachComponent">If true and type is MonoBehaviour, attach as component</param>
/// <param name="errorMessage">Output error message if operation fails</param>
/// <returns>True if successful, false otherwise</returns>
public bool CompileAndExecute(
string sourceCode,
string typeName,
string methodName,
GameObject targetObject,
bool shouldAttachComponent,
out string errorMessage)
{
errorMessage = null;
// Validate inputs
if (string.IsNullOrWhiteSpace(sourceCode))
{
errorMessage = "Source code cannot be empty.";
return false;
}
if (string.IsNullOrWhiteSpace(typeName))
{
errorMessage = "Type name cannot be empty.";
return false;
}
// Set properties
code = sourceCode;
entryTypeName = typeName;
entryMethodName = string.IsNullOrWhiteSpace(methodName) ? "Run" : methodName;
attachAsComponent = shouldAttachComponent;
targetGameObject = targetObject;
// Determine target GameObject first
GameObject target = targetGameObject != null ? targetGameObject : this.gameObject;
string targetName = target != null ? target.name : "null";
// Compile
if (!CompileInMemory(out string compileError))
{
errorMessage = $"Compilation failed:\n{compileError}";
AddHistoryEntry(sourceCode, typeName, entryMethodName, false, compileError, targetName);
return false;
}
if (target == null)
{
errorMessage = "No target GameObject available.";
AddHistoryEntry(sourceCode, typeName, entryMethodName, false, "No target GameObject", "null");
return false;
}
// Execute based on type
try
{
// MonoBehaviour attachment
if (shouldAttachComponent && entryType != null && typeof(MonoBehaviour).IsAssignableFrom(entryType))
{
if (!AttachMonoBehaviour(target, out string attachError))
{
errorMessage = $"Failed to attach MonoBehaviour:\n{attachError}";
AddHistoryEntry(sourceCode, typeName, entryMethodName, false, attachError, target.name);
return false;
}
Debug.Log($"[MCP] MonoBehaviour '{typeName}' successfully attached to '{target.name}'");
AddHistoryEntry(sourceCode, typeName, entryMethodName, true, "Component attached successfully", target.name);
return true;
}
// Coroutine invocation
if (entryMethod != null && typeof(System.Collections.IEnumerator).IsAssignableFrom(entryMethod.ReturnType))
{
var host = target.GetComponent<MonoBehaviour>() ?? this;
if (!InvokeCoroutine(host, out string coroutineError))
{
errorMessage = $"Failed to start coroutine:\n{coroutineError}";
AddHistoryEntry(sourceCode, typeName, entryMethodName, false, coroutineError, target.name);
return false;
}
Debug.Log($"[MCP] Coroutine '{methodName}' started on '{target.name}'");
AddHistoryEntry(sourceCode, typeName, entryMethodName, true, "Coroutine started successfully", target.name);
return true;
}
// Static method invocation
if (!InvokeEntry(target, out string invokeError))
{
errorMessage = $"Failed to invoke method:\n{invokeError}";
AddHistoryEntry(sourceCode, typeName, entryMethodName, false, invokeError, target.name);
return false;
}
Debug.Log($"[MCP] Method '{methodName}' executed successfully on '{target.name}'");
AddHistoryEntry(sourceCode, typeName, entryMethodName, true, "Method executed successfully", target.name);
return true;
}
catch (Exception ex)
{
errorMessage = $"Execution error:\n{ex.Message}\n{ex.StackTrace}";
return false;
}
}
/// <summary>
/// Simplified MCP-callable function with default parameters
/// </summary>
public bool CompileAndExecute(string sourceCode, string typeName, GameObject targetObject, out string errorMessage)
{
// Auto-detect if it's a MonoBehaviour by checking the source
bool shouldAttach = sourceCode.Contains(": MonoBehaviour") || sourceCode.Contains(":MonoBehaviour");
return CompileAndExecute(sourceCode, typeName, "Run", targetObject, shouldAttach, out errorMessage);
}
/// <summary>
/// MCP-callable: Compile and attach to current GameObject
/// </summary>
public bool CompileAndAttachToSelf(string sourceCode, string typeName, out string errorMessage)
{
return CompileAndExecute(sourceCode, typeName, "Run", this.gameObject, true, out errorMessage);
}
// helper: convenience method to compile + run on this.gameObject
public void CompileAndRunOnSelf()
{
if (CompileInMemory(out var diag))
{
if (!Application.isPlaying)
Debug.LogWarning("Running compiled code in Edit Mode. Some UnityEngine APIs may not behave as expected.");
GameObject target = targetGameObject != null ? targetGameObject : this.gameObject;
// Check if we should attach as component
if (attachAsComponent && entryType != null && typeof(MonoBehaviour).IsAssignableFrom(entryType))
{
if (AttachMonoBehaviour(target, out var attachErr))
{
Debug.Log($"MonoBehaviour '{entryTypeName}' attached successfully to '{target.name}'.");
}
else
{
Debug.LogError("Failed to attach MonoBehaviour: " + attachErr);
}
}
// Check if it's a coroutine
else if (entryMethod != null && typeof(System.Collections.IEnumerator).IsAssignableFrom(entryMethod.ReturnType))
{
var host = target.GetComponent<MonoBehaviour>() ?? this;
if (InvokeCoroutine(host, out var coroutineErr))
{
Debug.Log("Coroutine started successfully.");
}
else
{
Debug.LogError("Failed to start coroutine: " + coroutineErr);
}
}
// Regular static method invocation
else if (InvokeEntry(target, out var runtimeErr))
{
Debug.Log("Entry invoked successfully.");
}
else
{
Debug.LogError("Failed to invoke entry: " + runtimeErr);
}
}
else
{
Debug.LogError("Compile failed: " + lastCompileDiagnostics);
}
}
/// <summary>
/// Adds an entry to the compilation history
/// </summary>
private void AddHistoryEntry(string sourceCode, string typeName, string methodName, bool success, string diagnostics, string target)
{
if (!enableHistory) return;
var entry = new CompilationHistoryEntry
{
timestamp = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
sourceCode = sourceCode,
typeName = typeName,
methodName = methodName,
success = success,
diagnostics = diagnostics,
executionTarget = target
};
_sharedHistory.Add(entry);
// Trim if exceeded max
while (_sharedHistory.Count > maxHistoryEntries)
{
_sharedHistory.RemoveAt(0);
}
}
/// <summary>
/// Saves the compilation history to a JSON file outside Assets
/// </summary>
public bool SaveHistoryToFile(out string savedPath, out string error)
{
error = "";
savedPath = "";
try
{
string projectRoot = Application.dataPath.Replace("/Assets", "").Replace("\\Assets", "");
string historyDir = System.IO.Path.Combine(projectRoot, "RoslynHistory");
if (!System.IO.Directory.Exists(historyDir))
{
System.IO.Directory.CreateDirectory(historyDir);
}
string timestamp = System.DateTime.Now.ToString("yyyyMMdd_HHmmss");
string filename = $"RoslynHistory_{timestamp}.json";
savedPath = System.IO.Path.Combine(historyDir, filename);
string json = JsonUtility.ToJson(new HistoryWrapper { entries = _sharedHistory }, true);
System.IO.File.WriteAllText(savedPath, json);
Debug.Log($"[RuntimeRoslynDemo] Saved {_sharedHistory.Count} history entries to: {savedPath}");
return true;
}
catch (System.Exception ex)
{
error = ex.Message;
Debug.LogError($"[RuntimeRoslynDemo] Failed to save history: {error}");
return false;
}
}
/// <summary>
/// Saves a specific history entry as a standalone .cs file outside Assets
/// </summary>
public bool SaveHistoryEntryAsScript(int index, out string savedPath, out string error)
{
error = "";
savedPath = "";
if (index < 0 || index >= _sharedHistory.Count)
{
error = "Invalid history index";
return false;
}
try
{
var entry = _sharedHistory[index];
string projectRoot = Application.dataPath.Replace("/Assets", "").Replace("\\Assets", "");
string scriptsDir = System.IO.Path.Combine(projectRoot, "RoslynHistory", "Scripts");
if (!System.IO.Directory.Exists(scriptsDir))
{
System.IO.Directory.CreateDirectory(scriptsDir);
}
string timestamp = System.DateTime.Parse(entry.timestamp).ToString("yyyyMMdd_HHmmss");
string filename = $"{entry.typeName}_{timestamp}.cs";
savedPath = System.IO.Path.Combine(scriptsDir, filename);
// Add header comment
string header = $"// Roslyn Runtime Compiled Script\n// Original Timestamp: {entry.timestamp}\n// Type: {entry.typeName}\n// Method: {entry.methodName}\n// Success: {entry.success}\n// Target: {entry.executionTarget}\n\n";
System.IO.File.WriteAllText(savedPath, header + entry.sourceCode);
Debug.Log($"[RuntimeRoslynDemo] Saved script to: {savedPath}");
return true;
}
catch (System.Exception ex)
{
error = ex.Message;
Debug.LogError($"[RuntimeRoslynDemo] Failed to save script: {error}");
return false;
}
}
/// <summary>
/// Clears the compilation history
/// </summary>
public void ClearHistory()
{
_sharedHistory.Clear();
Debug.Log("[RuntimeRoslynDemo] Compilation history cleared");
}
[System.Serializable]
private class HistoryWrapper
{
public System.Collections.Generic.List<CompilationHistoryEntry> entries;
}
}
/// <summary>
/// Static helper class for MCP tools to compile and execute C# code at runtime
/// </summary>
public static class RoslynMCPHelper
{
private static RoslynRuntimeCompiler _compiler;
/// <summary>
/// Get or create the runtime compiler instance
/// </summary>
private static RoslynRuntimeCompiler GetOrCreateCompiler()
{
if (_compiler == null || _compiler.gameObject == null)
{
var existing = UnityEngine.Object.FindFirstObjectByType<RoslynRuntimeCompiler>();
if (existing != null)
{
_compiler = existing;
}
else
{
var go = new GameObject("MCPRoslynCompiler");
_compiler = go.AddComponent<RoslynRuntimeCompiler>();
if (!Application.isPlaying)
{
go.hideFlags = HideFlags.HideAndDontSave;
}
}
}
return _compiler;
}
/// <summary>
/// MCP Entry Point: Compile C# code and attach to a GameObject
/// </summary>
/// <param name="sourceCode">Complete C# source code</param>
/// <param name="className">Name of the class to instantiate</param>
/// <param name="targetGameObjectName">Name of GameObject to attach to (null = create new)</param>
/// <param name="result">Output result message</param>
/// <returns>True if successful</returns>
public static bool CompileAndAttach(string sourceCode, string className, string targetGameObjectName, out string result)
{
try
{
var compiler = GetOrCreateCompiler();
// Find or create target GameObject
GameObject target = null;
if (!string.IsNullOrEmpty(targetGameObjectName))
{
target = GameObject.Find(targetGameObjectName);
if (target == null)
{
result = $"GameObject '{targetGameObjectName}' not found.";
return false;
}
}
else
{
// Create a new GameObject for the script
target = new GameObject($"Generated_{className}");
UnityEngine.Debug.Log($"[MCP] Created new GameObject: {target.name}");
}
// Compile and execute
bool success = compiler.CompileAndExecute(sourceCode, className, target, out string error);
if (success)
{
result = $"Successfully compiled and attached '{className}' to '{target.name}'";
UnityEngine.Debug.Log($"[MCP] {result}");
return true;
}
else
{
result = $"Failed: {error}";
UnityEngine.Debug.LogError($"[MCP] {result}");
return false;
}
}
catch (Exception ex)
{
result = $"Exception: {ex.Message}";
UnityEngine.Debug.LogError($"[MCP] {result}\n{ex.StackTrace}");
return false;
}
}
/// <summary>
/// MCP Entry Point: Compile and execute static method
/// </summary>
/// <param name="sourceCode">Complete C# source code</param>
/// <param name="className">Name of the class containing the method</param>
/// <param name="methodName">Name of the static method to invoke</param>
/// <param name="targetGameObjectName">GameObject to pass as parameter (optional)</param>
/// <param name="result">Output result message</param>
/// <returns>True if successful</returns>
public static bool CompileAndExecuteStatic(string sourceCode, string className, string methodName, string targetGameObjectName, out string result)
{
try
{
var compiler = GetOrCreateCompiler();
GameObject target = compiler.gameObject;
if (!string.IsNullOrEmpty(targetGameObjectName))
{
var found = GameObject.Find(targetGameObjectName);
if (found != null)
{
target = found;
}
}
bool success = compiler.CompileAndExecute(sourceCode, className, methodName, target, false, out string error);
if (success)
{
result = $"Successfully compiled and executed '{className}.{methodName}'";
UnityEngine.Debug.Log($"[MCP] {result}");
return true;
}
else
{
result = $"Failed: {error}";
UnityEngine.Debug.LogError($"[MCP] {result}");
return false;
}
}
catch (Exception ex)
{
result = $"Exception: {ex.Message}";
UnityEngine.Debug.LogError($"[MCP] {result}\n{ex.StackTrace}");
return false;
}
}
/// <summary>
/// MCP Entry Point: Quick compile and attach MonoBehaviour
/// </summary>
/// <param name="sourceCode">MonoBehaviour source code</param>
/// <param name="className">MonoBehaviour class name</param>
/// <param name="gameObjectName">Target GameObject name (creates if null)</param>
/// <returns>Success status message</returns>
public static string QuickAttachScript(string sourceCode, string className, string gameObjectName = null)
{
bool success = CompileAndAttach(sourceCode, className, gameObjectName, out string result);
return result;
}
/// <summary>
/// MCP Entry Point: Execute code snippet with minimal parameters
/// </summary>
public static string ExecuteCode(string sourceCode, string className = "AIGenerated")
{
bool success = CompileAndExecuteStatic(sourceCode, className, "Run", null, out string result);
return result;
}
}
#if UNITY_EDITOR
// Editor window
public class RoslynRuntimeCompilerWindow : EditorWindow
{
private RoslynRuntimeCompiler helperInScene;
private Vector2 scrollPos;
private Vector2 diagScroll;
private Vector2 historyScroll;
private int selectedTab = 0;
private string[] tabNames = { "Compiler", "History" };
private int selectedHistoryIndex = -1;
private Vector2 historyCodeScroll;
// Editor UI state
private string codeText = string.Empty;
private string typeName = "AIGenerated";
private string methodName = "Run";
private bool attachAsComponent = false;
private GameObject targetGameObject = null;
[MenuItem("Window/Roslyn Runtime Compiler")]
public static void ShowWindow()
{
var w = GetWindow<RoslynRuntimeCompilerWindow>("Roslyn Runtime Compiler");
w.minSize = new Vector2(600, 400);
}
void OnEnable()
{
// try to find an existing helper in scene
helperInScene = FindFirstObjectByType<RoslynRuntimeCompiler>(FindObjectsInactive.Include);
if (helperInScene == null)
{
var go = new GameObject("RoslynRuntimeHelper");
helperInScene = go.AddComponent<RoslynRuntimeCompiler>();
// Don't save this helper into scene assets
go.hideFlags = HideFlags.HideAndDontSave;
}
if (helperInScene != null)
{
codeText = helperInScene.code;
typeName = helperInScene.entryTypeName;
methodName = helperInScene.entryMethodName;
attachAsComponent = helperInScene.attachAsComponent;
targetGameObject = helperInScene.targetGameObject;
}
}
void OnDisable()
{
// keep editor text back to helper if it still exists
if (helperInScene != null && helperInScene.gameObject != null)
{
helperInScene.code = codeText;
helperInScene.entryTypeName = typeName;
helperInScene.entryMethodName = methodName;
helperInScene.attachAsComponent = attachAsComponent;
helperInScene.targetGameObject = targetGameObject;
}
}
void OnDestroy()
{
// Clean up helper object when window is destroyed
if (helperInScene != null && helperInScene.gameObject != null)
{
DestroyImmediate(helperInScene.gameObject);
helperInScene = null;
}
}
void OnGUI()
{
// Ensure helper exists before drawing GUI - recreate if needed
if (helperInScene == null || helperInScene.gameObject == null)
{
// Try to find existing helper first
helperInScene = FindFirstObjectByType<RoslynRuntimeCompiler>(FindObjectsInactive.Include);
// If still not found, create a new one
if (helperInScene == null)
{
var go = new GameObject("RoslynRuntimeHelper");
helperInScene = go.AddComponent<RoslynRuntimeCompiler>();
go.hideFlags = HideFlags.HideAndDontSave;
// Initialize with default values
helperInScene.code = codeText;
helperInScene.entryTypeName = typeName;
helperInScene.entryMethodName = methodName;
helperInScene.attachAsComponent = attachAsComponent;
helperInScene.targetGameObject = targetGameObject;
}
else
{
// Load state from found helper
codeText = helperInScene.code;
typeName = helperInScene.entryTypeName;
methodName = helperInScene.entryMethodName;
attachAsComponent = helperInScene.attachAsComponent;
targetGameObject = helperInScene.targetGameObject;
}
}
EditorGUILayout.LabelField("Roslyn Runtime Compiler (Editor)", EditorStyles.boldLabel);
EditorGUILayout.Space();
// Tab selector
selectedTab = GUILayout.Toolbar(selectedTab, tabNames);
EditorGUILayout.Space();
if (selectedTab == 0)
{
DrawCompilerTab();
}
else if (selectedTab == 1)
{
DrawHistoryTab();
}
}
void DrawCompilerTab()
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Entry Type:", GUILayout.Width(70));
typeName = EditorGUILayout.TextField(typeName);
EditorGUILayout.LabelField("Method:", GUILayout.Width(50));
methodName = EditorGUILayout.TextField(methodName, GUILayout.Width(120));
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
attachAsComponent = EditorGUILayout.Toggle("Attach as Component", attachAsComponent, GUILayout.Width(200));
if (attachAsComponent)
{
EditorGUILayout.LabelField("Target:", GUILayout.Width(45));
targetGameObject = (GameObject)EditorGUILayout.ObjectField(targetGameObject, typeof(GameObject), true);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
EditorGUILayout.LabelField("Code (paste LLM output here):");
scrollPos = EditorGUILayout.BeginScrollView(scrollPos, GUILayout.Height(position.height * 0.55f));
codeText = EditorGUILayout.TextArea(codeText, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Compile"))
{
ApplyToHelper();
if (helperInScene != null)
{
var ok = helperInScene.CompileInMemory(out var diag);
Debug.Log(ok ? "Compile OK" : "Compile Failed\n" + diag);
}
}
bool canRun = helperInScene != null && helperInScene.HasCompiledAssembly &&
(helperInScene.HasEntryMethod || (helperInScene.HasEntryType && typeof(MonoBehaviour).IsAssignableFrom(helperInScene.EntryType)));
GUI.enabled = canRun;
if (GUILayout.Button("Run (invoke on selected)"))
{
ApplyToHelper();
var sel = Selection.activeGameObject;
if (sel == null && helperInScene != null && helperInScene.gameObject != null)
sel = helperInScene.gameObject;
if (sel != null && helperInScene != null)
{
if (helperInScene.InvokeEntry(sel, out var runtimeErr))
Debug.Log("Invocation OK on: " + sel.name);
else
Debug.LogError("Invocation failed: " + runtimeErr);
}
}
GUI.enabled = true;
if (GUILayout.Button("Compile & Run on helper"))
{
ApplyToHelper();
if (helperInScene != null)
{
helperInScene.CompileAndRunOnSelf();
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
EditorGUILayout.LabelField("Diagnostics:");
diagScroll = EditorGUILayout.BeginScrollView(diagScroll, GUILayout.Height(120));
string diagnosticsText = (helperInScene != null && helperInScene.lastCompileDiagnostics != null)
? helperInScene.lastCompileDiagnostics
: "No diagnostics available.";
EditorGUILayout.HelpBox(diagnosticsText, MessageType.Info);
EditorGUILayout.EndScrollView();
EditorGUILayout.Space();
EditorGUILayout.LabelField("Notes:");
EditorGUILayout.HelpBox("This compiles code in-memory using Roslyn. Do not write .cs files into Assets while running. Generated code runs with editor permissions.\n\n" +
"Supported patterns:\n" +
"1. Static method: public static void Run(GameObject host)\n" +
"2. MonoBehaviour: Enable 'Attach as Component' for classes inheriting MonoBehaviour\n" +
"3. Coroutine: public static IEnumerator RunCoroutine(MonoBehaviour host)\n" +
"4. Parameterless: public static void Run()", MessageType.None);
}
void DrawHistoryTab()
{
if (helperInScene == null) return;
var history = helperInScene.CompilationHistory;
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"Compilation History ({history.Count} entries)", EditorStyles.boldLabel);
if (GUILayout.Button("Save History JSON", GUILayout.Width(140)))
{
if (helperInScene.SaveHistoryToFile(out string path, out string error))
{
EditorUtility.DisplayDialog("Success", $"History saved to:\n{path}", "OK");
}
else
{
EditorUtility.DisplayDialog("Error", $"Failed to save history:\n{error}", "OK");
}
}
if (GUILayout.Button("Clear History", GUILayout.Width(100)))
{
if (EditorUtility.DisplayDialog("Clear History", "Are you sure you want to clear all compilation history?", "Yes", "No"))
{
helperInScene.ClearHistory();
selectedHistoryIndex = -1;
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
if (history.Count == 0)
{
EditorGUILayout.HelpBox("No compilation history yet. Compile and run scripts to see them here.", MessageType.Info);
return;
}
EditorGUILayout.BeginHorizontal();
// Left panel - history list
EditorGUILayout.BeginVertical(GUILayout.Width(position.width * 0.4f));
EditorGUILayout.LabelField("History Entries:", EditorStyles.boldLabel);
historyScroll = EditorGUILayout.BeginScrollView(historyScroll);
for (int i = history.Count - 1; i >= 0; i--) // Reverse order (newest first)
{
var entry = history[i];
GUIStyle entryStyle = new GUIStyle(GUI.skin.button);
entryStyle.alignment = TextAnchor.MiddleLeft;
entryStyle.normal.textColor = entry.success ? Color.green : Color.red;
if (selectedHistoryIndex == i)
{
entryStyle.normal.background = Texture2D.grayTexture;
}
string label = $"[{i}] {entry.timestamp} - {entry.typeName}.{entry.methodName}";
if (entry.success)
label += " ✓";
else
label += " ✗";
if (GUILayout.Button(label, entryStyle, GUILayout.Height(30)))
{
selectedHistoryIndex = i;
}
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
// Right panel - selected entry details
EditorGUILayout.BeginVertical();
if (selectedHistoryIndex >= 0 && selectedHistoryIndex < history.Count)
{
var entry = history[selectedHistoryIndex];
EditorGUILayout.LabelField("Entry Details:", EditorStyles.boldLabel);
EditorGUILayout.LabelField("Timestamp:", entry.timestamp);
EditorGUILayout.LabelField("Type:", entry.typeName);
EditorGUILayout.LabelField("Method:", entry.methodName);
EditorGUILayout.LabelField("Target:", entry.executionTarget);
EditorGUILayout.LabelField("Success:", entry.success ? "Yes" : "No");
EditorGUILayout.Space();
if (!string.IsNullOrEmpty(entry.diagnostics))
{
EditorGUILayout.LabelField("Diagnostics:");
EditorGUILayout.HelpBox(entry.diagnostics, entry.success ? MessageType.Info : MessageType.Error);
}
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Load to Compiler", GUILayout.Height(25)))
{
codeText = entry.sourceCode;
typeName = entry.typeName;
methodName = entry.methodName;
selectedTab = 0; // Switch to compiler tab
}
if (GUILayout.Button("Save as .cs File", GUILayout.Height(25)))
{
if (helperInScene.SaveHistoryEntryAsScript(selectedHistoryIndex, out string path, out string error))
{
EditorUtility.DisplayDialog("Success", $"Script saved to:\n{path}", "OK");
EditorUtility.RevealInFinder(path);
}
else
{
EditorUtility.DisplayDialog("Error", $"Failed to save script:\n{error}", "OK");
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
EditorGUILayout.LabelField("Source Code:");
historyCodeScroll = EditorGUILayout.BeginScrollView(historyCodeScroll, GUILayout.ExpandHeight(true));
EditorGUILayout.TextArea(entry.sourceCode, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
}
else
{
EditorGUILayout.HelpBox("Select a history entry to view details.", MessageType.Info);
}
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
}
void ApplyToHelper()
{
if (helperInScene == null || helperInScene.gameObject == null)
{
Debug.LogError("Helper object is missing or destroyed. Cannot apply settings.");
return;
}
helperInScene.code = codeText;
helperInScene.entryTypeName = typeName;
helperInScene.entryMethodName = methodName;
helperInScene.attachAsComponent = attachAsComponent;
helperInScene.targetGameObject = targetGameObject;
}
}
#endif