using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using MinecraftClient.CommandHandler; using MinecraftClient.Inventory; using MinecraftClient.Mapping; using MinecraftClient.Protocol.Message; using MinecraftClient.Scripting; namespace MinecraftClient.Mcp; public sealed class MccMcpCapabilities : IMccMcpCapabilities { private static readonly StringComparer NameComparer = StringComparer.OrdinalIgnoreCase; private static readonly double[] s_defaultDigAttemptDurations = [1.5, 3.0, 5.0]; private const int CoordinateRoundingPrecision = 2; private const double SelfEntityDistanceThreshold = 0.2; private const int MaxBlockScanRadius = 12; private const int MaxBlockFindRadius = 32; private const double DigReachDistance = 5.0; private const double DigReachDistanceSquared = DigReachDistance * DigReachDistance; private const int DefaultPathQueryTimeoutMs = 5000; private const int MinPathQueryTimeoutMs = 250; private const int MaxPathQueryTimeoutMs = 15000; private const int DefaultArrivalWaitMs = 3500; private const int MinArrivalWaitMs = 250; private const int MaxArrivalWaitMs = 15000; private const double DefaultArrivalTolerance = 1.5; private const int ArrivalPollIntervalMs = 125; private const int MaxBlockVerifyWaitMs = 12000; private sealed class InternalCommandInfo { public required string Name { get; init; } public required string Usage { get; init; } public required string Description { get; init; } } private sealed class NearbyPlayerSnapshot { public required int EntityId { get; init; } public required Guid Uuid { get; init; } public string? Name { get; set; } public string? CustomName { get; init; } public required double X { get; init; } public required double Y { get; init; } public required double Z { get; init; } public required double Distance { get; init; } public required int Latency { get; init; } } private sealed class NearbyItemSnapshot { public required int EntityId { get; init; } public required ItemType ItemType { get; init; } public required string TypeLabel { get; init; } public required int Count { get; init; } public required double X { get; init; } public required double Y { get; init; } public required double Z { get; init; } public required double Distance { get; init; } } private readonly Func togglesProvider; public MccMcpCapabilities(Func togglesProvider) { this.togglesProvider = togglesProvider; } private static McClient? GetClient() { return McClient.Instance as McClient; } private static MccMcpResult NotConnected() { return MccMcpResult.Fail("disconnected"); } private bool IsCategoryEnabled(Func selector) { return selector(togglesProvider()); } public MccMcpResult GetSessionStatus() { if (!IsCategoryEnabled(t => t.SessionStatus)) return MccMcpResult.Fail("capability_disabled"); McClient? client = GetClient(); if (client is null) return NotConnected(); return client.InvokeOnMainThread(() => { Location location = client.GetCurrentLocation(); return MccMcpResult.Ok(new { host = client.GetServerHost(), port = client.GetServerPort(), username = client.GetUsername(), protocolVersion = client.GetProtocolVersion(), terrainEnabled = client.GetTerrainEnabled(), inventoryEnabled = client.GetInventoryEnabled(), entityEnabled = client.GetEntityHandlingEnabled(), location = ToCoordinate(location) }); }); } public MccMcpResult GetServerInfo() { if (!IsCategoryEnabled(t => t.SessionStatus)) return MccMcpResult.Fail("capability_disabled"); McClient? client = GetClient(); if (client is null) return NotConnected(); return client.InvokeOnMainThread(() => MccMcpResult.Ok(new { host = client.GetServerHost(), port = client.GetServerPort(), tps = client.GetServerTPS() })); } public MccMcpResult GetPlayerState() { if (!IsCategoryEnabled(t => t.SessionStatus)) return MccMcpResult.Fail("capability_disabled"); McClient? client = GetClient(); if (client is null) return NotConnected(); return client.InvokeOnMainThread(() => { Location location = client.GetCurrentLocation(); Dictionary effects = client.GetPlayerEffects(); return MccMcpResult.Ok(new { nickname = client.GetUsername(), username = client.GetUsername(), health = client.GetHealth(), saturation = client.GetSaturation(), gamemode = client.GetGamemode(), currentSlot = client.GetCurrentSlot() + 1, yaw = client.GetYaw(), pitch = client.GetPitch(), location = ToCoordinate(location), effects = effects.Values.Select(effect => new { id = effect.Effect.ToString(), amplifier = effect.Amplifier, remainingSeconds = effect.RemainingSeconds, isInfinite = effect.IsInfinite }).ToArray() }); }); } public MccMcpResult GetPlayersList() { if (!IsCategoryEnabled(t => t.SessionStatus)) return MccMcpResult.Fail("capability_disabled"); McClient? client = GetClient(); if (client is null) return NotConnected(); return client.InvokeOnMainThread(() => MccMcpResult.Ok(new { players = client.GetOnlinePlayers() })); } public MccMcpResult GetChatHistory(int maxCount, bool includeJson) { if (!IsCategoryEnabled(t => t.SessionStatus)) return MccMcpResult.Fail("capability_disabled"); int count = Math.Clamp(maxCount, 1, 500); MccMcpChatHistoryEntry[] entries = MccMcpChatHistoryStore.GetLatest(count); return MccMcpResult.Ok(new { count = entries.Length, entries = entries.Select(entry => new { timestampUtc = entry.TimestampUtc, kind = entry.Kind, text = entry.Text, sender = entry.Sender, message = entry.Message, json = includeJson ? entry.Json : null }).ToArray() }); } public MccMcpResult GetInternalCommands() { if (!IsCategoryEnabled(t => t.ChatAndCommands)) return MccMcpResult.Fail("capability_disabled"); Type[] commandTypes = Program.GetTypesInNamespace("MinecraftClient.Commands"); List commands = new(); foreach (Type type in commandTypes) { if (!type.IsSubclassOf(typeof(Command))) continue; try { if (Activator.CreateInstance(type) is Command cmd) { commands.Add(new InternalCommandInfo { Name = cmd.CmdName, Usage = cmd.CmdUsage, Description = ChatBot.GetVerbatim(cmd.CmdDesc) }); } } catch { // ignore command constructors that fail for reflection-only list generation. } } InternalCommandInfo[] ordered = commands .OrderBy(command => command.Name, StringComparer.OrdinalIgnoreCase) .ToArray(); return MccMcpResult.Ok(new { count = ordered.Length, commands = ordered.Select(command => new { name = command.Name, usage = command.Usage, description = command.Description }).ToArray() }); } public MccMcpResult GetMaterialsList(string? filter, int maxCount) { if (!IsCategoryEnabled(t => t.EntityWorld)) return MccMcpResult.Fail("capability_disabled"); int limit = Math.Clamp(maxCount, 1, 5000); string? normalizedFilter = string.IsNullOrWhiteSpace(filter) ? null : filter.Trim(); Material[] allMaterials = Enum.GetValues(); var materials = allMaterials .Select(material => new { name = material.ToString(), typeLabel = GetMaterialTypeLabel(material) }) .Where(material => normalizedFilter is null || TextMatchesFilter(material.name, normalizedFilter) || TextMatchesFilter(material.typeLabel, normalizedFilter)) .OrderBy(material => material.name, StringComparer.OrdinalIgnoreCase) .Take(limit) .ToArray(); return MccMcpResult.Ok(new { total = allMaterials.Length, count = materials.Length, filter = normalizedFilter, materials }); } public MccMcpResult GetBlockTypesList(string? filter, int maxCount) { if (!IsCategoryEnabled(t => t.EntityWorld)) return MccMcpResult.Fail("capability_disabled"); int limit = Math.Clamp(maxCount, 1, 5000); string? normalizedFilter = string.IsNullOrWhiteSpace(filter) ? null : filter.Trim(); Material[] allMaterials = Enum.GetValues(); var blockTypes = allMaterials .Select(material => new { name = material.ToString(), typeLabel = GetMaterialTypeLabel(material) }) .Where(blockType => normalizedFilter is null || TextMatchesFilter(blockType.name, normalizedFilter) || TextMatchesFilter(blockType.typeLabel, normalizedFilter)) .OrderBy(blockType => blockType.name, StringComparer.OrdinalIgnoreCase) .Take(limit) .ToArray(); return MccMcpResult.Ok(new { total = allMaterials.Length, count = blockTypes.Length, filter = normalizedFilter, blockTypes }); } public MccMcpResult GetEntityTypesList(string? filter, int maxCount) { if (!IsCategoryEnabled(t => t.EntityWorld)) return MccMcpResult.Fail("capability_disabled"); int limit = Math.Clamp(maxCount, 1, 5000); string? normalizedFilter = string.IsNullOrWhiteSpace(filter) ? null : filter.Trim(); EntityType[] allEntityTypes = Enum.GetValues(); var entityTypes = allEntityTypes .Select(entityType => new { name = entityType.ToString(), typeLabel = Entity.GetTypeString(entityType) }) .Where(entityType => normalizedFilter is null || TextMatchesFilter(entityType.name, normalizedFilter) || TextMatchesFilter(entityType.typeLabel, normalizedFilter)) .OrderBy(entityType => entityType.name, StringComparer.OrdinalIgnoreCase) .Take(limit) .ToArray(); return MccMcpResult.Ok(new { total = allEntityTypes.Length, count = entityTypes.Length, filter = normalizedFilter, entityTypes }); } public MccMcpResult SendChat(string text) { if (!IsCategoryEnabled(t => t.ChatAndCommands)) return MccMcpResult.Fail("capability_disabled"); if (string.IsNullOrWhiteSpace(text)) return MccMcpResult.Fail("invalid_args"); string normalized = text.Trim(); if (normalized.Equals("quit", StringComparison.OrdinalIgnoreCase) || normalized.Equals("exit", StringComparison.OrdinalIgnoreCase)) { return MccMcpResult.Fail("internal_command_text_blocked"); } McClient? client = GetClient(); if (client is null) return NotConnected(); bool sent = client.InvokeOnMainThread(() => { client.SendText(normalized); return true; }); return sent ? MccMcpResult.Ok() : MccMcpResult.Fail("action_failed"); } public MccMcpResult QuitClient() { if (!IsCategoryEnabled(t => t.ChatAndCommands)) return MccMcpResult.Fail("capability_disabled"); McClient? client = GetClient(); if (client is null) return NotConnected(); _ = Task.Run(async () => { await Task.Delay(150).ConfigureAwait(false); Program.Exit(); }); return MccMcpResult.Ok(new { quitting = true }); } public MccMcpResult RunInternalCommand(string command) { if (!IsCategoryEnabled(t => t.ChatAndCommands)) return MccMcpResult.Fail("capability_disabled"); if (string.IsNullOrWhiteSpace(command)) return MccMcpResult.Fail("invalid_args"); McClient? client = GetClient(); if (client is null) return NotConnected(); return ExecuteInternalCommand(client, command.Trim()); } public MccMcpResult UseItemOnHand() { if (!IsCategoryEnabled(t => t.Movement)) return MccMcpResult.Fail("capability_disabled"); McClient? client = GetClient(); if (client is null) return NotConnected(); bool ok = client.InvokeOnMainThread(() => client.UseItemOnHand()); return MccMcpResult.Ok(new { success = ok }); } public MccMcpResult ChangeHotbarSlot(int slot) { if (!IsCategoryEnabled(t => t.Inventory)) return MccMcpResult.Fail("capability_disabled"); if (slot is < 1 or > 9) return MccMcpResult.Fail("invalid_args"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetInventoryEnabled()) return MccMcpResult.Fail("feature_disabled"); bool ok = client.InvokeOnMainThread(() => client.ChangeSlot((short)(slot - 1))); return MccMcpResult.Ok(new { success = ok, slot }); } public MccMcpResult UseItemOnBlock(double x, double y, double z) { if (!IsCategoryEnabled(t => t.Movement)) return MccMcpResult.Fail("capability_disabled"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetTerrainEnabled()) return MccMcpResult.Fail("feature_disabled"); string sx = x.ToString(CultureInfo.InvariantCulture); string sy = y.ToString(CultureInfo.InvariantCulture); string sz = z.ToString(CultureInfo.InvariantCulture); return ExecuteInternalCommand(client, $"useitem {sx} {sy} {sz}"); } public MccMcpResult DigBlock(double x, double y, double z, double durationSeconds) { if (!IsCategoryEnabled(t => t.Movement)) return MccMcpResult.Fail("capability_disabled"); if (durationSeconds < 0) { return MccMcpResult.Fail("invalid_args", data: new { parameter = "durationSeconds", min = 0 }); } McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetTerrainEnabled()) return MccMcpResult.Fail("feature_disabled"); Location target = ToBlockLocation(x, y, z); Location currentLocation = client.InvokeOnMainThread(client.GetCurrentLocation); Location eyesLocation = currentLocation.EyesLocation(); Location centeredTarget = target.ToCenter(); Block beforeBlock = client.InvokeOnMainThread(() => client.GetWorld().GetBlock(target)); if (beforeBlock.Type == Material.Air) { return MccMcpResult.Fail("invalid_state", data: new { target = ToCoordinate(target), beforeBlock = ToBlockState(beforeBlock) }); } double distance = eyesLocation.Distance(centeredTarget); if (distance > DigReachDistance) { return MccMcpResult.Fail("action_incomplete", data: new { reason = "too_far", target = ToCoordinate(target), playerLocation = ToCoordinate(currentLocation), distance, maxReach = DigReachDistance, beforeBlock = ToBlockState(beforeBlock) }); } double[] attemptDurations = GetDigAttemptDurations(durationSeconds); List attemptedDurations = new(); Block afterBlock = beforeBlock; bool changed = false; bool commandAccepted = false; foreach (double attemptDuration in attemptDurations) { attemptedDurations.Add(attemptDuration); bool accepted = client.InvokeOnMainThread(() => client.DigBlock(target, Direction.Down, duration: attemptDuration)); commandAccepted |= accepted; if (!accepted) continue; if (WaitForBlockChange(client, target, beforeBlock, GetDigVerifyWaitMs(attemptDuration), out afterBlock)) { changed = true; break; } } afterBlock = client.InvokeOnMainThread(() => client.GetWorld().GetBlock(target)); object resultData = new { success = changed, target = ToCoordinate(target), beforeBlock = ToBlockState(beforeBlock), afterBlock = ToBlockState(afterBlock), commandAccepted, changed, destroyed = changed && afterBlock.Type == Material.Air, attempts = attemptedDurations.Count, attemptedDurationsSeconds = attemptedDurations.ToArray(), distance, playerLocation = ToCoordinate(currentLocation) }; return changed ? MccMcpResult.Ok(resultData) : MccMcpResult.Fail("action_incomplete", data: resultData); } public MccMcpResult PlaceBlock(int x, int y, int z, string face, string hand, bool lookAtBlock) { if (!IsCategoryEnabled(t => t.Movement)) return MccMcpResult.Fail("capability_disabled"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetTerrainEnabled()) return MccMcpResult.Fail("feature_disabled"); if (!Enum.TryParse(face, true, out Direction parsedFace)) return MccMcpResult.Fail("invalid_args"); if (!Enum.TryParse(hand, true, out Hand parsedHand)) return MccMcpResult.Fail("invalid_args"); Location location = new(x, y, z); bool ok = client.InvokeOnMainThread(() => client.PlaceBlock(location, parsedFace, parsedHand, lookAtBlock)); return MccMcpResult.Ok(new { success = ok, x, y, z, face = parsedFace.ToString(), hand = parsedHand.ToString(), lookAtBlock }); } public MccMcpResult InteractEntity(int entityId, string interaction, string hand) { if (!IsCategoryEnabled(t => t.EntityWorld)) return MccMcpResult.Fail("capability_disabled"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetEntityHandlingEnabled()) return MccMcpResult.Fail("feature_disabled"); if (!Enum.TryParse(interaction, true, out InteractType interactType)) return MccMcpResult.Fail("invalid_args"); if (!Enum.TryParse(hand, true, out Hand parsedHand)) return MccMcpResult.Fail("invalid_args"); bool ok = client.InvokeOnMainThread(() => client.InteractEntity(entityId, interactType, parsedHand)); return MccMcpResult.Ok(new { success = ok, entityId, interaction = interactType.ToString(), hand = parsedHand.ToString() }); } public MccMcpResult ScanNearbyBlocks(int radius, int maxCount, string? materialFilter) { if (!IsCategoryEnabled(t => t.EntityWorld)) return MccMcpResult.Fail("capability_disabled"); if (radius is < 1 or > MaxBlockScanRadius) { return MccMcpResult.Fail("invalid_args", data: new { parameter = "radius", min = 1, max = MaxBlockScanRadius }); } McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetTerrainEnabled()) return MccMcpResult.Fail("feature_disabled"); int limit = Math.Clamp(maxCount, 1, 2000); string? filter = string.IsNullOrWhiteSpace(materialFilter) ? null : materialFilter.Trim(); return client.InvokeOnMainThread(() => { Location playerLocation = client.GetCurrentLocation(); int cx = (int)Math.Floor(playerLocation.X); int cy = (int)Math.Floor(playerLocation.Y) - 1; int cz = (int)Math.Floor(playerLocation.Z); List found = new(); World world = client.GetWorld(); for (int y = cy - radius; y <= cy + radius && found.Count < limit; y++) { for (int z = cz - radius; z <= cz + radius && found.Count < limit; z++) { for (int x = cx - radius; x <= cx + radius && found.Count < limit; x++) { Block block = world.GetBlock(new Location(x, y, z)); if (block.Type == Material.Air) continue; string material = block.Type.ToString(); string typeLabel = block.GetTypeString(); if (filter is not null && !TextMatchesFilter(material, filter) && !TextMatchesFilter(typeLabel, filter)) { continue; } double dx = x + 0.5 - playerLocation.X; double dy = y + 0.5 - playerLocation.Y; double dz = z + 0.5 - playerLocation.Z; found.Add(new { x, y, z, material, typeLabel, blockId = block.BlockId, blockMeta = block.BlockMeta, distance = Math.Sqrt(dx * dx + dy * dy + dz * dz) }); } } } return MccMcpResult.Ok(new { center = new { x = cx, y = cy, z = cz }, radius, count = found.Count, blocks = found.ToArray() }); }); } public MccMcpResult FindBlocks(string? query, int radius, int maxCount, bool exactMatch) { if (!IsCategoryEnabled(t => t.EntityWorld)) return MccMcpResult.Fail("capability_disabled"); if (radius is < 1 or > MaxBlockFindRadius) { return MccMcpResult.Fail("invalid_args", data: new { parameter = "radius", min = 1, max = MaxBlockFindRadius }); } McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetTerrainEnabled()) return MccMcpResult.Fail("feature_disabled"); int limit = Math.Clamp(maxCount, 1, 5000); string? filter = string.IsNullOrWhiteSpace(query) ? null : query.Trim(); ParseBlockQuery(filter, out int? blockIdFilter, out int? blockMetaFilter); return client.InvokeOnMainThread(() => { Location playerLocation = client.GetCurrentLocation(); int cx = (int)Math.Floor(playerLocation.X); int cy = (int)Math.Floor(playerLocation.Y) - 1; int cz = (int)Math.Floor(playerLocation.Z); List<(int x, int y, int z, string material, string typeLabel, int blockId, byte blockMeta, double distance)> found = new(); World world = client.GetWorld(); for (int y = cy - radius; y <= cy + radius && found.Count < limit; y++) { for (int z = cz - radius; z <= cz + radius && found.Count < limit; z++) { for (int x = cx - radius; x <= cx + radius && found.Count < limit; x++) { Block block = world.GetBlock(new Location(x, y, z)); if (block.Type == Material.Air) continue; if (!BlockMatches(block, filter, exactMatch, blockIdFilter, blockMetaFilter)) continue; double dx = x + 0.5 - playerLocation.X; double dy = y + 0.5 - playerLocation.Y; double dz = z + 0.5 - playerLocation.Z; found.Add(( x, y, z, block.Type.ToString(), block.GetTypeString(), block.BlockId, block.BlockMeta, Math.Sqrt(dx * dx + dy * dy + dz * dz))); } } } return MccMcpResult.Ok(new { center = new { x = cx, y = cy, z = cz }, radius, query = filter, exactMatch, count = found.Count, blocks = found .OrderBy(entry => entry.distance) .Select(entry => new { entry.x, entry.y, entry.z, entry.material, entry.typeLabel, entry.blockId, entry.blockMeta, entry.distance }) .ToArray() }); }); } public MccMcpResult CanReachPosition(double x, double y, double z, bool allowUnsafe, int maxOffset, int minOffset, int timeoutMs) { if (!IsCategoryEnabled(t => t.Movement)) return MccMcpResult.Fail("capability_disabled"); if (!AreValidPathOffsets(maxOffset, minOffset) || timeoutMs < 0) { return MccMcpResult.Fail("invalid_args", data: new { maxOffset, minOffset, timeoutMs }); } McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetTerrainEnabled()) return MccMcpResult.Fail("feature_disabled"); Location goal = new(x, y, z); Location startLocation = client.InvokeOnMainThread(client.GetCurrentLocation); World world = client.InvokeOnMainThread(client.GetWorld); int effectiveTimeoutMs = GetPathQueryTimeoutMs(timeoutMs); Queue? path = Movement.CalculatePath( world, startLocation, goal, allowUnsafe, maxOffset, minOffset, TimeSpan.FromMilliseconds(effectiveTimeoutMs)); Location? finalWaypoint = path?.LastOrDefault(); double? finalDistance = finalWaypoint is Location waypoint ? GetDistance(waypoint, goal) : null; return MccMcpResult.Ok(new { reachable = path is not null, exactReachable = finalWaypoint is Location location && location.ToFloor() == goal.ToFloor(), target = ToCoordinate(goal), startLocation = ToCoordinate(startLocation), finalWaypoint = finalWaypoint is Location finalLocation ? ToCoordinate(finalLocation) : null, finalDistance, waypointCount = path?.Count ?? 0, allowUnsafe, maxOffset, minOffset, timeoutMs = effectiveTimeoutMs }); } public MccMcpResult IsPlayerNearby(string? playerName, double radius, bool includeSelf) { if (!IsCategoryEnabled(t => t.EntityWorld)) return MccMcpResult.Fail("capability_disabled"); if (radius <= 0 || radius > 1024) return MccMcpResult.Fail("invalid_args"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetEntityHandlingEnabled()) return MccMcpResult.Fail("feature_disabled"); string? nameFilter = string.IsNullOrWhiteSpace(playerName) ? null : playerName.Trim(); return client.InvokeOnMainThread(() => { double radiusValue = radius; List trackedPlayers = BuildTrackedPlayerSnapshots(client, includeSelf); var players = trackedPlayers .Where(player => player.Distance <= radiusValue) .Where(player => { if (nameFilter is null) return true; return PlayerNameMatches(player, nameFilter); }) .OrderBy(player => player.Distance) .Select(player => new { entityId = player.EntityId, uuid = player.Uuid, name = player.Name, customName = player.CustomName, x = RoundCoordinate(player.X), y = RoundCoordinate(player.Y), z = RoundCoordinate(player.Z), distance = player.Distance, latency = player.Latency }) .ToArray(); return MccMcpResult.Ok(new { radius = radiusValue, playerName = nameFilter, includeSelf, anyNearby = players.Length > 0, count = players.Length, players }); }); } public MccMcpResult LocatePlayer(string playerName, bool includeSelf) { if (!IsCategoryEnabled(t => t.EntityWorld)) return MccMcpResult.Fail("capability_disabled"); if (string.IsNullOrWhiteSpace(playerName)) return MccMcpResult.Fail("invalid_args"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetEntityHandlingEnabled()) return MccMcpResult.Fail("feature_disabled"); string nameFilter = playerName.Trim(); return client.InvokeOnMainThread(() => { List trackedPlayers = BuildTrackedPlayerSnapshots(client, includeSelf); NearbyPlayerSnapshot[] matches = trackedPlayers .Where(player => PlayerNameMatches(player, nameFilter)) .OrderBy(player => player.Distance) .ToArray(); if (matches.Length == 0) { return MccMcpResult.Fail("invalid_state", data: new { playerName = nameFilter, trackedPlayers = trackedPlayers .Select(player => player.Name) .OfType() .Distinct(NameComparer) .ToArray() }); } NearbyPlayerSnapshot selected = matches[0]; return MccMcpResult.Ok(new { playerName = nameFilter, matchedName = selected.Name, entityId = selected.EntityId, uuid = selected.Uuid, x = RoundCoordinate(selected.X), y = RoundCoordinate(selected.Y), z = RoundCoordinate(selected.Z), distance = selected.Distance }); }); } public MccMcpResult MoveTo(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"); if (!AreValidPathOffsets(maxOffset, minOffset) || timeoutMs < 0) { return MccMcpResult.Fail("invalid_args", data: new { maxOffset, minOffset, timeoutMs }); } McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetTerrainEnabled()) return MccMcpResult.Fail("feature_disabled"); Location goal = new(x, y, 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 = null; bool arrived = pathFound && WaitForArrival(client, goal, verifyWaitMs, tolerance, out finalLocation); finalLocation ??= client.InvokeOnMainThread(client.GetCurrentLocation); object resultData = new { pathFound, arrived, tolerance, verifyWaitMs, target = ToCoordinate(goal), startLocation = ToCoordinate(startLocation), finalLocation = ToCoordinate(finalLocation.Value), finalDistance = GetDistance(finalLocation.Value, goal), distanceMoved = GetDistance(startLocation, finalLocation.Value), allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeoutMs }; return pathFound && arrived ? MccMcpResult.Ok(resultData) : MccMcpResult.Fail("action_incomplete", data: resultData); } public MccMcpResult MoveToPlayer(string playerName, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs) { if (!IsCategoryEnabled(t => t.Movement)) return MccMcpResult.Fail("capability_disabled"); if (string.IsNullOrWhiteSpace(playerName)) return MccMcpResult.Fail("invalid_args"); if (!AreValidPathOffsets(maxOffset, minOffset) || timeoutMs < 0) { return MccMcpResult.Fail("invalid_args", data: new { maxOffset, minOffset, timeoutMs }); } McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetTerrainEnabled()) return MccMcpResult.Fail("feature_disabled"); if (!client.GetEntityHandlingEnabled()) return MccMcpResult.Fail("feature_disabled"); string nameFilter = playerName.Trim(); NearbyPlayerSnapshot? target = client.InvokeOnMainThread(() => { List trackedPlayers = BuildTrackedPlayerSnapshots(client, includeSelf: false); return trackedPlayers .Where(player => PlayerNameMatches(player, nameFilter)) .OrderBy(player => player.Distance) .FirstOrDefault(); }); if (target is null) { string[] trackedPlayers = client.InvokeOnMainThread(() => BuildTrackedPlayerSnapshots(client, includeSelf: false) .Select(player => player.Name) .OfType() .Distinct(NameComparer) .ToArray()); return MccMcpResult.Fail("invalid_state", data: new { playerName = nameFilter, trackedPlayers }); } 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 = null; bool arrived = pathFound && WaitForArrival(client, goal, verifyWaitMs, tolerance, out finalLocation); finalLocation ??= client.InvokeOnMainThread(client.GetCurrentLocation); object resultData = new { pathFound, arrived, tolerance, verifyWaitMs, target = new { playerName = target.Name, entityId = target.EntityId, x = RoundCoordinate(target.X), y = RoundCoordinate(target.Y), z = RoundCoordinate(target.Z) }, startLocation = ToCoordinate(startLocation), finalLocation = ToCoordinate(finalLocation.Value), finalDistance = GetDistance(finalLocation.Value, goal), distanceMoved = GetDistance(startLocation, finalLocation.Value), allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeoutMs }; return pathFound && arrived ? MccMcpResult.Ok(resultData) : MccMcpResult.Fail("action_incomplete", data: resultData); } public MccMcpResult LookAt(double x, double y, double z) { if (!IsCategoryEnabled(t => t.Movement)) return MccMcpResult.Fail("capability_disabled"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetTerrainEnabled()) return MccMcpResult.Fail("feature_disabled"); Location target = new(x, y, z); client.InvokeOnMainThread(() => client.UpdateLocation(client.GetCurrentLocation(), target)); return MccMcpResult.Ok(); } public MccMcpResult GetInventorySnapshot(int inventoryId) { if (!IsCategoryEnabled(t => t.Inventory)) return MccMcpResult.Fail("capability_disabled"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetInventoryEnabled()) return MccMcpResult.Fail("feature_disabled"); return client.InvokeOnMainThread(() => { Dictionary inventories = client.GetInventories(); if (!inventories.TryGetValue(inventoryId, out Container? inventory)) return MccMcpResult.Fail("invalid_state"); var slots = inventory.Items.Select(item => new { slot = item.Key, type = item.Value.Type.ToString(), count = item.Value.Count }).ToArray(); return MccMcpResult.Ok(new { id = inventory.ID, type = inventory.Type.ToString(), title = inventory.Title, slotCount = inventory.Type.SlotCount(), slots }); }); } public MccMcpResult InventoryWindowAction(int inventoryId, int slotId, string actionType) { if (!IsCategoryEnabled(t => t.Inventory)) return MccMcpResult.Fail("capability_disabled"); if (string.IsNullOrWhiteSpace(actionType)) return MccMcpResult.Fail("invalid_args"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetInventoryEnabled()) return MccMcpResult.Fail("feature_disabled"); if (!TryParseWindowAction(actionType, out WindowActionType parsedAction)) return MccMcpResult.Fail("invalid_args"); bool ok = client.InvokeOnMainThread(() => client.DoWindowAction(inventoryId, slotId, parsedAction)); return MccMcpResult.Ok(new { success = ok, normalizedActionType = parsedAction.ToString() }); } public MccMcpResult DropInventoryItem(string itemType, int count, int inventoryId, bool preferStack) { if (!IsCategoryEnabled(t => t.Inventory)) return MccMcpResult.Fail("capability_disabled"); if (string.IsNullOrWhiteSpace(itemType) || count <= 0) return MccMcpResult.Fail("invalid_args"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetInventoryEnabled()) return MccMcpResult.Fail("feature_disabled"); if (!TryParseItemType(itemType, out ItemType parsedItemType)) { return MccMcpResult.Fail("invalid_args", data: new { itemType = itemType.Trim() }); } return client.InvokeOnMainThread(() => { Dictionary inventories = client.GetInventories(); if (!inventories.TryGetValue(inventoryId, out Container? inventory)) return MccMcpResult.Fail("invalid_state"); var matchingSlotQuery = inventory.Items .Where(pair => pair.Value.Type == parsedItemType && pair.Value.Count > 0) .Select(pair => new { slot = pair.Key, count = pair.Value.Count }); var matchingSlots = (preferStack ? matchingSlotQuery.OrderByDescending(pair => pair.count).ThenBy(pair => pair.slot) : matchingSlotQuery.OrderBy(pair => pair.count).ThenBy(pair => pair.slot)) .ToArray(); int beforeCount = matchingSlots.Sum(pair => pair.count); if (beforeCount < count) { return MccMcpResult.Fail("invalid_state", data: new { itemType = parsedItemType.ToString(), requestedCount = count, availableCount = beforeCount, inventoryId }); } int remaining = count; List touchedSlots = new(); foreach (var entry in matchingSlots) { if (remaining <= 0) break; if (!inventory.Items.TryGetValue(entry.slot, out Item? currentItem) || currentItem.Count <= 0) continue; int dropFromSlot = Math.Min(remaining, currentItem.Count); touchedSlots.Add(entry.slot); bool ok = true; if (dropFromSlot == currentItem.Count) { ok = client.DoWindowAction(inventoryId, entry.slot, WindowActionType.DropItemStack); } else { for (int i = 0; i < dropFromSlot; i++) { if (!client.DoWindowAction(inventoryId, entry.slot, WindowActionType.DropItem)) { ok = false; break; } } } if (!ok) { return MccMcpResult.Fail("action_failed", data: new { itemType = parsedItemType.ToString(), requestedCount = count, droppedCount = count - remaining, remainingCount = remaining, inventoryId, touchedSlots = touchedSlots.ToArray() }); } remaining -= dropFromSlot; } int afterCount = inventory.Items .Where(pair => pair.Value.Type == parsedItemType) .Sum(pair => pair.Value.Count); int droppedCount = beforeCount - afterCount; if (remaining > 0) { return MccMcpResult.Fail("action_failed", data: new { itemType = parsedItemType.ToString(), requestedCount = count, droppedCount, remainingCount = remaining, beforeCount, afterCount, inventoryId, touchedSlots = touchedSlots.ToArray() }); } return MccMcpResult.Ok(new { success = true, itemType = parsedItemType.ToString(), requestedCount = count, droppedCount, beforeCount, afterCount, inventoryId, touchedSlots = touchedSlots.ToArray() }); }); } public MccMcpResult QueryEntities(int maxCount) { if (!IsCategoryEnabled(t => t.EntityWorld)) return MccMcpResult.Fail("capability_disabled"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetEntityHandlingEnabled()) return MccMcpResult.Fail("feature_disabled"); int count = Math.Clamp(maxCount, 1, 1000); return client.InvokeOnMainThread(() => { Dictionary entities = client.GetEntities(); Dictionary playerNamesByEntityId = BuildTrackedPlayerSnapshots(client, includeSelf: true) .ToDictionary(player => player.EntityId, player => player.Name); var data = entities.Take(count) .Select(pair => new { id = pair.Key, type = pair.Value.Type.ToString(), name = pair.Value.Type == EntityType.Player && playerNamesByEntityId.TryGetValue(pair.Key, out string? mappedName) ? mappedName : pair.Value.Name, uuid = pair.Value.UUID, x = RoundCoordinate(pair.Value.Location.X), y = RoundCoordinate(pair.Value.Location.Y), z = RoundCoordinate(pair.Value.Location.Z) }) .ToArray(); return MccMcpResult.Ok(new { count = entities.Count, entities = data }); }); } public MccMcpResult ListEntities(int maxCount, string? typeFilter, double radius) { if (!IsCategoryEnabled(t => t.EntityWorld)) return MccMcpResult.Fail("capability_disabled"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetEntityHandlingEnabled()) return MccMcpResult.Fail("feature_disabled"); int count = Math.Clamp(maxCount, 1, 1000); string? filter = string.IsNullOrWhiteSpace(typeFilter) ? null : typeFilter.Trim(); double radiusValue = Math.Max(radius, 0); return client.InvokeOnMainThread(() => { Dictionary entities = client.GetEntities(); Location playerLocation = client.GetCurrentLocation(); Dictionary playerNamesByEntityId = BuildTrackedPlayerSnapshots(client, includeSelf: true) .ToDictionary(player => player.EntityId, player => player.Name); var data = entities.Values .Select(entity => { double dx = entity.Location.X - playerLocation.X; double dy = entity.Location.Y - playerLocation.Y; double dz = entity.Location.Z - playerLocation.Z; string? resolvedName = entity.Type == EntityType.Player && playerNamesByEntityId.TryGetValue(entity.ID, out string? mappedName) ? mappedName : entity.Name; return new { entity, distance = Math.Sqrt(dx * dx + dy * dy + dz * dz), resolvedName }; }) .Where(item => radiusValue <= 0 || item.distance <= radiusValue) .Where(item => { if (filter is null) return true; return item.entity.Type.ToString().Contains(filter, StringComparison.OrdinalIgnoreCase) || item.entity.GetTypeString().Contains(filter, StringComparison.OrdinalIgnoreCase); }) .OrderBy(item => item.distance) .Take(count) .Select(item => new { id = item.entity.ID, type = item.entity.Type.ToString(), typeLabel = item.entity.GetTypeString(), uuid = item.entity.UUID, name = item.resolvedName, customName = item.entity.CustomName, x = RoundCoordinate(item.entity.Location.X), y = RoundCoordinate(item.entity.Location.Y), z = RoundCoordinate(item.entity.Location.Z), distance = item.distance, health = item.entity.Health, pose = item.entity.Pose.ToString(), latency = item.entity.Latency }) .ToArray(); return MccMcpResult.Ok(new { totalTracked = entities.Count, count = data.Length, entities = data }); }); } public MccMcpResult GetEntityInfo(int entityId, bool includeMetadata, bool includeEquipment, bool includeEffects) { if (!IsCategoryEnabled(t => t.EntityWorld)) return MccMcpResult.Fail("capability_disabled"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetEntityHandlingEnabled()) return MccMcpResult.Fail("feature_disabled"); return client.InvokeOnMainThread(() => { Dictionary entities = client.GetEntities(); if (!entities.TryGetValue(entityId, out Entity? entity)) return MccMcpResult.Fail("invalid_state"); string? resolvedName = entity.Name; if (entity.Type == EntityType.Player) { Dictionary playerNamesByEntityId = BuildTrackedPlayerSnapshots(client, includeSelf: true) .ToDictionary(player => player.EntityId, player => player.Name); if (playerNamesByEntityId.TryGetValue(entityId, out string? mappedName)) resolvedName = mappedName; } object? metadata = includeMetadata ? entity.Metadata?.ToDictionary( pair => pair.Key.ToString(CultureInfo.InvariantCulture), pair => DescribeMetadataValue(pair.Value)) : null; object? equipment = includeEquipment ? entity.Equipment.Select(pair => new { slot = pair.Key, type = pair.Value.Type.ToString(), count = pair.Value.Count }).ToArray() : null; object? activeEffects = includeEffects ? entity.ActiveEffects.Values.Select(effect => new { id = effect.Effect.ToString(), amplifier = effect.Amplifier, remainingSeconds = effect.RemainingSeconds, isInfinite = effect.IsInfinite }).ToArray() : null; return MccMcpResult.Ok(new { id = entity.ID, type = entity.Type.ToString(), typeLabel = entity.GetTypeString(), uuid = entity.UUID, name = resolvedName, customName = entity.CustomName, customNameVisible = entity.IsCustomNameVisible, x = RoundCoordinate(entity.Location.X), y = RoundCoordinate(entity.Location.Y), z = RoundCoordinate(entity.Location.Z), yaw = entity.Yaw, pitch = entity.Pitch, health = entity.Health, pose = entity.Pose.ToString(), latency = entity.Latency, objectData = entity.ObjectData, metadata, equipment, activeEffects }); }); } public MccMcpResult FindSigns(string text, bool exactMatch, int radius, int maxCount, bool includeBackText) { if (!IsCategoryEnabled(t => t.EntityWorld)) return MccMcpResult.Fail("capability_disabled"); if (string.IsNullOrWhiteSpace(text) || radius is < 1 or > MaxBlockFindRadius) return MccMcpResult.Fail("invalid_args"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetTerrainEnabled()) return MccMcpResult.Fail("feature_disabled"); string filter = text.Trim(); int limit = Math.Clamp(maxCount, 1, 500); return client.InvokeOnMainThread(() => { Location playerLocation = client.GetCurrentLocation(); World world = client.GetWorld(); var signs = client.GetKnownSigns() .Select(sign => { double dx = sign.location.X + 0.5 - playerLocation.X; double dy = sign.location.Y + 0.5 - playerLocation.Y; double dz = sign.location.Z + 0.5 - playerLocation.Z; return new { sign, distance = Math.Sqrt(dx * dx + dy * dy + dz * dz) }; }) .Where(entry => entry.distance <= radius) .Where(entry => IsSignMaterial(world.GetBlock(entry.sign.location).Type)) .Select(entry => { string[] frontText = entry.sign.frontText.Where(line => !string.IsNullOrWhiteSpace(line)).ToArray(); string[] backText = includeBackText ? entry.sign.backText.Where(line => !string.IsNullOrWhiteSpace(line)).ToArray() : []; string[] matchedLines = frontText .Concat(backText) .Where(line => exactMatch ? TextEqualsFilter(line, filter) : TextMatchesFilter(line, filter)) .Distinct(NameComparer) .ToArray(); return new { entry.sign, entry.distance, frontText, backText, matchedLines }; }) .Where(entry => entry.matchedLines.Length > 0) .OrderBy(entry => entry.distance) .Take(limit) .Select(entry => new { x = (int)Math.Floor(entry.sign.location.X), y = (int)Math.Floor(entry.sign.location.Y), z = (int)Math.Floor(entry.sign.location.Z), material = entry.sign.material, typeLabel = entry.sign.typeLabel, distance = entry.distance, isWaxed = entry.sign.isWaxed, frontText = entry.frontText, backText = entry.backText, matchedLines = entry.matchedLines }) .ToArray(); return MccMcpResult.Ok(new { text = filter, exactMatch, radius, includeBackText, count = signs.Length, signs }); }); } public MccMcpResult ListItemEntities(string? itemType, double radius, int maxCount) { if (!IsCategoryEnabled(t => t.EntityWorld)) return MccMcpResult.Fail("capability_disabled"); if (radius <= 0 || radius > 1024) return MccMcpResult.Fail("invalid_args"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetEntityHandlingEnabled()) return MccMcpResult.Fail("feature_disabled"); ItemType? parsedItemType = null; string? itemTypeFilter = null; if (!string.IsNullOrWhiteSpace(itemType)) { itemTypeFilter = itemType.Trim(); if (!TryParseItemType(itemTypeFilter, out ItemType resolvedType)) return MccMcpResult.Fail("invalid_args"); parsedItemType = resolvedType; } int limit = Math.Clamp(maxCount, 1, 500); return client.InvokeOnMainThread(() => { NearbyItemSnapshot[] items = BuildNearbyItemSnapshots(client, parsedItemType, radius, limit); return MccMcpResult.Ok(new { itemType = parsedItemType?.ToString() ?? itemTypeFilter, radius, count = items.Length, items = items.Select(item => new { entityId = item.EntityId, itemType = item.ItemType.ToString(), typeLabel = item.TypeLabel, count = item.Count, x = RoundCoordinate(item.X), y = RoundCoordinate(item.Y), z = RoundCoordinate(item.Z), distance = item.Distance }).ToArray() }); }); } public MccMcpResult PickupItems(string itemType, double radius, int maxItems, bool allowUnsafe, int timeoutMs) { if (!IsCategoryEnabled(t => t.EntityWorld) || !IsCategoryEnabled(t => t.Movement)) return MccMcpResult.Fail("capability_disabled"); if (string.IsNullOrWhiteSpace(itemType) || radius <= 0 || radius > 1024 || maxItems < 1 || timeoutMs < 0) return MccMcpResult.Fail("invalid_args"); if (!TryParseItemType(itemType.Trim(), out ItemType parsedItemType)) return MccMcpResult.Fail("invalid_args"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetTerrainEnabled() || !client.GetEntityHandlingEnabled()) return MccMcpResult.Fail("feature_disabled"); int limit = Math.Clamp(maxItems, 1, 50); NearbyItemSnapshot[] targets = client.InvokeOnMainThread(() => BuildNearbyItemSnapshots(client, parsedItemType, radius, limit)); if (targets.Length == 0) { return MccMcpResult.Fail("invalid_state", data: new { itemType = parsedItemType.ToString(), radius, maxItems = limit }); } 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 = null; bool arrived = pathFound && WaitForArrival(client, targetLocation, verifyWaitMs, 2.0, out finalLocation); finalLocation ??= client.InvokeOnMainThread(client.GetCurrentLocation); bool entityGone = WaitForEntityRemoval(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 { entityId = target.EntityId, itemType = target.ItemType.ToString(), typeLabel = target.TypeLabel, expectedCount = target.Count, target = ToCoordinate(target.X, target.Y, target.Z), pathFound, arrived, entityGone, inventoryDelta, startLocation = ToCoordinate(startLocation), finalLocation = ToCoordinate(finalLocation.Value), finalDistance = GetDistance(finalLocation.Value, targetLocation) }); beforeCount = afterCount; } int remainingNearby = client.InvokeOnMainThread(() => BuildNearbyItemSnapshots(client, parsedItemType, radius, 1000).Length); int collectedCount = inventoryEnabled ? Math.Max(0, beforeCount - initialCount) : successfulPickups; object resultData = new { itemType = parsedItemType.ToString(), radius, maxItems = limit, allowUnsafe, timeoutMs = verifyWaitMs, attempted = attempts.Count, successfulPickups, collectedCount, initialInventoryCount = inventoryEnabled ? (int?)initialCount : null, finalInventoryCount = inventoryEnabled ? (int?)beforeCount : null, remainingNearby, attempts = attempts.ToArray() }; return successfulPickups > 0 || collectedCount > 0 ? MccMcpResult.Ok(resultData) : MccMcpResult.Fail("action_incomplete", data: resultData); } public MccMcpResult GetWorldBlockAt(int x, int y, int z) { if (!IsCategoryEnabled(t => t.EntityWorld)) return MccMcpResult.Fail("capability_disabled"); McClient? client = GetClient(); if (client is null) return NotConnected(); if (!client.GetTerrainEnabled()) return MccMcpResult.Fail("feature_disabled"); return client.InvokeOnMainThread(() => { Location location = new(x, y, z); Block block = client.GetWorld().GetBlock(location); return MccMcpResult.Ok(new { x, y, z, material = block.Type.ToString(), blockId = block.BlockId, blockMeta = block.BlockMeta }); }); } private static MccMcpResult ExecuteInternalCommand(McClient client, string command) { return client.InvokeOnMainThread(() => { CmdResult result = new(); bool ok = client.PerformInternalCommand(command, ref result); return MccMcpResult.Ok(new { success = ok, status = result.status.ToString(), output = result.ToString() }); }); } private static List BuildTrackedPlayerSnapshots(McClient client, bool includeSelf) { Location playerLocation = client.GetCurrentLocation(); string username = client.GetUsername(); Dictionary uuidToName = client.GetOnlinePlayersWithUUID(); string[] onlinePlayers = client.GetOnlinePlayers(); List trackedPlayers = client.GetEntities().Values .Where(entity => entity.Type == EntityType.Player) .Select(entity => { double dx = entity.Location.X - playerLocation.X; double dy = entity.Location.Y - playerLocation.Y; double dz = entity.Location.Z - playerLocation.Z; double distance = Math.Sqrt(dx * dx + dy * dy + dz * dz); string? rawName = ResolvePlayerEntityName(entity, uuidToName); return new NearbyPlayerSnapshot { EntityId = entity.ID, Uuid = entity.UUID, Name = rawName, CustomName = entity.CustomName, X = entity.Location.X, Y = entity.Location.Y, Z = entity.Location.Z, Distance = distance, Latency = entity.Latency }; }) .ToList(); if (!includeSelf) { trackedPlayers = trackedPlayers .Where(player => !string.Equals(player.Name, username, StringComparison.OrdinalIgnoreCase)) .Where(player => player.Distance > SelfEntityDistanceThreshold) .ToList(); } List unnamedTracked = trackedPlayers .Where(player => string.IsNullOrWhiteSpace(player.Name)) .OrderBy(player => player.Distance) .ToList(); if (unnamedTracked.Count == 0) return trackedPlayers; HashSet assignedNames = trackedPlayers .Select(player => player.Name) .OfType() .ToHashSet(NameComparer); string[] unmatchedOnline = onlinePlayers .Where(name => includeSelf || !string.Equals(name, username, StringComparison.OrdinalIgnoreCase)) .Where(name => !string.IsNullOrWhiteSpace(name)) .Where(name => !assignedNames.Contains(name)) .Distinct(NameComparer) .ToArray(); if (unmatchedOnline.Length == 0) return trackedPlayers; if (unnamedTracked.Count == 1 && unmatchedOnline.Length == 1) { unnamedTracked[0].Name = unmatchedOnline[0]; return trackedPlayers; } int pairCount = Math.Min(unnamedTracked.Count, unmatchedOnline.Length); string[] sortedNames = unmatchedOnline .OrderBy(name => name, NameComparer) .ToArray(); for (int i = 0; i < pairCount; i++) unnamedTracked[i].Name = sortedNames[i]; return trackedPlayers; } private static object? DescribeMetadataValue(object? value) { return value switch { null => null, string s => s, bool b => b, byte b => b, sbyte b => b, short s => s, ushort s => s, int i => i, uint i => i, long l => l, ulong l => l, float f => f, double d => d, decimal d => d, Enum e => e.ToString(), Location location => ToCoordinate(location), Item item => new { type = item.Type.ToString(), count = item.Count }, byte[] data => new { bytes = data.Length }, _ => value.ToString() }; } private static bool PlayerNameMatches(NearbyPlayerSnapshot player, string filter) { if (string.IsNullOrWhiteSpace(filter)) return true; string trimmed = filter.Trim(); if (!string.IsNullOrWhiteSpace(player.Name) && player.Name.Contains(trimmed, StringComparison.OrdinalIgnoreCase)) return true; if (!string.IsNullOrWhiteSpace(player.CustomName) && player.CustomName.Contains(trimmed, StringComparison.OrdinalIgnoreCase)) return true; return false; } private static bool TryParseWindowAction(string rawActionType, out WindowActionType actionType) { if (Enum.TryParse(rawActionType, true, out actionType)) return true; string normalized = NormalizeToken(rawActionType); if (normalized.Length == 0) return false; return normalized switch { "left" or "leftclick" => SetAction(WindowActionType.LeftClick, out actionType), "right" or "rightclick" => SetAction(WindowActionType.RightClick, out actionType), "middle" or "mid" or "middleclick" => SetAction(WindowActionType.MiddleClick, out actionType), "shift" or "shiftclick" => SetAction(WindowActionType.ShiftClick, out actionType), "shiftright" or "shiftrightclick" => SetAction(WindowActionType.ShiftRightClick, out actionType), "drop" or "dropitem" or "q" => SetAction(WindowActionType.DropItem, out actionType), "dropstack" or "dropall" or "dropitemstack" or "ctrlq" or "ctrldrop" => SetAction(WindowActionType.DropItemStack, out actionType), _ => false }; } private static bool SetAction(WindowActionType value, out WindowActionType actionType) { actionType = value; return true; } private static bool TryParseItemType(string rawItemType, out ItemType itemType) { if (Enum.TryParse(rawItemType, true, out itemType) && itemType is not (ItemType.Unknown or ItemType.Null)) return true; string normalized = NormalizeToken(rawItemType); if (normalized.Length == 0) { itemType = ItemType.Unknown; return false; } foreach (ItemType candidate in Enum.GetValues()) { if (candidate is ItemType.Unknown or ItemType.Null) continue; if (NormalizeToken(candidate.ToString()) == normalized) { itemType = candidate; return true; } } itemType = ItemType.Unknown; return false; } private static string NormalizeToken(string value) { if (string.IsNullOrWhiteSpace(value)) return string.Empty; char[] buffer = value .Where(char.IsLetterOrDigit) .Select(char.ToLowerInvariant) .ToArray(); return new string(buffer); } private static bool WaitForArrival(McClient client, Location goal, int waitMs, double tolerance, out Location? finalLocation) { finalLocation = null; DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); while (true) { Location location = client.InvokeOnMainThread(client.GetCurrentLocation); finalLocation = location; double distance = GetDistance(location, goal); if (distance <= tolerance) return true; if (DateTime.UtcNow >= deadline) return false; Thread.Sleep(ArrivalPollIntervalMs); } } private static double GetDistance(Location from, Location to) { double dx = from.X - to.X; double dy = from.Y - to.Y; double dz = from.Z - to.Z; return Math.Sqrt(dx * dx + dy * dy + dz * dz); } private static int GetArrivalWaitMs(int timeoutMs) { if (timeoutMs <= 0) return DefaultArrivalWaitMs; return Math.Clamp(timeoutMs, MinArrivalWaitMs, MaxArrivalWaitMs); } private static double GetArrivalTolerance(int maxOffset, int minOffset) { double toleranceFromOffset = Math.Max(maxOffset, minOffset) + 1.0; return Math.Max(DefaultArrivalTolerance, toleranceFromOffset); } private static int GetPathQueryTimeoutMs(int timeoutMs) { if (timeoutMs <= 0) return DefaultPathQueryTimeoutMs; return Math.Clamp(timeoutMs, MinPathQueryTimeoutMs, MaxPathQueryTimeoutMs); } private static bool WaitForBlockChange(McClient client, Location target, Block beforeBlock, int waitMs, out Block afterBlock) { afterBlock = beforeBlock; DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); while (true) { Block current = client.InvokeOnMainThread(() => client.GetWorld().GetBlock(target)); afterBlock = current; if (!AreEquivalentBlocks(current, beforeBlock)) return true; if (DateTime.UtcNow >= deadline) return false; Thread.Sleep(ArrivalPollIntervalMs); } } private static bool AreEquivalentBlocks(Block left, Block right) { return left.BlockId == right.BlockId && left.BlockMeta == right.BlockMeta && left.Type == right.Type; } private static double[] GetDigAttemptDurations(double durationSeconds) { if (durationSeconds > 0) return [durationSeconds]; return s_defaultDigAttemptDurations; } private static int GetDigVerifyWaitMs(double durationSeconds) { int waitMs = (int)Math.Ceiling(durationSeconds * 1000) + 2000; return Math.Clamp(waitMs, 1500, MaxBlockVerifyWaitMs); } private static bool AreValidPathOffsets(int maxOffset, int minOffset) { return maxOffset >= 0 && minOffset >= 0 && minOffset <= maxOffset; } private static NearbyItemSnapshot[] BuildNearbyItemSnapshots(McClient client, ItemType? itemType, double radius, int maxCount) { Location playerLocation = client.GetCurrentLocation(); return client.GetEntities().Values .Where(entity => entity.Type == EntityType.Item && !entity.Item.IsEmpty) .Where(entity => !itemType.HasValue || entity.Item.Type == itemType.Value) .Select(entity => { double dx = entity.Location.X - playerLocation.X; double dy = entity.Location.Y - playerLocation.Y; double dz = entity.Location.Z - playerLocation.Z; return new NearbyItemSnapshot { EntityId = entity.ID, ItemType = entity.Item.Type, TypeLabel = entity.Item.GetTypeString(), Count = entity.Item.Count, X = entity.Location.X, Y = entity.Location.Y, Z = entity.Location.Z, Distance = Math.Sqrt(dx * dx + dy * dy + dz * dz) }; }) .Where(item => item.Distance <= radius) .OrderBy(item => item.Distance) .Take(maxCount) .ToArray(); } private static bool WaitForEntityRemoval(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; Thread.Sleep(ArrivalPollIntervalMs); } } private static int GetInventoryItemCount(McClient client, ItemType itemType) { Container? inventory = client.GetInventory(0); if (inventory is null) return 0; return inventory.Items.Values .Where(item => item.Type == itemType) .Sum(item => item.Count); } private static object ToCoordinate(Location location) { return ToCoordinate(location.X, location.Y, location.Z); } private static object ToCoordinate(double x, double y, double z) { return new { x = RoundCoordinate(x), y = RoundCoordinate(y), z = RoundCoordinate(z) }; } private static double RoundCoordinate(double value) { return Math.Round(value, CoordinateRoundingPrecision, MidpointRounding.AwayFromZero); } private static Location ToBlockLocation(double x, double y, double z) { return new Location(Math.Floor(x), Math.Floor(y), Math.Floor(z)); } private static object ToBlockState(Block block) { return new { material = block.Type.ToString(), typeLabel = block.GetTypeString(), blockId = block.BlockId, blockMeta = block.BlockMeta }; } private static string GetMaterialTypeLabel(Material material) { string key = "block.minecraft." + ToTranslationKey(material.ToString()); string? translation = ChatParser.TranslateString(key); return string.IsNullOrEmpty(translation) ? material.ToString() : translation; } private static string ToTranslationKey(string value) { if (string.IsNullOrEmpty(value)) return string.Empty; List chars = new(value.Length * 2); for (int i = 0; i < value.Length; i++) { char current = value[i]; if (char.IsUpper(current) && i > 0 && (char.IsLower(value[i - 1]) || char.IsDigit(value[i - 1]))) chars.Add('_'); chars.Add(char.ToLowerInvariant(current)); } return new string(chars.ToArray()); } private static bool IsSignMaterial(Material material) { return material.ToString().Contains("Sign", StringComparison.Ordinal); } private static string? ResolvePlayerEntityName(Entity entity, IReadOnlyDictionary uuidToName) { if (!string.IsNullOrWhiteSpace(entity.Name)) return entity.Name; if (entity.UUID != Guid.Empty && uuidToName.TryGetValue(entity.UUID.ToString(), out string? mappedName) && !string.IsNullOrWhiteSpace(mappedName)) { return mappedName; } if (!string.IsNullOrWhiteSpace(entity.CustomName)) return entity.CustomName; return null; } private static bool BlockMatches(Block block, string? filter, bool exactMatch, int? blockIdFilter, int? blockMetaFilter) { if (blockIdFilter.HasValue) { if (block.BlockId != blockIdFilter.Value) return false; if (blockMetaFilter.HasValue && block.BlockMeta != blockMetaFilter.Value) return false; return true; } if (filter is null) return true; string material = block.Type.ToString(); string typeLabel = block.GetTypeString(); if (exactMatch) { return TextEqualsFilter(material, filter) || TextEqualsFilter(typeLabel, filter); } return TextMatchesFilter(material, filter) || TextMatchesFilter(typeLabel, filter); } private static bool TextEqualsFilter(string text, string filter) { return text.Equals(filter, StringComparison.OrdinalIgnoreCase) || NormalizeToken(text) == NormalizeToken(filter); } private static bool TextMatchesFilter(string text, string filter) { if (text.Contains(filter, StringComparison.OrdinalIgnoreCase)) return true; string normalizedFilter = NormalizeToken(filter); if (normalizedFilter.Length == 0) return false; return NormalizeToken(text).Contains(normalizedFilter, StringComparison.Ordinal); } private static void ParseBlockQuery(string? query, out int? blockId, out int? blockMeta) { blockId = null; blockMeta = null; if (string.IsNullOrWhiteSpace(query)) return; string trimmed = query.Trim(); int separator = trimmed.IndexOf(':'); if (separator >= 0) { string idPart = trimmed[..separator].Trim(); string metaPart = trimmed[(separator + 1)..].Trim(); if (int.TryParse(idPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedId)) { blockId = parsedId; if (int.TryParse(metaPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedMeta)) blockMeta = parsedMeta; } return; } if (int.TryParse(trimmed, NumberStyles.Integer, CultureInfo.InvariantCulture, out int blockStateId)) blockId = blockStateId; } }