Moved more stuff to async

This commit is contained in:
Anon 2026-04-04 16:45:13 +02:00
parent 3a20f2e235
commit 297d7dbc30
8 changed files with 389 additions and 44 deletions

View file

@ -567,6 +567,9 @@ internal sealed class DeterministicCapabilities : IMccMcpCapabilities
timeoutMs
});
public Task<MccMcpResult> 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<MccMcpResult> 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<MccMcpResult> 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<MccMcpResult> 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<MccMcpResult> 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;

View file

@ -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<bool> PerformAsync(Action action, int timeout, CancellationToken cancellationToken = default)
{
return PerformAsync(action, TimeSpan.FromMilliseconds(timeout), cancellationToken);
}
/// <summary>
/// Perform the specified action with specified timeout
/// </summary>
@ -30,14 +36,26 @@ namespace MinecraftClient
/// <returns>True if the action finished whithout timing out</returns>
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<bool> 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;
}
}
}
}
}

View file

@ -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<MccMcpResult> 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<MccMcpResult> 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<MccMcpResult> OpenContainerAtAsync(int x, int y, int z, int timeoutMs, bool closeCurrent);
MccMcpResult CloseContainer(int inventoryId, int timeoutMs);
Task<MccMcpResult> 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<MccMcpResult> PickupItemsAsync(string itemType, double radius, int maxItems, bool allowUnsafe, int timeoutMs);
MccMcpResult GetWorldBlockAt(int x, int y, int z);
}

View file

@ -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<MccMcpResult> 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<MccMcpResult> 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<MccMcpResult> 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<MccMcpResult> 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<MccMcpResult> 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<MccMcpResult> 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<bool> WaitForContainerOpenAsync(McClient client, ISet<int> beforeIds, int waitMs, Action<ContainerOpenState> 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<bool> 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;

View file

@ -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<object> 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<object> 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<object> 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<object> 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<object> 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.")]

View file

@ -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;
}
/// <summary>
@ -952,7 +962,7 @@ namespace MinecraftClient
/// </summary>
public static void Exit(int exitcode = 0)
{
StartLifecycleTask(Task.Run(() => DoExit(exitcode)));
StartLifecycleTask(DoExitAsync(exitcode));
}
private static void StartLifecycleTask(Task lifecycleTask)

View file

@ -93,11 +93,30 @@ namespace MinecraftClient.Protocol
/// <returns>TRUE if ping was successful</returns>
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);
}
}

View file

@ -704,9 +704,76 @@ public sealed class MccGameApi
/// <summary>
/// Run <see cref="MoveToPlayer"/> on a worker thread so ChatBot callbacks can poll the result without blocking MCC updates.
/// </summary>
public Task<MccGameResult<MccMoveToPlayerResult>> MoveToPlayerAsync(string playerName, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, int timeoutMs = 0)
public async Task<MccGameResult<MccMoveToPlayerResult>> 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<MccMoveToPlayerResult>.Fail("invalid_args");
if (!AreValidPathOffsets(maxOffset, minOffset) || timeoutMs < 0)
return MccGameResult<MccMoveToPlayerResult>.Fail("invalid_args");
McClient? client = clientProvider();
if (client is null)
return NotConnected<MccMoveToPlayerResult>();
if (!client.GetTerrainEnabled() || !client.GetEntityHandlingEnabled())
return MccGameResult<MccMoveToPlayerResult>.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<MccMoveToPlayerResult>.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<MccMoveToPlayerResult>.Ok(resultData)
: MccGameResult<MccMoveToPlayerResult>.Fail("action_incomplete", data: resultData);
}
/// <summary>
@ -1055,9 +1122,94 @@ public sealed class MccGameApi
/// <summary>
/// Run <see cref="PickupItems"/> on a worker thread so ChatBot callbacks can poll the result without blocking MCC updates.
/// </summary>
public Task<MccGameResult<MccPickupItemsResult>> PickupItemsAsync(string itemType, double radius = 16, int maxItems = 10, bool allowUnsafe = false, int timeoutMs = 0)
public async Task<MccGameResult<MccPickupItemsResult>> 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<MccPickupItemsResult>.Fail("invalid_args");
if (!MccGameCommon.TryParseItemType(itemType.Trim(), out ItemType parsedItemType))
return MccGameResult<MccPickupItemsResult>.Fail("invalid_args");
McClient? client = clientProvider();
if (client is null)
return NotConnected<MccPickupItemsResult>();
if (!client.GetTerrainEnabled() || !client.GetEntityHandlingEnabled())
return MccGameResult<MccPickupItemsResult>.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<MccPickupItemsResult>.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<MccPickupAttempt> 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<MccPickupItemsResult>.Ok(resultData)
: MccGameResult<MccPickupItemsResult>.Fail("action_incomplete", data: resultData);
}
private static MccGameResult<T> NotConnected<T>()
@ -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<bool> 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);