diff --git a/MinecraftClient/CSharpRunner.cs b/MinecraftClient/CSharpRunner.cs
new file mode 100644
index 00000000..46add112
--- /dev/null
+++ b/MinecraftClient/CSharpRunner.cs
@@ -0,0 +1,372 @@
+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;
+
+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
+ /// 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, 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("The provided script does not have a valid MCCScript header"));
+
+ //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();
+ foreach (string line in lines)
+ {
+ 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.Linq;",
+ "using System.Text;",
+ "using System.IO;",
+ "using System.Threading;",
+ "using MinecraftClient;",
+ "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;
+ 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
+ = CompileCache[scriptHash].CreateInstance("ScriptLoader.Script");
+ return
+ compiledScript
+ .GetType()
+ .GetMethod("__run")
+ .Invoke(compiledScript,
+ new object[] { new CSharpAPI(apiHandler, tickHandler), 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;
+
+ ///
+ /// Create a new C# API Wrapper
+ ///
+ /// ChatBot API Handler
+ /// ChatBot tick handler
+ public CSharpAPI(ChatBot apiHandler, ManualResetEvent tickHandler)
+ {
+ SetMaster(apiHandler);
+ this.tickHandler = tickHandler;
+ }
+
+ /* == 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 the text was sent with no error
+ new public bool SendText(object text)
+ {
+ bool result = base.SendText(text is string ? (string)text : text.ToString());
+ tickHandler.WaitOne();
+ Thread.Sleep(1000);
+ return result;
+ }
+
+ ///
+ /// Perform an internal MCC command (not a server command, use SendText() instead for that!)
+ ///
+ /// The command to process
+ /// TRUE if the command was indeed an internal MCC command
+ new public bool PerformInternalCommand(string command)
+ {
+ bool result = base.PerformInternalCommand(command);
+ 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
+ new public void ReconnectToTheServer(int extraAttempts = -999999)
+ {
+ 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();
+ }
+
+ /* == 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)
+ {
+ return Settings.GetVar(varName);
+ }
+
+ ///
+ /// Get a global variable by name, as a string
+ ///
+ /// Name of the variable
+ /// Value of the variable as string, or null if no variable
+ public string GetVarAsString(string varName)
+ {
+ object val = GetVar(varName);
+ if (val != null)
+ return val.ToString();
+ return null;
+ }
+
+ ///
+ /// Get a global variable by name, as an integer
+ ///
+ /// Name of the variable
+ /// Value of the variable as int, or 0 if no variable or not a number
+ public int GetVarAsInt(string varName)
+ {
+ if (GetVar(varName) is int)
+ return (int)GetVar(varName);
+ int result;
+ if (int.TryParse(GetVarAsString(varName), out result))
+ return result;
+ return 0;
+ }
+
+ ///
+ /// Get a global variable by name, as a boolean
+ ///
+ /// Name of the variable
+ /// Value of the variable as bool, or false if no variable or not a boolean
+ public bool GetVarAsBool(string varName)
+ {
+ if (GetVar(varName) is bool)
+ return (bool)GetVar(varName);
+ bool result;
+ if (bool.TryParse(GetVarAsString(varName), out result))
+ return result;
+ return false;
+ }
+
+ ///
+ /// 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)
+ {
+ return Settings.SetVar(varName, varValue);
+ }
+
+ ///
+ /// 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); }
+ catch (Exception e) { throw new CSharpException(CSErrorType.FileReadError, e); }
+ return CSharpRunner.Run(this, tickHandler, lines, args);
+ }
+ }
+}
diff --git a/MinecraftClient/ChatBot.cs b/MinecraftClient/ChatBot.cs
index b8c71706..4c0a5c09 100644
--- a/MinecraftClient/ChatBot.cs
+++ b/MinecraftClient/ChatBot.cs
@@ -85,12 +85,10 @@ namespace MinecraftClient
/// Text to send to the server
/// True if the text was sent with no error
- protected bool SendText(object text)
+ protected bool SendText(string text)
{
LogToConsole("Sending '" + text + "'");
- bool result = Handler.SendText(text is string ? (string)text : text.ToString());
- Thread.Sleep(1000);
- return result;
+ return Handler.SendText(text);
}
///
@@ -348,7 +346,7 @@ namespace MinecraftClient
///
/// Log text to write
- public void LogToConsole(object text)
+ protected void LogToConsole(object text)
{
ConsoleIO.WriteLogLine(String.Format("[{0}] {1}", this.GetType().Name, text));
string logfile = Settings.ExpandVars(Settings.chatbotLogFile);
@@ -458,59 +456,5 @@ namespace MinecraftClient
return new string[0];
}
}
-
- ///
- /// Set a custom %variable% which will be available through expandVars()
- ///
- /// Name of the variable
- /// Value of the variable
- /// True if the parameters were valid
-
- protected static bool SetVar(string varName, object varData)
- {
- return Settings.SetVar(varName, varData.ToString());
- }
-
- ///
- /// Get a custom %variable% or null if the variable does not exist
- ///
- /// Variable name
- /// The value or null if the variable does not exists
-
- protected static string GetVar(string varName)
- {
- return Settings.GetVar(varName);
- }
-
- ///
- /// Get a custom %variable% as an Integer or null if the variable does not exist
- ///
- /// Variable name
- /// The value or null if the variable does not exists
-
- protected static int GetVarAsInt(string varName)
- {
- return Settings.str2int(Settings.GetVar(varName));
- }
-
- ///
- /// Load login/password using an account alias
- ///
- /// True if the account was found and loaded
-
- protected static bool SetAccount(string accountAlias)
- {
- return Settings.SetAccount(accountAlias);
- }
-
- ///
- /// Load server information in ServerIP and ServerPort variables from a "serverip:port" couple or server alias
- ///
- /// True if the server IP was valid and loaded, false otherwise
-
- protected static bool SetServerIP(string server)
- {
- return Settings.SetServerIP(server);
- }
}
}
diff --git a/MinecraftClient/ChatBots/Script.cs b/MinecraftClient/ChatBots/Script.cs
index 0e1894d9..4e464f1a 100644
--- a/MinecraftClient/ChatBots/Script.cs
+++ b/MinecraftClient/ChatBots/Script.cs
@@ -5,6 +5,7 @@ using System.Text;
using System.Threading;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
+using System.Reflection;
namespace MinecraftClient.ChatBots
{
@@ -143,8 +144,18 @@ namespace MinecraftClient.ChatBots
tpause = new ManualResetEvent(false);
thread = new Thread(() =>
{
- if (!RunCSharpScript() && owner != null)
- SendPrivateMessage(owner, "Script '" + file + "' failed to run.");
+ try
+ {
+ CSharpRunner.Run(this, tpause, lines, args);
+ }
+ catch (CSharpException e)
+ {
+ string errorMessage = "Script '" + file + "' failed to run (" + e.ExceptionType + ").";
+ LogToConsole(errorMessage);
+ if (owner != null)
+ SendPrivateMessage(owner, errorMessage);
+ LogToConsole(e.InnerException);
+ }
});
thread.Start();
}
@@ -154,7 +165,7 @@ namespace MinecraftClient.ChatBots
{
tpause.Set();
tpause.Reset();
- if (thread.Join(100))
+ if (!thread.IsAlive)
UnloadBot();
}
}
@@ -205,84 +216,5 @@ namespace MinecraftClient.ChatBots
}
}
}
-
- private bool RunCSharpScript()
- {
- //Script compatibility check for handling future versions differently
- if (lines.Length < 1 || lines[0] != "//MCCScript 1.0")
- {
- LogToConsole("Script file '" + file + "' does not start with a valid //MCCScript identifier.");
- return false;
- }
-
- //Process different sections of the script file
- bool scriptMain = true;
- List script = new List();
- List extensions = new List();
- foreach (string line in lines)
- {
- if (line.StartsWith("//MCCScript"))
- {
- if (line.EndsWith("Extensions"))
- scriptMain = false;
- }
- else if (scriptMain)
- {
- script.Add(line);
- //Add breakpoints for step-by-step execution of the script
- if (tpause != null && line.Trim().EndsWith(";"))
- script.Add("tpause.WaitOne();");
- }
- else extensions.Add(line);
- }
-
- //Generate a ChatBot class, allowing access to the ChatBot API
- string code = String.Join("\n", new string[]
- {
- "using System;",
- "using System.IO;",
- "using System.Threading;",
- "using MinecraftClient;",
- "namespace ScriptLoader {",
- "public class Script : ChatBot {",
- "public void __run(ChatBot master, ManualResetEvent tpause, string[] args) {",
- "SetMaster(master);",
- 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;
- CompilerResults result
- = compiler.CompileAssemblyFromSource(parameters, code);
-
- //Process compile warnings and errors
- if (result.Errors.Count > 0)
- {
- LogToConsole("Error loading '" + file + "':\n" + result.Errors[0].ErrorText);
- return false;
- }
-
- //Run the compiled script with exception handling
- object compiledScript = result.CompiledAssembly.CreateInstance("ScriptLoader.Script");
- try { compiledScript.GetType().GetMethod("__run").Invoke(compiledScript, new object[] { this, tpause, args }); }
- catch (Exception e)
- {
- LogToConsole("Runtime error for '" + file + "':\n" + e);
- return false;
- }
-
- return true;
- }
}
}
diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj
index bc276c9e..9e1fc0c7 100644
--- a/MinecraftClient/MinecraftClient.csproj
+++ b/MinecraftClient/MinecraftClient.csproj
@@ -113,6 +113,7 @@
+
diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs
index 0985287e..40a51cb1 100644
--- a/MinecraftClient/Settings.cs
+++ b/MinecraftClient/Settings.cs
@@ -45,6 +45,7 @@ namespace MinecraftClient
public static char internalCmdChar = '/';
public static bool playerHeadAsIcon = false;
public static string chatbotLogFile = "";
+ public static bool CacheScripts = true;
//AntiAFK Settings
public static bool AntiAFK_Enabled = false;
@@ -94,7 +95,7 @@ namespace MinecraftClient
public static string AutoRespond_Matches = "matches.ini";
//Custom app variables and Minecraft accounts
- private static readonly Dictionary AppVars = new Dictionary();
+ private static readonly Dictionary AppVars = new Dictionary();
private static readonly Dictionary> Accounts = new Dictionary>();
private static readonly Dictionary> Servers = new Dictionary>();
@@ -159,6 +160,7 @@ namespace MinecraftClient
case "chatbotlogfile": chatbotLogFile = argValue; break;
case "mcversion": ServerVersion = argValue; break;
case "splitmessagedelay": splitMessageDelay = TimeSpan.FromSeconds(str2int(argValue)); break;
+ case "scriptcache": CacheScripts = str2bool(argValue); break;
case "botowners":
Bots_Owners.Clear();
@@ -366,6 +368,7 @@ namespace MinecraftClient
+ "serverlist=servers.txt\r\n"
+ "playerheadicon=true\r\n"
+ "exitonfailure=false\r\n"
+ + "scriptcache=true\r\n"
+ "timestamps=false\r\n"
+ "\r\n"
+ "[AppVars]\r\n"
@@ -491,7 +494,7 @@ namespace MinecraftClient
/// Value of the variable
/// True if the parameters were valid
- public static bool SetVar(string varName, string varData)
+ public static bool SetVar(string varName, object varData)
{
lock (AppVars)
{
@@ -511,7 +514,7 @@ namespace MinecraftClient
/// Variable name
/// The value or null if the variable does not exists
- public static string GetVar(string varName)
+ public static object GetVar(string varName)
{
if (AppVars.ContainsKey(varName))
return AppVars[varName];
@@ -559,7 +562,7 @@ namespace MinecraftClient
default:
if (AppVars.ContainsKey(varname_lower))
{
- result.Append(AppVars[varname_lower]);
+ result.Append(AppVars[varname_lower].ToString());
}
else result.Append("%" + varname + '%');
break;
diff --git a/MinecraftClient/config/sample-script-extended.cs b/MinecraftClient/config/sample-script-extended.cs
index 68ff8cd1..b8751f0b 100644
--- a/MinecraftClient/config/sample-script-extended.cs
+++ b/MinecraftClient/config/sample-script-extended.cs
@@ -9,8 +9,8 @@ if (args.Length > 0)
for (int i = 0; i < 5; i++)
{
- int count = GetVarAsInt("test") + 1;
- SetVar("test", count);
+ int count = MCC.GetVarAsInt("test") + 1;
+ MCC.SetVar("test", count);
SendHelloWorld(count, text);
SleepBetweenSends();
}
@@ -21,15 +21,11 @@ for (int i = 0; i < 5; i++)
void SendHelloWorld(int count, string text)
{
- /* Warning: Do not make more than one server-related call into a method
- * defined as a script extension eg SendText or switching servers,
- * as execution flow is not managed in the Extensions section */
-
- SendText("Hello World no. " + count + ": " + text);
+ MCC.SendText("Hello World no. " + count + ": " + text);
}
void SleepBetweenSends()
{
- LogToConsole("Sleeping for 5 seconds...");
+ MCC.LogToConsole("Sleeping for 5 seconds...");
Thread.Sleep(5000);
}
\ No newline at end of file
diff --git a/MinecraftClient/config/sample-script-with-chatbot.cs b/MinecraftClient/config/sample-script-with-chatbot.cs
index 264711fa..25cb13eb 100644
--- a/MinecraftClient/config/sample-script-with-chatbot.cs
+++ b/MinecraftClient/config/sample-script-with-chatbot.cs
@@ -3,7 +3,7 @@
/* This is a sample script that will load a ChatBot into Minecraft Console Client
* Simply execute the script once with /script or the script scheduler to load the bot */
-LoadBot(new ExampleBot());
+MCC.LoadBot(new ExampleBot());
//MCCScript Extensions
diff --git a/MinecraftClient/config/sample-script.cs b/MinecraftClient/config/sample-script.cs
index 3d61b25c..34d3354a 100644
--- a/MinecraftClient/config/sample-script.cs
+++ b/MinecraftClient/config/sample-script.cs
@@ -2,13 +2,13 @@
/* This is a sample script for Minecraft Console Client
* The code provided in this file will be compiled at runtime and executed
- * Allowed instructions: Any C# code AND all methods provided by the bot API */
+ * Allowed instructions: Any C# code AND methods provided by the MCC API */
for (int i = 0; i < 5; i++)
{
- int count = GetVarAsInt("test") + 1;
- SetVar("test", count);
- SendText("Hello World no. " + count);
- LogToConsole("Sleeping for 5 seconds...");
+ int count = MCC.GetVarAsInt("test") + 1;
+ MCC.SetVar("test", count);
+ MCC.SendText("Hello World no. " + count);
+ MCC.LogToConsole("Sleeping for 5 seconds...");
Thread.Sleep(5000);
}
\ No newline at end of file