From b80b9abe46adb822e31141478572777047d7949f Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Sat, 25 Oct 2025 01:13:58 -0400 Subject: [PATCH 1/4] 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. --- MCPForUnity/Editor/Tools/ManageScript.cs | 2 +- deploy-dev.bat | 4 ++-- restore-dev.bat | 22 ++++++++++++++++++---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/MCPForUnity/Editor/Tools/ManageScript.cs b/MCPForUnity/Editor/Tools/ManageScript.cs index 51669c65..db5679c0 100644 --- a/MCPForUnity/Editor/Tools/ManageScript.cs +++ b/MCPForUnity/Editor/Tools/ManageScript.cs @@ -1933,7 +1933,7 @@ string namespaceName /// private static ValidationLevel GetValidationLevelFromGUI() { - string savedLevel = EditorPrefs.GetString("MCPForUnity_ScriptValidationLevel", "standard"); + string savedLevel = EditorPrefs.GetString("MCPForUnity.ValidationLevel", "standard"); return savedLevel.ToLower() switch { "basic" => ValidationLevel.Basic, diff --git a/deploy-dev.bat b/deploy-dev.bat index 60a398bd..300856d3 100644 --- a/deploy-dev.bat +++ b/deploy-dev.bat @@ -11,7 +11,7 @@ set "SCRIPT_DIR=%~dp0" set "BRIDGE_SOURCE=%SCRIPT_DIR%MCPForUnity" set "SERVER_SOURCE=%SCRIPT_DIR%MCPForUnity\UnityMcpServer~\src" set "DEFAULT_BACKUP_DIR=%USERPROFILE%\Desktop\unity-mcp-backup" -set "DEFAULT_SERVER_PATH=%LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer\src" +set "DEFAULT_SERVER_PATH=%LOCALAPPDATA%\UnityMCP\UnityMcpServer\src" :: Get user inputs echo Please provide the following paths: @@ -19,7 +19,7 @@ echo. :: Package cache location echo Unity Package Cache Location: -echo Example: X:\UnityProject\Library\PackageCache\com.coplaydev.unity-mcp@1.0.0 +echo Example: X:\Unity\Projects\UnityMCPTestbed2\Library\PackageCache\com.coplaydev.unity-mcp@4c106125b342 set /p "PACKAGE_CACHE_PATH=Enter Unity package cache path: " if "%PACKAGE_CACHE_PATH%"=="" ( diff --git a/restore-dev.bat b/restore-dev.bat index 6f68be0b..15184412 100644 --- a/restore-dev.bat +++ b/restore-dev.bat @@ -102,7 +102,8 @@ echo =============================================== echo WARNING: This will overwrite current files! echo =============================================== echo Restoring from: %SELECTED_BACKUP% -echo Unity Bridge target: %PACKAGE_CACHE_PATH%\Editor +echo Unity Bridge Editor target: %PACKAGE_CACHE_PATH%\Editor +echo Unity Bridge Runtime target: %PACKAGE_CACHE_PATH%\Runtime echo Python Server target: %SERVER_PATH% echo. set /p "confirm=Continue with restore? (y/N): " @@ -119,16 +120,29 @@ echo =============================================== :: Restore Unity Bridge if exist "%SELECTED_BACKUP%\UnityBridge\Editor" ( - echo Restoring Unity Bridge files... + echo Restoring Unity Bridge Editor files... rd /s /q "%PACKAGE_CACHE_PATH%\Editor" 2>nul xcopy "%SELECTED_BACKUP%\UnityBridge\Editor\*" "%PACKAGE_CACHE_PATH%\Editor\" /E /I /Y > nul if !errorlevel! neq 0 ( - echo Error: Failed to restore Unity Bridge files + echo Error: Failed to restore Unity Bridge Editor files pause exit /b 1 ) ) else ( - echo Warning: No Unity Bridge backup found, skipping... + echo Warning: No Unity Bridge Editor backup found, skipping... +) + +if exist "%SELECTED_BACKUP%\UnityBridge\Runtime" ( + echo Restoring Unity Bridge Runtime files... + rd /s /q "%PACKAGE_CACHE_PATH%\Runtime" 2>nul + xcopy "%SELECTED_BACKUP%\UnityBridge\Runtime\*" "%PACKAGE_CACHE_PATH%\Runtime\" /E /I /Y > nul + if !errorlevel! neq 0 ( + echo Error: Failed to restore Unity Bridge Runtime files + pause + exit /b 1 + ) +) else ( + echo Warning: No Unity Bridge Runtime backup found, skipping... ) :: Restore Python Server From 4e9604f249cf9e13fd0750bef747f84ad9c35efa Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Sat, 25 Oct 2025 11:19:11 -0400 Subject: [PATCH 2/4] Further changes String to Int for consistency --- MCPForUnity/Editor/Tools/ManageScript.cs | 11 ++--------- restore-dev.bat | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/MCPForUnity/Editor/Tools/ManageScript.cs b/MCPForUnity/Editor/Tools/ManageScript.cs index db5679c0..b5cbbb1d 100644 --- a/MCPForUnity/Editor/Tools/ManageScript.cs +++ b/MCPForUnity/Editor/Tools/ManageScript.cs @@ -1933,15 +1933,8 @@ string namespaceName /// private static ValidationLevel GetValidationLevelFromGUI() { - string savedLevel = EditorPrefs.GetString("MCPForUnity.ValidationLevel", "standard"); - return savedLevel.ToLower() switch - { - "basic" => ValidationLevel.Basic, - "standard" => ValidationLevel.Standard, - "comprehensive" => ValidationLevel.Comprehensive, - "strict" => ValidationLevel.Strict, - _ => ValidationLevel.Standard // Default fallback - }; + int savedLevel = EditorPrefs.GetInt("MCPForUnity.ValidationLevel", (int)ValidationLevel.Standard); + return (ValidationLevel)Mathf.Clamp(savedLevel, 0, 3); } /// diff --git a/restore-dev.bat b/restore-dev.bat index 15184412..81311f6c 100644 --- a/restore-dev.bat +++ b/restore-dev.bat @@ -11,7 +11,7 @@ echo. :: Configuration set "DEFAULT_BACKUP_DIR=%USERPROFILE%\Desktop\unity-mcp-backup" -set "DEFAULT_SERVER_PATH=%LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer\src" +set "DEFAULT_SERVER_PATH=%LOCALAPPDATA%\UnityMCP\UnityMcpServer\src" :: Get user inputs echo Please provide the following paths: From c5b2d35254baa6316ae6e35dcb671515e9cbdb6b Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Sun, 9 Nov 2025 23:31:59 -0500 Subject: [PATCH 3/4] [Custom Tool] Roslyn Runtime Compilation Allows users to generate/compile codes during Playmode --- .../ManageRuntimeCompilation.cs | 526 +++++++ .../ManageRuntimeCompilation.cs.meta | 2 + .../PythonTools.asset.meta | 8 + .../RoslynRuntimeCompilation/RoslynRuntime.md | 3 + .../RoslynRuntimeCompiler.cs | 1211 +++++++++++++++++ .../RoslynRuntimeCompiler.cs.meta | 2 + .../runtime_compilation_tool.py | 276 ++++ .../runtime_compilation_tool.py.meta | 10 + 8 files changed, 2038 insertions(+) create mode 100644 CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs create mode 100644 CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs.meta create mode 100644 CustomTools/RoslynRuntimeCompilation/PythonTools.asset.meta create mode 100644 CustomTools/RoslynRuntimeCompilation/RoslynRuntime.md create mode 100644 CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs create mode 100644 CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs.meta create mode 100644 CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py create mode 100644 CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py.meta diff --git a/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs b/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs new file mode 100644 index 00000000..401c7530 --- /dev/null +++ b/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs @@ -0,0 +1,526 @@ +#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 +{ + /// + /// Runtime compilation tool for MCP Unity. + /// Compiles and loads C# code at runtime without triggering domain reload. + /// + [McpForUnityTool("runtime_compilation")] + public static class ManageRuntimeCompilation + { + private static readonly Dictionary LoadedAssemblies = new Dictionary(); + 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 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(); + string assemblyName = @params["assembly_name"]?.ToString() ?? $"DynamicAssembly_{DateTime.Now.Ticks}"; + string attachTo = @params["attach_to"]?.ToString(); + bool loadImmediately = @params["load_immediately"]?.ToObject() ?? 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 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 Response.Error("Compilation failed", new + { + errors = errors, + error_count = errors.Count + }); + } + + // Load assembly if requested + Assembly loadedAssembly = null; + List typeNames = new List(); + + 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 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 + }); + } + + /// + /// Execute code using RoslynRuntimeCompiler with full GUI tool integration + /// Supports MonoBehaviours, static methods, and coroutines + /// + 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() ?? 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 + }); + } + } + + /// + /// Get compilation history from RoslynRuntimeCompiler + /// + 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}"); + } + } + + /// + /// Save compilation history to JSON file + /// + 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}"); + } + } + + /// + /// Clear compilation history + /// + 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 GetDefaultReferences() + { + var references = new List(); + + // 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; + } + + /// + /// Get or create a RoslynRuntimeCompiler instance for GUI integration + /// This allows MCP commands to leverage the existing GUI tool + /// + private static RoslynRuntimeCompiler GetOrCreateRoslynCompiler() + { + var existing = UnityEngine.Object.FindFirstObjectByType(); + if (existing != null) + { + return existing; + } + + var go = new GameObject("MCPRoslynCompiler"); + var compiler = go.AddComponent(); + compiler.enableHistory = true; // Enable history tracking for MCP operations + if (!Application.isPlaying) + { + go.hideFlags = HideFlags.HideAndDontSave; + } + return compiler; + } + } +} diff --git a/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs.meta b/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs.meta new file mode 100644 index 00000000..b33b4f64 --- /dev/null +++ b/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1c3b2419382faa04481f4a631c510ee6 \ No newline at end of file diff --git a/CustomTools/RoslynRuntimeCompilation/PythonTools.asset.meta b/CustomTools/RoslynRuntimeCompilation/PythonTools.asset.meta new file mode 100644 index 00000000..58dd3a6f --- /dev/null +++ b/CustomTools/RoslynRuntimeCompilation/PythonTools.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a3b463767742cdf43b366f68a656e42e +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/CustomTools/RoslynRuntimeCompilation/RoslynRuntime.md b/CustomTools/RoslynRuntimeCompilation/RoslynRuntime.md new file mode 100644 index 00000000..91d05d5b --- /dev/null +++ b/CustomTools/RoslynRuntimeCompilation/RoslynRuntime.md @@ -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. diff --git a/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs b/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs new file mode 100644 index 00000000..79a26dcd --- /dev/null +++ b/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs @@ -0,0 +1,1211 @@ +// 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 _sharedHistory = new System.Collections.Generic.List(); + private static int _maxHistoryEntries = 50; + + public System.Collections.Generic.List 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(); + + // 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; + } + } + + /// + /// Attaches a dynamically compiled MonoBehaviour to a GameObject + /// + 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; + } + } + + /// + /// Invokes a coroutine on the compiled type if it returns IEnumerator + /// + 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; + } + } + + /// + /// MCP-callable function: Compiles code and optionally attaches to a GameObject + /// + /// C# source code to compile + /// Type name to instantiate/invoke + /// Method name to invoke (for static methods) + /// Target GameObject (null = this.gameObject) + /// If true and type is MonoBehaviour, attach as component + /// Output error message if operation fails + /// True if successful, false otherwise + 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() ?? 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; + } + } + + /// + /// Simplified MCP-callable function with default parameters + /// + 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); + } + + /// + /// MCP-callable: Compile and attach to current GameObject + /// + 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() ?? 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); + } + } + + /// + /// Adds an entry to the compilation history + /// + 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); + } + } + + /// + /// Saves the compilation history to a JSON file outside Assets + /// + 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; + } + } + + /// + /// Saves a specific history entry as a standalone .cs file outside Assets + /// + 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; + } + } + + /// + /// Clears the compilation history + /// + public void ClearHistory() + { + _sharedHistory.Clear(); + Debug.Log("[RuntimeRoslynDemo] Compilation history cleared"); + } + + [System.Serializable] + private class HistoryWrapper + { + public System.Collections.Generic.List entries; + } +} + +/// +/// Static helper class for MCP tools to compile and execute C# code at runtime +/// +public static class RoslynMCPHelper +{ + private static RoslynRuntimeCompiler _compiler; + + /// + /// Get or create the runtime compiler instance + /// + private static RoslynRuntimeCompiler GetOrCreateCompiler() + { + if (_compiler == null || _compiler.gameObject == null) + { + var existing = UnityEngine.Object.FindFirstObjectByType(); + if (existing != null) + { + _compiler = existing; + } + else + { + var go = new GameObject("MCPRoslynCompiler"); + _compiler = go.AddComponent(); + if (!Application.isPlaying) + { + go.hideFlags = HideFlags.HideAndDontSave; + } + } + } + return _compiler; + } + + /// + /// MCP Entry Point: Compile C# code and attach to a GameObject + /// + /// Complete C# source code + /// Name of the class to instantiate + /// Name of GameObject to attach to (null = create new) + /// Output result message + /// True if successful + 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; + } + } + + /// + /// MCP Entry Point: Compile and execute static method + /// + /// Complete C# source code + /// Name of the class containing the method + /// Name of the static method to invoke + /// GameObject to pass as parameter (optional) + /// Output result message + /// True if successful + 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; + } + } + + /// + /// MCP Entry Point: Quick compile and attach MonoBehaviour + /// + /// MonoBehaviour source code + /// MonoBehaviour class name + /// Target GameObject name (creates if null) + /// Success status message + public static string QuickAttachScript(string sourceCode, string className, string gameObjectName = null) + { + bool success = CompileAndAttach(sourceCode, className, gameObjectName, out string result); + return result; + } + + /// + /// MCP Entry Point: Execute code snippet with minimal parameters + /// + 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("Roslyn Runtime Compiler"); + w.minSize = new Vector2(600, 400); + } + + void OnEnable() + { + // try to find an existing helper in scene + helperInScene = FindFirstObjectByType(FindObjectsInactive.Include); + if (helperInScene == null) + { + var go = new GameObject("RoslynRuntimeHelper"); + helperInScene = go.AddComponent(); + // 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(FindObjectsInactive.Include); + + // If still not found, create a new one + if (helperInScene == null) + { + var go = new GameObject("RoslynRuntimeHelper"); + helperInScene = go.AddComponent(); + 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"); + System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path.Replace("/", "\\")}\""); + } + 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 diff --git a/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs.meta b/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs.meta new file mode 100644 index 00000000..066adb58 --- /dev/null +++ b/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 97f1198c66ce56043a3c8a5e05ba0150 \ No newline at end of file diff --git a/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py b/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py new file mode 100644 index 00000000..48850414 --- /dev/null +++ b/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py @@ -0,0 +1,276 @@ +""" +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'}") + await ctx.info(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...") + await ctx.info("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}") + await ctx.info(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}") + await ctx.info(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...") + await ctx.info("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...") + await ctx.info("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...") + await ctx.info("Clearing compilation history...") + + params = {"action": "clear_history"} + return handle_unity_command("runtime_compilation", params) diff --git a/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py.meta b/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py.meta new file mode 100644 index 00000000..5b00cfbf --- /dev/null +++ b/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 3934c3a018e9eb540a1b39056c193f71 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 11500000, guid: d68ef794590944f1ea7ee102c91887c7, type: 3} From 880593335d3bed1af5886ae1852f62ba4bb7bbd9 Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Mon, 10 Nov 2025 00:24:21 -0500 Subject: [PATCH 4/4] Fix based on CR --- .../ManageRuntimeCompilation.cs | 35 +++++++++++++++---- .../RoslynRuntimeCompiler.cs | 5 ++- .../runtime_compilation_tool.py | 21 ++++------- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs b/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs index 401c7530..c5b6daab 100644 --- a/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs +++ b/CustomTools/RoslynRuntimeCompilation/ManageRuntimeCompilation.cs @@ -84,7 +84,10 @@ private static object CompileAndLoad(JObject @params) try { string code = @params["code"]?.ToString(); - string assemblyName = @params["assembly_name"]?.ToString() ?? $"DynamicAssembly_{DateTime.Now.Ticks}"; + 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() ?? true; @@ -101,8 +104,21 @@ private static object CompileAndLoad(JObject @params) // Create output directory Directory.CreateDirectory(DynamicAssembliesPath); - string dllPath = Path.Combine(DynamicAssembliesPath, $"{assemblyName}.dll"); - + 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); @@ -121,7 +137,7 @@ private static object CompileAndLoad(JObject @params) // Emit to file EmitResult emitResult; - using (var stream = new FileStream(dllPath, FileMode.Create)) + using (var stream = new FileStream(dllPath, FileMode.Create, FileAccess.Write, FileShare.None)) { emitResult = compilation.Emit(stream); } @@ -227,7 +243,7 @@ private static object CompileAndLoad(JObject @params) } #endif } - + private static object ListLoadedAssemblies() { var assemblies = LoadedAssemblies.Values.Select(info => new @@ -238,7 +254,7 @@ private static object ListLoadedAssemblies() type_count = info.TypeNames.Count, types = info.TypeNames }).ToList(); - + return Response.Success($"Found {assemblies.Count} loaded dynamic assemblies", new { count = assemblies.Count, @@ -246,6 +262,13 @@ private static object ListLoadedAssemblies() }); } + 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(); diff --git a/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs b/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs index 79a26dcd..734e9489 100644 --- a/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs +++ b/CustomTools/RoslynRuntimeCompilation/RoslynRuntimeCompiler.cs @@ -93,7 +93,6 @@ public class CompilationHistoryEntry // Static shared history private static System.Collections.Generic.List _sharedHistory = new System.Collections.Generic.List(); - private static int _maxHistoryEntries = 50; public System.Collections.Generic.List CompilationHistory => _sharedHistory; @@ -584,7 +583,7 @@ private void AddHistoryEntry(string sourceCode, string typeName, string methodNa _sharedHistory.Add(entry); // Trim if exceeded max - while (_sharedHistory.Count > _maxHistoryEntries) + while (_sharedHistory.Count > maxHistoryEntries) { _sharedHistory.RemoveAt(0); } @@ -1167,7 +1166,7 @@ void DrawHistoryTab() if (helperInScene.SaveHistoryEntryAsScript(selectedHistoryIndex, out string path, out string error)) { EditorUtility.DisplayDialog("Success", $"Script saved to:\n{path}", "OK"); - System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path.Replace("/", "\\")}\""); + EditorUtility.RevealInFinder(path); } else { diff --git a/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py b/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py index 48850414..977b9f71 100644 --- a/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py +++ b/CustomTools/RoslynRuntimeCompilation/runtime_compilation_tool.py @@ -83,8 +83,7 @@ async def compile_runtime_code( } ``` """ - #await safe_info(ctx, f"Compiling runtime code for assembly: {assembly_name or 'auto-generated'}") - await ctx.info(f"Compiling runtime code for assembly: {assembly_name or 'auto-generated'}") + await safe_info(ctx, f"Compiling runtime code for assembly: {assembly_name or 'auto-generated'}") params = { "action": "compile_and_load", @@ -113,8 +112,7 @@ async def list_loaded_assemblies( - Load timestamps - DLL file paths """ - #await safe_info(ctx, "Retrieving loaded dynamic assemblies...") - await ctx.info("Retrieving loaded dynamic assemblies...") + await safe_info(ctx, "Retrieving loaded dynamic assemblies...") params = {"action": "list_loaded"} return handle_unity_command("runtime_compilation", params) @@ -135,8 +133,7 @@ async def get_assembly_types( - Finding MonoBehaviour classes to attach - Debugging compilation results """ - #await safe_info(ctx, f"Getting types from assembly: {assembly_name}") - await ctx.info(f"Getting types from assembly: {assembly_name}") + 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) @@ -187,8 +184,7 @@ async def execute_with_roslyn( } ``` """ - #await safe_info(ctx, f"Executing code with RoslynRuntimeCompiler: {class_name}.{method_name}") - await ctx.info(f"Executing code with RoslynRuntimeCompiler: {class_name}.{method_name}") + await safe_info(ctx, f"Executing code with RoslynRuntimeCompiler: {class_name}.{method_name}") params = { "action": "execute_with_roslyn", @@ -226,8 +222,7 @@ async def get_compilation_history( - Tracking execution flow - Auditing dynamic code changes """ - #await safe_info(ctx, "Retrieving compilation history...") - await ctx.info("Retrieving compilation history...") + await safe_info(ctx, "Retrieving compilation history...") params = {"action": "get_history"} return handle_unity_command("runtime_compilation", params) @@ -250,8 +245,7 @@ async def save_compilation_history( - Share compilation sessions with team members - Archive successful code patterns """ - #await safe_info(ctx, "Saving compilation history to file...") - await ctx.info("Saving compilation history to file...") + await safe_info(ctx, "Saving compilation history to file...") params = {"action": "save_history"} return handle_unity_command("runtime_compilation", params) @@ -269,8 +263,7 @@ async def clear_compilation_history( 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...") - await ctx.info("Clearing compilation history...") + await safe_info(ctx, "Clearing compilation history...") params = {"action": "clear_history"} return handle_unity_command("runtime_compilation", params)