mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Added inventory manipulation to the MCP, improved the test harness
This commit is contained in:
parent
7f9023e7bb
commit
cf382122e9
5 changed files with 948 additions and 7 deletions
|
|
@ -28,9 +28,14 @@ public interface IMccMcpCapabilities
|
|||
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 ListInventories();
|
||||
MccMcpResult GetInventorySnapshot(int inventoryId);
|
||||
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);
|
||||
MccMcpResult DropInventoryItem(string itemType, int count, int inventoryId, bool preferStack);
|
||||
MccMcpResult DepositContainerItem(string itemType, int count, int inventoryId, bool preferLargestStack);
|
||||
MccMcpResult WithdrawContainerItem(string itemType, int count, int inventoryId, bool preferLargestStack);
|
||||
MccMcpResult QueryEntities(int maxCount);
|
||||
MccMcpResult ListEntities(int maxCount, string? typeFilter, double radius);
|
||||
MccMcpResult GetEntityInfo(int entityId, bool includeMetadata, bool includeEquipment, bool includeEffects);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
private const double DefaultArrivalTolerance = 1.5;
|
||||
private const int ArrivalPollIntervalMs = 125;
|
||||
private const int MaxBlockVerifyWaitMs = 12000;
|
||||
private const int DefaultContainerWaitMs = 5000;
|
||||
private const int MinContainerWaitMs = 250;
|
||||
private const int MaxContainerWaitMs = 20000;
|
||||
private const int DefaultInventoryActionWaitMs = 3500;
|
||||
|
||||
private sealed class InternalCommandInfo
|
||||
{
|
||||
|
|
@ -64,6 +68,12 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
public required double Distance { get; init; }
|
||||
}
|
||||
|
||||
private enum InventoryTransferDirection
|
||||
{
|
||||
Deposit,
|
||||
Withdraw
|
||||
}
|
||||
|
||||
private readonly Func<MccMcpCapabilityToggles> togglesProvider;
|
||||
|
||||
public MccMcpCapabilities(Func<MccMcpCapabilityToggles> togglesProvider)
|
||||
|
|
@ -1122,6 +1132,120 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
});
|
||||
}
|
||||
|
||||
public MccMcpResult ListInventories()
|
||||
{
|
||||
if (!IsCategoryEnabled(t => t.Inventory))
|
||||
return MccMcpResult.Fail("capability_disabled");
|
||||
|
||||
McClient? client = GetClient();
|
||||
if (client is null)
|
||||
return NotConnected();
|
||||
|
||||
if (!client.GetInventoryEnabled())
|
||||
return MccMcpResult.Fail("feature_disabled");
|
||||
|
||||
return client.InvokeOnMainThread(() =>
|
||||
{
|
||||
var inventories = client.GetInventories()
|
||||
.OrderBy(entry => entry.Key)
|
||||
.Select(entry => new
|
||||
{
|
||||
id = entry.Key,
|
||||
type = entry.Value.Type.ToString(),
|
||||
title = entry.Value.Title,
|
||||
slotCount = entry.Value.Type.SlotCount(),
|
||||
nonEmptySlots = entry.Value.Items.Count,
|
||||
active = entry.Key > 0 && entry.Key == GetActiveContainerId(client)
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
return MccMcpResult.Ok(new
|
||||
{
|
||||
count = inventories.Length,
|
||||
inventories
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public MccMcpResult OpenContainerAt(int x, int y, int z, int timeoutMs, bool closeCurrent)
|
||||
{
|
||||
if (!IsCategoryEnabled(t => t.Inventory))
|
||||
return MccMcpResult.Fail("capability_disabled");
|
||||
|
||||
McClient? client = GetClient();
|
||||
if (client is null)
|
||||
return NotConnected();
|
||||
|
||||
if (!client.GetInventoryEnabled() || !client.GetTerrainEnabled())
|
||||
return MccMcpResult.Fail("feature_disabled");
|
||||
|
||||
Location location = new(x, y, z);
|
||||
int waitMs = GetContainerWaitMs(timeoutMs);
|
||||
(Block block, int activeContainerId) state = client.InvokeOnMainThread(() =>
|
||||
{
|
||||
Block block = client.GetWorld().GetBlock(location);
|
||||
return (block, GetActiveContainerId(client));
|
||||
});
|
||||
|
||||
if (!IsInteractableContainerMaterial(state.block.Type))
|
||||
{
|
||||
return MccMcpResult.Fail("invalid_state", data: new
|
||||
{
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
block = ToBlockState(state.block),
|
||||
activeContainerId = state.activeContainerId
|
||||
});
|
||||
}
|
||||
|
||||
return OpenContainerCore(client, location, state.block, state.activeContainerId, waitMs, closeCurrent);
|
||||
}
|
||||
|
||||
public MccMcpResult CloseContainer(int inventoryId, int timeoutMs)
|
||||
{
|
||||
if (!IsCategoryEnabled(t => t.Inventory))
|
||||
return MccMcpResult.Fail("capability_disabled");
|
||||
|
||||
McClient? client = GetClient();
|
||||
if (client is null)
|
||||
return NotConnected();
|
||||
|
||||
if (!client.GetInventoryEnabled())
|
||||
return MccMcpResult.Fail("feature_disabled");
|
||||
|
||||
int waitMs = GetContainerWaitMs(timeoutMs);
|
||||
int resolvedInventoryId = client.InvokeOnMainThread(() => ResolveContainerInventoryId(client, inventoryId));
|
||||
if (resolvedInventoryId <= 0)
|
||||
{
|
||||
if (inventoryId < 0)
|
||||
{
|
||||
return MccMcpResult.Ok(new
|
||||
{
|
||||
success = true,
|
||||
closed = false
|
||||
});
|
||||
}
|
||||
|
||||
return MccMcpResult.Fail("invalid_state", data: new { inventoryId });
|
||||
}
|
||||
|
||||
bool closeAccepted = client.CloseInventory(resolvedInventoryId);
|
||||
bool closed = closeAccepted && WaitForContainerClose(client, resolvedInventoryId, waitMs);
|
||||
var resultData = new
|
||||
{
|
||||
success = closeAccepted && closed,
|
||||
closeAccepted,
|
||||
closed,
|
||||
inventoryId = resolvedInventoryId,
|
||||
timeoutMs = waitMs
|
||||
};
|
||||
|
||||
return closeAccepted && closed
|
||||
? MccMcpResult.Ok(resultData)
|
||||
: MccMcpResult.Fail("action_incomplete", data: resultData);
|
||||
}
|
||||
|
||||
public MccMcpResult InventoryWindowAction(int inventoryId, int slotId, string actionType)
|
||||
{
|
||||
if (!IsCategoryEnabled(t => t.Inventory))
|
||||
|
|
@ -1274,6 +1398,16 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
});
|
||||
}
|
||||
|
||||
public MccMcpResult DepositContainerItem(string itemType, int count, int inventoryId, bool preferLargestStack)
|
||||
{
|
||||
return TransferContainerItem(itemType, count, inventoryId, preferLargestStack, InventoryTransferDirection.Deposit);
|
||||
}
|
||||
|
||||
public MccMcpResult WithdrawContainerItem(string itemType, int count, int inventoryId, bool preferLargestStack)
|
||||
{
|
||||
return TransferContainerItem(itemType, count, inventoryId, preferLargestStack, InventoryTransferDirection.Withdraw);
|
||||
}
|
||||
|
||||
public MccMcpResult QueryEntities(int maxCount)
|
||||
{
|
||||
if (!IsCategoryEnabled(t => t.EntityWorld))
|
||||
|
|
@ -1729,6 +1863,312 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
});
|
||||
}
|
||||
|
||||
private static MccMcpResult OpenContainerCore(McClient client, Location location, Block block, int activeContainerId, int waitMs, bool closeCurrent)
|
||||
{
|
||||
if (activeContainerId > 0)
|
||||
{
|
||||
if (!closeCurrent)
|
||||
{
|
||||
return MccMcpResult.Fail("invalid_state", data: new
|
||||
{
|
||||
reason = "container_already_open",
|
||||
activeContainerId,
|
||||
x = location.X,
|
||||
y = location.Y,
|
||||
z = location.Z,
|
||||
block = ToBlockState(block)
|
||||
});
|
||||
}
|
||||
|
||||
bool closeAccepted = client.CloseInventory(activeContainerId);
|
||||
bool closed = closeAccepted && WaitForContainerClose(client, activeContainerId, waitMs);
|
||||
if (!closeAccepted || !closed)
|
||||
{
|
||||
return MccMcpResult.Fail("action_incomplete", data: new
|
||||
{
|
||||
action = "close_previous_container",
|
||||
activeContainerId,
|
||||
closeAccepted,
|
||||
closed,
|
||||
timeoutMs = waitMs
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
HashSet<int> beforeIds = client.InvokeOnMainThread(() => client.GetInventories().Keys.Where(id => id > 0).ToHashSet());
|
||||
int openedInventoryId = 0;
|
||||
Container? openedInventory = null;
|
||||
bool openAccepted = client.InvokeOnMainThread(() => client.PlaceBlock(location, Direction.Down, Hand.MainHand, lookAtBlock: true));
|
||||
bool opened = openAccepted && WaitForContainerOpen(client, beforeIds, waitMs, out openedInventoryId, out openedInventory);
|
||||
var resultData = new
|
||||
{
|
||||
success = openAccepted && opened && openedInventory is not null,
|
||||
openAccepted,
|
||||
opened,
|
||||
timeoutMs = waitMs,
|
||||
x = location.X,
|
||||
y = location.Y,
|
||||
z = location.Z,
|
||||
block = ToBlockState(block),
|
||||
inventory = openedInventory is null
|
||||
? null
|
||||
: new
|
||||
{
|
||||
id = openedInventoryId,
|
||||
type = openedInventory.Type.ToString(),
|
||||
title = openedInventory.Title,
|
||||
slotCount = openedInventory.Type.SlotCount(),
|
||||
nonEmptySlots = openedInventory.Items.Count
|
||||
}
|
||||
};
|
||||
|
||||
return openAccepted && opened && openedInventory is not null
|
||||
? MccMcpResult.Ok(resultData)
|
||||
: MccMcpResult.Fail("action_incomplete", data: resultData);
|
||||
}
|
||||
|
||||
private MccMcpResult TransferContainerItem(string itemType, int count, int inventoryId, bool preferLargestStack, InventoryTransferDirection direction)
|
||||
{
|
||||
if (!IsCategoryEnabled(t => t.Inventory))
|
||||
return MccMcpResult.Fail("capability_disabled");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(itemType) || count <= 0)
|
||||
return MccMcpResult.Fail("invalid_args");
|
||||
|
||||
McClient? client = GetClient();
|
||||
if (client is null)
|
||||
return NotConnected();
|
||||
|
||||
if (!client.GetInventoryEnabled())
|
||||
return MccMcpResult.Fail("feature_disabled");
|
||||
|
||||
if (!TryParseItemType(itemType, out ItemType parsedItemType))
|
||||
{
|
||||
return MccMcpResult.Fail("invalid_args", data: new
|
||||
{
|
||||
itemType = itemType.Trim()
|
||||
});
|
||||
}
|
||||
|
||||
if (TryGetCursorItem(client, out Item? cursorItem))
|
||||
{
|
||||
return MccMcpResult.Fail("invalid_state", data: new
|
||||
{
|
||||
reason = "cursor_item_present",
|
||||
cursor = new { type = cursorItem!.Type.ToString(), count = cursorItem.Count }
|
||||
});
|
||||
}
|
||||
|
||||
int resolvedInventoryId = client.InvokeOnMainThread(() => ResolveContainerInventoryId(client, inventoryId));
|
||||
if (resolvedInventoryId <= 0)
|
||||
{
|
||||
return MccMcpResult.Fail("invalid_state", data: new
|
||||
{
|
||||
inventoryId
|
||||
});
|
||||
}
|
||||
|
||||
Container? initialInventory = client.InvokeOnMainThread(() => client.GetInventory(resolvedInventoryId));
|
||||
if (initialInventory is null)
|
||||
return MccMcpResult.Fail("invalid_state", data: new { inventoryId = resolvedInventoryId });
|
||||
|
||||
if (!TryGetContainerSlotRanges(initialInventory.Type, out int containerStart, out int containerEnd, out int playerStart, out int playerEnd))
|
||||
{
|
||||
return MccMcpResult.Fail("invalid_state", data: new
|
||||
{
|
||||
reason = "unsupported_container_type",
|
||||
inventoryId = resolvedInventoryId,
|
||||
type = initialInventory.Type.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
int sourceStart = direction == InventoryTransferDirection.Deposit ? playerStart : containerStart;
|
||||
int sourceEnd = direction == InventoryTransferDirection.Deposit ? playerEnd : containerEnd;
|
||||
int targetStart = direction == InventoryTransferDirection.Deposit ? containerStart : playerStart;
|
||||
int targetEnd = direction == InventoryTransferDirection.Deposit ? containerEnd : playerEnd;
|
||||
|
||||
int beforePlayerCount = CountItemInRange(initialInventory, parsedItemType, playerStart, playerEnd);
|
||||
int beforeContainerCount = CountItemInRange(initialInventory, parsedItemType, containerStart, containerEnd);
|
||||
int availableCount = CountItemInRange(initialInventory, parsedItemType, sourceStart, sourceEnd);
|
||||
if (availableCount < count)
|
||||
{
|
||||
return MccMcpResult.Fail("invalid_state", data: new
|
||||
{
|
||||
itemType = parsedItemType.ToString(),
|
||||
requestedCount = count,
|
||||
availableCount,
|
||||
inventoryId = resolvedInventoryId,
|
||||
direction = direction.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
int remaining = count;
|
||||
List<int> touchedSourceSlots = new();
|
||||
List<int> touchedTargetSlots = new();
|
||||
|
||||
while (remaining > 0)
|
||||
{
|
||||
Container? inventory = client.InvokeOnMainThread(() => client.GetInventory(resolvedInventoryId));
|
||||
if (inventory is null)
|
||||
return MccMcpResult.Fail("invalid_state", data: new { inventoryId = resolvedInventoryId });
|
||||
|
||||
if (TryGetCursorItem(client, out cursorItem))
|
||||
{
|
||||
return MccMcpResult.Fail("invalid_state", data: new
|
||||
{
|
||||
reason = "cursor_item_present_mid_transfer",
|
||||
cursor = new { type = cursorItem!.Type.ToString(), count = cursorItem.Count }
|
||||
});
|
||||
}
|
||||
|
||||
var sourceSlots = GetOrderedItemSlots(inventory, parsedItemType, sourceStart, sourceEnd, preferLargestStack);
|
||||
if (sourceSlots.Length == 0)
|
||||
break;
|
||||
|
||||
int beforeSourceCount = CountItemInRange(inventory, parsedItemType, sourceStart, sourceEnd);
|
||||
int beforeTargetCount = CountItemInRange(inventory, parsedItemType, targetStart, targetEnd);
|
||||
(int slot, int sourceCount) = sourceSlots[0];
|
||||
touchedSourceSlots.Add(slot);
|
||||
|
||||
int movedCount;
|
||||
List<int> usedTargetSlots = new();
|
||||
if (sourceCount <= remaining || direction == InventoryTransferDirection.Withdraw)
|
||||
{
|
||||
if (!client.DoWindowAction(resolvedInventoryId, slot, WindowActionType.ShiftClick))
|
||||
{
|
||||
return MccMcpResult.Fail("action_failed", data: new
|
||||
{
|
||||
itemType = parsedItemType.ToString(),
|
||||
requestedCount = count,
|
||||
remainingCount = remaining,
|
||||
inventoryId = resolvedInventoryId,
|
||||
sourceSlot = slot,
|
||||
direction = direction.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
if (direction == InventoryTransferDirection.Withdraw)
|
||||
{
|
||||
if (!WaitForRangeCount(client, resolvedInventoryId, parsedItemType, sourceStart, sourceEnd, countAfterShift => countAfterShift < beforeSourceCount, DefaultInventoryActionWaitMs, out Container? afterShift, out int afterSourceCount))
|
||||
{
|
||||
afterShift = client.InvokeOnMainThread(() => client.GetInventory(resolvedInventoryId));
|
||||
afterSourceCount = afterShift is null ? beforeSourceCount : CountItemInRange(afterShift, parsedItemType, sourceStart, sourceEnd);
|
||||
}
|
||||
|
||||
movedCount = beforeSourceCount - afterSourceCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!WaitForRangeCount(client, resolvedInventoryId, parsedItemType, targetStart, targetEnd, countAfterShift => countAfterShift > beforeTargetCount, DefaultInventoryActionWaitMs, out Container? afterShift, out int afterTargetCount))
|
||||
{
|
||||
afterShift = client.InvokeOnMainThread(() => client.GetInventory(resolvedInventoryId));
|
||||
afterTargetCount = afterShift is null ? beforeTargetCount : CountItemInRange(afterShift, parsedItemType, targetStart, targetEnd);
|
||||
}
|
||||
|
||||
movedCount = afterTargetCount - beforeTargetCount;
|
||||
}
|
||||
|
||||
if (direction == InventoryTransferDirection.Withdraw && movedCount > remaining)
|
||||
{
|
||||
int excessCount = movedCount - remaining;
|
||||
MccMcpResult returnExcess = TransferContainerItem(parsedItemType.ToString(), excessCount, resolvedInventoryId, preferLargestStack, InventoryTransferDirection.Deposit);
|
||||
if (!returnExcess.Success)
|
||||
{
|
||||
return MccMcpResult.Fail("action_incomplete", data: new
|
||||
{
|
||||
itemType = parsedItemType.ToString(),
|
||||
requestedCount = count,
|
||||
remainingCount = remaining,
|
||||
inventoryId = resolvedInventoryId,
|
||||
sourceSlot = slot,
|
||||
direction = direction.ToString(),
|
||||
excessCount,
|
||||
returnExcess
|
||||
});
|
||||
}
|
||||
|
||||
movedCount -= excessCount;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
movedCount = TransferPartialFromSlot(
|
||||
client,
|
||||
resolvedInventoryId,
|
||||
slot,
|
||||
parsedItemType,
|
||||
remaining,
|
||||
sourceStart,
|
||||
sourceEnd,
|
||||
targetStart,
|
||||
targetEnd,
|
||||
usedTargetSlots);
|
||||
}
|
||||
|
||||
if (movedCount <= 0)
|
||||
{
|
||||
Container? afterFailure = client.InvokeOnMainThread(() => client.GetInventory(resolvedInventoryId));
|
||||
return MccMcpResult.Fail("action_incomplete", data: new
|
||||
{
|
||||
itemType = parsedItemType.ToString(),
|
||||
requestedCount = count,
|
||||
remainingCount = remaining,
|
||||
inventoryId = resolvedInventoryId,
|
||||
sourceSlot = slot,
|
||||
direction = direction.ToString(),
|
||||
playerCount = afterFailure is null ? 0 : CountItemInRange(afterFailure, parsedItemType, playerStart, playerEnd),
|
||||
containerCount = afterFailure is null ? 0 : CountItemInRange(afterFailure, parsedItemType, containerStart, containerEnd)
|
||||
});
|
||||
}
|
||||
|
||||
remaining -= movedCount;
|
||||
touchedTargetSlots.AddRange(usedTargetSlots);
|
||||
}
|
||||
|
||||
Container? finalInventory = client.InvokeOnMainThread(() => client.GetInventory(resolvedInventoryId));
|
||||
if (finalInventory is null)
|
||||
return MccMcpResult.Fail("invalid_state", data: new { inventoryId = resolvedInventoryId });
|
||||
|
||||
int afterPlayerCount = CountItemInRange(finalInventory, parsedItemType, playerStart, playerEnd);
|
||||
int afterContainerCount = CountItemInRange(finalInventory, parsedItemType, containerStart, containerEnd);
|
||||
int playerDelta = afterPlayerCount - beforePlayerCount;
|
||||
int containerDelta = afterContainerCount - beforeContainerCount;
|
||||
int movedTotal = direction == InventoryTransferDirection.Deposit
|
||||
? afterContainerCount - beforeContainerCount
|
||||
: beforeContainerCount - afterContainerCount;
|
||||
bool countsVerified = direction == InventoryTransferDirection.Deposit
|
||||
? containerDelta == count
|
||||
: containerDelta == -count;
|
||||
bool playerCountsMatchExpected = direction == InventoryTransferDirection.Deposit
|
||||
? playerDelta == -count
|
||||
: playerDelta == count;
|
||||
bool succeeded = remaining == 0 && countsVerified;
|
||||
var resultData = new
|
||||
{
|
||||
success = succeeded,
|
||||
direction = direction.ToString().ToLowerInvariant(),
|
||||
itemType = parsedItemType.ToString(),
|
||||
requestedCount = count,
|
||||
movedCount = movedTotal,
|
||||
beforePlayerCount,
|
||||
afterPlayerCount,
|
||||
beforeContainerCount,
|
||||
afterContainerCount,
|
||||
playerDelta,
|
||||
containerDelta,
|
||||
playerCountsMatchExpected,
|
||||
verificationBasis = "container_delta",
|
||||
inventoryId = resolvedInventoryId,
|
||||
containerType = finalInventory.Type.ToString(),
|
||||
touchedSourceSlots = touchedSourceSlots.Distinct().OrderBy(slot => slot).ToArray(),
|
||||
touchedTargetSlots = touchedTargetSlots.Distinct().OrderBy(slot => slot).ToArray()
|
||||
};
|
||||
|
||||
return succeeded
|
||||
? MccMcpResult.Ok(resultData)
|
||||
: MccMcpResult.Fail("action_incomplete", data: resultData);
|
||||
}
|
||||
|
||||
private static MccMcpResult ExecuteInternalCommand(McClient client, string command)
|
||||
{
|
||||
return client.InvokeOnMainThread(() =>
|
||||
|
|
@ -1744,6 +2184,359 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
});
|
||||
}
|
||||
|
||||
private static bool TryGetCursorItem(McClient client, out Item? cursorItem)
|
||||
{
|
||||
cursorItem = client.InvokeOnMainThread(() =>
|
||||
{
|
||||
Container? playerInventory = client.GetInventory(0);
|
||||
return playerInventory is not null && playerInventory.Items.TryGetValue(-1, out Item? item) ? item : null;
|
||||
});
|
||||
return cursorItem is not null;
|
||||
}
|
||||
|
||||
private static int ResolveContainerInventoryId(McClient client, int inventoryId)
|
||||
{
|
||||
if (inventoryId > 0)
|
||||
{
|
||||
Container? inventory = client.GetInventory(inventoryId);
|
||||
return inventory is not null && inventoryId != 0 ? inventoryId : 0;
|
||||
}
|
||||
|
||||
return GetActiveContainerId(client);
|
||||
}
|
||||
|
||||
private static int GetActiveContainerId(McClient client)
|
||||
{
|
||||
return client.GetInventories().Keys.Where(id => id > 0).DefaultIfEmpty(0).Max();
|
||||
}
|
||||
|
||||
private static bool WaitForContainerOpen(McClient client, ISet<int> beforeIds, int waitMs, out int inventoryId, out Container? inventory)
|
||||
{
|
||||
inventoryId = 0;
|
||||
inventory = null;
|
||||
DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs);
|
||||
while (true)
|
||||
{
|
||||
(int activeId, Container? activeInventory) state = client.InvokeOnMainThread(() =>
|
||||
{
|
||||
int activeId = GetActiveContainerId(client);
|
||||
Container? activeInventory = activeId > 0 ? client.GetInventory(activeId) : null;
|
||||
return (activeId, activeInventory);
|
||||
});
|
||||
|
||||
if (state.activeId > 0 && (!beforeIds.Contains(state.activeId) || beforeIds.Count == 0) && state.activeInventory is not null)
|
||||
{
|
||||
inventoryId = state.activeId;
|
||||
inventory = state.activeInventory;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DateTime.UtcNow >= deadline)
|
||||
return false;
|
||||
|
||||
Thread.Sleep(ArrivalPollIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool WaitForContainerClose(McClient client, int inventoryId, int waitMs)
|
||||
{
|
||||
DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs);
|
||||
while (true)
|
||||
{
|
||||
bool stillOpen = client.InvokeOnMainThread(() => client.GetInventories().ContainsKey(inventoryId));
|
||||
if (!stillOpen)
|
||||
return true;
|
||||
|
||||
if (DateTime.UtcNow >= deadline)
|
||||
return false;
|
||||
|
||||
Thread.Sleep(ArrivalPollIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetContainerWaitMs(int timeoutMs)
|
||||
{
|
||||
if (timeoutMs <= 0)
|
||||
return DefaultContainerWaitMs;
|
||||
return Math.Clamp(timeoutMs, MinContainerWaitMs, MaxContainerWaitMs);
|
||||
}
|
||||
|
||||
private static bool TryGetContainerSlotRanges(ContainerType type, out int containerStart, out int containerEnd, out int playerStart, out int playerEnd)
|
||||
{
|
||||
containerStart = 0;
|
||||
containerEnd = -1;
|
||||
playerStart = 0;
|
||||
playerEnd = -1;
|
||||
|
||||
int containerSlots = type switch
|
||||
{
|
||||
ContainerType.Generic_9x1 => 9,
|
||||
ContainerType.Generic_9x2 => 18,
|
||||
ContainerType.Generic_9x3 => 27,
|
||||
ContainerType.Generic_9x4 => 36,
|
||||
ContainerType.Generic_9x5 => 45,
|
||||
ContainerType.Generic_9x6 => 54,
|
||||
ContainerType.Generic_3x3 => 9,
|
||||
ContainerType.Hopper => 5,
|
||||
ContainerType.ShulkerBox => 27,
|
||||
ContainerType.Furnace or ContainerType.BlastFurnace or ContainerType.Smoker => 3,
|
||||
ContainerType.Crafter => 9,
|
||||
_ => -1
|
||||
};
|
||||
|
||||
if (containerSlots <= 0)
|
||||
return false;
|
||||
|
||||
int slotCount = type.SlotCount();
|
||||
if (slotCount <= containerSlots)
|
||||
return false;
|
||||
|
||||
containerEnd = containerSlots - 1;
|
||||
playerStart = containerSlots;
|
||||
playerEnd = slotCount - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int CountItemInRange(Container inventory, ItemType itemType, int startSlot, int endSlot)
|
||||
{
|
||||
return inventory.Items
|
||||
.Where(entry => entry.Key >= startSlot && entry.Key <= endSlot)
|
||||
.Where(entry => entry.Value.Type == itemType)
|
||||
.Sum(entry => entry.Value.Count);
|
||||
}
|
||||
|
||||
private static (int slot, int count)[] GetOrderedItemSlots(Container inventory, ItemType itemType, int startSlot, int endSlot, bool preferLargestStack)
|
||||
{
|
||||
var query = inventory.Items
|
||||
.Where(entry => entry.Key >= startSlot && entry.Key <= endSlot)
|
||||
.Where(entry => entry.Value.Type == itemType && entry.Value.Count > 0)
|
||||
.Select(entry => (slot: entry.Key, count: entry.Value.Count));
|
||||
|
||||
return (preferLargestStack
|
||||
? query.OrderByDescending(entry => entry.count).ThenBy(entry => entry.slot)
|
||||
: query.OrderBy(entry => entry.count).ThenBy(entry => entry.slot))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static int TransferPartialFromSlot(McClient client, int inventoryId, int sourceSlot, ItemType itemType, int requestedCount, int sourceStart, int sourceEnd, int targetStart, int targetEnd, List<int> touchedTargetSlots)
|
||||
{
|
||||
Container? inventory = client.InvokeOnMainThread(() => client.GetInventory(inventoryId));
|
||||
if (inventory is null || !inventory.Items.TryGetValue(sourceSlot, out Item? sourceItem) || sourceItem.Count <= 0)
|
||||
return 0;
|
||||
|
||||
int amountToMove = Math.Min(requestedCount, sourceItem.Count);
|
||||
if (!client.DoWindowAction(inventoryId, sourceSlot, WindowActionType.LeftClick))
|
||||
return 0;
|
||||
|
||||
if (!WaitForCursorItem(client, itemType, DefaultInventoryActionWaitMs, out _))
|
||||
return 0;
|
||||
|
||||
int moved = 0;
|
||||
while (moved < amountToMove)
|
||||
{
|
||||
inventory = client.InvokeOnMainThread(() => client.GetInventory(inventoryId));
|
||||
if (inventory is null || !TryGetCursorItem(client, out Item? cursorItem) || cursorItem is null || cursorItem.Type != itemType)
|
||||
break;
|
||||
|
||||
if (!TryFindTransferTargetSlot(inventory, itemType, targetStart, targetEnd, out int targetSlot, out int capacity))
|
||||
break;
|
||||
|
||||
int step = Math.Min(amountToMove - moved, Math.Min(capacity, cursorItem.Count));
|
||||
int beforeTargetCount = GetSlotItemCount(inventory, targetSlot, itemType);
|
||||
int beforeCursorCount = cursorItem.Count;
|
||||
if (step <= 0 || !PlaceItemsFromCursor(client, inventoryId, targetSlot, step))
|
||||
break;
|
||||
|
||||
if (!WaitForPlacement(client, inventoryId, targetSlot, itemType, beforeTargetCount, beforeCursorCount, step))
|
||||
break;
|
||||
|
||||
touchedTargetSlots.Add(targetSlot);
|
||||
moved += step;
|
||||
}
|
||||
|
||||
if (TryGetCursorItem(client, out Item? remainingCursor) && remainingCursor is not null && remainingCursor.Count > 0)
|
||||
{
|
||||
inventory = client.InvokeOnMainThread(() => client.GetInventory(inventoryId));
|
||||
if (inventory is null)
|
||||
return 0;
|
||||
|
||||
int returnSlot = GetReturnSlot(inventory, itemType, sourceStart, sourceEnd, sourceSlot);
|
||||
if (!client.DoWindowAction(inventoryId, returnSlot, WindowActionType.LeftClick))
|
||||
return 0;
|
||||
|
||||
if (!WaitForCursorClear(client, DefaultInventoryActionWaitMs))
|
||||
return 0;
|
||||
}
|
||||
|
||||
return TryGetCursorItem(client, out _)
|
||||
? 0
|
||||
: moved;
|
||||
}
|
||||
|
||||
private static bool TryFindTransferTargetSlot(Container inventory, ItemType itemType, int startSlot, int endSlot, out int targetSlot, out int capacity)
|
||||
{
|
||||
int maxStack = itemType.StackCount();
|
||||
for (int slot = startSlot; slot <= endSlot; slot++)
|
||||
{
|
||||
if (inventory.Items.TryGetValue(slot, out Item? item) && item.Type == itemType && item.Count < maxStack)
|
||||
{
|
||||
targetSlot = slot;
|
||||
capacity = maxStack - item.Count;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (int slot = startSlot; slot <= endSlot; slot++)
|
||||
{
|
||||
if (!inventory.Items.ContainsKey(slot))
|
||||
{
|
||||
targetSlot = slot;
|
||||
capacity = maxStack;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
targetSlot = -1;
|
||||
capacity = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool PlaceItemsFromCursor(McClient client, int inventoryId, int targetSlot, int count)
|
||||
{
|
||||
if (count <= 0 || !TryGetCursorItem(client, out Item? cursorItem) || cursorItem is null)
|
||||
return false;
|
||||
|
||||
if (count == cursorItem.Count)
|
||||
return client.DoWindowAction(inventoryId, targetSlot, WindowActionType.LeftClick);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (!client.DoWindowAction(inventoryId, targetSlot, WindowActionType.RightClick))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int GetReturnSlot(Container inventory, ItemType itemType, int startSlot, int endSlot, int originalSourceSlot)
|
||||
{
|
||||
if (originalSourceSlot != 0)
|
||||
return originalSourceSlot;
|
||||
|
||||
int maxStack = itemType.StackCount();
|
||||
for (int slot = startSlot; slot <= endSlot; slot++)
|
||||
{
|
||||
if (slot == 0)
|
||||
continue;
|
||||
|
||||
if (inventory.Items.TryGetValue(slot, out Item? item) && item.Type == itemType && item.Count < maxStack)
|
||||
return slot;
|
||||
}
|
||||
|
||||
for (int slot = startSlot; slot <= endSlot; slot++)
|
||||
{
|
||||
if (slot == 0)
|
||||
continue;
|
||||
|
||||
if (!inventory.Items.ContainsKey(slot))
|
||||
return slot;
|
||||
}
|
||||
|
||||
return originalSourceSlot;
|
||||
}
|
||||
|
||||
private static int GetSlotItemCount(Container inventory, int slot, ItemType itemType)
|
||||
{
|
||||
return inventory.Items.TryGetValue(slot, out Item? item) && item.Type == itemType ? item.Count : 0;
|
||||
}
|
||||
|
||||
private static bool WaitForCursorItem(McClient client, ItemType itemType, int waitMs, out Item? cursorItem)
|
||||
{
|
||||
cursorItem = null;
|
||||
DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs);
|
||||
while (true)
|
||||
{
|
||||
if (TryGetCursorItem(client, out cursorItem) && cursorItem is not null && cursorItem.Type == itemType)
|
||||
return true;
|
||||
|
||||
if (DateTime.UtcNow >= deadline)
|
||||
return false;
|
||||
|
||||
Thread.Sleep(ArrivalPollIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool WaitForCursorClear(McClient client, int waitMs)
|
||||
{
|
||||
DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs);
|
||||
while (true)
|
||||
{
|
||||
if (!TryGetCursorItem(client, out _))
|
||||
return true;
|
||||
|
||||
if (DateTime.UtcNow >= deadline)
|
||||
return false;
|
||||
|
||||
Thread.Sleep(ArrivalPollIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool WaitForPlacement(McClient client, int inventoryId, int targetSlot, ItemType itemType, int beforeTargetCount, int beforeCursorCount, int placedCount)
|
||||
{
|
||||
DateTime deadline = DateTime.UtcNow.AddMilliseconds(DefaultInventoryActionWaitMs);
|
||||
while (true)
|
||||
{
|
||||
bool targetUpdated = false;
|
||||
bool cursorUpdated = false;
|
||||
|
||||
Container? inventory = client.InvokeOnMainThread(() => client.GetInventory(inventoryId));
|
||||
if (inventory is not null)
|
||||
{
|
||||
int currentTargetCount = GetSlotItemCount(inventory, targetSlot, itemType);
|
||||
targetUpdated = currentTargetCount >= beforeTargetCount + placedCount;
|
||||
}
|
||||
|
||||
if (placedCount >= beforeCursorCount)
|
||||
{
|
||||
cursorUpdated = !TryGetCursorItem(client, out _);
|
||||
}
|
||||
else if (TryGetCursorItem(client, out Item? cursorItem) && cursorItem is not null && cursorItem.Type == itemType)
|
||||
{
|
||||
cursorUpdated = cursorItem.Count <= beforeCursorCount - placedCount;
|
||||
}
|
||||
|
||||
if (targetUpdated && cursorUpdated)
|
||||
return true;
|
||||
|
||||
if (DateTime.UtcNow >= deadline)
|
||||
return false;
|
||||
|
||||
Thread.Sleep(ArrivalPollIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool WaitForRangeCount(McClient client, int inventoryId, ItemType itemType, int startSlot, int endSlot, Func<int, bool> predicate, int waitMs, out Container? inventory, out int itemCount)
|
||||
{
|
||||
inventory = null;
|
||||
itemCount = 0;
|
||||
DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs);
|
||||
while (true)
|
||||
{
|
||||
inventory = client.InvokeOnMainThread(() => client.GetInventory(inventoryId));
|
||||
if (inventory is not null)
|
||||
{
|
||||
itemCount = CountItemInRange(inventory, itemType, startSlot, endSlot);
|
||||
if (predicate(itemCount))
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DateTime.UtcNow >= deadline)
|
||||
return false;
|
||||
|
||||
Thread.Sleep(ArrivalPollIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<NearbyPlayerSnapshot> BuildTrackedPlayerSnapshots(McClient client, bool includeSelf)
|
||||
{
|
||||
Location playerLocation = client.GetCurrentLocation();
|
||||
|
|
@ -2140,6 +2933,21 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
|||
return material.ToString().Contains("Sign", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsInteractableContainerMaterial(Material material)
|
||||
{
|
||||
string name = material.ToString();
|
||||
return name.Contains("Chest", StringComparison.Ordinal)
|
||||
|| name.Contains("Barrel", StringComparison.Ordinal)
|
||||
|| name.Contains("ShulkerBox", StringComparison.Ordinal)
|
||||
|| name.Contains("Hopper", StringComparison.Ordinal)
|
||||
|| name.Contains("Dispenser", StringComparison.Ordinal)
|
||||
|| name.Contains("Dropper", StringComparison.Ordinal)
|
||||
|| name.Contains("Furnace", StringComparison.Ordinal)
|
||||
|| name.Contains("Smoker", StringComparison.Ordinal)
|
||||
|| name.Contains("BlastFurnace", StringComparison.Ordinal)
|
||||
|| name.Contains("Crafter", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string? ResolvePlayerEntityName(Entity entity, IReadOnlyDictionary<string, string> uuidToName)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(entity.Name))
|
||||
|
|
|
|||
|
|
@ -175,6 +175,24 @@ public sealed class MccMcpToolSet
|
|||
return capabilities.GetInventorySnapshot(inventoryId);
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "mcc_inventories_list"), Description("List currently open inventories and containers known to MCC.")]
|
||||
public object InventoriesList()
|
||||
{
|
||||
return capabilities.ListInventories();
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "mcc_container_open_at"), Description("Open an interactable container block at world coordinates and wait for the container inventory to appear.")]
|
||||
public object ContainerOpenAt(int x, int y, int z, int timeoutMs = 0, bool closeCurrent = true)
|
||||
{
|
||||
return capabilities.OpenContainerAt(x, y, z, timeoutMs, closeCurrent);
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "mcc_container_close"), Description("Close an open non-player container. Use inventoryId=-1 to close the active container.")]
|
||||
public object ContainerClose([Description("Container inventory ID, or -1 for the active non-player container.")] int inventoryId = -1, int timeoutMs = 0)
|
||||
{
|
||||
return capabilities.CloseContainer(inventoryId, timeoutMs);
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "mcc_inventory_window_action"), Description("Perform a window action on an inventory slot.")]
|
||||
public object InventoryWindowAction(int inventoryId, int slotId, [Description("WindowActionType enum name, e.g. LeftClick or ShiftClick.")] string actionType)
|
||||
{
|
||||
|
|
@ -191,6 +209,26 @@ public sealed class MccMcpToolSet
|
|||
return capabilities.DropInventoryItem(itemType, count, inventoryId, preferStack);
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "mcc_container_deposit_item"), Description("Move an exact item count from the player inventory into an open container and verify the transfer.")]
|
||||
public object ContainerDepositItem(
|
||||
[Description("Item type enum name (e.g. Diamond).")] string itemType,
|
||||
[Description("Exact number of items to move into the container.")] int count,
|
||||
[Description("Container inventory ID, or -1 for the active non-player container.")] int inventoryId = -1,
|
||||
[Description("Prefer larger source stacks first when true.")] bool preferLargestStack = true)
|
||||
{
|
||||
return capabilities.DepositContainerItem(itemType, count, inventoryId, preferLargestStack);
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "mcc_container_withdraw_item"), Description("Move an exact item count from an open container into the player inventory and verify the transfer.")]
|
||||
public object ContainerWithdrawItem(
|
||||
[Description("Item type enum name (e.g. Diamond).")] string itemType,
|
||||
[Description("Exact number of items to move into the player inventory.")] int count,
|
||||
[Description("Container inventory ID, or -1 for the active non-player container.")] int inventoryId = -1,
|
||||
[Description("Prefer larger source stacks first when true.")] bool preferLargestStack = true)
|
||||
{
|
||||
return capabilities.WithdrawContainerItem(itemType, count, inventoryId, preferLargestStack);
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "mcc_entities_query"), Description("Query tracked entities.")]
|
||||
public object EntitiesQuery([Description("Maximum entities to return.")] int maxCount = 50)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue