diff --git a/MinecraftClient/CSharpRunner.cs b/MinecraftClient/CSharpRunner.cs index 64f77ca1..3e7ce558 100644 --- a/MinecraftClient/CSharpRunner.cs +++ b/MinecraftClient/CSharpRunner.cs @@ -22,14 +22,13 @@ namespace MinecraftClient /// 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 /// Local variables passed along with 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, Dictionary localVars, bool run = true) + public static object Run(ChatBot apiHandler, string[] lines, string[] args, Dictionary localVars, bool run = true) { //Script compatibility check for handling future versions differently if (lines.Length < 1 || lines[0] != "//MCCScript 1.0") @@ -140,7 +139,7 @@ namespace MinecraftClient .GetType() .GetMethod("__run") .Invoke(compiledScript, - new object[] { new CSharpAPI(apiHandler, tickHandler, localVars), args }); + new object[] { new CSharpAPI(apiHandler, localVars), args }); } catch (Exception e) { throw new CSharpException(CSErrorType.RuntimeError, e); } } @@ -195,11 +194,6 @@ namespace MinecraftClient /// public class CSharpAPI : ChatBot { - /// - /// Thread blocking utility for stopping execution when making a ChatBot API call - /// - private ManualResetEvent tickHandler; - /// /// Holds local variables passed along with the script /// @@ -211,10 +205,9 @@ namespace MinecraftClient /// ChatBot API Handler /// ChatBot tick handler /// Local variables passed along with the script - public CSharpAPI(ChatBot apiHandler, ManualResetEvent tickHandler, Dictionary localVars) + public CSharpAPI(ChatBot apiHandler, Dictionary localVars) { SetMaster(apiHandler); - this.tickHandler = tickHandler; this.localVars = localVars; } @@ -237,7 +230,6 @@ namespace MinecraftClient public bool SendText(object text) { base.SendText(text is string ? (string)text : text.ToString()); - tickHandler.WaitOne(); return true; } @@ -252,7 +244,6 @@ namespace MinecraftClient if (localVars == null) localVars = this.localVars; bool result = base.PerformInternalCommand(command, localVars); - tickHandler.WaitOne(); return result; } @@ -267,7 +258,6 @@ namespace MinecraftClient if (extraAttempts == -999999) base.ReconnectToTheServer(); else base.ReconnectToTheServer(extraAttempts); - tickHandler.WaitOne(); } /// @@ -276,7 +266,6 @@ namespace MinecraftClient new public void DisconnectAndExit() { base.DisconnectAndExit(); - tickHandler.WaitOne(); } /// @@ -286,7 +275,6 @@ namespace MinecraftClient new public void LoadBot(ChatBot bot) { base.LoadBot(bot); - tickHandler.WaitOne(); } /// @@ -406,7 +394,7 @@ namespace MinecraftClient ChatBots.Script.LookForScript(ref script); try { lines = File.ReadAllLines(script, Encoding.UTF8); } catch (Exception e) { throw new CSharpException(CSErrorType.FileReadError, e); } - return CSharpRunner.Run(this, tickHandler, lines, args, localVars); + return CSharpRunner.Run(this, lines, args, localVars); } } } diff --git a/MinecraftClient/ChatBot.cs b/MinecraftClient/ChatBot.cs index e15bd771..59c10a76 100644 --- a/MinecraftClient/ChatBot.cs +++ b/MinecraftClient/ChatBot.cs @@ -42,7 +42,7 @@ namespace MinecraftClient private List registeredPluginChannels = new List(); private List registeredCommands = new List(); private object delayTasksLock = new object(); - private List delayTasks = new List(); + private List delayedTasks = new List(); private McClient Handler { get @@ -65,14 +65,14 @@ namespace MinecraftClient { lock (delayTasksLock) { - if (delayTasks.Count > 0) + if (delayedTasks.Count > 0) { List tasksToRemove = new List(); - for (int i = 0; i < delayTasks.Count; i++) + for (int i = 0; i < delayedTasks.Count; i++) { - if (delayTasks[i].Tick()) + if (delayedTasks[i].Tick()) { - delayTasks[i].Task.DynamicInvoke(); + delayedTasks[i].Task(); tasksToRemove.Add(i); } } @@ -81,7 +81,7 @@ namespace MinecraftClient tasksToRemove.Sort((a, b) => b.CompareTo(a)); // descending sort foreach (int index in tasksToRemove) { - delayTasks.RemoveAt(index); + delayedTasks.RemoveAt(index); } } } @@ -1390,46 +1390,61 @@ namespace MinecraftClient } /// - /// Schedule a task to run on main thread. Returned value cannot be retrieved + /// Invoke a task on the main thread, wait for completion and retrieve return value. + /// + /// Task to run with any type or return value + /// Any result returned from task, result type is inferred from the task + /// bool result = InvokeOnMainThread(methodThatReturnsAbool); + /// bool result = InvokeOnMainThread(() => methodThatReturnsAbool(argument)); + /// int result = InvokeOnMainThread(() => { yourCode(); return 42; }); + /// Type of the return value + protected T InvokeOnMainThread(Func task) + { + return Handler.InvokeOnMainThread(task); + } + + /// + /// Invoke a task on the main thread and wait for completion + /// + /// Task to run without return value + /// InvokeOnMainThread(methodThatReturnsNothing); + /// InvokeOnMainThread(() => methodThatReturnsNothing(argument)); + /// InvokeOnMainThread(() => { yourCode(); }); + protected void InvokeOnMainThread(Action task) + { + Handler.InvokeOnMainThread(task); + } + + /// + /// Schedule a task to run on the main thread, and do not wait for completion /// /// Task to run /// Run the task after X ticks (1 tick delay = ~100ms). 0 for no delay /// - /// // Delay ~10 seconds - /// ScheduleTaskDelayed(new Action(() => - /// { - /// /** Your code here **/ - /// Console.WriteLine("10 seconds has passed"); - /// }), 100); + /// InvokeOnMainThread(methodThatReturnsNothing, 10); + /// InvokeOnMainThread(() => methodThatReturnsNothing(argument), 10); + /// InvokeOnMainThread(() => { yourCode(); }, 10); /// - // TODO: Adapt to new IMinecraftComHandler API - /*protected void ScheduleTaskDelayed(Delegate task, int delayTicks = 0) + protected void ScheduleOnMainThread(Action task, int delayTicks = 0) { - if (delayTicks <= 0) + lock (delayTasksLock) { - // Immediately schedule to run on next update - Handler.InvokeOnMainThread(task); + delayedTasks.Add(new TaskWithDelay(task, delayTicks)); } - else - { - lock (delayTasksLock) - { - delayTasks.Add(new DelayedTask(task, delayTicks)); - } - } - }*/ + } /// - /// Schedule a task to run on main thread. + /// Schedule a task to run on the main thread, and do not wait for completion /// /// Task to run - /// Any value returned from the task - // TODO: Adapt to new IMinecraftComHandler API - /* - protected object ScheduleTask(Delegate task) + /// Run the task after the specified delay + protected void ScheduleOnMainThread(Action task, TimeSpan delay) { - return Handler.InvokeOnMainThread(task); - }*/ + lock (delayTasksLock) + { + delayedTasks.Add(new TaskWithDelay(task, delay)); + } + } /// /// Command runner definition. @@ -1475,35 +1490,5 @@ namespace MinecraftClient this.Runner = callback; } } - - private class DelayedTask - { - private Delegate task; - private int Counter; - - public Delegate Task { get { return task; } } - - public DelayedTask(Delegate task) - : this(task, 0) - { } - - public DelayedTask(Delegate task, int delayTicks) - { - this.task = task; - Counter = delayTicks; - } - - /// - /// Tick the counter - /// - /// Return true if counted to zero - public bool Tick() - { - Counter--; - if (Counter <= 0) - return true; - return false; - } - } } } diff --git a/MinecraftClient/ChatBots/Script.cs b/MinecraftClient/ChatBots/Script.cs index a86f49a1..c6c6c2c8 100644 --- a/MinecraftClient/ChatBots/Script.cs +++ b/MinecraftClient/ChatBots/Script.cs @@ -24,7 +24,6 @@ namespace MinecraftClient.ChatBots private string owner; private bool csharp; private Thread thread; - private ManualResetEvent tpause; private Dictionary localVars; public Script(string filename) @@ -157,12 +156,11 @@ namespace MinecraftClient.ChatBots //Initialize thread on first update if (thread == null) { - tpause = new ManualResetEvent(false); thread = new Thread(() => { try { - CSharpRunner.Run(this, tpause, lines, args, localVars); + CSharpRunner.Run(this, lines, args, localVars); } catch (CSharpException e) { @@ -173,16 +171,14 @@ namespace MinecraftClient.ChatBots LogToConsole(e.InnerException); } }); + thread.Name = "MCC Script - " + file; thread.Start(); } - //Let the thread run for a short span of time - if (thread != null) + //Unload bot once the thread has finished running + if (thread != null && !thread.IsAlive) { - tpause.Set(); - tpause.Reset(); - if (!thread.IsAlive) - UnloadBot(); + UnloadBot(); } } else //Classic MCC script interpreter diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index d825c95d..6e778828 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -764,13 +764,16 @@ namespace MinecraftClient /// public void BotLoad(ChatBot b, bool init = true) { - b.SetHandler(this); - bots.Add(b); - if (init) - DispatchBotEvent(bot => bot.Initialize(), new ChatBot[] { b }); - if (this.handler != null) - DispatchBotEvent(bot => bot.AfterGameJoined(), new ChatBot[] { b }); - Settings.SingleCommand = ""; + InvokeOnMainThread(() => + { + b.SetHandler(this); + bots.Add(b); + if (init) + DispatchBotEvent(bot => bot.Initialize(), new ChatBot[] { b }); + if (this.handler != null) + DispatchBotEvent(bot => bot.AfterGameJoined(), new ChatBot[] { b }); + Settings.SingleCommand = ""; + }); } /// @@ -778,14 +781,17 @@ namespace MinecraftClient /// public void BotUnLoad(ChatBot b) { - bots.RemoveAll(item => object.ReferenceEquals(item, b)); - - // ToList is needed to avoid an InvalidOperationException from modfiying the list while it's being iterated upon. - var botRegistrations = registeredBotPluginChannels.Where(entry => entry.Value.Contains(b)).ToList(); - foreach (var entry in botRegistrations) + InvokeOnMainThread(() => { - UnregisterPluginChannel(entry.Key, b); - } + bots.RemoveAll(item => object.ReferenceEquals(item, b)); + + // ToList is needed to avoid an InvalidOperationException from modfiying the list while it's being iterated upon. + var botRegistrations = registeredBotPluginChannels.Where(entry => entry.Value.Contains(b)).ToList(); + foreach (var entry in botRegistrations) + { + UnregisterPluginChannel(entry.Key, b); + } + }); } /// @@ -793,7 +799,7 @@ namespace MinecraftClient /// public void BotClear() { - bots.Clear(); + InvokeOnMainThread(bots.Clear); } /// @@ -812,57 +818,6 @@ namespace MinecraftClient return inventoryHandlingEnabled; } - /// - /// Enable or disable Terrain and Movements. - /// Please note that Enabling will be deferred until next relog, respawn or world change. - /// - /// Enabled - /// TRUE if the setting was applied immediately, FALSE if delayed. - public bool SetTerrainEnabled(bool enabled) - { - if (enabled) - { - if (!terrainAndMovementsEnabled) - { - terrainAndMovementsRequested = true; - return false; - } - } - else - { - terrainAndMovementsEnabled = false; - terrainAndMovementsRequested = false; - locationReceived = false; - world.Clear(); - } - return true; - } - - /// - /// Enable or disable Inventories. - /// Please note that Enabling will be deferred until next relog. - /// - /// Enabled - /// TRUE if the setting was applied immediately, FALSE if delayed. - public bool SetInventoryEnabled(bool enabled) - { - if (enabled) - { - if (!inventoryHandlingEnabled) - { - inventoryHandlingRequested = true; - return false; - } - } - else - { - inventoryHandlingEnabled = false; - inventoryHandlingRequested = false; - inventories.Clear(); - } - return true; - } - /// /// Get entity handling status /// @@ -873,6 +828,63 @@ namespace MinecraftClient return entityHandlingEnabled; } + /// + /// Enable or disable Terrain and Movements. + /// Please note that Enabling will be deferred until next relog, respawn or world change. + /// + /// Enabled + /// TRUE if the setting was applied immediately, FALSE if delayed. + public bool SetTerrainEnabled(bool enabled) + { + return InvokeOnMainThread(() => + { + if (enabled) + { + if (!terrainAndMovementsEnabled) + { + terrainAndMovementsRequested = true; + return false; + } + } + else + { + terrainAndMovementsEnabled = false; + terrainAndMovementsRequested = false; + locationReceived = false; + world.Clear(); + } + return true; + }); + } + + /// + /// Enable or disable Inventories. + /// Please note that Enabling will be deferred until next relog. + /// + /// Enabled + /// TRUE if the setting was applied immediately, FALSE if delayed. + public bool SetInventoryEnabled(bool enabled) + { + return InvokeOnMainThread(() => + { + if (enabled) + { + if (!inventoryHandlingEnabled) + { + inventoryHandlingRequested = true; + return false; + } + } + else + { + inventoryHandlingEnabled = false; + inventoryHandlingRequested = false; + inventories.Clear(); + } + return true; + }); + } + /// /// Enable or disable Entity handling. /// Please note that Enabling will be deferred until next relog. @@ -881,23 +893,26 @@ namespace MinecraftClient /// TRUE if the setting was applied immediately, FALSE if delayed. public bool SetEntityHandlingEnabled(bool enabled) { - if (!enabled) + return InvokeOnMainThread(() => { - if (entityHandlingEnabled) + if (!enabled) { - entityHandlingEnabled = false; - return true; + if (entityHandlingEnabled) + { + entityHandlingEnabled = false; + return true; + } + else + { + return false; + } } else { + // Entity Handling cannot be enabled in runtime (or after joining server) return false; } - } - else - { - // Entity Handling cannot be enabled in runtime (or after joining server) - return false; - } + }); } /// @@ -909,7 +924,7 @@ namespace MinecraftClient /// public void SetNetworkPacketCaptureEnabled(bool enabled) { - networkPacketCaptureEnabled = enabled; + InvokeOnMainThread(() => { networkPacketCaptureEnabled = enabled; }); } #endregion @@ -959,9 +974,12 @@ namespace MinecraftClient /// Item Dictionary indexed by Slot ID (Check wiki.vg for slot ID) public Container GetInventory(int inventoryID) { - if (inventories.ContainsKey(inventoryID)) - return inventories[inventoryID]; - return null; + return InvokeOnMainThread(() => + { + if (inventories.ContainsKey(inventoryID)) + return inventories[inventoryID]; + return null; + }); } /// @@ -1075,7 +1093,7 @@ namespace MinecraftClient /// True if packet successfully sent public bool SendRespawnPacket() { - return handler.SendRespawnPacket(); + return InvokeOnMainThread(handler.SendRespawnPacket); } /// @@ -1085,17 +1103,20 @@ namespace MinecraftClient /// The bot to register the channel for. public void RegisterPluginChannel(string channel, ChatBot bot) { - if (registeredBotPluginChannels.ContainsKey(channel)) + InvokeOnMainThread(() => { - registeredBotPluginChannels[channel].Add(bot); - } - else - { - List bots = new List(); - bots.Add(bot); - registeredBotPluginChannels[channel] = bots; - SendPluginChannelMessage("REGISTER", Encoding.UTF8.GetBytes(channel), true); - } + if (registeredBotPluginChannels.ContainsKey(channel)) + { + registeredBotPluginChannels[channel].Add(bot); + } + else + { + List bots = new List(); + bots.Add(bot); + registeredBotPluginChannels[channel] = bots; + SendPluginChannelMessage("REGISTER", Encoding.UTF8.GetBytes(channel), true); + } + }); } /// @@ -1105,16 +1126,19 @@ namespace MinecraftClient /// The bot to unregister the channel for. public void UnregisterPluginChannel(string channel, ChatBot bot) { - if (registeredBotPluginChannels.ContainsKey(channel)) + InvokeOnMainThread(() => { - List registeredBots = registeredBotPluginChannels[channel]; - registeredBots.RemoveAll(item => object.ReferenceEquals(item, bot)); - if (registeredBots.Count == 0) + if (registeredBotPluginChannels.ContainsKey(channel)) { - registeredBotPluginChannels.Remove(channel); - SendPluginChannelMessage("UNREGISTER", Encoding.UTF8.GetBytes(channel), true); + List registeredBots = registeredBotPluginChannels[channel]; + registeredBots.RemoveAll(item => object.ReferenceEquals(item, bot)); + if (registeredBots.Count == 0) + { + registeredBotPluginChannels.Remove(channel); + SendPluginChannelMessage("UNREGISTER", Encoding.UTF8.GetBytes(channel), true); + } } - } + }); } /// @@ -1127,18 +1151,21 @@ namespace MinecraftClient /// Whether the packet was sent: true if it was sent, false if there was a connection error or it wasn't registered. public bool SendPluginChannelMessage(string channel, byte[] data, bool sendEvenIfNotRegistered = false) { - if (!sendEvenIfNotRegistered) + return InvokeOnMainThread(() => { - if (!registeredBotPluginChannels.ContainsKey(channel)) + if (!sendEvenIfNotRegistered) { - return false; + if (!registeredBotPluginChannels.ContainsKey(channel)) + { + return false; + } + if (!registeredServerPluginChannels.Contains(channel)) + { + return false; + } } - if (!registeredServerPluginChannels.Contains(channel)) - { - return false; - } - } - return handler.SendPluginChannelPacket(channel, data); + return handler.SendPluginChannelPacket(channel, data); + }); } /// @@ -1147,7 +1174,7 @@ namespace MinecraftClient /// TRUE if the item was successfully used public bool SendEntityAction(EntityActionType entityAction) { - return handler.SendEntityAction(playerEntityID, (int)entityAction); + return InvokeOnMainThread(() => handler.SendEntityAction(playerEntityID, (int)entityAction)); } /// @@ -1156,7 +1183,7 @@ namespace MinecraftClient /// TRUE if the item was successfully used public bool UseItemOnHand() { - return handler.SendUseItem(0); + return InvokeOnMainThread(() => handler.SendUseItem(0)); } /// @@ -1165,287 +1192,290 @@ namespace MinecraftClient /// TRUE if the slot was successfully clicked public bool DoWindowAction(int windowId, int slotId, WindowActionType action) { - Item item = null; - if (inventories.ContainsKey(windowId) && inventories[windowId].Items.ContainsKey(slotId)) - item = inventories[windowId].Items[slotId]; - - // Inventory update must be after sending packet - bool result = handler.SendWindowAction(windowId, slotId, action, item); - - // Update our inventory base on action type - var inventory = GetInventory(windowId); - var playerInventory = GetInventory(0); - if (inventory != null) + return InvokeOnMainThread(() => { - switch (action) - { - case WindowActionType.LeftClick: - // Check if cursor have item (slot -1) - if (playerInventory.Items.ContainsKey(-1)) - { - // When item on cursor and clicking slot 0, nothing will happen - if (slotId == 0) break; + Item item = null; + if (inventories.ContainsKey(windowId) && inventories[windowId].Items.ContainsKey(slotId)) + item = inventories[windowId].Items[slotId]; - // Check target slot also have item? - if (inventory.Items.ContainsKey(slotId)) + // Inventory update must be after sending packet + bool result = handler.SendWindowAction(windowId, slotId, action, item); + + // Update our inventory base on action type + var inventory = GetInventory(windowId); + var playerInventory = GetInventory(0); + if (inventory != null) + { + switch (action) + { + case WindowActionType.LeftClick: + // Check if cursor have item (slot -1) + if (playerInventory.Items.ContainsKey(-1)) { - // Check if both item are the same? - if (inventory.Items[slotId].Type == playerInventory.Items[-1].Type) + // When item on cursor and clicking slot 0, nothing will happen + if (slotId == 0) break; + + // Check target slot also have item? + if (inventory.Items.ContainsKey(slotId)) { - int maxCount = inventory.Items[slotId].Type.StackCount(); - // Check item stacking - if ((inventory.Items[slotId].Count + playerInventory.Items[-1].Count) <= maxCount) + // Check if both item are the same? + if (inventory.Items[slotId].Type == playerInventory.Items[-1].Type) { - // Put cursor item to target - inventory.Items[slotId].Count += playerInventory.Items[-1].Count; - playerInventory.Items.Remove(-1); + int maxCount = inventory.Items[slotId].Type.StackCount(); + // Check item stacking + if ((inventory.Items[slotId].Count + playerInventory.Items[-1].Count) <= maxCount) + { + // Put cursor item to target + inventory.Items[slotId].Count += playerInventory.Items[-1].Count; + playerInventory.Items.Remove(-1); + } + else + { + // Leave some item on cursor + playerInventory.Items[-1].Count -= (maxCount - inventory.Items[slotId].Count); + inventory.Items[slotId].Count = maxCount; + } } else { - // Leave some item on cursor - playerInventory.Items[-1].Count -= (maxCount - inventory.Items[slotId].Count); - inventory.Items[slotId].Count = maxCount; + // Swap two items + var itemTmp = playerInventory.Items[-1]; + playerInventory.Items[-1] = inventory.Items[slotId]; + inventory.Items[slotId] = itemTmp; } } else { - // Swap two items - var itemTmp = playerInventory.Items[-1]; - playerInventory.Items[-1] = inventory.Items[slotId]; - inventory.Items[slotId] = itemTmp; + // Put cursor item to target + inventory.Items[slotId] = playerInventory.Items[-1]; + playerInventory.Items.Remove(-1); } } else { - // Put cursor item to target - inventory.Items[slotId] = playerInventory.Items[-1]; - playerInventory.Items.Remove(-1); - } - } - else - { - // Check target slot have item? - if (inventory.Items.ContainsKey(slotId)) - { - // When taking item from slot 0, server will update us - if (slotId == 0) break; + // Check target slot have item? + if (inventory.Items.ContainsKey(slotId)) + { + // When taking item from slot 0, server will update us + if (slotId == 0) break; - // Put target slot item to cursor - playerInventory.Items[-1] = inventory.Items[slotId]; - inventory.Items.Remove(slotId); - } - } - break; - case WindowActionType.RightClick: - // Check if cursor have item (slot -1) - if (playerInventory.Items.ContainsKey(-1)) - { - // When item on cursor and clicking slot 0, nothing will happen - if (slotId == 0) break; - - // Check target slot have item? - if (inventory.Items.ContainsKey(slotId)) - { - // Check if both item are the same? - if (inventory.Items[slotId].Type == playerInventory.Items[-1].Type) - { - // Check item stacking - if (inventory.Items[slotId].Count < inventory.Items[slotId].Type.StackCount()) - { - // Drop 1 item count from cursor - playerInventory.Items[-1].Count--; - inventory.Items[slotId].Count++; - } - } - else - { - // Swap two items - var itemTmp = playerInventory.Items[-1]; - playerInventory.Items[-1] = inventory.Items[slotId]; - inventory.Items[slotId] = itemTmp; - } - } - else - { - // Drop 1 item count from cursor - var itemTmp = playerInventory.Items[-1]; - var itemClone = new Item(itemTmp.Type, 1, itemTmp.NBT); - inventory.Items[slotId] = itemClone; - playerInventory.Items[-1].Count--; - } - } - else - { - // Check target slot have item? - if (inventory.Items.ContainsKey(slotId)) - { - if (slotId == 0) - { - // no matter how many item in slot 0, only 1 will be taken out - // Also server will update us - break; - } - if (inventory.Items[slotId].Count == 1) - { - // Only 1 item count. Put it to cursor + // Put target slot item to cursor playerInventory.Items[-1] = inventory.Items[slotId]; inventory.Items.Remove(slotId); } - else + } + break; + case WindowActionType.RightClick: + // Check if cursor have item (slot -1) + if (playerInventory.Items.ContainsKey(-1)) + { + // When item on cursor and clicking slot 0, nothing will happen + if (slotId == 0) break; + + // Check target slot have item? + if (inventory.Items.ContainsKey(slotId)) { - // Take half of the item stack to cursor - if (inventory.Items[slotId].Count % 2 == 0) + // Check if both item are the same? + if (inventory.Items[slotId].Type == playerInventory.Items[-1].Type) { - // Can be evenly divided - Item itemTmp = inventory.Items[slotId]; - playerInventory.Items[-1] = new Item(itemTmp.Type, itemTmp.Count / 2, itemTmp.NBT); - inventory.Items[slotId].Count = itemTmp.Count / 2; + // Check item stacking + if (inventory.Items[slotId].Count < inventory.Items[slotId].Type.StackCount()) + { + // Drop 1 item count from cursor + playerInventory.Items[-1].Count--; + inventory.Items[slotId].Count++; + } } else { - // Cannot be evenly divided. item count on cursor is always larger than item on inventory - Item itemTmp = inventory.Items[slotId]; - playerInventory.Items[-1] = new Item(itemTmp.Type, (itemTmp.Count + 1) / 2, itemTmp.NBT); - inventory.Items[slotId].Count = (itemTmp.Count - 1) / 2; + // Swap two items + var itemTmp = playerInventory.Items[-1]; + playerInventory.Items[-1] = inventory.Items[slotId]; + inventory.Items[slotId] = itemTmp; } } - } - } - break; - case WindowActionType.ShiftClick: - if (slotId == 0) break; - if (inventory.Items.ContainsKey(slotId)) - { - /* Target slot have item */ - - int upperStartSlot = 9; - int upperEndSlot = 35; - - switch (inventory.Type) - { - case ContainerType.PlayerInventory: - upperStartSlot = 9; - upperEndSlot = 35; - break; - case ContainerType.Crafting: - upperStartSlot = 1; - upperEndSlot = 9; - break; - // TODO: Define more container type here - } - - // Cursor have item or not doesn't matter - // If hotbar already have same item, will put on it first until every stack are full - // If no more same item , will put on the first empty slot (smaller slot id) - // If inventory full, item will not move - if (slotId <= upperEndSlot) - { - // Clicked slot is on upper side inventory, put it to hotbar - // Now try to find same item and put on them - var itemsClone = playerInventory.Items.ToDictionary(entry => entry.Key, entry => entry.Value); - foreach (KeyValuePair _item in itemsClone) + else { - if (_item.Key <= upperEndSlot) continue; - - int maxCount = _item.Value.Type.StackCount(); - if (_item.Value.Type == inventory.Items[slotId].Type && _item.Value.Count < maxCount) - { - // Put item on that stack - int spaceLeft = maxCount - _item.Value.Count; - if (inventory.Items[slotId].Count <= spaceLeft) - { - // Can fit into the stack - inventory.Items[_item.Key].Count += inventory.Items[slotId].Count; - inventory.Items.Remove(slotId); - } - else - { - inventory.Items[slotId].Count -= spaceLeft; - inventory.Items[_item.Key].Count = inventory.Items[_item.Key].Type.StackCount(); - } - } - } - if (inventory.Items[slotId].Count > 0) - { - int[] emptySlots = inventory.GetEmpytSlots(); - int emptySlot = -2; - foreach (int slot in emptySlots) - { - if (slot <= upperEndSlot) continue; - emptySlot = slot; - break; - } - if (emptySlot != -2) - { - var itemTmp = inventory.Items[slotId]; - inventory.Items[emptySlot] = new Item(itemTmp.Type, itemTmp.Count, itemTmp.NBT); - inventory.Items.Remove(slotId); - } + // Drop 1 item count from cursor + var itemTmp = playerInventory.Items[-1]; + var itemClone = new Item(itemTmp.Type, 1, itemTmp.NBT); + inventory.Items[slotId] = itemClone; + playerInventory.Items[-1].Count--; } } else { - // Clicked slot is on hotbar, put it to upper inventory - // Now try to find same item and put on them - var itemsClone = playerInventory.Items.ToDictionary(entry => entry.Key, entry => entry.Value); - foreach (KeyValuePair _item in itemsClone) + // Check target slot have item? + if (inventory.Items.ContainsKey(slotId)) { - if (_item.Key < upperStartSlot) continue; - if (_item.Key >= upperEndSlot) break; - - int maxCount = _item.Value.Type.StackCount(); - if (_item.Value.Type == inventory.Items[slotId].Type && _item.Value.Count < maxCount) + if (slotId == 0) { - // Put item on that stack - int spaceLeft = maxCount - _item.Value.Count; - if (inventory.Items[slotId].Count <= spaceLeft) + // no matter how many item in slot 0, only 1 will be taken out + // Also server will update us + break; + } + if (inventory.Items[slotId].Count == 1) + { + // Only 1 item count. Put it to cursor + playerInventory.Items[-1] = inventory.Items[slotId]; + inventory.Items.Remove(slotId); + } + else + { + // Take half of the item stack to cursor + if (inventory.Items[slotId].Count % 2 == 0) { - // Can fit into the stack - inventory.Items[_item.Key].Count += inventory.Items[slotId].Count; - inventory.Items.Remove(slotId); + // Can be evenly divided + Item itemTmp = inventory.Items[slotId]; + playerInventory.Items[-1] = new Item(itemTmp.Type, itemTmp.Count / 2, itemTmp.NBT); + inventory.Items[slotId].Count = itemTmp.Count / 2; } else { - inventory.Items[slotId].Count -= spaceLeft; - inventory.Items[_item.Key].Count = inventory.Items[_item.Key].Type.StackCount(); + // Cannot be evenly divided. item count on cursor is always larger than item on inventory + Item itemTmp = inventory.Items[slotId]; + playerInventory.Items[-1] = new Item(itemTmp.Type, (itemTmp.Count + 1) / 2, itemTmp.NBT); + inventory.Items[slotId].Count = (itemTmp.Count - 1) / 2; } } } - if (inventory.Items[slotId].Count > 0) + } + break; + case WindowActionType.ShiftClick: + if (slotId == 0) break; + if (inventory.Items.ContainsKey(slotId)) + { + /* Target slot have item */ + + int upperStartSlot = 9; + int upperEndSlot = 35; + + switch (inventory.Type) { - int[] emptySlots = inventory.GetEmpytSlots(); - int emptySlot = -2; - foreach (int slot in emptySlots) - { - if (slot < upperStartSlot) continue; - if (slot >= upperEndSlot) break; - emptySlot = slot; + case ContainerType.PlayerInventory: + upperStartSlot = 9; + upperEndSlot = 35; break; - } - if (emptySlot != -2) + case ContainerType.Crafting: + upperStartSlot = 1; + upperEndSlot = 9; + break; + // TODO: Define more container type here + } + + // Cursor have item or not doesn't matter + // If hotbar already have same item, will put on it first until every stack are full + // If no more same item , will put on the first empty slot (smaller slot id) + // If inventory full, item will not move + if (slotId <= upperEndSlot) + { + // Clicked slot is on upper side inventory, put it to hotbar + // Now try to find same item and put on them + var itemsClone = playerInventory.Items.ToDictionary(entry => entry.Key, entry => entry.Value); + foreach (KeyValuePair _item in itemsClone) { - var itemTmp = inventory.Items[slotId]; - inventory.Items[emptySlot] = new Item(itemTmp.Type, itemTmp.Count, itemTmp.NBT); - inventory.Items.Remove(slotId); + if (_item.Key <= upperEndSlot) continue; + + int maxCount = _item.Value.Type.StackCount(); + if (_item.Value.Type == inventory.Items[slotId].Type && _item.Value.Count < maxCount) + { + // Put item on that stack + int spaceLeft = maxCount - _item.Value.Count; + if (inventory.Items[slotId].Count <= spaceLeft) + { + // Can fit into the stack + inventory.Items[_item.Key].Count += inventory.Items[slotId].Count; + inventory.Items.Remove(slotId); + } + else + { + inventory.Items[slotId].Count -= spaceLeft; + inventory.Items[_item.Key].Count = inventory.Items[_item.Key].Type.StackCount(); + } + } + } + if (inventory.Items[slotId].Count > 0) + { + int[] emptySlots = inventory.GetEmpytSlots(); + int emptySlot = -2; + foreach (int slot in emptySlots) + { + if (slot <= upperEndSlot) continue; + emptySlot = slot; + break; + } + if (emptySlot != -2) + { + var itemTmp = inventory.Items[slotId]; + inventory.Items[emptySlot] = new Item(itemTmp.Type, itemTmp.Count, itemTmp.NBT); + inventory.Items.Remove(slotId); + } + } + } + else + { + // Clicked slot is on hotbar, put it to upper inventory + // Now try to find same item and put on them + var itemsClone = playerInventory.Items.ToDictionary(entry => entry.Key, entry => entry.Value); + foreach (KeyValuePair _item in itemsClone) + { + if (_item.Key < upperStartSlot) continue; + if (_item.Key >= upperEndSlot) break; + + int maxCount = _item.Value.Type.StackCount(); + if (_item.Value.Type == inventory.Items[slotId].Type && _item.Value.Count < maxCount) + { + // Put item on that stack + int spaceLeft = maxCount - _item.Value.Count; + if (inventory.Items[slotId].Count <= spaceLeft) + { + // Can fit into the stack + inventory.Items[_item.Key].Count += inventory.Items[slotId].Count; + inventory.Items.Remove(slotId); + } + else + { + inventory.Items[slotId].Count -= spaceLeft; + inventory.Items[_item.Key].Count = inventory.Items[_item.Key].Type.StackCount(); + } + } + } + if (inventory.Items[slotId].Count > 0) + { + int[] emptySlots = inventory.GetEmpytSlots(); + int emptySlot = -2; + foreach (int slot in emptySlots) + { + if (slot < upperStartSlot) continue; + if (slot >= upperEndSlot) break; + emptySlot = slot; + break; + } + if (emptySlot != -2) + { + var itemTmp = inventory.Items[slotId]; + inventory.Items[emptySlot] = new Item(itemTmp.Type, itemTmp.Count, itemTmp.NBT); + inventory.Items.Remove(slotId); + } } } } - } - break; - case WindowActionType.DropItem: - if (inventory.Items.ContainsKey(slotId)) - inventory.Items[slotId].Count--; - - if (inventory.Items[slotId].Count <= 0) - inventory.Items.Remove(slotId); - break; - case WindowActionType.DropItemStack: - inventory.Items.Remove(slotId); - break; - } - } + break; + case WindowActionType.DropItem: + if (inventory.Items.ContainsKey(slotId)) + inventory.Items[slotId].Count--; - return result; + if (inventory.Items[slotId].Count <= 0) + inventory.Items.Remove(slotId); + break; + case WindowActionType.DropItemStack: + inventory.Items.Remove(slotId); + break; + } + } + + return result; + }); } /// @@ -1459,7 +1489,7 @@ namespace MinecraftClient /// TRUE if item given successfully public bool DoCreativeGive(int slot, ItemType itemType, int count, Dictionary nbt = null) { - return handler.SendCreativeInventoryAction(slot, itemType, count, nbt); + return InvokeOnMainThread(() => handler.SendCreativeInventoryAction(slot, itemType, count, nbt)); } /// @@ -1469,7 +1499,7 @@ namespace MinecraftClient /// TRUE if animation successfully done public bool DoAnimation(int animation) { - return handler.SendAnimation(animation, playerEntityID); + return InvokeOnMainThread(() => handler.SendAnimation(animation, playerEntityID)); } /// @@ -1480,13 +1510,16 @@ namespace MinecraftClient /// Sending close window for inventory 0 can cause server to update our inventory if there are any item in the crafting area public bool CloseInventory(int windowId) { - if (inventories.ContainsKey(windowId)) + return InvokeOnMainThread(() => { - if (windowId != 0) - inventories.Remove(windowId); - return handler.SendCloseWindow(windowId); - } - return false; + if (inventories.ContainsKey(windowId)) + { + if (windowId != 0) + inventories.Remove(windowId); + return handler.SendCloseWindow(windowId); + } + return false; + }); } /// @@ -1495,13 +1528,14 @@ namespace MinecraftClient /// TRUE if the uccessfully clear public bool ClearInventories() { - if (inventoryHandlingEnabled) + if (!inventoryHandlingEnabled) + return false; + return InvokeOnMainThread(() => { inventories.Clear(); inventories[0] = new Container(0, ContainerType.PlayerInventory, "Player Inventory"); return true; - } - else { return false; } + }); } /// @@ -1513,18 +1547,21 @@ namespace MinecraftClient /// TRUE if interaction succeeded public bool InteractEntity(int EntityID, int type, Hand hand = Hand.MainHand) { - if (entities.ContainsKey(EntityID)) + return InvokeOnMainThread(() => { - if (type == 0) + if (entities.ContainsKey(EntityID)) { - return handler.SendInteractEntity(EntityID, type, (int)hand); + if (type == 0) + { + return handler.SendInteractEntity(EntityID, type, (int)hand); + } + else + { + return handler.SendInteractEntity(EntityID, type); + } } - else - { - return handler.SendInteractEntity(EntityID, type); - } - } - else { return false; } + else return false; + }); } /// @@ -1535,7 +1572,7 @@ namespace MinecraftClient /// TRUE if successfully placed public bool PlaceBlock(Location location, Direction blockFace, Hand hand = Hand.MainHand) { - return handler.SendPlayerBlockPlacement((int)hand, location, blockFace); + return InvokeOnMainThread(() => handler.SendPlayerBlockPlacement((int)hand, location, blockFace)); } /// @@ -1546,7 +1583,9 @@ namespace MinecraftClient /// Also look at the block before digging public bool DigBlock(Location location, bool swingArms = true, bool lookAtBlock = true) { - if (GetTerrainEnabled()) + if (!GetTerrainEnabled()) + return false; + return InvokeOnMainThread(() => { // TODO select best face from current player location Direction blockFace = Direction.Down; @@ -1560,8 +1599,7 @@ namespace MinecraftClient return handler.SendPlayerDigging(0, location, blockFace) && (!swingArms || DoAnimation((int)Hand.MainHand)) && handler.SendPlayerDigging(2, location, blockFace); - } - else return false; + }); } /// @@ -1573,8 +1611,11 @@ namespace MinecraftClient { if (slot >= 0 && slot <= 8) { - CurrentSlot = Convert.ToByte(slot); - return handler.SendHeldItemChange(slot); + return InvokeOnMainThread(() => + { + CurrentSlot = Convert.ToByte(slot); + return handler.SendHeldItemChange(slot); + }); } else return false; } @@ -1590,7 +1631,7 @@ namespace MinecraftClient public bool UpdateSign(Location location, string line1, string line2, string line3, string line4) { // TODO Open sign editor first https://wiki.vg/Protocol#Open_Sign_Editor - return handler.SendUpdateSign(location, line1, line2, line3, line4); + return InvokeOnMainThread(() => handler.SendUpdateSign(location, line1, line2, line3, line4)); } /// @@ -1599,7 +1640,7 @@ namespace MinecraftClient /// The slot of the trade, starts at 0. public bool SelectTrade(int selectedSlot) { - return handler.SelectTrade(selectedSlot); + return InvokeOnMainThread(() => handler.SelectTrade(selectedSlot)); } /// @@ -1611,7 +1652,7 @@ namespace MinecraftClient /// command block flags public bool UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags) { - return handler.UpdateCommandBlock(location, command, mode, flags); + return InvokeOnMainThread(() => handler.UpdateCommandBlock(location, command, mode, flags)); } #endregion diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index 8850677f..b4c605de 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -222,6 +222,7 @@ + diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 5018080b..e3c0616c 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -423,7 +423,7 @@ namespace MinecraftClient.Protocol.Handlers int compressedDataSize = dataTypes.ReadNextInt(packetData); byte[] compressed = dataTypes.ReadData(compressedDataSize, packetData); byte[] decompressed = ZlibUtils.Decompress(compressed); - new Thread(() => { + new Task(() => { pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, addBitmap, currentDimension == 0, chunksContinuous, currentDimension, new Queue(decompressed)); }).Start(); } @@ -449,7 +449,7 @@ namespace MinecraftClient.Protocol.Handlers else dataTypes.ReadData(1024 * 4, packetData); // Biomes - 1.15 and above } int dataSize = dataTypes.ReadNextVarInt(packetData); - new Thread(() => { + new Task(() => { pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, 0, false, chunksContinuous, currentDimension, packetData); }).Start(); } diff --git a/MinecraftClient/TaskWithDelay.cs b/MinecraftClient/TaskWithDelay.cs new file mode 100644 index 00000000..55b09495 --- /dev/null +++ b/MinecraftClient/TaskWithDelay.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MinecraftClient +{ + /// + /// Holds a task with delay + /// + class TaskWithDelay + { + private Action _task; + private int tickCounter; + private DateTime dateToLaunch; + + public Action Task { get { return _task; } } + + public TaskWithDelay(Action task, int delayTicks) + { + _task = task; + tickCounter = delayTicks; + dateToLaunch = DateTime.MaxValue; + } + + public TaskWithDelay(Action task, TimeSpan delay) + { + _task = task; + tickCounter = int.MaxValue; + dateToLaunch = DateTime.Now + delay; + } + + /// + /// Tick the counter + /// + /// Return true if the task should run now + public bool Tick() + { + tickCounter--; + if (tickCounter <= 0 || dateToLaunch < DateTime.Now) + return true; + return false; + } + } +}