Added a bunch of new useful MCP Tools

This commit is contained in:
Anon 2026-03-30 23:14:28 +02:00
parent 7b415d5388
commit 968800b95a
9 changed files with 2453 additions and 155 deletions

View file

@ -1,4 +1,5 @@
using System;
using MinecraftClient.Mapping;
using MinecraftClient.Mcp;
using MinecraftClient.Scripting;
using Tomlet.Attributes;
@ -58,7 +59,7 @@ namespace MinecraftClient.ChatBots
if (!Config.Enabled)
return;
MccMcpChatHistoryStore.Clear();
ClearStores();
MccMcpConfig mcpConfig = new()
{
@ -86,15 +87,20 @@ namespace MinecraftClient.ChatBots
public override bool OnDisconnect(DisconnectReason reason, string message)
{
MccMcpRecentEventStore.Add("disconnect", new
{
reason = reason.ToString(),
message
});
StopHost();
MccMcpChatHistoryStore.Clear();
ClearStores();
return false;
}
public override void OnUnload()
{
StopHost();
MccMcpChatHistoryStore.Clear();
ClearStores();
}
public override void GetText(string text, string? json)
@ -133,6 +139,120 @@ namespace MinecraftClient.ChatBots
});
}
public override void OnTimeUpdate(long WorldAge, long TimeOfDay)
{
MccMcpRuntimeStateStore.SetTime(WorldAge, TimeOfDay);
}
public override void OnRainLevelChange(float level)
{
MccMcpRuntimeStateStore.SetRainLevel(level);
MccMcpRecentEventStore.Add("weather_rain", new { level });
}
public override void OnThunderLevelChange(float level)
{
MccMcpRuntimeStateStore.SetThunderLevel(level);
MccMcpRecentEventStore.Add("weather_thunder", new { level });
}
public override void OnDeath()
{
MccMcpRecentEventStore.Add("death");
}
public override void OnRespawn()
{
MccMcpRecentEventStore.Add("respawn");
}
public override void OnPlayerJoin(Guid uuid, string name)
{
MccMcpRecentEventStore.Add("player_join", new
{
uuid,
name
});
}
public override void OnPlayerLeave(Guid uuid, string? name)
{
MccMcpRecentEventStore.Add("player_leave", new
{
uuid,
name
});
}
public override void OnInventoryOpen(int inventoryId)
{
MccMcpRecentEventStore.Add("inventory_open", new { inventoryId });
}
public override void OnInventoryClose(int inventoryId)
{
MccMcpRecentEventStore.Add("inventory_close", new { inventoryId });
}
public override void OnTitle(int action, string titletext, string subtitletext, string actionbartext, int fadein, int stay, int fadeout, string json)
{
if (action == 2)
{
MccMcpRecentEventStore.Add("actionbar", new
{
action,
text = actionbartext,
fadein,
stay,
fadeout,
json
});
return;
}
if (action is 0 or 1)
{
MccMcpRecentEventStore.Add("title", new
{
action,
titleText = titletext,
subtitleText = subtitletext,
fadein,
stay,
fadeout,
json
});
}
}
public override void OnBlockBreakAnimation(Entity entity, Location location, byte stage)
{
MccMcpRecentEventStore.Add("block_break_animation", new
{
entityId = entity.ID,
entityType = entity.Type.ToString(),
stage,
location = new
{
x = location.X,
y = location.Y,
z = location.Z
}
});
}
public override void OnEntityAnimation(Entity entity, byte animation)
{
MccMcpRecentEventStore.Add("entity_animation", new
{
entityId = entity.ID,
entityType = entity.Type.ToString(),
animation,
name = entity.Name,
customName = entity.CustomName
});
}
private void StopHost()
{
if (host is null || !host.IsRunning)
@ -143,5 +263,12 @@ namespace MinecraftClient.ChatBots
else
LogToConsole(string.Format(Translations.bot_mcpserver_stop_failed, error ?? "unknown"));
}
private static void ClearStores()
{
MccMcpChatHistoryStore.Clear();
MccMcpRuntimeStateStore.Clear();
MccMcpRecentEventStore.Clear();
}
}
}

View file

@ -5,7 +5,16 @@ public interface IMccMcpCapabilities
MccMcpResult GetSessionStatus();
MccMcpResult GetServerInfo();
MccMcpResult GetPlayerState();
MccMcpResult GetWorldState();
MccMcpResult GetChunkStatus(double? x, double? y, double? z);
MccMcpResult RaycastBlock(double maxDistance, bool includeNeighbors);
MccMcpResult PreviewPath(double x, double y, double z, bool allowUnsafe, int maxOffset, int minOffset, int timeoutMs, int maxWaypoints);
MccMcpResult GetPlayersList();
MccMcpResult GetPlayersDetailed(bool includeSelf, bool includeCoordinates);
MccMcpResult GetPlayerStats();
MccMcpResult GetStatusEffects();
MccMcpResult GetRecentEvents(long afterId, int maxCount, string? typeFilter);
MccMcpResult GetLoadedBots();
MccMcpResult GetChatHistory(int maxCount, bool includeJson);
MccMcpResult GetInternalCommands();
MccMcpResult GetMaterialsList(string? filter, int maxCount);
@ -13,23 +22,34 @@ public interface IMccMcpCapabilities
MccMcpResult GetEntityTypesList(string? filter, int maxCount);
MccMcpResult SendChat(string text);
MccMcpResult QuitClient();
MccMcpResult DisconnectClient();
MccMcpResult Respawn();
MccMcpResult RunInternalCommand(string command);
MccMcpResult PlayAnimation(string hand);
MccMcpResult ToggleSneak(bool enabled);
MccMcpResult ToggleSprint(bool enabled);
MccMcpResult UseItemOnHand();
MccMcpResult ChangeHotbarSlot(int slot);
MccMcpResult SelectHotbarItem(string itemType, bool preferLowestSlot);
MccMcpResult UseItemOnBlock(double x, double y, double z);
MccMcpResult DigBlock(double x, double y, double z, double durationSeconds);
MccMcpResult PlaceBlock(int x, int y, int z, string face, string hand, bool lookAtBlock);
MccMcpResult InteractEntity(int entityId, string interaction, string hand);
MccMcpResult AttackEntity(int entityId);
MccMcpResult ScanNearbyBlocks(int radius, int maxCount, string? materialFilter);
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 FindNearestEntity(string? typeFilter, string? nameFilter, double radius, bool includePlayers);
MccMcpResult CanReachPosition(double x, double y, double z, bool allowUnsafe, int maxOffset, int minOffset, int timeoutMs);
MccMcpResult MoveTo(double x, double y, double z, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs);
MccMcpResult MoveToPlayer(string playerName, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs);
MccMcpResult LookAt(double x, double y, double z);
MccMcpResult LookDirection(string direction);
MccMcpResult LookAngles(float yaw, float pitch);
MccMcpResult ListInventories();
MccMcpResult GetInventorySnapshot(int inventoryId);
MccMcpResult SearchInventories(string query, int maxCount, bool exactMatch, bool includeContainers);
MccMcpResult OpenContainerAt(int x, int y, int z, int timeoutMs, bool closeCurrent);
MccMcpResult CloseContainer(int inventoryId, int timeoutMs);
MccMcpResult InventoryWindowAction(int inventoryId, int slotId, string actionType);

View file

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using MinecraftClient.CommandHandler;
using MinecraftClient.Inventory;
using MinecraftClient.Mapping;
using MinecraftClient.Protocol;
using MinecraftClient.Protocol.Message;
using MinecraftClient.Scripting;
@ -20,6 +21,7 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
private const double SelfEntityDistanceThreshold = 0.2;
private const int MaxBlockScanRadius = 12;
private const int MaxBlockFindRadius = 32;
private const double MaxRaycastDistance = 128.0;
private const double DigReachDistance = 5.0;
private const double DigReachDistanceSquared = DigReachDistance * DigReachDistance;
private const int DefaultPathQueryTimeoutMs = 5000;
@ -35,6 +37,7 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
private const int MinContainerWaitMs = 250;
private const int MaxContainerWaitMs = 20000;
private const int DefaultInventoryActionWaitMs = 3500;
private const int MaxPathPreviewWaypoints = 1000;
private sealed class InternalCommandInfo
{
@ -174,6 +177,226 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
});
}
public MccMcpResult GetWorldState()
{
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();
World world = client.GetWorld();
Dimension dimension = World.GetDimension();
MccMcpRuntimeStateSnapshot runtimeState = MccMcpRuntimeStateStore.GetSnapshot();
int totalChunkCount = world.chunkCnt;
int pendingChunkCount = Math.Max(0, world.chunkLoadNotCompleted);
int loadedChunkCount = GetLoadedChunkCount(world);
return MccMcpResult.Ok(new
{
host = client.GetServerHost(),
port = client.GetServerPort(),
username = client.GetUsername(),
protocol = client.GetProtocolVersion(),
protocolVersion = client.GetProtocolVersion(),
terrainEnabled = client.GetTerrainEnabled(),
inventoryEnabled = client.GetInventoryEnabled(),
entityEnabled = client.GetEntityHandlingEnabled(),
entityHandlingEnabled = client.GetEntityHandlingEnabled(),
location = ToCoordinate(location),
tps = client.GetServerTPS(),
dimension = dimension.Name,
dimensionDetails = new
{
name = dimension.Name,
minY = dimension.minY,
maxY = dimension.maxY,
height = dimension.height,
logicalHeight = dimension.logicalHeight,
coordinateScale = dimension.coordinateScale,
hasSkylight = dimension.hasSkylight,
hasCeiling = dimension.hasCeiling,
fixedTime = dimension.fixedTime >= 0 ? dimension.fixedTime : (long?)null
},
loadedChunkCount,
pendingChunkCount,
totalChunkCount,
loadRatio = GetChunkLoadRatio(world),
worldAge = runtimeState.WorldAge,
timeOfDay = runtimeState.TimeOfDay,
rainLevel = runtimeState.RainLevel,
thunderLevel = runtimeState.ThunderLevel
});
});
}
public MccMcpResult GetChunkStatus(double? x, double? y, double? z)
{
if (!IsCategoryEnabled(t => t.EntityWorld))
return MccMcpResult.Fail("capability_disabled");
if (!HasCompleteCoordinateTriple(x, y, z))
return MccMcpResult.Fail("invalid_args");
McClient? client = GetClient();
if (client is null)
return NotConnected();
if (!client.GetTerrainEnabled())
return MccMcpResult.Fail("feature_disabled");
return client.InvokeOnMainThread(() =>
{
Location queryLocation = x.HasValue && y.HasValue && z.HasValue
? new Location(x.Value, y.Value, z.Value)
: client.GetCurrentLocation();
World world = client.GetWorld();
ChunkColumn? chunkColumn = world.GetChunkColumn(queryLocation);
return MccMcpResult.Ok(new
{
location = ToCoordinate(queryLocation),
chunk = new
{
x = queryLocation.ChunkX,
z = queryLocation.ChunkZ
},
chunkX = queryLocation.ChunkX,
chunkZ = queryLocation.ChunkZ,
loaded = chunkColumn is not null,
fullyLoaded = chunkColumn?.FullyLoaded ?? false,
loadedChunkCount = GetLoadedChunkCount(world),
pendingChunkCount = Math.Max(0, world.chunkLoadNotCompleted),
totalChunkCount = world.chunkCnt,
loadRatio = GetChunkLoadRatio(world)
});
});
}
public MccMcpResult RaycastBlock(double maxDistance, bool includeNeighbors)
{
if (!IsCategoryEnabled(t => t.EntityWorld))
return MccMcpResult.Fail("capability_disabled");
if (maxDistance <= 0 || maxDistance > MaxRaycastDistance)
{
return MccMcpResult.Fail("invalid_args", data: new
{
parameter = "maxDistance",
minExclusive = 0,
max = MaxRaycastDistance
});
}
McClient? client = GetClient();
if (client is null)
return NotConnected();
if (!client.GetTerrainEnabled())
return MccMcpResult.Fail("feature_disabled");
return client.InvokeOnMainThread(() =>
{
Location playerLocation = client.GetCurrentLocation();
Location eyeLocation = playerLocation.EyesLocation();
Tuple<bool, Location, Block> raycast = RaycastHelper.RaycastBlock(client, maxDistance, includeFluids: false);
if (!raycast.Item1)
{
return MccMcpResult.Ok(new
{
hit = false,
maxDistance,
playerLocation = ToCoordinate(playerLocation),
eyeLocation = ToCoordinate(eyeLocation),
location = (object?)null,
block = (object?)null,
distance = (double?)null,
eyeDistance = (double?)null,
neighbors = (object?)null
});
}
Location blockLocation = raycast.Item2;
Block block = raycast.Item3;
Location targetCenter = blockLocation.ToCenter();
object? neighbors = includeNeighbors ? GetNeighborBlockSnapshot(client.GetWorld(), blockLocation) : null;
return MccMcpResult.Ok(new
{
hit = true,
maxDistance,
playerLocation = ToCoordinate(playerLocation),
eyeLocation = ToCoordinate(eyeLocation),
location = ToCoordinate(blockLocation),
block = ToBlockState(block),
distance = playerLocation.Distance(targetCenter),
eyeDistance = eyeLocation.Distance(targetCenter),
neighbors
});
});
}
public MccMcpResult PreviewPath(double x, double y, double z, bool allowUnsafe, int maxOffset, int minOffset, int timeoutMs, int maxWaypoints)
{
if (!IsCategoryEnabled(t => t.Movement))
return MccMcpResult.Fail("capability_disabled");
if (!AreValidPathOffsets(maxOffset, minOffset) || timeoutMs < 0 || maxWaypoints <= 0)
{
return MccMcpResult.Fail("invalid_args", data: new
{
maxOffset,
minOffset,
timeoutMs,
maxWaypoints
});
}
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);
int waypointLimit = Math.Clamp(maxWaypoints, 1, MaxPathPreviewWaypoints);
Queue<Location>? path = Movement.CalculatePath(
world,
startLocation,
goal,
allowUnsafe,
maxOffset,
minOffset,
TimeSpan.FromMilliseconds(effectiveTimeoutMs));
Location[] waypoints = path?.Take(waypointLimit).ToArray() ?? [];
Location? finalWaypoint = path is not null && path.Count > 0 ? path.Last() : null;
return MccMcpResult.Ok(new
{
pathFound = path is not null,
exactReachable = finalWaypoint is Location location && location.ToFloor() == goal.ToFloor(),
target = ToCoordinate(goal),
startLocation = ToCoordinate(startLocation),
finalWaypoint = finalWaypoint is Location waypoint ? ToCoordinate(waypoint) : (object?)null,
finalDistance = finalWaypoint is Location endWaypoint ? GetDistance(endWaypoint, goal) : (double?)null,
waypointCount = path?.Count ?? 0,
truncated = path is not null && path.Count > waypointLimit,
waypoints = waypoints.Select(ToCoordinate).ToArray(),
allowUnsafe,
maxOffset,
minOffset,
timeoutMs = effectiveTimeoutMs
});
}
public MccMcpResult GetPlayersList()
{
if (!IsCategoryEnabled(t => t.SessionStatus))
@ -189,6 +412,202 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
}));
}
public MccMcpResult GetPlayersDetailed(bool includeSelf, bool includeCoordinates)
{
if (!IsCategoryEnabled(t => t.SessionStatus))
return MccMcpResult.Fail("capability_disabled");
McClient? client = GetClient();
if (client is null)
return NotConnected();
return client.InvokeOnMainThread(() =>
{
Dictionary<string, string> onlinePlayers = client.GetOnlinePlayersWithUUID();
Dictionary<Guid, NearbyPlayerSnapshot>? trackedPlayers = client.GetEntityHandlingEnabled()
? BuildTrackedPlayerSnapshots(client, includeSelf: true).ToDictionary(player => player.Uuid)
: null;
Guid selfUuid = client.GetUserUuid();
string selfName = client.GetUsername();
var players = onlinePlayers
.Select(pair =>
{
if (!Guid.TryParse(pair.Key, out Guid uuid))
return null;
bool isSelf = uuid == selfUuid || NameComparer.Equals(pair.Value, selfName);
if (!includeSelf && isSelf)
return null;
PlayerInfo? playerInfo = client.GetPlayerInfo(uuid);
NearbyPlayerSnapshot? trackedPlayer = trackedPlayers is not null
&& trackedPlayers.TryGetValue(uuid, out NearbyPlayerSnapshot? resolvedTrackedPlayer)
? resolvedTrackedPlayer
: null;
Location? selfLocation = isSelf ? client.GetCurrentLocation() : null;
int? entityId = trackedPlayer?.EntityId ?? (isSelf ? client.GetPlayerEntityID() : null);
double? x = includeCoordinates
? trackedPlayer?.X is double trackedX ? RoundCoordinate(trackedX)
: selfLocation.HasValue ? RoundCoordinate(selfLocation.Value.X)
: (double?)null
: null;
double? y = includeCoordinates
? trackedPlayer?.Y is double trackedY ? RoundCoordinate(trackedY)
: selfLocation.HasValue ? RoundCoordinate(selfLocation.Value.Y)
: (double?)null
: null;
double? z = includeCoordinates
? trackedPlayer?.Z is double trackedZ ? RoundCoordinate(trackedZ)
: selfLocation.HasValue ? RoundCoordinate(selfLocation.Value.Z)
: (double?)null
: null;
return new
{
name = playerInfo?.Name ?? pair.Value,
uuid,
ping = playerInfo?.Ping ?? trackedPlayer?.Latency ?? 0,
gamemode = playerInfo?.Gamemode ?? -1,
listed = playerInfo?.Listed ?? true,
displayName = playerInfo?.DisplayName,
entityId,
x,
y,
z
};
})
.Where(player => player is not null)
.OrderBy(player => player!.name, StringComparer.OrdinalIgnoreCase)
.ToArray();
return MccMcpResult.Ok(new
{
includeSelf,
includeCoordinates,
count = players.Length,
players
});
});
}
public MccMcpResult GetPlayerStats()
{
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
{
username = client.GetUsername(),
health = client.GetHealth(),
saturation = client.GetSaturation(),
level = client.GetLevel(),
totalExperience = client.GetTotalExperience(),
gamemode = client.GetGamemode(),
playerEntityId = client.GetPlayerEntityID(),
currentSlot = client.GetCurrentSlot() + 1,
yaw = client.GetYaw(),
pitch = client.GetPitch(),
location = ToCoordinate(location),
tps = client.GetServerTPS()
});
});
}
public MccMcpResult GetStatusEffects()
{
if (!IsCategoryEnabled(t => t.SessionStatus))
return MccMcpResult.Fail("capability_disabled");
McClient? client = GetClient();
if (client is null)
return NotConnected();
return client.InvokeOnMainThread(() =>
{
var effects = client.GetPlayerEffects()
.Values
.Where(effect => !effect.IsExpired)
.OrderBy(effect => effect.Effect)
.Select(effect => new
{
id = effect.Effect.ToString(),
name = effect.GetDisplayName(),
amplifier = effect.Amplifier,
remainingSeconds = effect.RemainingSeconds,
isInfinite = effect.IsInfinite
})
.ToArray();
return MccMcpResult.Ok(new
{
count = effects.Length,
effects
});
});
}
public MccMcpResult GetRecentEvents(long afterId, int maxCount, string? typeFilter)
{
if (!IsCategoryEnabled(t => t.SessionStatus))
return MccMcpResult.Fail("capability_disabled");
McClient? client = GetClient();
if (client is null)
return NotConnected();
MccMcpRecentEventEntry[] events = MccMcpRecentEventStore.GetAfter(afterId, maxCount, typeFilter);
return MccMcpResult.Ok(new
{
afterId,
latestId = MccMcpRecentEventStore.GetLatestId(),
count = events.Length,
events = events.Select(entry => new
{
id = entry.Id,
timestampUtc = entry.TimestampUtc,
type = entry.Type,
data = entry.Data
}).ToArray()
});
}
public MccMcpResult GetLoadedBots()
{
if (!IsCategoryEnabled(t => t.SessionStatus))
return MccMcpResult.Fail("capability_disabled");
McClient? client = GetClient();
if (client is null)
return NotConnected();
return client.InvokeOnMainThread(() =>
{
var bots = client.GetLoadedChatBots()
.Select(bot => new
{
name = bot.GetType().Name,
fullTypeName = bot.GetType().FullName,
isScript = bot is MinecraftClient.ChatBots.Script
})
.OrderBy(bot => bot.name, StringComparer.OrdinalIgnoreCase)
.ToArray();
return MccMcpResult.Ok(new
{
count = bots.Length,
bots
});
});
}
public MccMcpResult GetChatHistory(int maxCount, bool includeJson)
{
if (!IsCategoryEnabled(t => t.SessionStatus))
@ -394,6 +813,48 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
return MccMcpResult.Ok(new { quitting = true });
}
public MccMcpResult DisconnectClient()
{
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);
client.Disconnect();
});
return MccMcpResult.Ok(new { disconnecting = true });
}
public MccMcpResult Respawn()
{
if (!IsCategoryEnabled(t => t.ChatAndCommands))
return MccMcpResult.Fail("capability_disabled");
McClient? client = GetClient();
if (client is null)
return NotConnected();
float health = client.InvokeOnMainThread(client.GetHealth);
if (health > 0)
{
return MccMcpResult.Fail("invalid_state", data: new
{
health
});
}
bool ok = client.InvokeOnMainThread(client.SendRespawnPacket);
return ok
? MccMcpResult.Ok(new { success = true })
: MccMcpResult.Fail("action_failed", data: new { success = false });
}
public MccMcpResult RunInternalCommand(string command)
{
if (!IsCategoryEnabled(t => t.ChatAndCommands))
@ -409,6 +870,67 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
return ExecuteInternalCommand(client, command.Trim());
}
public MccMcpResult PlayAnimation(string hand)
{
if (!IsCategoryEnabled(t => t.Movement))
return MccMcpResult.Fail("capability_disabled");
if (string.IsNullOrWhiteSpace(hand) || !Enum.TryParse(hand, true, out Hand parsedHand))
return MccMcpResult.Fail("invalid_args");
McClient? client = GetClient();
if (client is null)
return NotConnected();
int animation = parsedHand == Hand.MainHand ? 1 : 0;
bool ok = client.DoAnimation(animation);
object resultData = new { success = ok, hand = parsedHand.ToString() };
return ok
? MccMcpResult.Ok(resultData)
: MccMcpResult.Fail("action_failed", data: resultData);
}
public MccMcpResult ToggleSneak(bool enabled)
{
if (!IsCategoryEnabled(t => t.Movement))
return MccMcpResult.Fail("capability_disabled");
McClient? client = GetClient();
if (client is null)
return NotConnected();
EntityActionType action = enabled ? EntityActionType.StartSneaking : EntityActionType.StopSneaking;
bool ok = client.InvokeOnMainThread(() =>
{
bool actionResult = client.SendEntityAction(action);
if (actionResult)
client.IsSneaking = enabled;
return actionResult;
});
object resultData = new { success = ok, enabled };
return ok
? MccMcpResult.Ok(resultData)
: MccMcpResult.Fail("action_failed", data: resultData);
}
public MccMcpResult ToggleSprint(bool enabled)
{
if (!IsCategoryEnabled(t => t.Movement))
return MccMcpResult.Fail("capability_disabled");
McClient? client = GetClient();
if (client is null)
return NotConnected();
EntityActionType action = enabled ? EntityActionType.StartSprinting : EntityActionType.StopSprinting;
bool ok = client.SendEntityAction(action);
object resultData = new { success = ok, enabled };
return ok
? MccMcpResult.Ok(resultData)
: MccMcpResult.Fail("action_failed", data: resultData);
}
public MccMcpResult UseItemOnHand()
{
if (!IsCategoryEnabled(t => t.Movement))
@ -441,6 +963,77 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
return MccMcpResult.Ok(new { success = ok, slot });
}
public MccMcpResult SelectHotbarItem(string itemType, bool preferLowestSlot)
{
if (!IsCategoryEnabled(t => t.Inventory))
return MccMcpResult.Fail("capability_disabled");
if (string.IsNullOrWhiteSpace(itemType))
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(() =>
{
Container? inventory = client.GetInventory(0);
if (inventory is null)
return MccMcpResult.Fail("invalid_state");
var matches = inventory.Items
.Where(pair => pair.Value.Type == parsedItemType && pair.Value.Count > 0)
.Select(pair =>
{
bool isHotbar = inventory.IsHotbar(pair.Key, out int hotbar);
return new
{
inventorySlot = pair.Key,
hotbar,
isHotbar,
count = pair.Value.Count
};
})
.Where(match => match.isHotbar)
.OrderBy(match => preferLowestSlot ? match.hotbar : -match.hotbar)
.ToArray();
if (matches.Length == 0)
{
return MccMcpResult.Fail("invalid_state", data: new
{
itemType = parsedItemType.ToString()
});
}
var selected = matches[0];
bool ok = client.ChangeSlot((short)selected.hotbar);
object resultData = new
{
success = ok,
itemType = parsedItemType.ToString(),
inventorySlot = selected.inventorySlot,
selectedSlot = selected.hotbar + 1,
count = selected.count
};
return ok
? MccMcpResult.Ok(resultData)
: MccMcpResult.Fail("action_failed", data: resultData);
});
}
public MccMcpResult UseItemOnBlock(double x, double y, double z)
{
if (!IsCategoryEnabled(t => t.Movement))
@ -595,6 +1188,37 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
return MccMcpResult.Ok(new { success = ok, entityId, interaction = interactType.ToString(), hand = parsedHand.ToString() });
}
public MccMcpResult AttackEntity(int entityId)
{
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(() =>
{
if (!client.GetEntities().ContainsKey(entityId))
return MccMcpResult.Fail("invalid_state", data: new { entityId });
bool ok = client.InteractEntity(entityId, InteractType.Attack);
object resultData = new
{
success = ok,
entityId,
interaction = InteractType.Attack.ToString()
};
return ok
? MccMcpResult.Ok(resultData)
: MccMcpResult.Fail("action_failed", data: resultData);
});
}
public MccMcpResult ScanNearbyBlocks(int radius, int maxCount, string? materialFilter)
{
if (!IsCategoryEnabled(t => t.EntityWorld))
@ -931,6 +1555,86 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
});
}
public MccMcpResult FindNearestEntity(string? typeFilter, string? nameFilter, double radius, bool includePlayers)
{
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? normalizedTypeFilter = string.IsNullOrWhiteSpace(typeFilter) ? null : typeFilter.Trim();
string? normalizedNameFilter = string.IsNullOrWhiteSpace(nameFilter) ? null : nameFilter.Trim();
return client.InvokeOnMainThread(() =>
{
Location playerLocation = client.GetCurrentLocation();
Dictionary<int, string?> playerNamesByEntityId = BuildTrackedPlayerSnapshots(client, includeSelf: true)
.ToDictionary(player => player.EntityId, player => player.Name);
var nearest = client.GetEntities().Values
.Where(entity => includePlayers || 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;
string? resolvedName = entity.Type == EntityType.Player
&& playerNamesByEntityId.TryGetValue(entity.ID, out string? mappedName)
? mappedName
: entity.Name;
return new
{
entity,
resolvedName,
distance = Math.Sqrt(dx * dx + dy * dy + dz * dz)
};
})
.Where(item => item.distance <= radius)
.Where(item => normalizedTypeFilter is null
|| TextMatchesFilter(item.entity.Type.ToString(), normalizedTypeFilter)
|| TextMatchesFilter(item.entity.GetTypeString(), normalizedTypeFilter))
.Where(item => normalizedNameFilter is null || EntityNameMatches(item.resolvedName, item.entity.CustomName, normalizedNameFilter))
.OrderBy(item => item.distance)
.FirstOrDefault();
if (nearest is null)
{
return MccMcpResult.Fail("invalid_state", data: new
{
typeFilter = normalizedTypeFilter,
nameFilter = normalizedNameFilter,
radius,
includePlayers
});
}
return MccMcpResult.Ok(new
{
id = nearest.entity.ID,
type = nearest.entity.Type.ToString(),
typeLabel = nearest.entity.GetTypeString(),
uuid = nearest.entity.UUID,
name = nearest.resolvedName,
customName = nearest.entity.CustomName,
x = RoundCoordinate(nearest.entity.Location.X),
y = RoundCoordinate(nearest.entity.Location.Y),
z = RoundCoordinate(nearest.entity.Location.Z),
distance = nearest.distance,
health = nearest.entity.Health,
pose = nearest.entity.Pose.ToString(),
latency = nearest.entity.Latency
});
});
}
public MccMcpResult MoveTo(double x, double y, double z, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs)
{
if (!IsCategoryEnabled(t => t.Movement))
@ -1096,6 +1800,60 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
return MccMcpResult.Ok();
}
public MccMcpResult LookDirection(string direction)
{
if (!IsCategoryEnabled(t => t.Movement))
return MccMcpResult.Fail("capability_disabled");
if (string.IsNullOrWhiteSpace(direction) || !Enum.TryParse(direction, true, out Direction parsedDirection) || !IsSupportedLookDirection(parsedDirection))
return MccMcpResult.Fail("invalid_args");
McClient? client = GetClient();
if (client is null)
return NotConnected();
if (!client.GetTerrainEnabled())
return MccMcpResult.Fail("feature_disabled");
return client.InvokeOnMainThread(() =>
{
Location current = client.GetCurrentLocation();
client.UpdateLocation(current, parsedDirection);
return MccMcpResult.Ok(new
{
direction = parsedDirection.ToString(),
yaw = client.GetYaw(),
pitch = client.GetPitch(),
location = ToCoordinate(current)
});
});
}
public MccMcpResult LookAngles(float yaw, float pitch)
{
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");
return client.InvokeOnMainThread(() =>
{
Location current = client.GetCurrentLocation();
client.UpdateLocation(current, yaw, pitch);
return MccMcpResult.Ok(new
{
yaw = client.GetYaw(),
pitch = client.GetPitch(),
location = ToCoordinate(current)
});
});
}
public MccMcpResult GetInventorySnapshot(int inventoryId)
{
if (!IsCategoryEnabled(t => t.Inventory))
@ -1132,6 +1890,69 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
});
}
public MccMcpResult SearchInventories(string query, int maxCount, bool exactMatch, bool includeContainers)
{
if (!IsCategoryEnabled(t => t.Inventory))
return MccMcpResult.Fail("capability_disabled");
if (string.IsNullOrWhiteSpace(query))
return MccMcpResult.Fail("invalid_args");
McClient? client = GetClient();
if (client is null)
return NotConnected();
if (!client.GetInventoryEnabled())
return MccMcpResult.Fail("feature_disabled");
string normalizedQuery = query.Trim();
ItemType? parsedItemType = exactMatch && TryParseItemType(normalizedQuery, out ItemType exactItemType)
? exactItemType
: null;
int limit = Math.Clamp(maxCount, 1, 1000);
return client.InvokeOnMainThread(() =>
{
var matches = client.GetInventories()
.Where(entry => includeContainers || entry.Key == 0)
.OrderBy(entry => entry.Key)
.SelectMany(entry =>
{
Container inventory = entry.Value;
return inventory.Items
.Where(pair => pair.Key >= 0 && pair.Value.Count > 0)
.Where(pair => ItemMatches(pair.Value, normalizedQuery, exactMatch, parsedItemType))
.Select(pair =>
{
bool isHotbar = inventory.IsHotbar(pair.Key, out int hotbar);
return new
{
inventoryId = entry.Key,
inventoryType = inventory.Type.ToString(),
inventoryTitle = inventory.Title,
slot = pair.Key,
itemType = pair.Value.Type.ToString(),
typeLabel = pair.Value.GetTypeString(),
count = pair.Value.Count,
isPlayerInventory = entry.Key == 0,
hotbarSlot = isHotbar ? hotbar + 1 : (int?)null
};
});
})
.Take(limit)
.ToArray();
return MccMcpResult.Ok(new
{
query = normalizedQuery,
exactMatch,
includeContainers,
count = matches.Length,
matches
});
});
}
public MccMcpResult ListInventories()
{
if (!IsCategoryEnabled(t => t.Inventory))
@ -2812,6 +3633,67 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
return maxOffset >= 0 && minOffset >= 0 && minOffset <= maxOffset;
}
private static bool HasCompleteCoordinateTriple(double? x, double? y, double? z)
{
return x.HasValue == y.HasValue && y.HasValue == z.HasValue;
}
private static int GetLoadedChunkCount(World world)
{
return Math.Max(0, world.chunkCnt - Math.Max(0, world.chunkLoadNotCompleted));
}
private static double GetChunkLoadRatio(World world)
{
return world.chunkCnt > 0
? GetLoadedChunkCount(world) / (double)world.chunkCnt
: 0.0;
}
private static object GetNeighborBlockSnapshot(World world, Location location)
{
Location blockLocation = location.ToFloor();
Location north = new(blockLocation.X, blockLocation.Y, blockLocation.Z - 1);
Location south = new(blockLocation.X, blockLocation.Y, blockLocation.Z + 1);
Location east = new(blockLocation.X + 1, blockLocation.Y, blockLocation.Z);
Location west = new(blockLocation.X - 1, blockLocation.Y, blockLocation.Z);
Location above = new(blockLocation.X, blockLocation.Y + 1, blockLocation.Z);
Location below = new(blockLocation.X, blockLocation.Y - 1, blockLocation.Z);
return new
{
north = new { location = ToCoordinate(north), block = ToBlockState(world.GetBlock(north)) },
south = new { location = ToCoordinate(south), block = ToBlockState(world.GetBlock(south)) },
east = new { location = ToCoordinate(east), block = ToBlockState(world.GetBlock(east)) },
west = new { location = ToCoordinate(west), block = ToBlockState(world.GetBlock(west)) },
above = new { location = ToCoordinate(above), block = ToBlockState(world.GetBlock(above)) },
below = new { location = ToCoordinate(below), block = ToBlockState(world.GetBlock(below)) }
};
}
private static bool ItemMatches(Item item, string query, bool exactMatch, ItemType? exactItemType)
{
if (exactItemType.HasValue)
return item.Type == exactItemType.Value;
string typeName = item.Type.ToString();
string typeLabel = item.GetTypeString();
return exactMatch
? TextEqualsFilter(typeName, query) || TextEqualsFilter(typeLabel, query)
: TextMatchesFilter(typeName, query) || TextMatchesFilter(typeLabel, query);
}
private static bool EntityNameMatches(string? name, string? customName, string filter)
{
return (!string.IsNullOrWhiteSpace(name) && TextMatchesFilter(name, filter))
|| (!string.IsNullOrWhiteSpace(customName) && TextMatchesFilter(customName, filter));
}
private static bool IsSupportedLookDirection(Direction direction)
{
return direction is Direction.Up or Direction.Down or Direction.North or Direction.South or Direction.East or Direction.West;
}
private static NearbyItemSnapshot[] BuildNearbyItemSnapshots(McClient client, ItemType? itemType, double radius, int maxCount)
{
Location playerLocation = client.GetCurrentLocation();

View file

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MinecraftClient.Mcp;
public sealed class MccMcpRecentEventEntry
{
public required long Id { get; init; }
public required DateTimeOffset TimestampUtc { get; init; }
public required string Type { get; init; }
public object? Data { get; init; }
}
public static class MccMcpRecentEventStore
{
private static readonly object historyLock = new();
private static readonly List<MccMcpRecentEventEntry> history = new();
private const int MaxEntries = 500;
private static long nextId = 1;
public static long Add(string type, object? data = null)
{
ArgumentException.ThrowIfNullOrEmpty(type);
lock (historyLock)
{
long id = nextId++;
history.Add(new MccMcpRecentEventEntry
{
Id = id,
TimestampUtc = DateTimeOffset.UtcNow,
Type = type,
Data = data
});
if (history.Count > MaxEntries)
history.RemoveRange(0, history.Count - MaxEntries);
return id;
}
}
public static long GetLatestId()
{
lock (historyLock)
{
return history.Count > 0 ? history[^1].Id : 0;
}
}
public static MccMcpRecentEventEntry[] GetAfter(long afterId, int maxCount, string? typeFilter = null)
{
int count = Math.Clamp(maxCount, 1, MaxEntries);
string? normalizedFilter = string.IsNullOrWhiteSpace(typeFilter) ? null : typeFilter.Trim();
lock (historyLock)
{
return history
.Where(entry => entry.Id > afterId)
.Where(entry => normalizedFilter is null
|| entry.Type.Contains(normalizedFilter, StringComparison.OrdinalIgnoreCase))
.Take(count)
.ToArray();
}
}
public static void Clear()
{
lock (historyLock)
{
history.Clear();
}
}
}

View file

@ -0,0 +1,70 @@
using System;
namespace MinecraftClient.Mcp;
public sealed class MccMcpRuntimeStateSnapshot
{
public long? WorldAge { get; init; }
public long? TimeOfDay { get; init; }
public float? RainLevel { get; init; }
public float? ThunderLevel { get; init; }
}
public static class MccMcpRuntimeStateStore
{
private static readonly object stateLock = new();
private static long? worldAge;
private static long? timeOfDay;
private static float? rainLevel;
private static float? thunderLevel;
public static void SetTime(long newWorldAge, long newTimeOfDay)
{
lock (stateLock)
{
worldAge = newWorldAge;
timeOfDay = newTimeOfDay;
}
}
public static void SetRainLevel(float level)
{
lock (stateLock)
{
rainLevel = level;
}
}
public static void SetThunderLevel(float level)
{
lock (stateLock)
{
thunderLevel = level;
}
}
public static MccMcpRuntimeStateSnapshot GetSnapshot()
{
lock (stateLock)
{
return new MccMcpRuntimeStateSnapshot
{
WorldAge = worldAge,
TimeOfDay = timeOfDay,
RainLevel = rainLevel,
ThunderLevel = thunderLevel
};
}
}
public static void Clear()
{
lock (stateLock)
{
worldAge = null;
timeOfDay = null;
rainLevel = null;
thunderLevel = null;
}
}
}

View file

@ -33,12 +33,66 @@ public sealed class MccMcpToolSet
return capabilities.GetPlayerState();
}
[McpServerTool(Name = "mcc_world_state"), Description("Get current world state, chunk loading progress, and last observed runtime time/weather values.")]
public object WorldState()
{
return capabilities.GetWorldState();
}
[McpServerTool(Name = "mcc_chunk_status"), Description("Get chunk loading status for the player location or an explicit world coordinate.")]
public object ChunkStatus(double? x = null, double? y = null, double? z = null)
{
return capabilities.GetChunkStatus(x, y, z);
}
[McpServerTool(Name = "mcc_raycast_block"), Description("Raycast from the player's current view and return the first non-air block hit.")]
public object RaycastBlock(double maxDistance = 8.0, bool includeNeighbors = false)
{
return capabilities.RaycastBlock(maxDistance, includeNeighbors);
}
[McpServerTool(Name = "mcc_path_preview"), Description("Compute a path preview to a target world coordinate without moving there.")]
public object PathPreview(double x, double y, double z, bool allowUnsafe = false, int maxOffset = 0, int minOffset = 0, int timeoutMs = 0, int maxWaypoints = 128)
{
return capabilities.PreviewPath(x, y, z, allowUnsafe, maxOffset, minOffset, timeoutMs, maxWaypoints);
}
[McpServerTool(Name = "mcc_players_list"), Description("List currently known online players.")]
public object PlayersList()
{
return capabilities.GetPlayersList();
}
[McpServerTool(Name = "mcc_players_detailed"), Description("List online players with UUID, latency, gamemode, and tracked coordinates when available.")]
public object PlayersDetailed(bool includeSelf = false, bool includeCoordinates = true)
{
return capabilities.GetPlayersDetailed(includeSelf, includeCoordinates);
}
[McpServerTool(Name = "mcc_player_stats"), Description("Get current controlled player stats, orientation, and location.")]
public object PlayerStats()
{
return capabilities.GetPlayerStats();
}
[McpServerTool(Name = "mcc_status_effects"), Description("Get active player status effects only.")]
public object StatusEffects()
{
return capabilities.GetStatusEffects();
}
[McpServerTool(Name = "mcc_recent_events"), Description("Get recent high-signal MCP runtime events after a given event ID.")]
public object RecentEvents(long afterId = 0, int maxCount = 50, string? typeFilter = null)
{
return capabilities.GetRecentEvents(afterId, maxCount, typeFilter);
}
[McpServerTool(Name = "mcc_loaded_bots"), Description("List currently loaded MCC bots and scripts.")]
public object LoadedBots()
{
return capabilities.GetLoadedBots();
}
[McpServerTool(Name = "mcc_chat_history"), Description("Get recent chat/system lines seen by MCC.")]
public object ChatHistory(int maxCount = 50, bool includeJson = false)
{
@ -87,18 +141,54 @@ public sealed class MccMcpToolSet
return capabilities.QuitClient();
}
[McpServerTool(Name = "mcc_disconnect"), Description("Disconnect MCC from the current server without quitting the process.")]
public object Disconnect()
{
return capabilities.DisconnectClient();
}
[McpServerTool(Name = "mcc_respawn"), Description("Send the respawn packet when the controlled player is dead.")]
public object Respawn()
{
return capabilities.Respawn();
}
[McpServerTool(Name = "mcc_run_internal_command"), Description("Run an internal MCC command.")]
public object RunInternalCommand([Description("MCC command line without leading slash.")] string command)
{
return capabilities.RunInternalCommand(command);
}
[McpServerTool(Name = "mcc_animation"), Description("Play a hand-swing animation with the selected hand.")]
public object Animation(string hand = "MainHand")
{
return capabilities.PlayAnimation(hand);
}
[McpServerTool(Name = "mcc_toggle_sneak"), Description("Explicitly enable or disable sneaking.")]
public object ToggleSneak(bool enabled)
{
return capabilities.ToggleSneak(enabled);
}
[McpServerTool(Name = "mcc_toggle_sprint"), Description("Explicitly send start or stop sprinting entity actions.")]
public object ToggleSprint(bool enabled)
{
return capabilities.ToggleSprint(enabled);
}
[McpServerTool(Name = "mcc_change_hotbar_slot"), Description("Change active hotbar slot (1-9).")]
public object ChangeHotbarSlot(int slot)
{
return capabilities.ChangeHotbarSlot(slot);
}
[McpServerTool(Name = "mcc_select_item"), Description("Select a hotbar item by item type without rearranging inventory contents.")]
public object SelectItem(string itemType, bool preferLowestSlot = true)
{
return capabilities.SelectHotbarItem(itemType, preferLowestSlot);
}
[McpServerTool(Name = "mcc_use_item_on_hand"), Description("Use the currently held item.")]
public object UseItemOnHand()
{
@ -129,6 +219,12 @@ public sealed class MccMcpToolSet
return capabilities.InteractEntity(entityId, interaction, hand);
}
[McpServerTool(Name = "mcc_entity_attack"), Description("Attack a tracked entity explicitly.")]
public object EntityAttack(int entityId)
{
return capabilities.AttackEntity(entityId);
}
[McpServerTool(Name = "mcc_block_scan"), Description("Scan nearby blocks around player location.")]
public object BlockScan(int radius = 3, int maxCount = 200, string? materialFilter = null)
{
@ -153,6 +249,12 @@ public sealed class MccMcpToolSet
return capabilities.LocatePlayer(playerName, includeSelf);
}
[McpServerTool(Name = "mcc_entity_nearest"), Description("Return the nearest tracked entity matching the requested filters.")]
public object EntityNearest(string? typeFilter = null, string? nameFilter = null, double radius = 64.0, bool includePlayers = true)
{
return capabilities.FindNearestEntity(typeFilter, nameFilter, radius, includePlayers);
}
[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)
{
@ -177,12 +279,30 @@ public sealed class MccMcpToolSet
return capabilities.LookAt(x, y, z);
}
[McpServerTool(Name = "mcc_look_direction"), Description("Rotate player view to a cardinal direction or straight up/down.")]
public object LookDirection(string direction)
{
return capabilities.LookDirection(direction);
}
[McpServerTool(Name = "mcc_look_angles"), Description("Rotate player view to explicit yaw and pitch angles.")]
public object LookAngles(float yaw, float pitch)
{
return capabilities.LookAngles(yaw, pitch);
}
[McpServerTool(Name = "mcc_inventory_snapshot"), Description("Get a snapshot of one inventory.")]
public object InventorySnapshot([Description("Inventory ID. 0 is the player inventory.")] int inventoryId = 0)
{
return capabilities.GetInventorySnapshot(inventoryId);
}
[McpServerTool(Name = "mcc_inventory_search"), Description("Search the player inventory and optionally open containers for items matching a query.")]
public object InventorySearch(string query, int maxCount = 100, bool exactMatch = false, bool includeContainers = true)
{
return capabilities.SearchInventories(query, maxCount, exactMatch, includeContainers);
}
[McpServerTool(Name = "mcc_inventories_list"), Description("List currently open inventories and containers known to MCC.")]
public object InventoriesList()
{

View file

@ -807,13 +807,14 @@ namespace MinecraftClient
/// <param name="keepAccountAndServerSettings">Optional, keep account and server settings</param>
public static void Restart(int delaySeconds = 0, bool keepAccountAndServerSettings = false)
{
ConsoleIO.Backend.StopReadThread();
ConsoleIO.Backend?.StopReadThread();
new Thread(new ThreadStart(delegate
{
if (client is not null) { client.Disconnect(); ConsoleIO.Reset(); }
if (offlinePrompt is not null)
{
ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler;
if (ConsoleIO.Backend is not null)
ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler;
offlinePrompt.Item2.Cancel(); offlinePrompt.Item1.Join(); offlinePrompt = null; ConsoleIO.Reset();
}
if (delaySeconds > 0)
@ -835,7 +836,8 @@ namespace MinecraftClient
if (client is not null) { client.Disconnect(); ConsoleIO.Reset(); }
if (offlinePrompt is not null)
{
ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler;
if (ConsoleIO.Backend is not null)
ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler;
offlinePrompt.Item2.Cancel();
if (Thread.CurrentThread != offlinePrompt.Item1)
offlinePrompt.Item1.Join(1000);
@ -907,8 +909,9 @@ namespace MinecraftClient
if (offlinePrompt is null)
{
ConsoleIO.Backend.StopReadThread();
ConsoleIO.Backend.OnInputChange += ConsoleIO.OfflineAutocompleteHandler;
ConsoleIO.Backend?.StopReadThread();
if (ConsoleIO.Backend is not null)
ConsoleIO.Backend.OnInputChange += ConsoleIO.OfflineAutocompleteHandler;
var cancellationTokenSource = new CancellationTokenSource();
offlinePrompt = new(new Thread(new ThreadStart(delegate