using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using Microsoft.CSharp; using System.CodeDom.Compiler; using System.Reflection; using System.Threading; using System.ComponentModel; namespace MinecraftClient { /// /// C# Script runner - Compile on-the-fly and run C# scripts /// class CSharpRunner { private static readonly Dictionary CompileCache = new Dictionary(); /// /// Run the specified C# script file /// /// ChatBot handler for accessing ChatBot API /// Tick handler for waiting after some API calls /// 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, ManualResetEvent tickHandler, string[] lines, string[] args, Dictionary localVars, bool run = true) { //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.Get("exception.csrunner.invalid_head"))); //Script hash for determining if it was previously compiled ulong scriptHash = QuickHash(lines); Assembly assembly = null; //No need to compile two scripts at the same time lock (CompileCache) { ///Process and compile script only if not already compiled if (!Settings.CacheScripts || !CompileCache.ContainsKey(scriptHash)) { //Process different sections of the script file bool scriptMain = true; List script = new List(); List extensions = new List(); List libs = new List(); List dlls = new List(); 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.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), "}}", }); //Compile the C# class in memory using all the currently loaded assemblies CSharpCodeProvider compiler = new CSharpCodeProvider(); CompilerParameters parameters = new CompilerParameters(); parameters.ReferencedAssemblies .AddRange(AppDomain.CurrentDomain .GetAssemblies() .Where(a => !a.IsDynamic) .Select(a => a.Location).ToArray()); parameters.CompilerOptions = "/t:library"; parameters.GenerateInMemory = true; parameters.ReferencedAssemblies.AddRange(dlls.ToArray()); CompilerResults result = compiler.CompileAssemblyFromSource(parameters, code); //Process compile warnings and errors if (result.Errors.Count > 0) throw new CSharpException(CSErrorType.LoadError, new InvalidOperationException(result.Errors[0].ErrorText)); //Retrieve compiled assembly assembly = result.CompiledAssembly; if (Settings.CacheScripts) CompileCache[scriptHash] = result.CompiledAssembly; } else if (Settings.CacheScripts) assembly = CompileCache[scriptHash]; } //Run the compiled assembly with exception handling if (run) { try { object compiledScript = assembly.CreateInstance("ScriptLoader.Script"); return compiledScript .GetType() .GetMethod("__run") .Invoke(compiledScript, new object[] { new CSharpAPI(apiHandler, tickHandler, localVars), args }); } 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 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 { /// /// Thread blocking utility for stopping execution when making a ChatBot API call /// private ManualResetEvent tickHandler; /// /// Holds local variables passed along with the script /// private 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, ManualResetEvent tickHandler, Dictionary localVars) { SetMaster(apiHandler); this.tickHandler = tickHandler; 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) { base.SendText(text is string ? (string)text : text.ToString()); tickHandler.WaitOne(); return true; } /// /// 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) { if (localVars == null) localVars = this.localVars; bool result = base.PerformInternalCommand(command, localVars); tickHandler.WaitOne(); return result; } /// /// 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) { if (extraAttempts == -999999) base.ReconnectToTheServer(); else base.ReconnectToTheServer(extraAttempts); tickHandler.WaitOne(); } /// /// Disconnect from the server and exit the program /// new public void DisconnectAndExit() { base.DisconnectAndExit(); tickHandler.WaitOne(); } /// /// Load the provided ChatBot object /// /// Bot to load new public void LoadBot(ChatBot bot) { base.LoadBot(bot); tickHandler.WaitOne(); } /// /// 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]; return Settings.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 Settings.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) return (T)value; if (value != null) { try { TypeConverter converter = TypeDescriptor.GetConverter(typeof(T)); if (converter != null) return (T)converter.ConvertFromString(value.ToString()); } catch (NotSupportedException) { /* Was worth trying */ } } return default(T); } //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 = Settings.SetAccount(accountAlias); if (result && andReconnect) ReconnectToTheServer(); 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 = Settings.SetServerIP(server); if (result && andReconnect) ReconnectToTheServer(); 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) { string[] lines = null; ChatBots.Script.LookForScript(ref script); try { lines = File.ReadAllLines(script, Encoding.UTF8); } catch (Exception e) { throw new CSharpException(CSErrorType.FileReadError, e); } return CSharpRunner.Run(this, tickHandler, lines, args, localVars); } } }