using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using MinecraftClient.Scripting.DynamicRun.Builder; using static MinecraftClient.Settings; namespace MinecraftClient.Scripting { /// /// C# Script runner - Compile on-the-fly and run C# scripts /// class CSharpRunner { private static readonly Dictionary CompileCache = new(); private readonly record struct ScriptSourceLine(int LineNumber, string Text); /// /// Run the specified C# script file /// /// ChatBot handler for accessing ChatBot API /// Lines of the script file to run /// Arguments to pass to the script /// Local variables passed along with the script /// Set to false to compile and cache the script without launching it /// Thrown if an error occured /// Result of the execution, returned by the script public static object? Run(ChatBot apiHandler, string[] lines, string[] args, Dictionary? localVars, bool run = true, string scriptName = "Unknown Script", string? scriptOwnerKey = null) { //Script compatibility check for handling future versions differently if (lines.Length < 1 || lines[0] != "//MCCScript 1.0") throw new CSharpException(CSErrorType.InvalidScript, new InvalidDataException(Translations.exception_csrunner_invalid_head)); //Script hash for determining if it was previously compiled ulong scriptHash = QuickHash(lines); byte[]? assembly = null; Compiler compiler = new(); CompileRunner runner = new(); //No need to compile two scripts at the same time lock (CompileCache) { ///Process and compile script only if not already compiled if (!Config.Main.Advanced.CacheScript || !CompileCache.ContainsKey(scriptHash)) { //Process different sections of the script file bool scriptMain = true; List script = new(); List extensions = new(); List libs = new(); List dlls = new(); for (int i = 0; i < lines.Length; i++) { string line = lines[i]; if (line.StartsWith("//using")) { libs.Add(NormalizeUsingDirective(line)); } else if (line.StartsWith("//dll")) { dlls.Add(line.Replace("//dll ", "").Trim()); } else if (line.StartsWith("//MCCScript")) { if (line.EndsWith("Extensions")) scriptMain = false; } (scriptMain ? script : extensions).Add(new(i + 1, line)); } //Add return statement if missing 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 = BuildScriptCode(scriptName, script, extensions, libs, hasImplicitReturn); 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); //Process compile warnings and errors if (result.Failures is not null) { ConsoleIO.WriteLogLine(Translations.script_compile_failed); foreach (var failure in result.Failures) { ConsoleIO.WriteLogLine(FormatCompilationFailure(failure, scriptName)); } throw new CSharpException(CSErrorType.InvalidScript, new InvalidProgramException("Compilation failed due to error(s).")); } ConsoleIO.WriteLogLine(Translations.script_compile_succeeded); //Retrieve compiled assembly assembly = result.Assembly; if (Config.Main.Advanced.CacheScript) CompileCache[scriptHash] = assembly!; } else if (Config.Main.Advanced.CacheScript) assembly = CompileCache[scriptHash]; } //Run the compiled assembly with exception handling if (run) { try { var compiled = runner.Execute(assembly!, args, localVars, apiHandler, scriptOwnerKey); return compiled; } catch (Exception e) { throw new CSharpException(CSErrorType.RuntimeError, e); } } else return null; } internal static string NormalizeUsingDirective(string line) { string directive = line[2..].Trim(); return directive.EndsWith(';') ? directive : $"{directive};"; } 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 /// /// script lines /// Quick hash as unsigned long private static ulong QuickHash(string[] lines) { ulong hashedValue = 3074457345618258791ul; for (int i = 0; i < lines.Length; i++) { for (int j = 0; j < lines[i].Length; j++) { hashedValue += lines[i][j]; hashedValue *= 3074457345618258799ul; } hashedValue += '\n'; hashedValue *= 3074457345618258799ul; } return hashedValue; } } /// /// Describe a C# script error type /// public enum CSErrorType { FileReadError, InvalidScript, LoadError, RuntimeError }; /// /// Describe a C# script error with associated error type /// public class CSharpException : Exception { private readonly CSErrorType _type; public CSErrorType ExceptionType { get { return _type; } } public override string Message { get { return InnerException!.Message; } } public override string ToString() { return InnerException!.ToString(); } public CSharpException(CSErrorType type, Exception inner) : base(inner.Message, inner) { _type = type; } } /// /// Represents the C# API object accessible from C# Scripts /// public class CSharpAPI : ChatBot { /// /// Holds local variables passed along with the script /// private readonly Dictionary? localVars; /// /// Create a new C# API Wrapper /// /// ChatBot API Handler /// ChatBot tick handler /// Local variables passed along with the script public CSharpAPI(ChatBot apiHandler, Dictionary? localVars, string? scriptOwnerKey = null) { SetMaster(apiHandler); this.localVars = localVars; SetScriptOwnerKey(scriptOwnerKey); } /// /// Access the shared MCC gameplay API used by bots and the embedded MCP server. /// new public MccGameApi Game => base.Game; /* == Wrappers for ChatBot API with public visibility and call limit to one per tick for safety == */ /// /// Write some text in the console. Nothing will be sent to the server. /// /// Log text to write new public void LogToConsole(object text) { base.LogToConsole(text); } /// /// Send text to the server. Can be anything such as chat messages or commands /// /// Text to send to the server /// TRUE if successfully sent (Deprectated, always returns TRUE for compatibility purposes with existing scripts) public bool SendText(object text) { return base.SendText(text is string str ? str : text.ToString() ?? string.Empty); } /// /// Perform an internal MCC command (not a server command, use SendText() instead for that!) /// /// The command to process /// Local variables passed along with the internal command /// TRUE if the command was indeed an internal MCC command new public bool PerformInternalCommand(string command, Dictionary? localVars = null) { localVars ??= this.localVars; return base.PerformInternalCommand(command, localVars); } /// /// Disconnect from the server and restart the program /// It will unload and reload all the bots and then reconnect to the server /// /// If connection fails, the client will make X extra attempts /// Optional delay, in seconds, before restarting new public void ReconnectToTheServer(int extraAttempts = -999999, int delaySeconds = 0, bool keepAccountAndServerSettings = false) { if (extraAttempts == -999999) base.ReconnectToTheServer(delaySeconds: delaySeconds, keepAccountAndServerSettings: keepAccountAndServerSettings); else base.ReconnectToTheServer(extraAttempts, delaySeconds, keepAccountAndServerSettings); } /// /// Disconnect from the server and exit the program /// new public void DisconnectAndExit() { base.DisconnectAndExit(); } /// /// Load the provided ChatBot object /// /// Bot to load new public void LoadBot(ChatBot bot) { base.LoadBot(bot); } /// /// Return the list of currently online players /// /// List of online players new public string[] GetOnlinePlayers() { return base.GetOnlinePlayers(); } /// /// Get a dictionary of online player names and their corresponding UUID /// /// /// dictionary of online player whereby /// UUID represents the key /// playername represents the value new public Dictionary GetOnlinePlayersWithUUID() { return base.GetOnlinePlayersWithUUID(); } /* == Additional Methods useful for Script API == */ /// /// Get a global variable by name /// /// Name of the variable /// Value of the variable or null if no variable public object? GetVar(string varName) { if (localVars is not null && localVars.ContainsKey(varName)) return localVars[varName]; else return Config.AppVar.GetVar(varName); } /// /// Set a global variable for further use in any other script /// /// Name of the variable /// Value of the variable public bool SetVar(string varName, object varValue) { if (localVars is not null && localVars.ContainsKey(varName)) localVars.Remove(varName); return Config.AppVar.SetVar(varName, varValue); } /// /// Get a global variable by name, as the specified type, and try converting it if possible. /// If you know what you are doing and just want a cast, use (T)MCC.GetVar("name") instead. /// /// Variable type /// Variable name /// Variable as specified type or default value for this type public T? GetVar(string varName) { object? value = GetVar(varName); if (value is T Tval) return Tval; if (value is not null) { try { TypeConverter converter = TypeDescriptor.GetConverter(typeof(T)); if (converter is not null) return (T?)converter.ConvertFromString(value.ToString() ?? string.Empty); } catch (NotSupportedException) { /* Was worth trying */ } } return default; } //Named shortcuts for GetVar(varname) public string? GetVarAsString(string varName) { return GetVar(varName); } public int GetVarAsInt(string varName) { return GetVar(varName); } public double GetVarAsDouble(string varName) { return GetVar(varName); } public bool GetVarAsBool(string varName) { return GetVar(varName); } /// /// Load login/password using an account alias and optionally reconnect to the server /// /// Account alias /// Set to true to reconnecto to the server afterwards /// True if the account was found and loaded public bool SetAccount(string accountAlias, bool andReconnect = false) { bool result = Config.Main.Advanced.SetAccount(accountAlias); if (result && andReconnect) ReconnectToTheServer(keepAccountAndServerSettings: true); return result; } /// /// Load new server information and optionally reconnect to the server /// /// "serverip:port" couple or server alias /// True if the server IP was valid and loaded, false otherwise public bool SetServer(string server, bool andReconnect = false) { bool result = Config.Main.SetServerIP(new MainConfigHelper.MainConfig.ServerInfoConfig(server), true); if (result && andReconnect) ReconnectToTheServer(keepAccountAndServerSettings: true); return result; } /// /// Synchronously call another script and retrieve the result /// /// Script to call /// Arguments to pass to the script /// An object returned by the script, or null public object? CallScript(string script, string[] args) { ChatBots.Script.LookForScript(ref script); string[] lines; try { lines = File.ReadAllLines(script, Encoding.UTF8); } catch (Exception e) { throw new CSharpException(CSErrorType.FileReadError, e); } return CSharpRunner.Run(this, lines, args, localVars, scriptName: script); } } }