mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
More tools, added debug tools, fixed issues with some tools.
This commit is contained in:
parent
79b091cab6
commit
7f9023e7bb
16 changed files with 4005 additions and 72 deletions
14
DebugTools/MccMcpSampleClient/MccMcpSampleClient.csproj
Normal file
14
DebugTools/MccMcpSampleClient/MccMcpSampleClient.csproj
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
211
DebugTools/MccMcpSampleClient/Program.cs
Normal file
211
DebugTools/MccMcpSampleClient/Program.cs
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
string endpoint = Environment.GetEnvironmentVariable("MCC_MCP_ENDPOINT") ?? "http://127.0.0.1:33333/mcp";
|
||||
string model = "minimax/minimax-m2.7";
|
||||
bool useStdio = string.Equals(Environment.GetEnvironmentVariable("MCC_MCP_USE_STDIO"), "1", StringComparison.Ordinal);
|
||||
string? openRouterApiKey = Environment.GetEnvironmentVariable("OPENROUTER_API_KEY");
|
||||
string openRouterBaseUrl = Environment.GetEnvironmentVariable("OPENROUTER_BASE_URL") ?? "https://openrouter.ai/api/v1";
|
||||
string? mcpAuthToken = Environment.GetEnvironmentVariable("MCC_MCP_AUTH_TOKEN");
|
||||
|
||||
await using McpClient client = useStdio
|
||||
? await McpClient.CreateAsync(new StdioClientTransport(CreateStdioOptions()))
|
||||
: await McpClient.CreateAsync(new HttpClientTransport(new HttpClientTransportOptions
|
||||
{
|
||||
Endpoint = new Uri(endpoint),
|
||||
TransportMode = HttpTransportMode.AutoDetect,
|
||||
AdditionalHeaders = string.IsNullOrWhiteSpace(mcpAuthToken)
|
||||
? null
|
||||
: new Dictionary<string, string> { ["Authorization"] = $"Bearer {mcpAuthToken}" }
|
||||
}));
|
||||
|
||||
var executed = new List<object>();
|
||||
|
||||
CallToolResult sessionStatus = await CallAndStore("mcc_session_status");
|
||||
await CallAndStore("mcc_players_list");
|
||||
await CallAndStore("mcc_send_chat", new Dictionary<string, object?> { ["text"] = "/say mcp_full_sweep" });
|
||||
await CallAndStore("mcc_run_internal_command", new Dictionary<string, object?> { ["command"] = "debug state" });
|
||||
|
||||
(double lookX, double lookY, double lookZ) = GetLookTarget(sessionStatus);
|
||||
await CallAndStore("mcc_look_at", new Dictionary<string, object?> { ["x"] = lookX, ["y"] = lookY, ["z"] = lookZ });
|
||||
await CallAndStore("mcc_move_to", new Dictionary<string, object?> { ["x"] = lookX, ["y"] = lookY, ["z"] = lookZ, ["timeoutMs"] = 2000 });
|
||||
|
||||
CallToolResult inventorySnapshot = await CallAndStore("mcc_inventory_snapshot", new Dictionary<string, object?> { ["inventoryId"] = 0 });
|
||||
int actionSlot = GetInventoryActionSlot(inventorySnapshot);
|
||||
await CallAndStore("mcc_inventory_window_action", new Dictionary<string, object?> { ["inventoryId"] = 0, ["slotId"] = actionSlot, ["actionType"] = "LeftClick" });
|
||||
|
||||
await CallAndStore("mcc_entities_query", new Dictionary<string, object?> { ["maxCount"] = 20 });
|
||||
CallToolResult entitiesList = await CallAndStore("mcc_entities_list", new Dictionary<string, object?> { ["maxCount"] = 20 });
|
||||
int? firstEntityId = GetFirstEntityId(entitiesList);
|
||||
if (firstEntityId.HasValue)
|
||||
{
|
||||
await CallAndStore("mcc_entity_info", new Dictionary<string, object?>
|
||||
{
|
||||
["entityId"] = firstEntityId.Value,
|
||||
["includeMetadata"] = false,
|
||||
["includeEquipment"] = true,
|
||||
["includeEffects"] = true
|
||||
});
|
||||
}
|
||||
await CallAndStore("mcc_blocks_find", new Dictionary<string, object?> { ["query"] = "Grass", ["radius"] = 6, ["maxCount"] = 50 });
|
||||
await CallAndStore("mcc_player_nearby", new Dictionary<string, object?> { ["radius"] = 48.0, ["includeSelf"] = false });
|
||||
await CallAndStore("mcc_world_block_at", new Dictionary<string, object?> { ["x"] = 0, ["y"] = 80, ["z"] = 0 });
|
||||
|
||||
string evidenceJson = JsonSerializer.Serialize(executed, new JsonSerializerOptions { WriteIndented = true });
|
||||
Console.WriteLine(evidenceJson);
|
||||
|
||||
if (!useStdio && !string.IsNullOrWhiteSpace(openRouterApiKey))
|
||||
{
|
||||
using HttpClient http = new();
|
||||
http.BaseAddress = new Uri(openRouterBaseUrl.TrimEnd('/') + "/");
|
||||
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", openRouterApiKey);
|
||||
http.DefaultRequestHeaders.Add("HTTP-Referer", "https://localhost/mcc-mcp-sample");
|
||||
http.DefaultRequestHeaders.Add("X-Title", "MCC MCP Sample Client");
|
||||
|
||||
var payload = new
|
||||
{
|
||||
model,
|
||||
messages = new object[]
|
||||
{
|
||||
new { role = "system", content = "Summarize the MCP tool execution output briefly." },
|
||||
new { role = "user", content = evidenceJson }
|
||||
}
|
||||
};
|
||||
|
||||
HttpResponseMessage response = await http.PostAsync(
|
||||
"chat/completions",
|
||||
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
|
||||
|
||||
string body = await response.Content.ReadAsStringAsync();
|
||||
Console.WriteLine(body);
|
||||
}
|
||||
|
||||
async Task<CallToolResult> CallAndStore(string toolName, IReadOnlyDictionary<string, object?>? args = null)
|
||||
{
|
||||
CallToolResult result = await client.CallToolAsync(toolName, args);
|
||||
executed.Add(new
|
||||
{
|
||||
tool = toolName,
|
||||
arguments = args,
|
||||
isError = result.IsError,
|
||||
result = result
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
static (double x, double y, double z) GetLookTarget(CallToolResult sessionStatus)
|
||||
{
|
||||
JsonElement? data = TryReadData(sessionStatus);
|
||||
if (data is JsonElement jsonData &&
|
||||
jsonData.TryGetProperty("location", out JsonElement location) &&
|
||||
TryReadDouble(location, "x", out double x) &&
|
||||
TryReadDouble(location, "y", out double y) &&
|
||||
TryReadDouble(location, "z", out double z))
|
||||
{
|
||||
return (x, y, z);
|
||||
}
|
||||
|
||||
return (0.5, 80.0, 0.5);
|
||||
}
|
||||
|
||||
static int GetInventoryActionSlot(CallToolResult inventorySnapshot)
|
||||
{
|
||||
JsonElement? data = TryReadData(inventorySnapshot);
|
||||
if (data is JsonElement jsonData &&
|
||||
jsonData.TryGetProperty("slots", out JsonElement slots) &&
|
||||
slots.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (JsonElement slot in slots.EnumerateArray())
|
||||
{
|
||||
if (TryReadInt(slot, "slot", out int slotId))
|
||||
return slotId;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int? GetFirstEntityId(CallToolResult entitiesList)
|
||||
{
|
||||
JsonElement? data = TryReadData(entitiesList);
|
||||
if (data is not JsonElement jsonData)
|
||||
return null;
|
||||
|
||||
if (!jsonData.TryGetProperty("entities", out JsonElement entities)
|
||||
|| entities.ValueKind != JsonValueKind.Array
|
||||
|| entities.GetArrayLength() == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
JsonElement first = entities[0];
|
||||
if (TryReadInt(first, "id", out int entityId))
|
||||
return entityId;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static JsonElement? TryReadData(CallToolResult result)
|
||||
{
|
||||
if (result.Content is null)
|
||||
return null;
|
||||
|
||||
foreach (ContentBlock content in result.Content)
|
||||
{
|
||||
if (content is TextContentBlock text &&
|
||||
!string.IsNullOrWhiteSpace(text.Text))
|
||||
{
|
||||
using JsonDocument doc = JsonDocument.Parse(text.Text);
|
||||
if (doc.RootElement.TryGetProperty("data", out JsonElement data))
|
||||
return data.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool TryReadDouble(JsonElement element, string property, out double value)
|
||||
{
|
||||
value = 0;
|
||||
return element.TryGetProperty(property, out JsonElement prop) && prop.TryGetDouble(out value);
|
||||
}
|
||||
|
||||
static bool TryReadInt(JsonElement element, string property, out int value)
|
||||
{
|
||||
value = 0;
|
||||
return element.TryGetProperty(property, out JsonElement prop) && prop.TryGetInt32(out value);
|
||||
}
|
||||
|
||||
static StdioClientTransportOptions CreateStdioOptions()
|
||||
{
|
||||
string? stdioBin = Environment.GetEnvironmentVariable("MCC_MCP_STDIO_BIN");
|
||||
if (!string.IsNullOrWhiteSpace(stdioBin))
|
||||
{
|
||||
return new StdioClientTransportOptions
|
||||
{
|
||||
Name = "MCC MCP Stdio Harness",
|
||||
Command = stdioBin,
|
||||
Arguments = [],
|
||||
ShutdownTimeout = TimeSpan.FromSeconds(5)
|
||||
};
|
||||
}
|
||||
|
||||
return new StdioClientTransportOptions
|
||||
{
|
||||
Name = "MCC MCP Stdio Harness",
|
||||
Command = "dotnet",
|
||||
Arguments =
|
||||
[
|
||||
"run",
|
||||
"--project",
|
||||
"DebugTools/MccMcpStdioHarness",
|
||||
"-c",
|
||||
"Release",
|
||||
"--no-build"
|
||||
],
|
||||
ShutdownTimeout = TimeSpan.FromSeconds(5)
|
||||
};
|
||||
}
|
||||
18
DebugTools/MccMcpStdioHarness/MccMcpStdioHarness.csproj
Normal file
18
DebugTools/MccMcpStdioHarness/MccMcpStdioHarness.csproj
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\MinecraftClient\MinecraftClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
469
DebugTools/MccMcpStdioHarness/Program.cs
Normal file
469
DebugTools/MccMcpStdioHarness/Program.cs
Normal file
|
|
@ -0,0 +1,469 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MinecraftClient.Mcp;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
builder.Logging.AddConsole(options =>
|
||||
{
|
||||
options.LogToStandardErrorThreshold = LogLevel.Trace;
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<IMccMcpCapabilities, DeterministicCapabilities>();
|
||||
builder.Services.AddMcpServer()
|
||||
.WithStdioServerTransport()
|
||||
.WithTools<MccMcpToolSet>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
|
||||
internal sealed class DeterministicCapabilities : IMccMcpCapabilities
|
||||
{
|
||||
private static double C(double value) => Math.Round(value, 2, MidpointRounding.AwayFromZero);
|
||||
|
||||
public MccMcpResult GetSessionStatus() =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
connected = true,
|
||||
host = "deterministic.local",
|
||||
port = 25565,
|
||||
username = "HarnessBot",
|
||||
location = new { x = C(0.5), y = C(80.0), z = C(0.5) }
|
||||
});
|
||||
|
||||
public MccMcpResult GetServerInfo() =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
host = "deterministic.local",
|
||||
port = 25565,
|
||||
tps = 20.0
|
||||
});
|
||||
|
||||
public MccMcpResult GetPlayerState() =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
nickname = "HarnessBot",
|
||||
username = "HarnessBot",
|
||||
health = 20.0f,
|
||||
saturation = 20,
|
||||
gamemode = 1,
|
||||
currentSlot = 1,
|
||||
yaw = 0.0f,
|
||||
pitch = 0.0f,
|
||||
location = new { x = C(0.5), y = C(80.0), z = C(0.5) },
|
||||
effects = new object[0]
|
||||
});
|
||||
|
||||
public MccMcpResult GetPlayersList() =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
players = new[] { "HarnessBot", "PlayerOne" }
|
||||
});
|
||||
|
||||
public MccMcpResult GetChatHistory(int maxCount, bool includeJson) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
count = 2,
|
||||
entries = new object[]
|
||||
{
|
||||
new { timestampUtc = DateTimeOffset.UtcNow.AddSeconds(-10), kind = "chat", text = "<PlayerOne> hello", sender = "PlayerOne", message = "hello", json = includeJson ? "{}" : null },
|
||||
new { timestampUtc = DateTimeOffset.UtcNow.AddSeconds(-5), kind = "system", text = "HarnessBot joined the game", sender = (string?)null, message = (string?)null, json = includeJson ? "{}" : null }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult GetInternalCommands() =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
count = 4,
|
||||
commands = new[]
|
||||
{
|
||||
new { name = "debug", usage = "debug [on|off|state]", description = "Toggle debug or print state." },
|
||||
new { name = "move", usage = "move <x> <y> <z>", description = "Move to location." },
|
||||
new { name = "useitem", usage = "useitem [x] [y] [z]", description = "Use current held item." },
|
||||
new { name = "dig", usage = "dig <x> <y> <z> [duration]", description = "Dig block at location." }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult GetMaterialsList(string? filter, int maxCount) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
total = 3,
|
||||
count = 3,
|
||||
filter,
|
||||
materials = new[]
|
||||
{
|
||||
new { name = "Air", typeLabel = "Air" },
|
||||
new { name = "GrassBlock", typeLabel = "Grass Block" },
|
||||
new { name = "OakLog", typeLabel = "Oak Log" }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult GetBlockTypesList(string? filter, int maxCount) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
total = 3,
|
||||
count = 3,
|
||||
filter,
|
||||
blockTypes = new[]
|
||||
{
|
||||
new { name = "Air", typeLabel = "Air" },
|
||||
new { name = "GrassBlock", typeLabel = "Grass Block" },
|
||||
new { name = "OakLog", typeLabel = "Oak Log" }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult GetEntityTypesList(string? filter, int maxCount) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
total = 3,
|
||||
count = 3,
|
||||
filter,
|
||||
entityTypes = new[]
|
||||
{
|
||||
new { name = "Player", typeLabel = "Player" },
|
||||
new { name = "Item", typeLabel = "Item" },
|
||||
new { name = "Villager", typeLabel = "Villager" }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult SendChat(string text) =>
|
||||
MccMcpResult.Ok(new { echoed = text });
|
||||
|
||||
public MccMcpResult QuitClient() =>
|
||||
MccMcpResult.Ok(new { quitting = true });
|
||||
|
||||
public MccMcpResult RunInternalCommand(string command) =>
|
||||
MccMcpResult.Ok(new { command, status = "Done", output = "deterministic" });
|
||||
|
||||
public MccMcpResult UseItemOnHand() =>
|
||||
MccMcpResult.Ok(new { success = true, action = "use_item_on_hand" });
|
||||
|
||||
public MccMcpResult ChangeHotbarSlot(int slot) =>
|
||||
MccMcpResult.Ok(new { success = true, slot });
|
||||
|
||||
public MccMcpResult UseItemOnBlock(double x, double y, double z) =>
|
||||
MccMcpResult.Ok(new { success = true, x = C(x), y = C(y), z = C(z), action = "useitem" });
|
||||
|
||||
public MccMcpResult DigBlock(double x, double y, double z, double durationSeconds) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
success = true,
|
||||
target = new { x = C(x), y = C(y), z = C(z) },
|
||||
beforeBlock = new { material = "OakLog", typeLabel = "Oak Log", blockId = 137, blockMeta = 0 },
|
||||
afterBlock = new { material = "Air", typeLabel = "Air", blockId = 0, blockMeta = 0 },
|
||||
commandAccepted = true,
|
||||
changed = true,
|
||||
destroyed = true,
|
||||
attempts = 1,
|
||||
attemptedDurationsSeconds = new[] { durationSeconds > 0 ? durationSeconds : 1.5 },
|
||||
distance = 1.5,
|
||||
playerLocation = new { x = C(0.5), y = C(80.0), z = C(0.5) }
|
||||
});
|
||||
|
||||
public MccMcpResult PlaceBlock(int x, int y, int z, string face, string hand, bool lookAtBlock) =>
|
||||
MccMcpResult.Ok(new { success = true, x, y, z, face, hand, lookAtBlock, action = "place_block" });
|
||||
|
||||
public MccMcpResult InteractEntity(int entityId, string interaction, string hand) =>
|
||||
MccMcpResult.Ok(new { success = true, entityId, interaction, hand });
|
||||
|
||||
public MccMcpResult ScanNearbyBlocks(int radius, int maxCount, string? materialFilter) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
center = new { x = 0, y = 79, z = 0 },
|
||||
radius,
|
||||
count = 1,
|
||||
blocks = new[]
|
||||
{
|
||||
new { x = 0, y = 79, z = 0, material = materialFilter ?? "GrassBlock", blockId = 9, blockMeta = 0, distance = 0.0 }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult FindBlocks(string? query, int radius, int maxCount, bool exactMatch) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
center = new { x = 0, y = 79, z = 0 },
|
||||
radius,
|
||||
query,
|
||||
exactMatch,
|
||||
count = 2,
|
||||
blocks = new object[]
|
||||
{
|
||||
new { x = 1, y = 79, z = 0, material = "GrassBlock", typeLabel = "Grass Block", blockId = 9, blockMeta = 0, distance = 1.0 },
|
||||
new { x = 2, y = 79, z = 0, material = "Dirt", typeLabel = "Dirt", blockId = 10, blockMeta = 0, distance = 2.0 }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult IsPlayerNearby(string? playerName, double radius, bool includeSelf) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
radius,
|
||||
playerName,
|
||||
includeSelf,
|
||||
anyNearby = true,
|
||||
count = 1,
|
||||
players = new object[]
|
||||
{
|
||||
new
|
||||
{
|
||||
entityId = 1,
|
||||
uuid = Guid.Empty,
|
||||
name = "PlayerOne",
|
||||
customName = (string?)null,
|
||||
x = C(3.5),
|
||||
y = C(80.0),
|
||||
z = C(0.5),
|
||||
distance = 3.0,
|
||||
latency = 5
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult LocatePlayer(string playerName, bool includeSelf) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
playerName,
|
||||
matchedName = "PlayerOne",
|
||||
entityId = 1,
|
||||
uuid = Guid.Empty,
|
||||
x = C(3.5),
|
||||
y = C(80.0),
|
||||
z = C(0.5),
|
||||
distance = 3.0
|
||||
});
|
||||
|
||||
public MccMcpResult CanReachPosition(double x, double y, double z, bool allowUnsafe, int maxOffset, int minOffset, int timeoutMs) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
reachable = true,
|
||||
exactReachable = true,
|
||||
target = new { x = C(x), y = C(y), z = C(z) },
|
||||
startLocation = new { x = C(0.5), y = C(80.0), z = C(0.5) },
|
||||
finalWaypoint = new { x = C(x), y = C(y), z = C(z) },
|
||||
finalDistance = 0.0,
|
||||
waypointCount = 4,
|
||||
allowUnsafe,
|
||||
maxOffset,
|
||||
minOffset,
|
||||
timeoutMs = timeoutMs <= 0 ? 5000 : timeoutMs
|
||||
});
|
||||
|
||||
public MccMcpResult MoveTo(double x, double y, double z, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
pathFound = true,
|
||||
arrived = true,
|
||||
tolerance = 1.5,
|
||||
verifyWaitMs = 250,
|
||||
target = new { x = C(x), y = C(y), z = C(z) },
|
||||
startLocation = new { x = C(0.5), y = C(80.0), z = C(0.5) },
|
||||
finalLocation = new { x = C(x), y = C(y), z = C(z) },
|
||||
finalDistance = 0.0,
|
||||
distanceMoved = 3.0,
|
||||
allowUnsafe,
|
||||
allowDirectTeleport,
|
||||
maxOffset,
|
||||
minOffset,
|
||||
timeoutMs
|
||||
});
|
||||
|
||||
public MccMcpResult MoveToPlayer(string playerName, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
pathFound = true,
|
||||
arrived = true,
|
||||
tolerance = 1.5,
|
||||
verifyWaitMs = 250,
|
||||
target = new
|
||||
{
|
||||
playerName = "PlayerOne",
|
||||
entityId = 1,
|
||||
x = C(3.5),
|
||||
y = C(80.0),
|
||||
z = C(0.5)
|
||||
},
|
||||
startLocation = new { x = C(0.5), y = C(80.0), z = C(0.5) },
|
||||
finalLocation = new { x = C(3.5), y = C(80.0), z = C(0.5) },
|
||||
finalDistance = 0.0,
|
||||
distanceMoved = 3.0,
|
||||
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) });
|
||||
|
||||
public MccMcpResult GetInventorySnapshot(int inventoryId) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
id = inventoryId,
|
||||
slots = new[]
|
||||
{
|
||||
new { slot = 0, type = "Stone", count = 64 }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult InventoryWindowAction(int inventoryId, int slotId, string actionType) =>
|
||||
MccMcpResult.Ok(new { success = true, inventoryId, slotId, actionType });
|
||||
|
||||
public MccMcpResult DropInventoryItem(string itemType, int count, int inventoryId, bool preferStack) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
success = true,
|
||||
itemType,
|
||||
requestedCount = count,
|
||||
droppedCount = count,
|
||||
beforeCount = 64,
|
||||
afterCount = Math.Max(0, 64 - count),
|
||||
inventoryId,
|
||||
touchedSlots = new[] { 36 },
|
||||
preferStack
|
||||
});
|
||||
|
||||
public MccMcpResult QueryEntities(int maxCount) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
count = 1,
|
||||
entities = new[]
|
||||
{
|
||||
new { id = 1, type = "Player", x = C(0.5), y = C(80.0), z = C(0.5) }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult ListEntities(int maxCount, string? typeFilter, double radius) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
totalTracked = 1,
|
||||
count = 1,
|
||||
entities = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
id = 1,
|
||||
type = "Player",
|
||||
typeLabel = "Player",
|
||||
uuid = Guid.Empty,
|
||||
name = "HarnessBot",
|
||||
customName = (string?)null,
|
||||
x = C(0.5),
|
||||
y = C(80.0),
|
||||
z = C(0.5),
|
||||
distance = 0.0,
|
||||
health = 20.0f,
|
||||
pose = "Standing",
|
||||
latency = 5
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult GetEntityInfo(int entityId, bool includeMetadata, bool includeEquipment, bool includeEffects) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
id = entityId,
|
||||
type = "Player",
|
||||
typeLabel = "Player",
|
||||
uuid = Guid.Empty,
|
||||
name = "HarnessBot",
|
||||
customName = (string?)null,
|
||||
customNameVisible = false,
|
||||
x = C(0.5),
|
||||
y = C(80.0),
|
||||
z = C(0.5),
|
||||
yaw = 0.0f,
|
||||
pitch = 0.0f,
|
||||
health = 20.0f,
|
||||
pose = "Standing",
|
||||
latency = 5,
|
||||
objectData = -1,
|
||||
metadata = includeMetadata ? new { flags = 0 } : null,
|
||||
equipment = includeEquipment ? new[] { new { slot = 0, type = "Stone", count = 1 } } : null,
|
||||
activeEffects = includeEffects ? new object[0] : null
|
||||
});
|
||||
|
||||
public MccMcpResult FindSigns(string text, bool exactMatch, int radius, int maxCount, bool includeBackText) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
text,
|
||||
exactMatch,
|
||||
radius,
|
||||
includeBackText,
|
||||
count = 1,
|
||||
signs = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
x = 2,
|
||||
y = 80,
|
||||
z = 1,
|
||||
material = "OakSign",
|
||||
typeLabel = "Oak Sign",
|
||||
distance = 1.8,
|
||||
isWaxed = false,
|
||||
frontText = new[] { "home", "storage" },
|
||||
backText = includeBackText ? new[] { "north wall" } : Array.Empty<string>(),
|
||||
matchedLines = new[] { text }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult ListItemEntities(string? itemType, double radius, int maxCount) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
itemType = itemType ?? "OakLog",
|
||||
radius,
|
||||
count = 1,
|
||||
items = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
entityId = 99,
|
||||
itemType = "OakLog",
|
||||
typeLabel = "Oak Log",
|
||||
count = 3,
|
||||
x = C(2.5),
|
||||
y = C(80.0),
|
||||
z = C(1.5),
|
||||
distance = 2.24
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult PickupItems(string itemType, double radius, int maxItems, bool allowUnsafe, int timeoutMs) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
itemType,
|
||||
radius,
|
||||
maxItems,
|
||||
allowUnsafe,
|
||||
timeoutMs = timeoutMs <= 0 ? 2500 : timeoutMs,
|
||||
attempted = 1,
|
||||
successfulPickups = 1,
|
||||
collectedCount = 3,
|
||||
initialInventoryCount = 0,
|
||||
finalInventoryCount = 3,
|
||||
remainingNearby = 0,
|
||||
attempts = new object[]
|
||||
{
|
||||
new
|
||||
{
|
||||
entityId = 99,
|
||||
itemType,
|
||||
typeLabel = "Oak Log",
|
||||
expectedCount = 3,
|
||||
target = new { x = C(2.5), y = C(80.0), z = C(1.5) },
|
||||
pathFound = true,
|
||||
arrived = true,
|
||||
entityGone = true,
|
||||
inventoryDelta = 3,
|
||||
startLocation = new { x = C(0.5), y = C(80.0), z = C(0.5) },
|
||||
finalLocation = new { x = C(2.5), y = C(80.0), z = C(1.5) },
|
||||
finalDistance = 0.0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult GetWorldBlockAt(int x, int y, int z) =>
|
||||
MccMcpResult.Ok(new { x, y, z, material = "Air", blockId = 0, blockMeta = 0 });
|
||||
}
|
||||
14
DebugTools/MccMcpWebPlayground/MccMcpWebPlayground.csproj
Normal file
14
DebugTools/MccMcpWebPlayground/MccMcpWebPlayground.csproj
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AllowMissingPrunePackageData>true</AllowMissingPrunePackageData>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
1116
DebugTools/MccMcpWebPlayground/Program.cs
Normal file
1116
DebugTools/MccMcpWebPlayground/Program.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5295",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7104;http://localhost:5295",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
DebugTools/MccMcpWebPlayground/appsettings.json
Normal file
9
DebugTools/MccMcpWebPlayground/appsettings.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
1117
DebugTools/MccMcpWebPlayground/wwwroot/index.html
Normal file
1117
DebugTools/MccMcpWebPlayground/wwwroot/index.html
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -113,6 +113,8 @@ namespace MinecraftClient
|
|||
|
||||
// Entity handling
|
||||
private readonly Dictionary<int, Entity> entities = new();
|
||||
private readonly Lock signDataLock = new();
|
||||
private readonly Dictionary<(int x, int y, int z), (string material, string typeLabel, string[] frontText, string[] backText, bool isWaxed)> knownSigns = new();
|
||||
|
||||
// server TPS
|
||||
private long lastAge = 0;
|
||||
|
|
@ -166,6 +168,21 @@ namespace MinecraftClient
|
|||
public void GetCookie(string key, out byte[]? data) => Cookies.TryGetValue(key, out data);
|
||||
public void SetCookie(string key, byte[] data) => Cookies[key] = data;
|
||||
public void DeleteCookie(string key) => Cookies.Remove(key, out var data);
|
||||
public (Location location, string material, string typeLabel, string[] frontText, string[] backText, bool isWaxed)[] GetKnownSigns()
|
||||
{
|
||||
lock (signDataLock)
|
||||
{
|
||||
return knownSigns
|
||||
.Select(pair => (
|
||||
location: new Location(pair.Key.x, pair.Key.y, pair.Key.z),
|
||||
material: pair.Value.material,
|
||||
typeLabel: pair.Value.typeLabel,
|
||||
frontText: (string[])pair.Value.frontText.Clone(),
|
||||
backText: (string[])pair.Value.backText.Clone(),
|
||||
isWaxed: pair.Value.isWaxed))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
TcpClient client = null!;
|
||||
IMinecraftCom handler = null!;
|
||||
|
|
@ -478,6 +495,7 @@ namespace MinecraftClient
|
|||
physicsInput.Reset();
|
||||
world.Clear();
|
||||
entities.Clear();
|
||||
ClearKnownSigns();
|
||||
ClearInventories();
|
||||
}
|
||||
|
||||
|
|
@ -763,6 +781,7 @@ namespace MinecraftClient
|
|||
handler.Dispose();
|
||||
|
||||
world.Clear();
|
||||
ClearKnownSigns();
|
||||
|
||||
if (timeoutdetector is not null)
|
||||
{
|
||||
|
|
@ -2804,6 +2823,7 @@ namespace MinecraftClient
|
|||
}
|
||||
|
||||
entities.Clear();
|
||||
ClearKnownSigns();
|
||||
ClearInventories();
|
||||
DispatchBotEvent(bot => bot.OnRespawn());
|
||||
}
|
||||
|
|
@ -4036,9 +4056,16 @@ namespace MinecraftClient
|
|||
public void OnBlockChange(Location location, Block block)
|
||||
{
|
||||
world.SetBlock(location, block);
|
||||
if (!IsSignMaterial(block.Type))
|
||||
RemoveKnownSign(location);
|
||||
DispatchBotEvent(bot => bot.OnBlockChange(location, block));
|
||||
}
|
||||
|
||||
public void OnBlockEntityData(Location location, Dictionary<string, object>? nbt)
|
||||
{
|
||||
UpdateKnownSign(location, nbt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when "AutoComplete" completes.
|
||||
/// </summary>
|
||||
|
|
@ -4068,6 +4095,137 @@ namespace MinecraftClient
|
|||
return handler.ClickContainerButton(windowId, buttonId);
|
||||
}
|
||||
|
||||
private void ClearKnownSigns()
|
||||
{
|
||||
lock (signDataLock)
|
||||
{
|
||||
knownSigns.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveKnownSign(Location location)
|
||||
{
|
||||
var key = ToBlockKey(location);
|
||||
lock (signDataLock)
|
||||
{
|
||||
knownSigns.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateKnownSign(Location location, Dictionary<string, object>? nbt)
|
||||
{
|
||||
var key = ToBlockKey(location);
|
||||
var block = world.GetBlock(new Location(key.x, key.y, key.z));
|
||||
if (!IsSignMaterial(block.Type) || !TryExtractSignText(nbt, out string[] frontText, out string[] backText, out bool isWaxed))
|
||||
{
|
||||
lock (signDataLock)
|
||||
{
|
||||
knownSigns.Remove(key);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
lock (signDataLock)
|
||||
{
|
||||
knownSigns[key] = (block.Type.ToString(), block.GetTypeString(), frontText, backText, isWaxed);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryExtractSignText(Dictionary<string, object>? nbt, out string[] frontText, out string[] backText, out bool isWaxed)
|
||||
{
|
||||
frontText = ExtractSignLines(nbt, "front_text");
|
||||
backText = ExtractSignLines(nbt, "back_text");
|
||||
if (frontText.Length == 0 && backText.Length == 0)
|
||||
frontText = ExtractLegacySignLines(nbt);
|
||||
|
||||
isWaxed = nbt is not null
|
||||
&& nbt.TryGetValue("is_waxed", out object? waxedValue)
|
||||
&& waxedValue is bool waxed
|
||||
&& waxed;
|
||||
return frontText.Length > 0 || backText.Length > 0;
|
||||
}
|
||||
|
||||
private static string[] ExtractSignLines(Dictionary<string, object>? nbt, string sideKey)
|
||||
{
|
||||
if (nbt is null
|
||||
|| !nbt.TryGetValue(sideKey, out object? sideValue)
|
||||
|| sideValue is not Dictionary<string, object> sideData
|
||||
|| !sideData.TryGetValue("messages", out object? messagesValue)
|
||||
|| messagesValue is not object[] messages)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return messages
|
||||
.Take(4)
|
||||
.Select(ConvertSignMessage)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static string[] ExtractLegacySignLines(Dictionary<string, object>? nbt)
|
||||
{
|
||||
if (nbt is null)
|
||||
return [];
|
||||
|
||||
List<string> lines = new(4);
|
||||
for (int i = 1; i <= 4; i++)
|
||||
{
|
||||
if (nbt.TryGetValue($"Text{i}", out object? value))
|
||||
lines.Add(ConvertSignMessage(value));
|
||||
}
|
||||
|
||||
return lines.ToArray();
|
||||
}
|
||||
|
||||
private static string ConvertSignMessage(object? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => string.Empty,
|
||||
string text => ParseMaybeJsonText(text),
|
||||
Dictionary<string, object> nbt => ChatParser.ParseText(nbt),
|
||||
object[] items => string.Concat(items.Select(ConvertSignMessage)),
|
||||
_ => value.ToString() ?? string.Empty
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return value?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ParseMaybeJsonText(string text)
|
||||
{
|
||||
string trimmed = text.Trim();
|
||||
if ((trimmed.StartsWith("{", StringComparison.Ordinal) && trimmed.EndsWith("}", StringComparison.Ordinal))
|
||||
|| (trimmed.StartsWith("[", StringComparison.Ordinal) && trimmed.EndsWith("]", StringComparison.Ordinal)))
|
||||
{
|
||||
try
|
||||
{
|
||||
return ChatParser.ParseText(trimmed);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private static bool IsSignMaterial(Material material)
|
||||
{
|
||||
return material.ToString().Contains("Sign", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static (int x, int y, int z) ToBlockKey(Location location)
|
||||
{
|
||||
Location blockLocation = location.ToFloor();
|
||||
return ((int)blockLocation.X, (int)blockLocation.Y, (int)blockLocation.Z);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ public interface IMccMcpCapabilities
|
|||
MccMcpResult GetPlayersList();
|
||||
MccMcpResult GetChatHistory(int maxCount, bool includeJson);
|
||||
MccMcpResult GetInternalCommands();
|
||||
MccMcpResult GetMaterialsList(string? filter, int maxCount);
|
||||
MccMcpResult GetBlockTypesList(string? filter, int maxCount);
|
||||
MccMcpResult GetEntityTypesList(string? filter, int maxCount);
|
||||
MccMcpResult SendChat(string text);
|
||||
MccMcpResult QuitClient();
|
||||
MccMcpResult RunInternalCommand(string command);
|
||||
|
|
@ -21,6 +24,7 @@ public interface IMccMcpCapabilities
|
|||
MccMcpResult FindBlocks(string? query, int radius, int maxCount, bool exactMatch);
|
||||
MccMcpResult IsPlayerNearby(string? playerName, double radius, bool includeSelf);
|
||||
MccMcpResult LocatePlayer(string playerName, bool includeSelf);
|
||||
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);
|
||||
MccMcpResult MoveToPlayer(string playerName, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs);
|
||||
MccMcpResult LookAt(double x, double y, double z);
|
||||
|
|
@ -30,5 +34,8 @@ public interface IMccMcpCapabilities
|
|||
MccMcpResult QueryEntities(int maxCount);
|
||||
MccMcpResult ListEntities(int maxCount, string? typeFilter, double radius);
|
||||
MccMcpResult GetEntityInfo(int entityId, bool includeMetadata, bool includeEquipment, bool includeEffects);
|
||||
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);
|
||||
MccMcpResult GetWorldBlockAt(int x, int y, int z);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
|||
using MinecraftClient.CommandHandler;
|
||||
using MinecraftClient.Inventory;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Protocol.Message;
|
||||
using MinecraftClient.Scripting;
|
||||
|
||||
namespace MinecraftClient.Mcp;
|
||||
|
|
@ -14,13 +15,22 @@ 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
|
||||
{
|
||||
|
|
@ -42,6 +52,18 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
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<MccMcpCapabilityToggles> togglesProvider;
|
||||
|
||||
public MccMcpCapabilities(Func<MccMcpCapabilityToggles> togglesProvider)
|
||||
|
|
@ -226,6 +248,96 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
});
|
||||
}
|
||||
|
||||
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<Material>();
|
||||
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<Material>();
|
||||
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<EntityType>();
|
||||
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))
|
||||
|
|
@ -343,7 +455,13 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
return MccMcpResult.Fail("capability_disabled");
|
||||
|
||||
if (durationSeconds < 0)
|
||||
return MccMcpResult.Fail("invalid_args");
|
||||
{
|
||||
return MccMcpResult.Fail("invalid_args", data: new
|
||||
{
|
||||
parameter = "durationSeconds",
|
||||
min = 0
|
||||
});
|
||||
}
|
||||
|
||||
McClient? client = GetClient();
|
||||
if (client is null)
|
||||
|
|
@ -352,13 +470,74 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
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);
|
||||
string command = durationSeconds > 0
|
||||
? $"dig {sx} {sy} {sz} {durationSeconds.ToString(CultureInfo.InvariantCulture)}"
|
||||
: $"dig {sx} {sy} {sz}";
|
||||
return ExecuteInternalCommand(client, command);
|
||||
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<double> 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)
|
||||
|
|
@ -411,8 +590,15 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
if (!IsCategoryEnabled(t => t.EntityWorld))
|
||||
return MccMcpResult.Fail("capability_disabled");
|
||||
|
||||
if (radius is < 1 or > 8)
|
||||
return MccMcpResult.Fail("invalid_args");
|
||||
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)
|
||||
|
|
@ -444,8 +630,13 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
continue;
|
||||
|
||||
string material = block.Type.ToString();
|
||||
if (filter is not null && !material.Contains(filter, StringComparison.OrdinalIgnoreCase))
|
||||
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;
|
||||
|
|
@ -456,6 +647,7 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
y,
|
||||
z,
|
||||
material,
|
||||
typeLabel,
|
||||
blockId = block.BlockId,
|
||||
blockMeta = block.BlockMeta,
|
||||
distance = Math.Sqrt(dx * dx + dy * dy + dz * dz)
|
||||
|
|
@ -479,8 +671,15 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
if (!IsCategoryEnabled(t => t.EntityWorld))
|
||||
return MccMcpResult.Fail("capability_disabled");
|
||||
|
||||
if (radius is < 1 or > 16)
|
||||
return MccMcpResult.Fail("invalid_args");
|
||||
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)
|
||||
|
|
@ -558,6 +757,61 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
});
|
||||
}
|
||||
|
||||
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<Location>? 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))
|
||||
|
|
@ -672,6 +926,16 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
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();
|
||||
|
|
@ -680,6 +944,7 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
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));
|
||||
|
||||
|
|
@ -687,16 +952,28 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
double tolerance = GetArrivalTolerance(maxOffset, minOffset);
|
||||
Location? finalLocation = null;
|
||||
bool arrived = pathFound && WaitForArrival(client, goal, verifyWaitMs, tolerance, out finalLocation);
|
||||
|
||||
return MccMcpResult.Ok(new
|
||||
finalLocation ??= client.InvokeOnMainThread(client.GetCurrentLocation);
|
||||
object resultData = new
|
||||
{
|
||||
pathFound,
|
||||
arrived,
|
||||
tolerance,
|
||||
verifyWaitMs,
|
||||
target = ToCoordinate(goal),
|
||||
finalLocation = finalLocation is Location location ? ToCoordinate(location) : null
|
||||
});
|
||||
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)
|
||||
|
|
@ -707,6 +984,16 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
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();
|
||||
|
|
@ -718,37 +1005,41 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
return MccMcpResult.Fail("feature_disabled");
|
||||
|
||||
string nameFilter = playerName.Trim();
|
||||
return client.InvokeOnMainThread(() =>
|
||||
NearbyPlayerSnapshot? target = client.InvokeOnMainThread(() =>
|
||||
{
|
||||
List<NearbyPlayerSnapshot> trackedPlayers = BuildTrackedPlayerSnapshots(client, includeSelf: false);
|
||||
NearbyPlayerSnapshot? target = trackedPlayers
|
||||
return trackedPlayers
|
||||
.Where(player => PlayerNameMatches(player, nameFilter))
|
||||
.OrderBy(player => player.Distance)
|
||||
.FirstOrDefault();
|
||||
});
|
||||
|
||||
if (target is null)
|
||||
{
|
||||
return MccMcpResult.Fail("invalid_state", data: new
|
||||
{
|
||||
playerName = nameFilter,
|
||||
trackedPlayers = trackedPlayers
|
||||
string[] trackedPlayers = client.InvokeOnMainThread(() => BuildTrackedPlayerSnapshots(client, includeSelf: false)
|
||||
.Select(player => player.Name)
|
||||
.OfType<string>()
|
||||
.Distinct(NameComparer)
|
||||
.ToArray()
|
||||
.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.MoveTo(goal, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeout);
|
||||
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);
|
||||
|
||||
return MccMcpResult.Ok(new
|
||||
object resultData = new
|
||||
{
|
||||
pathFound,
|
||||
arrived,
|
||||
|
|
@ -762,9 +1053,20 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
y = RoundCoordinate(target.Y),
|
||||
z = RoundCoordinate(target.Z)
|
||||
},
|
||||
finalLocation = finalLocation is Location location ? ToCoordinate(location) : null
|
||||
});
|
||||
});
|
||||
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)
|
||||
|
|
@ -1168,6 +1470,237 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
});
|
||||
}
|
||||
|
||||
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<object> 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))
|
||||
|
|
@ -1436,6 +1969,112 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
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);
|
||||
|
|
@ -1456,6 +2095,51 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
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<char> 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<string, string> uuidToName)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(entity.Name))
|
||||
|
|
@ -1492,12 +2176,30 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
string typeLabel = block.GetTypeString();
|
||||
if (exactMatch)
|
||||
{
|
||||
return material.Equals(filter, StringComparison.OrdinalIgnoreCase)
|
||||
|| typeLabel.Equals(filter, StringComparison.OrdinalIgnoreCase);
|
||||
return TextEqualsFilter(material, filter)
|
||||
|| TextEqualsFilter(typeLabel, filter);
|
||||
}
|
||||
|
||||
return material.Contains(filter, StringComparison.OrdinalIgnoreCase)
|
||||
|| typeLabel.Contains(filter, StringComparison.OrdinalIgnoreCase);
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -49,6 +49,24 @@ public sealed class MccMcpToolSet
|
|||
return capabilities.GetInternalCommands();
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "mcc_materials_list"), Description("List known MCC material names with optional filtering.")]
|
||||
public object MaterialsList(string? filter = null, int maxCount = 500)
|
||||
{
|
||||
return capabilities.GetMaterialsList(filter, maxCount);
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "mcc_block_types_list"), Description("List known MCC block type names with optional filtering.")]
|
||||
public object BlockTypesList(string? filter = null, int maxCount = 500)
|
||||
{
|
||||
return capabilities.GetBlockTypesList(filter, maxCount);
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "mcc_entity_types_list"), Description("List known MCC entity type names with optional filtering.")]
|
||||
public object EntityTypesList(string? filter = null, int maxCount = 500)
|
||||
{
|
||||
return capabilities.GetEntityTypesList(filter, maxCount);
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "mcc_send_chat"), Description("Send chat text or slash-command to the connected Minecraft server.")]
|
||||
public object SendChat([Description("Text to send to server chat.")] string text)
|
||||
{
|
||||
|
|
@ -127,6 +145,12 @@ public sealed class MccMcpToolSet
|
|||
return capabilities.LocatePlayer(playerName, includeSelf);
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "mcc_can_reach_position"), Description("Check whether MCC can currently path to a world coordinate without moving there.")]
|
||||
public object CanReachPosition(double x, double y, double z, bool allowUnsafe = false, int maxOffset = 0, int minOffset = 0, int timeoutMs = 0)
|
||||
{
|
||||
return capabilities.CanReachPosition(x, y, z, allowUnsafe, maxOffset, minOffset, timeoutMs);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
|
|
@ -185,6 +209,24 @@ public sealed class MccMcpToolSet
|
|||
return capabilities.GetEntityInfo(entityId, includeMetadata, includeEquipment, includeEffects);
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "mcc_signs_find"), Description("Find nearby signs whose text exactly matches or contains the requested text.")]
|
||||
public object SignsFind(string text, bool exactMatch = false, int radius = 16, int maxCount = 50, bool includeBackText = true)
|
||||
{
|
||||
return capabilities.FindSigns(text, exactMatch, radius, maxCount, includeBackText);
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "mcc_items_list"), Description("List nearby dropped item entities with optional item type filtering.")]
|
||||
public object ItemsList(string? itemType = null, double radius = 32, int maxCount = 100)
|
||||
{
|
||||
return capabilities.ListItemEntities(itemType, radius, maxCount);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
return capabilities.PickupItems(itemType, radius, maxItems, allowUnsafe, timeoutMs);
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "mcc_world_block_at"), Description("Get block information at world coordinates.")]
|
||||
public object WorldBlockAt(int x, int y, int z)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1569,6 +1569,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
var dataSize = dataTypes.ReadNextVarInt(packetData); // Size
|
||||
|
||||
pTerrain.ProcessChunkColumnData(chunkX, chunkZ, verticalStripBitmask, packetData);
|
||||
ProcessChunkBlockEntityData(chunkX, chunkZ, packetData);
|
||||
Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted);
|
||||
|
||||
// Block Entity data: ignored
|
||||
|
|
@ -2957,17 +2958,16 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
|
||||
// TODO: Use
|
||||
break;
|
||||
|
||||
// Temporarily disabled until I find a fix
|
||||
/*case PacketTypesIn.BlockEntityData:
|
||||
case PacketTypesIn.BlockEntityData:
|
||||
if (handler.GetTerrainEnabled() && protocolVersion >= MC_1_17_Version)
|
||||
{
|
||||
var location_ = dataTypes.ReadNextLocation(packetData);
|
||||
var type_ = dataTypes.ReadNextInt(packetData);
|
||||
dataTypes.ReadNextVarInt(packetData); // Block entity type registry id
|
||||
var nbt = dataTypes.ReadNextNbt(packetData);
|
||||
var nbtJson = JsonConvert.SerializeObject(nbt["messages"]);
|
||||
handler.OnBlockEntityData(location_, nbt);
|
||||
}
|
||||
|
||||
//log.Info($"BLOCK ENTITY DATA -> {location_.ToString()} [{type_}] -> NBT: {nbtJson}");
|
||||
|
||||
break;*/
|
||||
break;
|
||||
|
||||
case PacketTypesIn.SetTickingState:
|
||||
dataTypes.ReadNextFloat(packetData);
|
||||
|
|
@ -3162,6 +3162,24 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
SendPacket(packetPalette.GetOutgoingIdByType(packet), packetData);
|
||||
}
|
||||
|
||||
private void ProcessChunkBlockEntityData(int chunkX, int chunkZ, Queue<byte> packetData)
|
||||
{
|
||||
if (protocolVersion < MC_1_17_Version || packetData.Count == 0)
|
||||
return;
|
||||
|
||||
int blockEntityCount = dataTypes.ReadNextVarInt(packetData);
|
||||
for (int i = 0; i < blockEntityCount; i++)
|
||||
{
|
||||
int packedXZ = dataTypes.ReadNextByte(packetData);
|
||||
int y = dataTypes.ReadNextShort(packetData);
|
||||
dataTypes.ReadNextVarInt(packetData); // Block entity type registry id
|
||||
Dictionary<string, object>? nbt = dataTypes.ReadNextNbt(packetData);
|
||||
int blockX = chunkX * Chunk.SizeX + ((packedXZ >> 4) & 0x0F);
|
||||
int blockZ = chunkZ * Chunk.SizeZ + (packedXZ & 0x0F);
|
||||
handler.OnBlockEntityData(new Location(blockX, y, blockZ), nbt);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a configuration packet to the server. Packet ID, compression, and encryption will be handled automatically.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -508,6 +508,13 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="block">The block</param>
|
||||
public void OnBlockChange(Location location, Block block);
|
||||
|
||||
/// <summary>
|
||||
/// Called when block entity update data is received for a loaded block.
|
||||
/// </summary>
|
||||
/// <param name="location">The block location.</param>
|
||||
/// <param name="nbt">The block entity NBT payload.</param>
|
||||
public void OnBlockEntityData(Location location, Dictionary<string, object>? nbt);
|
||||
|
||||
/// <summary>
|
||||
/// Called when "AutoComplete" completes.
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue