diff --git a/MinecraftClient/ChatBots/Script.cs b/MinecraftClient/ChatBots/Script.cs index e2bb6636..4f764aa4 100644 --- a/MinecraftClient/ChatBots/Script.cs +++ b/MinecraftClient/ChatBots/Script.cs @@ -25,6 +25,7 @@ namespace MinecraftClient.ChatBots private bool csharp; private Thread? thread; private readonly Dictionary? 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? localVars, string? scriptOwnerKey) + : this(filename, ownername, localVars) + { + this.scriptOwnerKey = scriptOwnerKey; + SetScriptOwnerKey(scriptOwnerKey); + } + private void ParseArguments(string argstr) { List 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) { diff --git a/MinecraftClient/ChatBots/ScriptScheduler.cs b/MinecraftClient/ChatBots/ScriptScheduler.cs index 8da875f9..f5af44ec 100644 --- a/MinecraftClient/ChatBots/ScriptScheduler.cs +++ b/MinecraftClient/ChatBots/ScriptScheduler.cs @@ -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( diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 7e7f0435..a0a0ca4d 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -54,6 +54,7 @@ namespace MinecraftClient private readonly List bots = new(); private static readonly List botsOnHold = new(); private static readonly Dictionary inventories = new(); + private static readonly HashSet inventoriesWithFullContents = new(); private readonly Dictionary unlockedRecipes = new(StringComparer.Ordinal); private readonly Dictionary 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); + } + } + /// /// Clear bots /// @@ -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)curId, curItem)); changedSlots.Add(new Tuple((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 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; + } + /// /// Click a slot in the specified window /// @@ -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)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)slotId, inventory.Items[slotId])); } } + if (item!.Count <= 0 && inventory.Items.ContainsKey(slotId)) + { + inventory.Items.Remove(slotId); + changedSlots.Add(new Tuple((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(ClearInventories); inventories.Clear(); + inventoriesWithFullContents.Clear(); inventories[0] = new Container(0, ContainerType.PlayerInventory, "Player Inventory"); ClearUnlockedRecipes(); return true; @@ -3704,6 +3813,7 @@ namespace MinecraftClient /// Inventory ID 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)); diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index 52ec91ae..2330a891 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -4135,7 +4135,7 @@ namespace MinecraftClient { } /// - /// Looks up a localized string similar to Shift clicking slot {0} in window #{1}. + /// Looks up a localized string similar to Shift. /// internal static string cmd_inventory_shiftclick { get { @@ -4153,7 +4153,7 @@ namespace MinecraftClient { } /// - /// Looks up a localized string similar to Shift right-clicking slot {0} in window #{1}. + /// Looks up a localized string similar to Shift right. /// internal static string cmd_inventory_shiftrightclick { get { diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index 6052e661..cb2dae94 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -1457,7 +1457,7 @@ Note that parameters in '[]' are optional. Right - Shift clicking slot {0} in window #{1} + Shift Shift click failed, this may be because this container type is not supported @@ -2329,7 +2329,7 @@ Logging in... Minimum number that you have provided is bigger than the maximum, swapping them around! - Shift right-clicking slot {0} in window #{1} + Shift right Avaliable profiles: diff --git a/MinecraftClient/Scripting/CSharpRunner.cs b/MinecraftClient/Scripting/CSharpRunner.cs index 158b6eae..668ab336 100644 --- a/MinecraftClient/Scripting/CSharpRunner.cs +++ b/MinecraftClient/Scripting/CSharpRunner.cs @@ -26,7 +26,7 @@ namespace MinecraftClient.Scripting /// 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, string[] lines, string[] args, Dictionary? localVars, bool run = true, string scriptName = "Unknown Script") + public static object? Run(ChatBot apiHandler, string[] lines, string[] args, Dictionary? 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 /// ChatBot API Handler /// ChatBot tick handler /// Local variables passed along with the script - public CSharpAPI(ChatBot apiHandler, Dictionary? localVars) + public CSharpAPI(ChatBot apiHandler, Dictionary? localVars, string? scriptOwnerKey = null) { SetMaster(apiHandler); this.localVars = localVars; + SetScriptOwnerKey(scriptOwnerKey); } /// diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index 3206092a..b8f21e35 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -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 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 registeredPluginChannels = new(); diff --git a/MinecraftClient/Scripting/DynamicRun/Builder/CompileRunner.cs b/MinecraftClient/Scripting/DynamicRun/Builder/CompileRunner.cs index ee5bf460..cdd102a1 100644 --- a/MinecraftClient/Scripting/DynamicRun/Builder/CompileRunner.cs +++ b/MinecraftClient/Scripting/DynamicRun/Builder/CompileRunner.cs @@ -13,9 +13,9 @@ namespace MinecraftClient.Scripting.DynamicRun.Builder { internal class CompileRunner { - public object? Execute(byte[] compiledAssembly, string[] args, Dictionary? localVars, ChatBot apiHandler) + public object? Execute(byte[] compiledAssembly, string[] args, Dictionary? 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 LoadAndExecute(byte[] compiledAssembly, string[] args, Dictionary? localVars, ChatBot apiHandler) + private static Tuple LoadAndExecute(byte[] compiledAssembly, string[] args, Dictionary? 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); } } -} \ No newline at end of file +}