mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2025-10-14 21:22:49 +00:00
* Update CI to detect the word "skipci" * Make script compilation errors more verbose Rather than just giving the line in which the error has been found, return the actual text content of the line itself * Attempt to bubble up errors in the script chain, so it says the reason for any NotRun errors. The exception message gets eaten up when the script is running, and an exception happens. Also put in a default result message for the CmdResult, instead of having it default to null. * Trim the whitespace off returned script compilation error line
409 lines
17 KiB
C#
409 lines
17 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// C# Script runner - Compile on-the-fly and run C# scripts
|
|
/// </summary>
|
|
class CSharpRunner
|
|
{
|
|
private static readonly Dictionary<ulong, byte[]> CompileCache = new();
|
|
|
|
/// <summary>
|
|
/// Run the specified C# script file
|
|
/// </summary>
|
|
/// <param name="apiHandler">ChatBot handler for accessing ChatBot API</param>
|
|
/// <param name="lines">Lines of the script file to run</param>
|
|
/// <param name="args">Arguments to pass to the script</param>
|
|
/// <param name="localVars">Local variables passed along with the script</param>
|
|
/// <param name="run">Set to false to compile and cache the script without launching it</param>
|
|
/// <exception cref="CSharpException">Thrown if an error occured</exception>
|
|
/// <returns>Result of the execution, returned by the script</returns>
|
|
public static object? Run(ChatBot apiHandler, string[] lines, string[] args, Dictionary<string, object>? 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<string> script = new();
|
|
List<string> extensions = new();
|
|
List<string> libs = new();
|
|
List<string> 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)
|
|
{
|
|
// Get the line that contains the error:
|
|
|
|
var loc = failure.Location.GetMappedLineSpan();
|
|
var line = code.Split('\n')[loc.StartLinePosition.Line];
|
|
|
|
ConsoleIO.WriteLogLine($"[Script] Error in {scriptName}, on line ({line.Trim()}): [{failure.Id}] {failure.GetMessage()}");
|
|
}
|
|
|
|
throw new CSharpException(CSErrorType.InvalidScript, new InvalidProgramException("Compilation failed due to error(s)."));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Quickly calculate a hash for the given script
|
|
/// </summary>
|
|
/// <param name="lines">script lines</param>
|
|
/// <returns>Quick hash as unsigned long</returns>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Describe a C# script error type
|
|
/// </summary>
|
|
public enum CSErrorType { FileReadError, InvalidScript, LoadError, RuntimeError };
|
|
|
|
/// <summary>
|
|
/// Describe a C# script error with associated error type
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents the C# API object accessible from C# Scripts
|
|
/// </summary>
|
|
public class CSharpAPI : ChatBot
|
|
{
|
|
/// <summary>
|
|
/// Holds local variables passed along with the script
|
|
/// </summary>
|
|
private readonly Dictionary<string, object>? localVars;
|
|
|
|
/// <summary>
|
|
/// Create a new C# API Wrapper
|
|
/// </summary>
|
|
/// <param name="apiHandler">ChatBot API Handler</param>
|
|
/// <param name="tickHandler">ChatBot tick handler</param>
|
|
/// <param name="localVars">Local variables passed along with the script</param>
|
|
public CSharpAPI(ChatBot apiHandler, Dictionary<string, object>? localVars)
|
|
{
|
|
SetMaster(apiHandler);
|
|
this.localVars = localVars;
|
|
}
|
|
|
|
/* == Wrappers for ChatBot API with public visibility and call limit to one per tick for safety == */
|
|
|
|
/// <summary>
|
|
/// Write some text in the console. Nothing will be sent to the server.
|
|
/// </summary>
|
|
/// <param name="text">Log text to write</param>
|
|
new public void LogToConsole(object text)
|
|
{
|
|
base.LogToConsole(text);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Send text to the server. Can be anything such as chat messages or commands
|
|
/// </summary>
|
|
/// <param name="text">Text to send to the server</param>
|
|
/// <returns>TRUE if successfully sent (Deprectated, always returns TRUE for compatibility purposes with existing scripts)</returns>
|
|
public bool SendText(object text)
|
|
{
|
|
return base.SendText(text is string str ? str : text.ToString() ?? string.Empty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Perform an internal MCC command (not a server command, use SendText() instead for that!)
|
|
/// </summary>
|
|
/// <param name="command">The command to process</param>
|
|
/// <param name="localVars">Local variables passed along with the internal command</param>
|
|
/// <returns>TRUE if the command was indeed an internal MCC command</returns>
|
|
new public bool PerformInternalCommand(string command, Dictionary<string, object>? localVars = null)
|
|
{
|
|
localVars ??= this.localVars;
|
|
return base.PerformInternalCommand(command, localVars);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disconnect from the server and restart the program
|
|
/// It will unload and reload all the bots and then reconnect to the server
|
|
/// </summary>
|
|
/// <param name="extraAttempts">If connection fails, the client will make X extra attempts</param>
|
|
/// <param name="delaySeconds">Optional delay, in seconds, before restarting</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disconnect from the server and exit the program
|
|
/// </summary>
|
|
new public void DisconnectAndExit()
|
|
{
|
|
base.DisconnectAndExit();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load the provided ChatBot object
|
|
/// </summary>
|
|
/// <param name="bot">Bot to load</param>
|
|
new public void LoadBot(ChatBot bot)
|
|
{
|
|
base.LoadBot(bot);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return the list of currently online players
|
|
/// </summary>
|
|
/// <returns>List of online players</returns>
|
|
new public string[] GetOnlinePlayers()
|
|
{
|
|
return base.GetOnlinePlayers();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a dictionary of online player names and their corresponding UUID
|
|
/// </summary>
|
|
/// <returns>
|
|
/// dictionary of online player whereby
|
|
/// UUID represents the key
|
|
/// playername represents the value</returns>
|
|
new public Dictionary<string, string> GetOnlinePlayersWithUUID()
|
|
{
|
|
return base.GetOnlinePlayersWithUUID();
|
|
}
|
|
|
|
/* == Additional Methods useful for Script API == */
|
|
|
|
/// <summary>
|
|
/// Get a global variable by name
|
|
/// </summary>
|
|
/// <param name="varName">Name of the variable</param>
|
|
/// <returns>Value of the variable or null if no variable</returns>
|
|
public object? GetVar(string varName)
|
|
{
|
|
if (localVars != null && localVars.ContainsKey(varName))
|
|
return localVars[varName];
|
|
else
|
|
return Config.AppVar.GetVar(varName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set a global variable for further use in any other script
|
|
/// </summary>
|
|
/// <param name="varName">Name of the variable</param>
|
|
/// <param name="varValue">Value of the variable</param>
|
|
public bool SetVar(string varName, object varValue)
|
|
{
|
|
if (localVars != null && localVars.ContainsKey(varName))
|
|
localVars.Remove(varName);
|
|
return Config.AppVar.SetVar(varName, varValue);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <typeparam name="T">Variable type</typeparam>
|
|
/// <param name="varName">Variable name</param>
|
|
/// <returns>Variable as specified type or default value for this type</returns>
|
|
public T? GetVar<T>(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<type>(varname)
|
|
public string? GetVarAsString(string varName) { return GetVar<string>(varName); }
|
|
public int GetVarAsInt(string varName) { return GetVar<int>(varName); }
|
|
public double GetVarAsDouble(string varName) { return GetVar<double>(varName); }
|
|
public bool GetVarAsBool(string varName) { return GetVar<bool>(varName); }
|
|
|
|
/// <summary>
|
|
/// Load login/password using an account alias and optionally reconnect to the server
|
|
/// </summary>
|
|
/// <param name="accountAlias">Account alias</param>
|
|
/// <param name="andReconnect">Set to true to reconnecto to the server afterwards</param>
|
|
/// <returns>True if the account was found and loaded</returns>
|
|
public bool SetAccount(string accountAlias, bool andReconnect = false)
|
|
{
|
|
bool result = Config.Main.Advanced.SetAccount(accountAlias);
|
|
if (result && andReconnect)
|
|
ReconnectToTheServer(keepAccountAndServerSettings: true);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load new server information and optionally reconnect to the server
|
|
/// </summary>
|
|
/// <param name="server">"serverip:port" couple or server alias</param>
|
|
/// <returns>True if the server IP was valid and loaded, false otherwise</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Synchronously call another script and retrieve the result
|
|
/// </summary>
|
|
/// <param name="script">Script to call</param>
|
|
/// <param name="args">Arguments to pass to the script</param>
|
|
/// <returns>An object returned by the script, or null</returns>
|
|
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);
|
|
}
|
|
}
|
|
}
|