From 297d7dbc30b1a21fd1c1ff605fdc28fd6b18e936 Mon Sep 17 00:00:00 2001 From: Anon Date: Sat, 4 Apr 2026 16:45:13 +0200 Subject: [PATCH] Moved more stuff to async --- DebugTools/MccMcpStdioHarness/Program.cs | 15 ++ MinecraftClient/AutoTimeout.cs | 32 +++- MinecraftClient/Mcp/IMccMcpCapabilities.cs | 7 + MinecraftClient/Mcp/MccMcpCapabilities.cs | 119 ++++++++++-- MinecraftClient/Mcp/MccMcpToolSet.cs | 21 ++- MinecraftClient/Program.cs | 14 +- MinecraftClient/Protocol/ProtocolHandler.cs | 31 +++- MinecraftClient/Scripting/MccGameApi.cs | 194 +++++++++++++++++++- 8 files changed, 389 insertions(+), 44 deletions(-) diff --git a/DebugTools/MccMcpStdioHarness/Program.cs b/DebugTools/MccMcpStdioHarness/Program.cs index 1b90ddbb..bb085f27 100644 --- a/DebugTools/MccMcpStdioHarness/Program.cs +++ b/DebugTools/MccMcpStdioHarness/Program.cs @@ -567,6 +567,9 @@ internal sealed class DeterministicCapabilities : IMccMcpCapabilities timeoutMs }); + public Task MoveToAsync(double x, double y, double z, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs) => + Task.FromResult(MoveTo(x, y, z, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeoutMs)); + public MccMcpResult MoveToPlayer(string playerName, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs) => MccMcpResult.Ok(new { @@ -593,6 +596,9 @@ internal sealed class DeterministicCapabilities : IMccMcpCapabilities timeoutMs }); + public Task MoveToPlayerAsync(string playerName, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs) => + Task.FromResult(MoveToPlayer(playerName, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeoutMs)); + public MccMcpResult LookAt(double x, double y, double z) => MccMcpResult.Ok(new { looked = true, x = C(x), y = C(y), z = C(z) }); @@ -755,6 +761,9 @@ internal sealed class DeterministicCapabilities : IMccMcpCapabilities }); } + public Task OpenContainerAtAsync(int x, int y, int z, int timeoutMs, bool closeCurrent) => + Task.FromResult(OpenContainerAt(x, y, z, timeoutMs, closeCurrent)); + public MccMcpResult CloseContainer(int inventoryId, int timeoutMs) { int resolvedInventoryId = inventoryId <= 0 ? 1 : inventoryId; @@ -768,6 +777,9 @@ internal sealed class DeterministicCapabilities : IMccMcpCapabilities }); } + public Task CloseContainerAsync(int inventoryId, int timeoutMs) => + Task.FromResult(CloseContainer(inventoryId, timeoutMs)); + public MccMcpResult InventoryWindowAction(int inventoryId, int slotId, string actionType) => MccMcpResult.Ok(new { success = true, inventoryId, slotId, actionType }); @@ -963,6 +975,9 @@ internal sealed class DeterministicCapabilities : IMccMcpCapabilities } }); + public Task PickupItemsAsync(string itemType, double radius, int maxItems, bool allowUnsafe, int timeoutMs) => + Task.FromResult(PickupItems(itemType, radius, maxItems, allowUnsafe, timeoutMs)); + public MccMcpResult Respawn() { health = 20.0f; diff --git a/MinecraftClient/AutoTimeout.cs b/MinecraftClient/AutoTimeout.cs index 786be4db..f08dc5e5 100644 --- a/MinecraftClient/AutoTimeout.cs +++ b/MinecraftClient/AutoTimeout.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using System.Threading.Tasks; namespace MinecraftClient { @@ -22,6 +23,11 @@ namespace MinecraftClient return Perform(action, TimeSpan.FromMilliseconds(timeout)); } + public static Task PerformAsync(Action action, int timeout, CancellationToken cancellationToken = default) + { + return PerformAsync(action, TimeSpan.FromMilliseconds(timeout), cancellationToken); + } + /// /// Perform the specified action with specified timeout /// @@ -30,14 +36,26 @@ namespace MinecraftClient /// True if the action finished whithout timing out public static bool Perform(Action action, TimeSpan timeout) { - Thread thread = new(new ThreadStart(action)); - thread.Start(); + return PerformAsync(action, timeout).GetAwaiter().GetResult(); + } - bool success = thread.Join(timeout); - if (!success) - thread.Interrupt(); + public static async Task PerformAsync(Action action, TimeSpan timeout, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(action); - return success; + try + { + await Task.Run(action, cancellationToken).WaitAsync(timeout, cancellationToken); + return true; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + return false; + } + catch (TimeoutException) + { + return false; + } } } -} \ No newline at end of file +} diff --git a/MinecraftClient/Mcp/IMccMcpCapabilities.cs b/MinecraftClient/Mcp/IMccMcpCapabilities.cs index 055fb561..1054bfb6 100644 --- a/MinecraftClient/Mcp/IMccMcpCapabilities.cs +++ b/MinecraftClient/Mcp/IMccMcpCapabilities.cs @@ -1,3 +1,5 @@ +using System.Threading.Tasks; + namespace MinecraftClient.Mcp; public interface IMccMcpCapabilities @@ -43,7 +45,9 @@ public interface IMccMcpCapabilities MccMcpResult FindNearestEntity(string? typeFilter, string? nameFilter, double radius, bool includePlayers); MccMcpResult CanReachPosition(double x, double y, double z, bool allowUnsafe, int maxOffset, int minOffset, int timeoutMs); MccMcpResult MoveTo(double x, double y, double z, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs); + Task MoveToAsync(double x, double y, double z, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs); MccMcpResult MoveToPlayer(string playerName, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs); + Task MoveToPlayerAsync(string playerName, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs); MccMcpResult LookAt(double x, double y, double z); MccMcpResult LookDirection(string direction); MccMcpResult LookAngles(float yaw, float pitch); @@ -51,7 +55,9 @@ public interface IMccMcpCapabilities MccMcpResult GetInventorySnapshot(int inventoryId); MccMcpResult SearchInventories(string query, int maxCount, bool exactMatch, bool includeContainers); MccMcpResult OpenContainerAt(int x, int y, int z, int timeoutMs, bool closeCurrent); + Task OpenContainerAtAsync(int x, int y, int z, int timeoutMs, bool closeCurrent); MccMcpResult CloseContainer(int inventoryId, int timeoutMs); + Task CloseContainerAsync(int inventoryId, int timeoutMs); MccMcpResult InventoryWindowAction(int inventoryId, int slotId, string actionType); MccMcpResult DropInventoryItem(string itemType, int count, int inventoryId, bool preferStack); MccMcpResult DepositContainerItem(string itemType, int count, int inventoryId, bool preferLargestStack); @@ -62,5 +68,6 @@ public interface IMccMcpCapabilities MccMcpResult FindSigns(string text, bool exactMatch, int radius, int maxCount, bool includeBackText); MccMcpResult ListItemEntities(string? itemType, double radius, int maxCount); MccMcpResult PickupItems(string itemType, double radius, int maxItems, bool allowUnsafe, int timeoutMs); + Task PickupItemsAsync(string itemType, double radius, int maxItems, bool allowUnsafe, int timeoutMs); MccMcpResult GetWorldBlockAt(int x, int y, int z); } diff --git a/MinecraftClient/Mcp/MccMcpCapabilities.cs b/MinecraftClient/Mcp/MccMcpCapabilities.cs index 60a03f76..a1127d18 100644 --- a/MinecraftClient/Mcp/MccMcpCapabilities.cs +++ b/MinecraftClient/Mcp/MccMcpCapabilities.cs @@ -71,6 +71,8 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities public required double Distance { get; init; } } + private readonly record struct ContainerOpenState(int InventoryId, Container? Inventory); + private enum InventoryTransferDirection { Deposit, @@ -1274,6 +1276,11 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities } public MccMcpResult MoveTo(double x, double y, double z, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs) + { + return MoveToAsync(x, y, z, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeoutMs).GetAwaiter().GetResult(); + } + + public async Task MoveToAsync(double x, double y, double z, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs) { if (!IsCategoryEnabled(t => t.Movement)) return MccMcpResult.Fail("capability_disabled"); @@ -1302,9 +1309,12 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities int verifyWaitMs = GetArrivalWaitMs(timeoutMs); double tolerance = GetArrivalTolerance(maxOffset, minOffset); - Location? finalLocation = null; - bool arrived = pathFound && WaitForArrival(client, goal, verifyWaitMs, tolerance, out finalLocation); - finalLocation ??= client.InvokeOnMainThread(client.GetCurrentLocation); + Location finalLocation = client.InvokeOnMainThread(client.GetCurrentLocation); + bool arrived = false; + if (pathFound) + { + (arrived, finalLocation) = await WaitForArrivalAsync(client, goal, verifyWaitMs, tolerance); + } object resultData = new { pathFound, @@ -1313,9 +1323,9 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities verifyWaitMs, target = ToCoordinate(goal), startLocation = ToCoordinate(startLocation), - finalLocation = ToCoordinate(finalLocation.Value), - finalDistance = GetDistance(finalLocation.Value, goal), - distanceMoved = GetDistance(startLocation, finalLocation.Value), + finalLocation = ToCoordinate(finalLocation), + finalDistance = GetDistance(finalLocation, goal), + distanceMoved = GetDistance(startLocation, finalLocation), allowUnsafe, allowDirectTeleport, maxOffset, @@ -1329,10 +1339,15 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities } public MccMcpResult MoveToPlayer(string playerName, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs) + { + return MoveToPlayerAsync(playerName, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeoutMs).GetAwaiter().GetResult(); + } + + public async Task MoveToPlayerAsync(string playerName, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs) { if (!IsCategoryEnabled(t => t.Movement)) return MccMcpResult.Fail("capability_disabled"); - return ToMcpResult(game.MoveToPlayer(playerName, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeoutMs)); + return ToMcpResult(await game.MoveToPlayerAsync(playerName, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeoutMs)); } public MccMcpResult LookAt(double x, double y, double z) @@ -1464,6 +1479,11 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities } public MccMcpResult OpenContainerAt(int x, int y, int z, int timeoutMs, bool closeCurrent) + { + return OpenContainerAtAsync(x, y, z, timeoutMs, closeCurrent).GetAwaiter().GetResult(); + } + + public async Task OpenContainerAtAsync(int x, int y, int z, int timeoutMs, bool closeCurrent) { if (!IsCategoryEnabled(t => t.Inventory)) return MccMcpResult.Fail("capability_disabled"); @@ -1495,10 +1515,15 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities }); } - return OpenContainerCore(client, location, state.block, state.activeContainerId, waitMs, closeCurrent); + return await OpenContainerCoreAsync(client, location, state.block, state.activeContainerId, waitMs, closeCurrent); } public MccMcpResult CloseContainer(int inventoryId, int timeoutMs) + { + return CloseContainerAsync(inventoryId, timeoutMs).GetAwaiter().GetResult(); + } + + public async Task CloseContainerAsync(int inventoryId, int timeoutMs) { if (!IsCategoryEnabled(t => t.Inventory)) return MccMcpResult.Fail("capability_disabled"); @@ -1527,7 +1552,7 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities } bool closeAccepted = client.CloseInventory(resolvedInventoryId); - bool closed = closeAccepted && WaitForContainerClose(client, resolvedInventoryId, waitMs); + bool closed = closeAccepted && await WaitForContainerCloseAsync(client, resolvedInventoryId, waitMs); var resultData = new { success = closeAccepted && closed, @@ -1824,10 +1849,15 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities } public MccMcpResult PickupItems(string itemType, double radius, int maxItems, bool allowUnsafe, int timeoutMs) + { + return PickupItemsAsync(itemType, radius, maxItems, allowUnsafe, timeoutMs).GetAwaiter().GetResult(); + } + + public async Task PickupItemsAsync(string itemType, double radius, int maxItems, bool allowUnsafe, int timeoutMs) { if (!IsCategoryEnabled(t => t.EntityWorld) || !IsCategoryEnabled(t => t.Movement)) return MccMcpResult.Fail("capability_disabled"); - return ToMcpResult(game.PickupItems(itemType, radius, maxItems, allowUnsafe, timeoutMs)); + return ToMcpResult(await game.PickupItemsAsync(itemType, radius, maxItems, allowUnsafe, timeoutMs)); } public MccMcpResult GetWorldBlockAt(int x, int y, int z) @@ -1858,7 +1888,7 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities }); } - private static MccMcpResult OpenContainerCore(McClient client, Location location, Block block, int activeContainerId, int waitMs, bool closeCurrent) + private static async Task OpenContainerCoreAsync(McClient client, Location location, Block block, int activeContainerId, int waitMs, bool closeCurrent) { if (activeContainerId > 0) { @@ -1876,7 +1906,7 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities } bool closeAccepted = client.CloseInventory(activeContainerId); - bool closed = closeAccepted && WaitForContainerClose(client, activeContainerId, waitMs); + bool closed = closeAccepted && await WaitForContainerCloseAsync(client, activeContainerId, waitMs); if (!closeAccepted || !closed) { return MccMcpResult.Fail("action_incomplete", data: new @@ -1894,7 +1924,11 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities int openedInventoryId = 0; Container? openedInventory = null; bool openAccepted = client.InvokeOnMainThread(() => client.PlaceBlock(location, Direction.Down, Hand.MainHand, lookAtBlock: true)); - bool opened = openAccepted && WaitForContainerOpen(client, beforeIds, waitMs, out openedInventoryId, out openedInventory); + bool opened = openAccepted && await WaitForContainerOpenAsync(client, beforeIds, waitMs, result => + { + openedInventoryId = result.InventoryId; + openedInventory = result.Inventory; + }); var resultData = new { success = openAccepted && opened && openedInventory is not null, @@ -2233,6 +2267,31 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities } } + private static async Task WaitForContainerOpenAsync(McClient client, ISet beforeIds, int waitMs, Action onOpened) + { + DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); + while (true) + { + (int activeId, Container? activeInventory) state = client.InvokeOnMainThread(() => + { + int activeId = GetActiveContainerId(client); + Container? activeInventory = activeId > 0 ? client.GetInventory(activeId) : null; + return (activeId, activeInventory); + }); + + if (state.activeId > 0 && (!beforeIds.Contains(state.activeId) || beforeIds.Count == 0) && state.activeInventory is not null) + { + onOpened(new ContainerOpenState(state.activeId, state.activeInventory)); + return true; + } + + if (DateTime.UtcNow >= deadline) + return false; + + await Task.Delay(ArrivalPollIntervalMs); + } + } + private static bool WaitForContainerClose(McClient client, int inventoryId, int waitMs) { DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); @@ -2249,6 +2308,22 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities } } + private static async Task WaitForContainerCloseAsync(McClient client, int inventoryId, int waitMs) + { + DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); + while (true) + { + bool stillOpen = client.InvokeOnMainThread(() => client.GetInventories().ContainsKey(inventoryId)); + if (!stillOpen) + return true; + + if (DateTime.UtcNow >= deadline) + return false; + + await Task.Delay(ArrivalPollIntervalMs); + } + } + private static int GetContainerWaitMs(int timeoutMs) { if (timeoutMs <= 0) @@ -2901,6 +2976,24 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities } } + private static async Task<(bool Arrived, Location FinalLocation)> WaitForArrivalAsync(McClient client, Location goal, int waitMs, double tolerance) + { + DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); + Location finalLocation = client.InvokeOnMainThread(client.GetCurrentLocation); + while (true) + { + finalLocation = client.InvokeOnMainThread(client.GetCurrentLocation); + double distance = GetDistance(finalLocation, goal); + if (distance <= tolerance) + return (true, finalLocation); + + if (DateTime.UtcNow >= deadline) + return (false, finalLocation); + + await Task.Delay(ArrivalPollIntervalMs); + } + } + private static double GetDistance(Location from, Location to) { double dx = from.X - to.X; diff --git a/MinecraftClient/Mcp/MccMcpToolSet.cs b/MinecraftClient/Mcp/MccMcpToolSet.cs index 84305e34..46fe955e 100644 --- a/MinecraftClient/Mcp/MccMcpToolSet.cs +++ b/MinecraftClient/Mcp/MccMcpToolSet.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Threading.Tasks; using ModelContextProtocol.Server; namespace MinecraftClient.Mcp; @@ -262,15 +263,15 @@ public sealed class MccMcpToolSet } [McpServerTool(Name = "mcc_move_to"), Description("Request movement/pathing to a world coordinate and verify arrival.")] - public object MoveTo(double x, double y, double z, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, int timeoutMs = 0) + public async Task MoveTo(double x, double y, double z, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, int timeoutMs = 0) { - return capabilities.MoveTo(x, y, z, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeoutMs); + return await capabilities.MoveToAsync(x, y, z, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeoutMs); } [McpServerTool(Name = "mcc_move_to_player"), Description("Locate a tracked player entity, request movement/pathing, and verify arrival.")] - public object MoveToPlayer(string playerName, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, int timeoutMs = 0) + public async Task MoveToPlayer(string playerName, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, int timeoutMs = 0) { - return capabilities.MoveToPlayer(playerName, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeoutMs); + return await capabilities.MoveToPlayerAsync(playerName, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeoutMs); } [McpServerTool(Name = "mcc_look_at"), Description("Rotate player view toward world coordinates.")] @@ -310,15 +311,15 @@ public sealed class MccMcpToolSet } [McpServerTool(Name = "mcc_container_open_at"), Description("Open an interactable container block at world coordinates and wait for the container inventory to appear.")] - public object ContainerOpenAt(int x, int y, int z, int timeoutMs = 0, bool closeCurrent = true) + public async Task ContainerOpenAt(int x, int y, int z, int timeoutMs = 0, bool closeCurrent = true) { - return capabilities.OpenContainerAt(x, y, z, timeoutMs, closeCurrent); + return await capabilities.OpenContainerAtAsync(x, y, z, timeoutMs, closeCurrent); } [McpServerTool(Name = "mcc_container_close"), Description("Close an open non-player container. Use inventoryId=-1 to close the active container.")] - public object ContainerClose([Description("Container inventory ID, or -1 for the active non-player container.")] int inventoryId = -1, int timeoutMs = 0) + public async Task ContainerClose([Description("Container inventory ID, or -1 for the active non-player container.")] int inventoryId = -1, int timeoutMs = 0) { - return capabilities.CloseContainer(inventoryId, timeoutMs); + return await capabilities.CloseContainerAsync(inventoryId, timeoutMs); } [McpServerTool(Name = "mcc_inventory_window_action"), Description("Perform a window action on an inventory slot.")] @@ -388,9 +389,9 @@ public sealed class MccMcpToolSet } [McpServerTool(Name = "mcc_items_pickup"), Description("Move to and pick up nearby dropped items of a given item type.")] - public object ItemsPickup(string itemType, double radius = 32, int maxItems = 20, bool allowUnsafe = false, int timeoutMs = 0) + public async Task ItemsPickup(string itemType, double radius = 32, int maxItems = 20, bool allowUnsafe = false, int timeoutMs = 0) { - return capabilities.PickupItems(itemType, radius, maxItems, allowUnsafe, timeoutMs); + return await capabilities.PickupItemsAsync(itemType, radius, maxItems, allowUnsafe, timeoutMs); } [McpServerTool(Name = "mcc_world_block_at"), Description("Get block information at world coordinates.")] diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 6870ada6..cfdb84fc 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -763,11 +763,15 @@ namespace MinecraftClient ConsoleIO.WriteLine(Translations.mcc_forge); else ConsoleIO.WriteLine(Translations.mcc_retrieve); - if (!ProtocolHandler.GetServerInfo(InternalConfig.ServerIP, InternalConfig.ServerPort, ref protocolversion, ref forgeInfo)) + var serverInfo = await ProtocolHandler.GetServerInfoAsync(InternalConfig.ServerIP, InternalConfig.ServerPort, protocolversion); + if (!serverInfo.Success) { HandleFailure(Translations.error_ping, true, ChatBot.DisconnectReason.ConnectionLost); return; } + + protocolversion = serverInfo.ProtocolVersion; + forgeInfo = serverInfo.ForgeInfo; } if ((Config.Main.General.AccountType == LoginType.microsoft || Config.Main.General.AccountType == LoginType.yggdrasil) @@ -927,6 +931,11 @@ namespace MinecraftClient } public static void DoExit(int exitcode = 0) + { + DoExitAsync(exitcode).GetAwaiter().GetResult(); + } + + private static Task DoExitAsync(int exitcode = 0) { WriteBackSettings(); ConsoleIO.WriteLineFormatted("§a" + string.Format(Translations.config_saving, settingsIniPath)); @@ -945,6 +954,7 @@ namespace MinecraftClient if (Config.Main.Advanced.PlayerHeadAsIcon) { ConsoleIcon.RevertToMCCIcon(); } ConsoleIO.Backend?.Shutdown(); Environment.Exit(exitcode); + return Task.CompletedTask; } /// @@ -952,7 +962,7 @@ namespace MinecraftClient /// public static void Exit(int exitcode = 0) { - StartLifecycleTask(Task.Run(() => DoExit(exitcode))); + StartLifecycleTask(DoExitAsync(exitcode)); } private static void StartLifecycleTask(Task lifecycleTask) diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index cb2dc827..1a9c5b25 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -93,11 +93,30 @@ namespace MinecraftClient.Protocol /// TRUE if ping was successful public static bool GetServerInfo(string serverIP, ushort serverPort, ref int protocolversion, ref ForgeInfo? forgeInfo) + { + (bool success, int resolvedProtocolVersion, ForgeInfo? resolvedForgeInfo) = + GetServerInfoAsync(serverIP, serverPort, protocolversion).GetAwaiter().GetResult(); + + if (!success) + return false; + + if (protocolversion != 0 && protocolversion != resolvedProtocolVersion) + ConsoleIO.WriteLineFormatted("§8" + Translations.error_version_different, acceptnewlines: true); + if (protocolversion == 0 && resolvedProtocolVersion <= 1) + ConsoleIO.WriteLineFormatted("§8" + Translations.error_no_version_report, acceptnewlines: true); + if (protocolversion == 0) + protocolversion = resolvedProtocolVersion; + + forgeInfo = resolvedForgeInfo; + return true; + } + + public static async Task<(bool Success, int ProtocolVersion, ForgeInfo? ForgeInfo)> GetServerInfoAsync(string serverIP, ushort serverPort, int protocolversion) { bool success = false; int protocolversionTmp = 0; ForgeInfo? forgeInfoTmp = null; - if (AutoTimeout.Perform(() => + if (await AutoTimeout.PerformAsync(() => { try { @@ -120,19 +139,15 @@ namespace MinecraftClient.Protocol ? 10 : 30))) { - if (protocolversion != 0 && protocolversion != protocolversionTmp) - ConsoleIO.WriteLineFormatted("§8" + Translations.error_version_different, acceptnewlines: true); - if (protocolversion == 0 && protocolversionTmp <= 1) - ConsoleIO.WriteLineFormatted("§8" + Translations.error_no_version_report, acceptnewlines: true); if (protocolversion == 0) protocolversion = protocolversionTmp; - forgeInfo = forgeInfoTmp; - return success; + + return (success, protocolversion, forgeInfoTmp); } else { ConsoleIO.WriteLineFormatted("§8" + Translations.error_connection_timeout, acceptnewlines: true); - return false; + return (false, protocolversion, forgeInfoTmp); } } diff --git a/MinecraftClient/Scripting/MccGameApi.cs b/MinecraftClient/Scripting/MccGameApi.cs index 391ece50..4bd18f1b 100644 --- a/MinecraftClient/Scripting/MccGameApi.cs +++ b/MinecraftClient/Scripting/MccGameApi.cs @@ -704,9 +704,76 @@ public sealed class MccGameApi /// /// Run on a worker thread so ChatBot callbacks can poll the result without blocking MCC updates. /// - public Task> MoveToPlayerAsync(string playerName, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, int timeoutMs = 0) + public async Task> MoveToPlayerAsync(string playerName, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, int timeoutMs = 0) { - return Task.Run(() => MoveToPlayer(playerName, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeoutMs)); + if (string.IsNullOrWhiteSpace(playerName)) + return MccGameResult.Fail("invalid_args"); + + if (!AreValidPathOffsets(maxOffset, minOffset) || timeoutMs < 0) + return MccGameResult.Fail("invalid_args"); + + McClient? client = clientProvider(); + if (client is null) + return NotConnected(); + + if (!client.GetTerrainEnabled() || !client.GetEntityHandlingEnabled()) + return MccGameResult.Fail("feature_disabled"); + + string nameFilter = playerName.Trim(); + NearbyPlayerSnapshot? target = client.InvokeOnMainThread(() => + { + return BuildTrackedPlayerSnapshots(client, includeSelf: false) + .Where(player => PlayerNameMatches(player, nameFilter)) + .OrderBy(player => player.Distance) + .FirstOrDefault(); + }); + + if (target is null) + return MccGameResult.Fail("invalid_state"); + + Location goal = new(target.X, target.Y, target.Z); + Location startLocation = client.InvokeOnMainThread(client.GetCurrentLocation); + TimeSpan? timeout = timeoutMs > 0 ? TimeSpan.FromMilliseconds(timeoutMs) : null; + bool pathFound = client.InvokeOnMainThread(() => client.MoveTo(goal, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeout)); + + int verifyWaitMs = GetArrivalWaitMs(timeoutMs); + double tolerance = GetArrivalTolerance(maxOffset, minOffset); + Location finalLocation = client.InvokeOnMainThread(client.GetCurrentLocation); + bool arrived = false; + + if (pathFound) + { + (arrived, finalLocation) = await WaitForArrivalAsync(client, goal, verifyWaitMs, tolerance); + } + + MccMoveToPlayerResult resultData = new() + { + PathFound = pathFound, + Arrived = arrived, + Tolerance = tolerance, + VerifyWaitMs = verifyWaitMs, + Target = new MccMoveToPlayerTarget + { + PlayerName = target.Name, + EntityId = target.EntityId, + X = MccGameCommon.RoundCoordinate(target.X), + Y = MccGameCommon.RoundCoordinate(target.Y), + Z = MccGameCommon.RoundCoordinate(target.Z) + }, + StartLocation = MccGameCommon.ToCoordinate(startLocation), + FinalLocation = MccGameCommon.ToCoordinate(finalLocation), + FinalDistance = MccGameCommon.GetDistance(finalLocation, goal), + DistanceMoved = MccGameCommon.GetDistance(startLocation, finalLocation), + AllowUnsafe = allowUnsafe, + AllowDirectTeleport = allowDirectTeleport, + MaxOffset = maxOffset, + MinOffset = minOffset, + TimeoutMs = timeoutMs + }; + + return pathFound && arrived + ? MccGameResult.Ok(resultData) + : MccGameResult.Fail("action_incomplete", data: resultData); } /// @@ -1055,9 +1122,94 @@ public sealed class MccGameApi /// /// Run on a worker thread so ChatBot callbacks can poll the result without blocking MCC updates. /// - public Task> PickupItemsAsync(string itemType, double radius = 16, int maxItems = 10, bool allowUnsafe = false, int timeoutMs = 0) + public async Task> PickupItemsAsync(string itemType, double radius = 16, int maxItems = 10, bool allowUnsafe = false, int timeoutMs = 0) { - return Task.Run(() => PickupItems(itemType, radius, maxItems, allowUnsafe, timeoutMs)); + if (string.IsNullOrWhiteSpace(itemType) || radius <= 0 || radius > 1024 || maxItems < 1 || timeoutMs < 0) + return MccGameResult.Fail("invalid_args"); + + if (!MccGameCommon.TryParseItemType(itemType.Trim(), out ItemType parsedItemType)) + return MccGameResult.Fail("invalid_args"); + + McClient? client = clientProvider(); + if (client is null) + return NotConnected(); + + if (!client.GetTerrainEnabled() || !client.GetEntityHandlingEnabled()) + return MccGameResult.Fail("feature_disabled"); + + int limit = Math.Clamp(maxItems, 1, 50); + NearbyItemSnapshot[] targets = client.InvokeOnMainThread(() => BuildNearbyItemSnapshots(client, parsedItemType, radius, limit)); + if (targets.Length == 0) + return MccGameResult.Fail("invalid_state"); + + bool inventoryEnabled = client.GetInventoryEnabled(); + int beforeCount = inventoryEnabled ? client.InvokeOnMainThread(() => GetInventoryItemCount(client, parsedItemType)) : 0; + int initialCount = beforeCount; + int verifyWaitMs = timeoutMs > 0 ? Math.Clamp(timeoutMs, MinArrivalWaitMs, MaxArrivalWaitMs) : 2500; + List attempts = new(targets.Length); + int successfulPickups = 0; + + foreach (NearbyItemSnapshot target in targets) + { + Location targetLocation = new(target.X, target.Y, target.Z); + Location startLocation = client.InvokeOnMainThread(client.GetCurrentLocation); + TimeSpan? moveTimeout = timeoutMs > 0 ? TimeSpan.FromMilliseconds(timeoutMs) : null; + bool pathFound = client.InvokeOnMainThread(() => client.MoveTo(targetLocation, allowUnsafe, false, 0, 0, moveTimeout)); + + Location finalLocation = client.InvokeOnMainThread(client.GetCurrentLocation); + bool arrived = false; + if (pathFound) + { + (arrived, finalLocation) = await WaitForArrivalAsync(client, targetLocation, verifyWaitMs, 2.0); + } + + bool entityGone = await WaitForEntityRemovalAsync(client, target.EntityId, verifyWaitMs); + int afterCount = inventoryEnabled ? client.InvokeOnMainThread(() => GetInventoryItemCount(client, parsedItemType)) : beforeCount; + int inventoryDelta = inventoryEnabled ? Math.Max(0, afterCount - beforeCount) : 0; + bool pickedUp = entityGone || inventoryDelta > 0; + if (pickedUp) + successfulPickups++; + + attempts.Add(new MccPickupAttempt + { + EntityId = target.EntityId, + ItemType = target.ItemType.ToString(), + TypeLabel = target.TypeLabel, + ExpectedCount = target.Count, + Target = MccGameCommon.ToCoordinate(target.X, target.Y, target.Z), + PathFound = pathFound, + Arrived = arrived, + EntityGone = entityGone, + InventoryDelta = inventoryDelta, + StartLocation = MccGameCommon.ToCoordinate(startLocation), + FinalLocation = MccGameCommon.ToCoordinate(finalLocation), + FinalDistance = MccGameCommon.GetDistance(finalLocation, targetLocation) + }); + + beforeCount = afterCount; + } + + int remainingNearby = client.InvokeOnMainThread(() => BuildNearbyItemSnapshots(client, parsedItemType, radius, 1000).Length); + int collectedCount = inventoryEnabled ? Math.Max(0, beforeCount - initialCount) : successfulPickups; + MccPickupItemsResult resultData = new() + { + ItemType = parsedItemType.ToString(), + Radius = radius, + MaxItems = limit, + AllowUnsafe = allowUnsafe, + TimeoutMs = verifyWaitMs, + Attempted = attempts.Count, + SuccessfulPickups = successfulPickups, + CollectedCount = collectedCount, + InitialInventoryCount = inventoryEnabled ? initialCount : null, + FinalInventoryCount = inventoryEnabled ? beforeCount : null, + RemainingNearby = remainingNearby, + Attempts = attempts.ToArray() + }; + + return successfulPickups > 0 + ? MccGameResult.Ok(resultData) + : MccGameResult.Fail("action_incomplete", data: resultData); } private static MccGameResult NotConnected() @@ -1132,6 +1284,24 @@ public sealed class MccGameApi } } + private static async Task<(bool Arrived, Location FinalLocation)> WaitForArrivalAsync(McClient client, Location goal, int waitMs, double tolerance) + { + DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); + Location finalLocation = client.InvokeOnMainThread(client.GetCurrentLocation); + + while (true) + { + finalLocation = client.InvokeOnMainThread(client.GetCurrentLocation); + if (MccGameCommon.GetDistance(finalLocation, goal) <= tolerance) + return (true, finalLocation); + + if (DateTime.UtcNow >= deadline) + return (false, finalLocation); + + await Task.Delay(ArrivalPollIntervalMs); + } + } + private static bool WaitForEntityRemoval(McClient client, int entityId, int waitMs) { DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); @@ -1148,6 +1318,22 @@ public sealed class MccGameApi } } + private static async Task WaitForEntityRemovalAsync(McClient client, int entityId, int waitMs) + { + DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); + while (true) + { + bool exists = client.InvokeOnMainThread(() => client.GetEntities().ContainsKey(entityId)); + if (!exists) + return true; + + if (DateTime.UtcNow >= deadline) + return false; + + await Task.Delay(ArrivalPollIntervalMs); + } + } + private static int GetInventoryItemCount(McClient client, ItemType itemType) { Container? inventory = client.GetInventory(0);