diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs
index ab775990..de89b133 100644
--- a/MinecraftClient/Resources/Translations/Translations.Designer.cs
+++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs
@@ -1780,6 +1780,51 @@ namespace MinecraftClient {
return ResourceManager.GetString("bot.script.pm.loaded", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to Error in {0} at line {1}, column {2}: [{3}] {4}.
+ ///
+ internal static string script_compile_error {
+ get {
+ return ResourceManager.GetString("script.compile.error", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Error in {0}: [{1}] {2}.
+ ///
+ internal static string script_compile_error_no_location {
+ get {
+ return ResourceManager.GetString("script.compile.error_no_location", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to [Script] Compilation failed with error(s):.
+ ///
+ internal static string script_compile_failed {
+ get {
+ return ResourceManager.GetString("script.compile.failed", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to [Script] Starting compilation for {0}....
+ ///
+ internal static string script_compile_started {
+ get {
+ return ResourceManager.GetString("script.compile.started", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to [Script] Compilation done with no errors..
+ ///
+ internal static string script_compile_succeeded {
+ get {
+ return ResourceManager.GetString("script.compile.succeeded", resourceCulture);
+ }
+ }
///
/// Looks up a localized string similar to Loaded task:
diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx
index 5494dafc..882faab3 100644
--- a/MinecraftClient/Resources/Translations/Translations.resx
+++ b/MinecraftClient/Resources/Translations/Translations.resx
@@ -693,6 +693,21 @@ cooldown: {6}
Script '{0}' loaded.
+
+ Error in {0} at line {1}, column {2}: [{3}] {4}
+
+
+ Error in {0}: [{1}] {2}
+
+
+ [Script] Compilation failed with error(s):
+
+
+ [Script] Starting compilation for {0}...
+
+
+ [Script] Compilation done with no errors.
+
Loaded task:
{0}
diff --git a/MinecraftClient/Scripting/CSharpRunner.cs b/MinecraftClient/Scripting/CSharpRunner.cs
index 668ab336..724ccb7f 100644
--- a/MinecraftClient/Scripting/CSharpRunner.cs
+++ b/MinecraftClient/Scripting/CSharpRunner.cs
@@ -4,6 +4,7 @@ using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
+using Microsoft.CodeAnalysis;
using MinecraftClient.Scripting.DynamicRun.Builder;
using static MinecraftClient.Settings;
@@ -16,6 +17,8 @@ namespace MinecraftClient.Scripting
{
private static readonly Dictionary CompileCache = new();
+ private readonly record struct ScriptSourceLine(int LineNumber, string Text);
+
///
/// Run the specified C# script file
///
@@ -48,12 +51,13 @@ namespace MinecraftClient.Scripting
{
//Process different sections of the script file
bool scriptMain = true;
- List script = new();
- List extensions = new();
+ List script = new();
+ List extensions = new();
List libs = new();
List dlls = new();
- foreach (string line in lines)
+ for (int i = 0; i < lines.Length; i++)
{
+ string line = lines[i];
if (line.StartsWith("//using"))
{
libs.Add(line.Replace("//", "").Trim());
@@ -67,43 +71,18 @@ namespace MinecraftClient.Scripting
if (line.EndsWith("Extensions"))
scriptMain = false;
}
- else if (scriptMain)
- script.Add(line);
- else extensions.Add(line);
+
+ (scriptMain ? script : extensions).Add(new(i + 1, line));
}
//Add return statement if missing
- if (script.All(line => !line.StartsWith("return ") && !line.Contains(" return ")))
- script.Add("return null;");
+ bool hasImplicitReturn = script.All(line => !line.Text.StartsWith("return ", StringComparison.Ordinal)
+ && !line.Text.Contains(" return ", StringComparison.Ordinal));
//Generate a class from the given script
- string code = string.Join("\n", new string[]
- {
- "using System;",
- "using System.Collections.Generic;",
- "using System.Text.RegularExpressions;",
- "using System.Linq;",
- "using System.Text;",
- "using System.IO;",
- "using System.Net;",
- "using System.Threading;",
- "using MinecraftClient;",
- "using MinecraftClient.Scripting;",
- "using MinecraftClient.Mapping;",
- "using MinecraftClient.Inventory;",
- string.Join("\n", libs),
- "namespace ScriptLoader {",
- "public class Script {",
- "public CSharpAPI MCC;",
- "public object __run(CSharpAPI __apiHandler, string[] args) {",
- "this.MCC = __apiHandler;",
- string.Join("\n", script),
- "}",
- string.Join("\n", extensions),
- "}}",
- });
+ string code = BuildScriptCode(scriptName, script, extensions, libs, hasImplicitReturn);
- ConsoleIO.WriteLogLine($"[Script] Starting compilation for {scriptName}...");
+ ConsoleIO.WriteLogLine(string.Format(Translations.script_compile_started, scriptName));
//Compile the C# class in memory using all the currently loaded assemblies
var result = compiler.Compile(code, Guid.NewGuid().ToString(), dlls);
@@ -112,22 +91,17 @@ namespace MinecraftClient.Scripting
if (result.Failures is not null)
{
- ConsoleIO.WriteLogLine("[Script] Compilation failed with error(s):");
+ ConsoleIO.WriteLogLine(Translations.script_compile_failed);
foreach (var failure in result.Failures)
{
- // Get the line that contains the error:
-
- var loc = failure.Location.GetMappedLineSpan();
- var line = code.Split('\n')[loc.StartLinePosition.Line];
-
- ConsoleIO.WriteLogLine($"[Script] Error in {scriptName}, on line ({line.Trim()}): [{failure.Id}] {failure.GetMessage()}");
+ ConsoleIO.WriteLogLine(FormatCompilationFailure(failure, scriptName));
}
throw new CSharpException(CSErrorType.InvalidScript, new InvalidProgramException("Compilation failed due to error(s)."));
}
- ConsoleIO.WriteLogLine("[Script] Compilation done with no errors.");
+ ConsoleIO.WriteLogLine(Translations.script_compile_succeeded);
//Retrieve compiled assembly
assembly = result.Assembly;
@@ -151,6 +125,77 @@ namespace MinecraftClient.Scripting
else return null;
}
+ private static string BuildScriptCode(string scriptName, IEnumerable script, IEnumerable extensions, IEnumerable libs, bool hasImplicitReturn)
+ {
+ StringBuilder codeBuilder = new();
+ codeBuilder.AppendLine("using System;");
+ codeBuilder.AppendLine("using System.Collections.Generic;");
+ codeBuilder.AppendLine("using System.Text.RegularExpressions;");
+ codeBuilder.AppendLine("using System.Linq;");
+ codeBuilder.AppendLine("using System.Text;");
+ codeBuilder.AppendLine("using System.IO;");
+ codeBuilder.AppendLine("using System.Net;");
+ codeBuilder.AppendLine("using System.Threading;");
+ codeBuilder.AppendLine("using MinecraftClient;");
+ codeBuilder.AppendLine("using MinecraftClient.Scripting;");
+ codeBuilder.AppendLine("using MinecraftClient.Mapping;");
+ codeBuilder.AppendLine("using MinecraftClient.Inventory;");
+
+ foreach (string lib in libs)
+ codeBuilder.AppendLine(lib);
+
+ codeBuilder.AppendLine("namespace ScriptLoader {");
+ codeBuilder.AppendLine("public class Script {");
+ codeBuilder.AppendLine("public CSharpAPI MCC;");
+ codeBuilder.AppendLine("public object __run(CSharpAPI __apiHandler, string[] args) {");
+ codeBuilder.AppendLine("this.MCC = __apiHandler;");
+ AppendMappedSection(codeBuilder, scriptName, script);
+
+ if (hasImplicitReturn)
+ codeBuilder.AppendLine("return null;");
+
+ codeBuilder.AppendLine("}");
+ AppendMappedSection(codeBuilder, scriptName, extensions);
+ codeBuilder.AppendLine("}}");
+ return codeBuilder.ToString();
+ }
+
+ private static void AppendMappedSection(StringBuilder codeBuilder, string scriptName, IEnumerable lines)
+ {
+ ScriptSourceLine[] sourceLines = lines.ToArray();
+ if (sourceLines.Length == 0)
+ return;
+
+ codeBuilder.AppendLine($@"#line {sourceLines[0].LineNumber} ""{EscapeLineDirectivePath(scriptName)}""");
+
+ foreach (ScriptSourceLine line in sourceLines)
+ codeBuilder.AppendLine(line.Text);
+
+ codeBuilder.AppendLine("#line default");
+ }
+
+ private static string EscapeLineDirectivePath(string path)
+ {
+ return path.Replace("\\", "\\\\").Replace("\"", "\\\"");
+ }
+
+ private static string FormatCompilationFailure(Diagnostic failure, string scriptName)
+ {
+ if (failure.Location.IsInSource)
+ {
+ var location = failure.Location.GetMappedLineSpan();
+ string sourcePath = string.IsNullOrWhiteSpace(location.Path) ? scriptName : location.Path;
+ return string.Format(Translations.script_compile_error,
+ sourcePath,
+ location.StartLinePosition.Line + 1,
+ location.StartLinePosition.Character + 1,
+ failure.Id,
+ failure.GetMessage());
+ }
+
+ return string.Format(Translations.script_compile_error_no_location, scriptName, failure.Id, failure.GetMessage());
+ }
+
///
/// Quickly calculate a hash for the given script
///