using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Text; 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(); /// /// 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") { //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(); foreach (string line in lines) { if (line.StartsWith("//using")) { libs.Add(line.Replace("//", "").Trim()); } else if (line.StartsWith("//dll")) { dlls.Add(line.Replace("//dll ", "").Trim()); } else if (line.StartsWith("//MCCScript")) { if (line.EndsWith("Extensions")) scriptMain = false; } else if (scriptMain) script.Add(line); else extensions.Add(line); } //Add return statement if missing if (script.All(line => !line.StartsWith("return ") && !line.Contains(" return "))) script.Add("return null;"); //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), "}}", }); ConsoleIO.WriteLogLine($"[Script] Starting compilation for {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 != null) { ConsoleIO.WriteLogLine("[Script] Compilation failed with error(s):"); foreach (var failure in result.Failures) { ConsoleIO.WriteLogLine($"[Script] Error in {scriptName}, line:col{failure.Location.GetMappedLineSpan()}: [{failure.Id}] {failure.GetMessage()}"); } throw new CSharpException(CSErrorType.InvalidScript, new InvalidProgramException("Compilation failed due to error.")); } ConsoleIO.WriteLogLine("[Script] Compilation done with no errors."); //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); return compiled; } catch (Exception e) { throw new CSharpException(CSErrorType.RuntimeError, e); } } else return null; } /// /// 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 != null ? 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) { SetMaster(apiHandler); this.localVars = localVars; } /* == 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 != 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 != 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 != null) { try { TypeConverter converter = TypeDescriptor.GetConverter(typeof(T)); if (converter != 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); } } }