bugfix: Fixed inventory sync and Script Scheduler dupliating instances of bots on reconnects

bugfix: Fixed inventory sync and Script Scheduler dupliating instances of bots on reconnects
This commit is contained in:
Anon 2026-06-06 13:14:29 +02:00 committed by GitHub
commit 861084db9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 251 additions and 66 deletions

View file

@ -25,6 +25,7 @@ namespace MinecraftClient.ChatBots
private bool csharp;
private Thread? thread;
private readonly Dictionary<string, object>? localVars;
private readonly string? scriptOwnerKey;
public Script(string filename)
{
@ -38,6 +39,13 @@ namespace MinecraftClient.ChatBots
this.localVars = localVars;
}
internal Script(string filename, string? ownername, Dictionary<string, object>? localVars, string? scriptOwnerKey)
: this(filename, ownername, localVars)
{
this.scriptOwnerKey = scriptOwnerKey;
SetScriptOwnerKey(scriptOwnerKey);
}
private void ParseArguments(string argstr)
{
List<string> args = new();
@ -166,7 +174,7 @@ namespace MinecraftClient.ChatBots
{
try
{
CSharpRunner.Run(this, lines, args, localVars, scriptName: file!);
CSharpRunner.Run(this, lines, args, localVars, scriptName: file!, scriptOwnerKey: scriptOwnerKey);
}
catch (CSharpException e)
{

View file

@ -183,64 +183,54 @@ namespace MinecraftClient.ChatBots
private int verifytasks_timeleft = Settings.ClientTicksPerSecond;
private readonly int verifytasks_delay = Settings.ClientTicksPerSecond;
public override void AfterGameJoined()
{
if (serverlogin_done)
return;
serverlogin_done = true;
verifytasks_timeleft = verifytasks_delay;
RunLoginTasks();
}
public override void Update()
{
if (!serverlogin_done)
return;
if (verifytasks_timeleft <= 0)
{
verifytasks_timeleft = verifytasks_delay;
if (serverlogin_done)
for (int taskIndex = 0; taskIndex < Config.TaskList.Length; taskIndex++)
{
foreach (TaskConfig task in Config.TaskList)
TaskConfig task = Config.TaskList[taskIndex];
if (task.Trigger_On_Times.Enable)
{
if (task.Trigger_On_Times.Enable)
{
bool matching_time_found = false;
bool matching_time_found = false;
foreach (TimeSpan time in task.Trigger_On_Times.Times)
foreach (TimeSpan time in task.Trigger_On_Times.Times)
{
if (time.Hours == DateTime.Now.Hour && time.Minutes == DateTime.Now.Minute)
{
if (time.Hours == DateTime.Now.Hour && time.Minutes == DateTime.Now.Minute)
matching_time_found = true;
if (!task.Trigger_On_Time_Already_Triggered)
{
matching_time_found = true;
if (!task.Trigger_On_Time_Already_Triggered)
{
task.Trigger_On_Time_Already_Triggered = true;
LogDebugToConsole(string.Format(Translations.bot_scriptScheduler_running_time, task.Action));
CmdResult response = new();
PerformInternalCommand(task.Action, ref response);
if (response.status != CmdResult.Status.Done || !string.IsNullOrWhiteSpace(response.result))
LogToConsole(response);
}
task.Trigger_On_Time_Already_Triggered = true;
RunTaskAction(task, taskIndex, string.Format(Translations.bot_scriptScheduler_running_time, task.Action));
}
}
if (!matching_time_found)
task.Trigger_On_Time_Already_Triggered = false;
}
if (!matching_time_found)
task.Trigger_On_Time_Already_Triggered = false;
}
}
else
{
foreach (TaskConfig task in Config.TaskList)
{
if (task.Trigger_On_Login || (firstlogin_done == false && task.Trigger_On_First_Login))
{
LogDebugToConsole(string.Format(Translations.bot_scriptScheduler_running_login, task.Action));
CmdResult response = new();
PerformInternalCommand(task.Action, ref response);
if (response.status != CmdResult.Status.Done || !string.IsNullOrWhiteSpace(response.result))
LogToConsole(response);
}
}
firstlogin_done = true;
serverlogin_done = true;
}
}
else verifytasks_timeleft--;
foreach (TaskConfig task in Config.TaskList)
for (int taskIndex = 0; taskIndex < Config.TaskList.Length; taskIndex++)
{
TaskConfig task = Config.TaskList[taskIndex];
if (task.Trigger_On_Interval.Enable)
{
if (task.Trigger_On_Interval_Countdown == 0)
@ -248,11 +238,7 @@ namespace MinecraftClient.ChatBots
task.Trigger_On_Interval_Countdown = random.Next(
Settings.DoubleToTick(task.Trigger_On_Interval.MinTime), Settings.DoubleToTick(task.Trigger_On_Interval.MaxTime)
);
LogDebugToConsole(string.Format(Translations.bot_scriptScheduler_running_inverval, task.Action));
CmdResult response = new();
PerformInternalCommand(task.Action, ref response);
if (response.status != CmdResult.Status.Done || !string.IsNullOrWhiteSpace(response.result))
LogToConsole(response);
RunTaskAction(task, taskIndex, string.Format(Translations.bot_scriptScheduler_running_inverval, task.Action));
}
else task.Trigger_On_Interval_Countdown--;
}
@ -265,6 +251,58 @@ namespace MinecraftClient.ChatBots
return false;
}
private void RunLoginTasks()
{
bool isFirstLogin = !firstlogin_done;
for (int taskIndex = 0; taskIndex < Config.TaskList.Length; taskIndex++)
{
TaskConfig task = Config.TaskList[taskIndex];
if (task.Trigger_On_Login || (isFirstLogin && task.Trigger_On_First_Login))
RunTaskAction(task, taskIndex, string.Format(Translations.bot_scriptScheduler_running_login, task.Action));
}
firstlogin_done = true;
}
private void RunTaskAction(TaskConfig task, int taskIndex, string debugMessage)
{
LogDebugToConsole(debugMessage);
if (TryRunOwnedScript(task, taskIndex))
return;
CmdResult response = new();
PerformInternalCommand(task.Action, ref response);
if (response.status != CmdResult.Status.Done || !string.IsNullOrWhiteSpace(response.result))
LogToConsole(response);
}
private bool TryRunOwnedScript(TaskConfig task, int taskIndex)
{
string action = task.Action.Trim();
const string scriptCommand = "script";
if (!action.StartsWith(scriptCommand, StringComparison.OrdinalIgnoreCase))
return false;
if (action.Length == scriptCommand.Length || !char.IsWhiteSpace(action[scriptCommand.Length]))
return false;
string scriptArgs = action[scriptCommand.Length..].Trim();
if (string.IsNullOrWhiteSpace(scriptArgs))
return false;
string scriptOwnerKey = BuildScriptOwnerKey(task, taskIndex);
Handler.UnloadBotsByScriptOwnerKey(scriptOwnerKey);
Handler.BotLoad(new Script(scriptArgs, null, null, scriptOwnerKey));
return true;
}
private static string BuildScriptOwnerKey(TaskConfig task, int taskIndex)
{
return $"{nameof(ScriptScheduler)}:{taskIndex}:{task.Task_Name}:{task.Action.Trim()}";
}
private static string Task2String(TaskConfig task)
{
return string.Format(

View file

@ -54,6 +54,7 @@ namespace MinecraftClient
private readonly List<ChatBot> bots = new();
private static readonly List<ChatBot> botsOnHold = new();
private static readonly Dictionary<int, Container> inventories = new();
private static readonly HashSet<int> inventoriesWithFullContents = new();
private readonly Dictionary<string, RecipeBookRecipeEntry> unlockedRecipes = new(StringComparer.Ordinal);
private readonly Dictionary<string, Achievement> achievements = new(StringComparer.Ordinal);
private string? activeAdvancementTab;
@ -866,8 +867,11 @@ namespace MinecraftClient
DispatchBotEvent(bot => bot.OnDisconnect(ChatBot.DisconnectReason.UserLogout, ""));
foreach (ChatBot bot in bots.Where(bot => bot.ScriptOwnerKey is not null).ToList())
BotUnLoad(bot);
botsOnHold.Clear();
botsOnHold.AddRange(bots);
botsOnHold.AddRange(bots.Where(bot => bot.ScriptOwnerKey is null));
if (handler is not null)
{
@ -1262,7 +1266,7 @@ namespace MinecraftClient
bots.Add(b);
if (init)
DispatchBotEvent(bot => bot.Initialize(), [b]);
if (handler is not null)
if (CanSendMessage)
DispatchBotEvent(bot => bot.AfterGameJoined(), [b]);
}
@ -1290,6 +1294,21 @@ namespace MinecraftClient
}
}
internal void UnloadBotsByScriptOwnerKey(string scriptOwnerKey)
{
if (InvokeRequired)
{
InvokeOnMainThread(() => UnloadBotsByScriptOwnerKey(scriptOwnerKey));
return;
}
foreach (ChatBot bot in GetLoadedChatBots())
{
if (bot.ScriptOwnerKey == scriptOwnerKey)
BotUnLoad(bot);
}
}
/// <summary>
/// Clear bots
/// </summary>
@ -2004,8 +2023,8 @@ namespace MinecraftClient
if (item.Count <= spaceLeft)
{
// Can fit into the stack
item.Count = 0;
curItem.Count += item.Count;
item.Count = 0;
changedSlots.Add(new Tuple<short, Item?>((short)curId, curItem));
changedSlots.Add(new Tuple<short, Item?>((short)slotId, null));
@ -2062,6 +2081,24 @@ namespace MinecraftClient
};
}
private static bool TryGetMirroredPlayerInventoryRange(Container inventory, out int firstWindowSlot, out int lastWindowSlot)
{
firstWindowSlot = -1;
lastWindowSlot = -1;
if (inventory.Type is ContainerType.PlayerInventory or ContainerType.Unknown)
return false;
const int mirroredPlayerInventorySlotCount = 36;
int slotCount = inventory.Type.SlotCount();
if (slotCount <= mirroredPlayerInventorySlotCount)
return false;
firstWindowSlot = slotCount - mirroredPlayerInventorySlotCount;
lastWindowSlot = slotCount - 1;
return true;
}
private static bool AreSameInventorySlot(Item? left, Item? right)
{
if (left is null || left.IsEmpty)
@ -2081,18 +2118,71 @@ namespace MinecraftClient
if (!inventories.TryGetValue(0, out Container? playerInventory))
return false;
if (item is null || item.IsEmpty)
return playerInventory.Items.Remove(playerInventorySlot);
Item itemClone = item.CloneWithCount(item.Count);
playerInventory.Items.TryGetValue(playerInventorySlot, out Item? previousItem);
if (AreSameInventorySlot(previousItem, item))
if (AreSameInventorySlot(previousItem, itemClone))
return false;
if (item is null || item.IsEmpty)
playerInventory.Items.Remove(playerInventorySlot);
else
playerInventory.Items[playerInventorySlot] = item;
playerInventory.Items[playerInventorySlot] = itemClone;
return true;
}
private bool SyncPlayerInventorySlotsFromWindow(Container? inventory)
{
if (inventory is null)
return false;
if (!inventoriesWithFullContents.Contains(inventory.ID))
return false;
if (!TryGetMirroredPlayerInventoryRange(inventory, out int firstWindowSlot, out int lastWindowSlot))
return false;
if (!inventories.TryGetValue(0, out Container? playerInventory))
return false;
const int firstPlayerInventorySlot = 9;
const int lastPlayerInventorySlot = firstPlayerInventorySlot + 36 - 1;
Dictionary<int, Item> mirroredItems = new();
for (int windowSlot = firstWindowSlot; windowSlot <= lastWindowSlot; windowSlot++)
{
if (!inventory.Items.TryGetValue(windowSlot, out Item? item) || item.IsEmpty)
continue;
int playerInventorySlot = windowSlot - firstWindowSlot + firstPlayerInventorySlot;
mirroredItems[playerInventorySlot] = item.CloneWithCount(item.Count);
}
bool changed = false;
for (int playerInventorySlot = firstPlayerInventorySlot; playerInventorySlot <= lastPlayerInventorySlot; playerInventorySlot++)
{
playerInventory.Items.TryGetValue(playerInventorySlot, out Item? previousItem);
mirroredItems.TryGetValue(playerInventorySlot, out Item? mirroredItem);
if (AreSameInventorySlot(previousItem, mirroredItem))
continue;
changed = true;
break;
}
if (!changed)
return false;
for (int playerInventorySlot = firstPlayerInventorySlot; playerInventorySlot <= lastPlayerInventorySlot; playerInventorySlot++)
playerInventory.Items.Remove(playerInventorySlot);
foreach ((int playerInventorySlot, Item item) in mirroredItems)
playerInventory.Items[playerInventorySlot] = item;
return changed;
}
/// <summary>
/// Click a slot in the specified window
/// </summary>
@ -2157,6 +2247,10 @@ namespace MinecraftClient
playerInventory.Items.Remove(-1);
}
// Clean up cursor item if count reached zero
if (playerInventory.Items.TryGetValue(-1, out Item? cursorAfterLeft) && cursorAfterLeft.IsEmpty)
playerInventory.Items.Remove(-1);
if (inventory.Items.ContainsKey(slotId))
changedSlots.Add(new Tuple<short, Item?>((short)slotId, inventory.Items[slotId]));
else
@ -2213,6 +2307,10 @@ namespace MinecraftClient
inventory.Items[slotId] = itemClone;
playerInventory.Items[-1].Count--;
}
// Clean up cursor item if count reached zero
if (playerInventory.Items.TryGetValue(-1, out Item? cursorItem) && cursorItem.IsEmpty)
playerInventory.Items.Remove(-1);
}
else
{
@ -2798,6 +2896,11 @@ namespace MinecraftClient
changedSlots.Add(new Tuple<short, Item?>((short)slotId, inventory.Items[slotId]));
}
}
if (item!.Count <= 0 && inventory.Items.ContainsKey(slotId))
{
inventory.Items.Remove(slotId);
changedSlots.Add(new Tuple<short, Item?>((short)slotId, null));
}
}
break;
case WindowActionType.DropItem:
@ -2821,6 +2924,8 @@ namespace MinecraftClient
}
}
SyncPlayerInventorySlotsFromWindow(inventory);
return handler.SendWindowAction(windowId, slotId, action, item, changedSlots, inventories[windowId].StateID);
}
@ -2871,7 +2976,10 @@ namespace MinecraftClient
if (inventories.ContainsKey(windowId))
{
if (windowId != 0)
{
inventories.Remove(windowId);
inventoriesWithFullContents.Remove(windowId);
}
bool result = handler.SendCloseWindow(windowId);
DispatchBotEvent(bot => bot.OnInventoryClose(windowId));
return result;
@ -2892,6 +3000,7 @@ namespace MinecraftClient
return InvokeOnMainThread<bool>(ClearInventories);
inventories.Clear();
inventoriesWithFullContents.Clear();
inventories[0] = new Container(0, ContainerType.PlayerInventory, "Player Inventory");
ClearUnlockedRecipes();
return true;
@ -3704,6 +3813,7 @@ namespace MinecraftClient
/// <param name="inventoryID">Inventory ID</param>
public void OnInventoryOpen(int inventoryID, Container inventory)
{
inventoriesWithFullContents.Remove(inventoryID);
inventories[inventoryID] = inventory;
if (inventoryID != 0)
@ -3730,9 +3840,15 @@ namespace MinecraftClient
if (inventories.ContainsKey(inventoryID))
{
if (inventoryID == 0)
{
inventories[0].Items.Clear(); // Don't delete player inventory
inventoriesWithFullContents.Clear();
}
else
{
inventories.Remove(inventoryID);
inventoriesWithFullContents.Remove(inventoryID);
}
}
if (inventoryID != 0)
@ -3858,8 +3974,16 @@ namespace MinecraftClient
{
if (inventories.ContainsKey(inventoryID))
{
// Filter out empty items (Count=0 or Air) that some servers may send
foreach (int key in itemList.Where(slot => slot.Value.IsEmpty).Select(slot => slot.Key).ToList())
itemList.Remove(key);
inventories[inventoryID].Items = itemList;
inventories[inventoryID].StateID = stateId;
inventoriesWithFullContents.Add(inventoryID);
bool playerInventoryChanged = SyncPlayerInventorySlotsFromWindow(inventories[inventoryID]);
if (playerInventoryChanged)
DispatchBotEvent(bot => bot.OnInventoryUpdate(0));
DispatchBotEvent(bot => bot.OnInventoryUpdate(inventoryID));
}
}
@ -3884,7 +4008,7 @@ namespace MinecraftClient
inventoryID = 0; // Prevent key not found for some bots relied to this event
if (inventories.ContainsKey(0))
{
if (item is not null)
if (item is not null && !item.IsEmpty)
inventories[0].Items[-1] = item;
else
inventories[0].Items.Remove(-1);
@ -3900,6 +4024,9 @@ namespace MinecraftClient
inventories[inventoryID].Items.Remove(slotID);
}
else inventories[inventoryID].Items[slotID] = item;
if (SyncPlayerInventorySlotsFromWindow(inventories[inventoryID]))
DispatchBotEvent(bot => bot.OnInventoryUpdate(0));
}
}
DispatchBotEvent(bot => bot.OnInventoryUpdate(inventoryID));

View file

@ -4135,7 +4135,7 @@ namespace MinecraftClient {
}
/// <summary>
/// Looks up a localized string similar to Shift clicking slot {0} in window #{1}.
/// Looks up a localized string similar to Shift.
/// </summary>
internal static string cmd_inventory_shiftclick {
get {
@ -4153,7 +4153,7 @@ namespace MinecraftClient {
}
/// <summary>
/// Looks up a localized string similar to Shift right-clicking slot {0} in window #{1}.
/// Looks up a localized string similar to Shift right.
/// </summary>
internal static string cmd_inventory_shiftrightclick {
get {

View file

@ -1457,7 +1457,7 @@ Note that parameters in '[]' are optional.</value>
<value>Right</value>
</data>
<data name="cmd.inventory.shiftclick" xml:space="preserve">
<value>Shift clicking slot {0} in window #{1}</value>
<value>Shift</value>
</data>
<data name="cmd.inventory.shiftclick_fail" xml:space="preserve">
<value>Shift click failed, this may be because this container type is not supported</value>
@ -2329,7 +2329,7 @@ Logging in...</value>
<value>Minimum number that you have provided is bigger than the maximum, swapping them around!</value>
</data>
<data name="cmd.inventory.shiftrightclick" xml:space="preserve">
<value>Shift right-clicking slot {0} in window #{1}</value>
<value>Shift right</value>
</data>
<data name="mcc.avaliable_profiles" xml:space="preserve">
<value>Avaliable profiles:</value>

View file

@ -26,7 +26,7 @@ namespace MinecraftClient.Scripting
/// <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")
public static object? Run(ChatBot apiHandler, string[] lines, string[] args, Dictionary<string, object>? localVars, bool run = true, string scriptName = "Unknown Script", string? scriptOwnerKey = null)
{
//Script compatibility check for handling future versions differently
if (lines.Length < 1 || lines[0] != "//MCCScript 1.0")
@ -143,7 +143,7 @@ namespace MinecraftClient.Scripting
{
try
{
var compiled = runner.Execute(assembly!, args, localVars, apiHandler);
var compiled = runner.Execute(assembly!, args, localVars, apiHandler, scriptOwnerKey);
return compiled;
}
catch (Exception e) { throw new CSharpException(CSErrorType.RuntimeError, e); }
@ -209,10 +209,11 @@ namespace MinecraftClient.Scripting
/// <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)
public CSharpAPI(ChatBot apiHandler, Dictionary<string, object>? localVars, string? scriptOwnerKey = null)
{
SetMaster(apiHandler);
this.localVars = localVars;
SetScriptOwnerKey(scriptOwnerKey);
}
/// <summary>

View file

@ -39,9 +39,20 @@ namespace MinecraftClient.Scripting
//Handler will be automatically set on bot loading, don't worry about this
public void SetHandler(McClient handler) { _handler = handler; }
protected void SetMaster(ChatBot master) { this.master = master; }
protected void LoadBot(ChatBot bot) { Handler.BotUnLoad(bot); Handler.BotLoad(bot); }
protected void LoadBot(ChatBot bot)
{
if (ScriptOwnerKey is not null)
bot.SetScriptOwnerKey(ScriptOwnerKey);
if (Handler.GetLoadedChatBots().Any(loadedBot => ReferenceEquals(loadedBot, bot)))
Handler.BotUnLoad(bot);
Handler.BotLoad(bot);
}
protected List<ChatBot> GetLoadedChatBots() { return Handler.GetLoadedChatBots(); }
protected void UnLoadBot(ChatBot bot) { Handler.BotUnLoad(bot); }
internal string? ScriptOwnerKey { get; private set; }
internal void SetScriptOwnerKey(string? scriptOwnerKey) { ScriptOwnerKey = scriptOwnerKey; }
private McClient? _handler = null;
private ChatBot? master = null;
private readonly List<string> registeredPluginChannels = new();

View file

@ -13,9 +13,9 @@ namespace MinecraftClient.Scripting.DynamicRun.Builder
{
internal class CompileRunner
{
public object? Execute(byte[] compiledAssembly, string[] args, Dictionary<string, object>? localVars, ChatBot apiHandler)
public object? Execute(byte[] compiledAssembly, string[] args, Dictionary<string, object>? localVars, ChatBot apiHandler, string? scriptOwnerKey = null)
{
var assemblyLoadContextWeakRef = LoadAndExecute(compiledAssembly, args, localVars, apiHandler);
var assemblyLoadContextWeakRef = LoadAndExecute(compiledAssembly, args, localVars, apiHandler, scriptOwnerKey);
for (var i = 0; i < 8 && assemblyLoadContextWeakRef.Item1.IsAlive; i++)
{
@ -28,18 +28,18 @@ namespace MinecraftClient.Scripting.DynamicRun.Builder
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static Tuple<WeakReference, object?> LoadAndExecute(byte[] compiledAssembly, string[] args, Dictionary<string, object>? localVars, ChatBot apiHandler)
private static Tuple<WeakReference, object?> LoadAndExecute(byte[] compiledAssembly, string[] args, Dictionary<string, object>? localVars, ChatBot apiHandler, string? scriptOwnerKey)
{
using var asm = new MemoryStream(compiledAssembly);
var assemblyLoadContext = new SimpleUnloadableAssemblyLoadContext();
var assembly = assemblyLoadContext.LoadFromStream(asm);
var compiledScript = assembly.CreateInstance("ScriptLoader.Script")!;
var execResult = compiledScript.GetType().GetMethod("__run")!.Invoke(compiledScript, new object[] { new CSharpAPI(apiHandler, localVars), args });
var execResult = compiledScript.GetType().GetMethod("__run")!.Invoke(compiledScript, new object[] { new CSharpAPI(apiHandler, localVars, scriptOwnerKey), args });
assemblyLoadContext.Unload();
return new(new WeakReference(assemblyLoadContext), execResult);
}
}
}
}