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
/// 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.Text.RegularExpressions;",
"using System.Linq;",
"using System.Text;",
"using System.IO;",
"using System.Threading;",
"using MinecraftClient;",
"using MinecraftClient.Mapping;",
"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 = assembly.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
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
/// 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();
}
/* == 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);
}
///
/// 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);
}
///
/// 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); }
catch (Exception e) { throw new CSharpException(CSErrorType.FileReadError, e); }
return CSharpRunner.Run(this, tickHandler, lines, args);
}
}
}