Minecraft-Console-Client/MinecraftClient/ChatBots/Script.cs
copilot-swe-agent[bot] 8757533fb9
Fix Script bots accumulating on reconnect by unloading on disconnect
Script ChatBots were persisted in the static botsOnHold list across
reconnects. When ScriptScheduler triggered new scripts on login, the
old Script bots were still running, causing duplicate commands to be
sent simultaneously. This led to command spam and crashes.

Adding OnDisconnect to the Script ChatBot ensures scripts are removed
from the bots list before they can be saved to botsOnHold, preventing
accumulation of duplicate script instances on reconnect.
2026-06-05 06:29:43 +00:00

275 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;
using MinecraftClient.CommandHandler;
using MinecraftClient.Scripting;
namespace MinecraftClient.ChatBots
{
/// <summary>
/// Runs a list of commands
/// </summary>
public class Script : ChatBot
{
private string? file;
private string[] lines = Array.Empty<string>();
private string[] args = Array.Empty<string>();
private int sleepticks = Settings.ClientTicksPerSecond;
private int nextline = 0;
private readonly string? owner;
private bool csharp;
private Thread? thread;
private readonly Dictionary<string, object>? localVars;
public Script(string filename)
{
ParseArguments(filename);
}
public Script(string filename, string? ownername, Dictionary<string, object>? localVars)
: this(filename)
{
owner = ownername;
this.localVars = localVars;
}
private void ParseArguments(string argstr)
{
List<string> args = new();
StringBuilder str = new();
bool escape = false;
bool quotes = false;
foreach (char c in argstr)
{
if (escape)
{
if (c != '"')
str.Append('\\');
str.Append(c);
escape = false;
}
else
{
if (c == '\\')
escape = true;
else if (c == '"')
quotes = !quotes;
else if (c == ' ' && !quotes)
{
if (str.Length > 0)
args.Add(str.ToString());
str.Clear();
}
else str.Append(c);
}
}
if (str.Length > 0)
args.Add(str.ToString());
if (args.Count > 0)
{
file = args[0];
args.RemoveAt(0);
this.args = args.ToArray();
}
else file = "";
}
public static bool LookForScript(ref string filename)
{
//Automatically look in subfolders and try to add ".txt" file extension
char dir_slash = Path.DirectorySeparatorChar;
string[] files = new string[]
{
filename,
filename + ".txt",
filename + ".cs",
"scripts" + dir_slash + filename,
"scripts" + dir_slash + filename + ".txt",
"scripts" + dir_slash + filename + ".cs",
"config" + dir_slash + filename,
"config" + dir_slash + filename + ".txt",
"config" + dir_slash + filename + ".cs",
};
foreach (string possible_file in files)
{
if (System.IO.File.Exists(possible_file))
{
filename = possible_file;
return true;
}
}
if (Settings.Config.Logging.DebugMessages)
{
string caller = "Script";
try
{
StackFrame frame = new(1);
MethodBase method = frame.GetMethod()!;
Type type = method.DeclaringType!;
caller = type.Name;
}
catch { }
ConsoleIO.WriteLineFormatted(string.Format(Translations.bot_script_not_found, caller, filename));
}
return false;
}
public override void Initialize()
{
//Load the given file from the startup parameters
if (LookForScript(ref file!))
{
lines = System.IO.File.ReadAllLines(file, Encoding.UTF8);
csharp = file.EndsWith(".cs");
thread = null;
if (!String.IsNullOrEmpty(owner))
SendPrivateMessage(owner, string.Format(Translations.bot_script_pm_loaded, file));
}
else
{
LogToConsole(string.Format(Translations.bot_script_file_not_found, System.IO.Path.GetFullPath(file)));
if (!String.IsNullOrEmpty(owner))
SendPrivateMessage(owner, string.Format(Translations.bot_script_file_not_found, file));
UnloadBot(); //No need to keep the bot active
}
}
public override bool OnDisconnect(DisconnectReason reason, string message)
{
UnloadBot();
return false;
}
public override void Update()
{
if (csharp) //C# compiled script
{
//Initialize thread on first update
if (thread is null)
{
thread = new Thread(() =>
{
try
{
CSharpRunner.Run(this, lines, args, localVars, scriptName: file!);
}
catch (CSharpException e)
{
string errorMessage = string.Format(Translations.bot_script_fail, file, e.ExceptionType);
LogToConsole(errorMessage);
if (owner is not null)
SendPrivateMessage(owner, errorMessage);
LogToConsole(e.InnerException);
}
})
{
Name = "MCC Script - " + file
};
thread.Start();
}
//Unload bot once the thread has finished running
if (thread is not null && !thread.IsAlive)
{
UnloadBot();
}
}
else //Classic MCC script interpreter
{
if (sleepticks > 0) { sleepticks--; }
else
{
if (nextline < lines.Length) //Is there an instruction left to interpret?
{
string instruction_line = lines[nextline].Trim(); // Removes all whitespaces at start and end of current line
nextline++; //Move the cursor so that the next time the following line will be interpreted
if (instruction_line.Length > 1)
{
if (instruction_line[0] != '#' && instruction_line[0] != '/' && instruction_line[1] != '/')
{
instruction_line = Settings.Config.AppVar.ExpandVars(instruction_line, localVars);
string instruction_name = instruction_line.Split(' ')[0];
switch (instruction_name.ToLower())
{
case "wait":
int ticks = Settings.ClientTicksPerSecond;
try
{
if (instruction_line[5..].Contains("to", StringComparison.OrdinalIgnoreCase) ||
instruction_line[5..].Contains("-"))
{
var processedLine = instruction_line.Replace("wait", "")
.Trim()
.ToLower();
processedLine = string.Join("", processedLine.Split(default(string[]), StringSplitOptions.RemoveEmptyEntries));
var parts = processedLine.Contains("to") ? processedLine.Split("to") : processedLine.Split("-");
if (parts.Length == 2)
{
var min = Convert.ToInt32(parts[0]);
var max = Convert.ToInt32(parts[1]);
if (min > max)
{
(min, max) = (max, min);
LogToConsole(Translations.cmd_wait_random_min_bigger);
}
ticks = new Random().Next(min, max);
}
else ticks = Convert.ToInt32(instruction_line[5..]);
}
else ticks = Convert.ToInt32(instruction_line[5..]);
}
catch { }
sleepticks = ticks;
break;
default:
CmdResult response = new();
if (PerformInternalCommand(instruction_line, ref response))
{
if (instruction_name.ToLower() != "log")
{
LogToConsole(instruction_line);
}
if (response.status != CmdResult.Status.Done || !string.IsNullOrWhiteSpace(response.result))
{
LogToConsole(response);
}
}
else
{
Update(); //Unknown command : process next line immediately
}
break;
}
}
else { Update(); } //Comment: process next line immediately
}
}
else
{
//No more instructions to interpret
UnloadBot();
}
}
}
}
}
}