using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Scripting; using RobotNet10.ScriptEngine; using RobotNet10.ScriptEngine.Models; using RobotNet10.ScriptEngine.Shared; using System.Collections.Immutable; using System.Text; namespace RobotNet10.ScriptEngine.Helpers; public class ScriptBuilder { public readonly string DllPath = "dlls"; public ScriptOptions ScriptOptions { get; private set; } = ScriptOptions.Default; public ImmutableArray MetadataReferences { get; private set; } = []; public readonly string UsingNamespacesScript = ""; public string VerifyGlobalsScript = ""; public string RuntimeGlobalsScript = ""; public ScriptBuilder(IScriptEngineResource scriptResource, string dllPath) { if (!string.IsNullOrEmpty(dllPath)) { DllPath = dllPath; } UsingNamespacesScript = string.Join(Environment.NewLine, scriptResource.UsingNamespaces.Select(ns => $"using {ns};")); var sb = new StringBuilder(); sb.AppendLine($"private Dictionary {nameof(ScriptGlobals.RobotNet)} = [];"); sb.AppendLine($"private Dictionary {nameof(ScriptGlobals.AppApis)} = [];"); sb.AppendLine($"private Dictionary {nameof(ScriptGlobals.GlobalVariables)} = [];"); sb.AppendLine($"private Dictionary {nameof(ScriptGlobals.MissionParameters)} = [];"); VerifyGlobalsScript = sb.ToString(); RuntimeGlobalsScript = ScriptHelper.BuildGlobalsScript(scriptResource.AppGlobalType); List metadataRefs = []; var currentDirectory = Directory.GetCurrentDirectory(); if (Directory.Exists(DllPath)) { foreach (var dll in Directory.GetFiles(DllPath, "*.dll")) { metadataRefs.Add(MetadataReference.CreateFromFile(Path.Combine(currentDirectory, dll), properties: MetadataReferenceProperties.Assembly)); } } MetadataReferences = [.. metadataRefs]; var options = ScriptOptions.Default; options.MetadataReferences.Clear(); ScriptOptions = options.AddReferences(MetadataReferences).AddImports(scriptResource.UsingNamespaces).WithEmitDebugInformation(false); } public void Build(string code, out IEnumerable variables, out IEnumerable tasks, out IEnumerable missions) { try { var listVariables = new List(); var wrappedCode = string.Join(Environment.NewLine, [UsingNamespacesScript, "public class DummyClass", "{", VerifyGlobalsScript, RuntimeGlobalsScript, code, "}"]); var devCompilation = CSharpCompilation.Create("CodeAnalysis") .WithReferences(MetadataReferences) .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) .AddSyntaxTrees(CSharpSyntaxTree.ParseText(wrappedCode)); var diagnostics = devCompilation.GetDiagnostics(); if (diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error)) { var message = new StringBuilder("Script compilation errors:\r\n"); foreach (var diag in diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)) { var location = diag.Location; var lineSpan = location.GetLineSpan(); message.AppendLine($"\t❌ Error: {diag.GetMessage()}"); message.AppendLine($"\t Location: Line {lineSpan.StartLinePosition.Line + 1}, Column {lineSpan.StartLinePosition.Character + 1}"); if (location.SourceTree != null && lineSpan.IsValid) { var line = location.SourceTree.GetText().Lines[lineSpan.StartLinePosition.Line]; message.AppendLine($"\t Code: {line.ToString().Trim()}"); } } throw new ScriptCompilationException(message.ToString().TrimEnd()); } wrappedCode = string.Join(Environment.NewLine, [UsingNamespacesScript, "public class DummyClass", "{", code, "}"]); var syntaxTree = CSharpSyntaxTree.ParseText(wrappedCode); var root = syntaxTree.GetCompilationUnitRoot(); var runCompilation = CSharpCompilation.Create("CodeAnalysis") .AddSyntaxTrees(syntaxTree) .WithReferences(MetadataReferences) .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); var classNode = root.DescendantNodes().OfType().FirstOrDefault(c => c.Identifier.Text.Equals("DummyClass")); if (classNode == null) { throw new ScriptCompilationException("No class named 'DummyClass' found in the script. This is an internal error."); } var semanticModel = runCompilation.GetSemanticModel(syntaxTree); try { variables = ScriptHelper.ExportScriptVariables(classNode, semanticModel, ScriptOptions); } catch (Exception ex) { throw new ScriptCompilationException($"Failed to export script variables: {ex.Message}", ex); } try { ScriptHelper.ExportScriptTasksAndMissions(classNode, semanticModel, RuntimeGlobalsScript, ScriptOptions, out var scriptTasks, out var scriptMissions); tasks = scriptTasks; missions = scriptMissions; } catch (Exception ex) { throw new ScriptCompilationException($"Failed to export script tasks and missions: {ex.Message}", ex); } } catch (ScriptCompilationException) { throw; } catch (Exception ex) { throw new ScriptCompilationException($"Unexpected error during script compilation: {ex.Message}", ex); } finally { // Only force GC if we're dealing with large scripts or many compilations // In normal cases, let GC handle it naturally // GC.Collect(); } } }