Interact with inventories

The /inventory command allow listing inventory and clicking on items
Should be enough for operating GUI menus such as Server chooser/teleporter
This commit is contained in:
ORelio 2020-03-29 18:41:26 +02:00
parent bc3d6aba00
commit e04f06cece
13 changed files with 574 additions and 217 deletions

Binary file not shown.

View file

@ -149,6 +149,22 @@ namespace MinecraftClient.Protocol.Handlers
g = readNextBool(ref packetData); g = readNextBool(ref packetData);
Console.WriteLine("[C -> S] Location: " + x + ", " + y + ", " + z + ", (look)" + ", " + g); Console.WriteLine("[C -> S] Location: " + x + ", " + y + ", " + z + ", (look)" + ", " + g);
break; break;
case 0x09:
byte window = readNextByte(ref packetData);
short slot = readNextShort(ref packetData);
byte button = readNextByte(ref packetData);
short action = readNextShort(ref packetData);
int mode = readNextVarInt(ref packetData);
bool slotPresent = readNextBool(ref packetData);
int itemId = -1;
byte itemCount = 0;
if (slotPresent)
{
itemId = readNextVarInt(ref packetData);
itemCount = readNextByte(ref packetData);
}
Console.WriteLine("[C -> S] Window #" + window + " click: #" + slot + " button " + button + " action " + action + " mode " + mode + " item " + itemId + " x" + itemCount);
break;
} }
} }
} }
@ -209,6 +225,19 @@ namespace MinecraftClient.Protocol.Handlers
return rawValue[0] != 0; return rawValue[0] != 0;
} }
private byte readNextByte(ref byte[] cache)
{
byte[] rawValue = readData(1, ref cache);
return rawValue[0];
}
private short readNextShort(ref byte[] cache)
{
byte[] rawValue = readData(2, ref cache);
Array.Reverse(rawValue); //Endianness
return BitConverter.ToInt16(rawValue, 0);
}
private double readNextDouble(ref byte[] cache) private double readNextDouble(ref byte[] cache)
{ {
byte[] rawValue = readData(8, ref cache); byte[] rawValue = readData(8, ref cache);

View file

@ -6,23 +6,63 @@ using MinecraftClient.Inventory;
namespace MinecraftClient.Commands namespace MinecraftClient.Commands
{ {
class GetInventory : Command class Inventory : Command
{ {
public override string CMDName { get { return "inventory"; } } public override string CMDName { get { return "inventory"; } }
public override string CMDDesc { get { return "inventory: Show your inventory."; } } public override string CMDDesc { get { return "inventory <id> <list|close|click <slot>>: Interact with inventories"; } }
public override string Run(McTcpClient handler, string command, Dictionary<string, object> localVars) public override string Run(McTcpClient handler, string command, Dictionary<string, object> localVars)
{ {
List<string> response = new List<string>(); if (handler.GetInventoryEnabled())
response.Add("Inventory slots:");
foreach (KeyValuePair<int, Item> item in handler.GetPlayerInventory().Items)
{ {
response.Add(String.Format(" #{0}: {1} x{2}", item.Key, item.Value.Type, item.Value.Count)); string[] args = getArgs(command);
if (args.Length >= 1)
{
try
{
int inventoryId = int.Parse(args[0]);
string action = args.Length > 1
? args[1].ToLower()
: "list";
switch (action)
{
case "close":
if (handler.CloseInventory(inventoryId))
return "Closing Inventoy #" + inventoryId;
else return "Failed to close Inventory #" + inventoryId;
case "list":
Container inventory = handler.GetInventory(inventoryId);
List<string> response = new List<string>();
response.Add("Inventory #" + inventoryId + " - " + inventory.Title + "§8");
foreach (KeyValuePair<int, Item> item in inventory.Items)
response.Add(String.Format(" #{0}: {1} x{2}", item.Key, item.Value.Type, item.Value.Count));
return String.Join("\n", response.ToArray());
case "click":
if (args.Length == 3)
{
int slot = int.Parse(args[2]);
handler.ClickWindowSlot(inventoryId, slot);
return "Clicking slot " + slot + " in window #" + inventoryId;
}
else return CMDDesc;
default:
return CMDDesc;
}
}
catch (FormatException) { return CMDDesc; }
}
else
{
Dictionary<int, Container> inventories = handler.GetInventories();
List<string> response = new List<string>();
response.Add("Inventories:");
foreach (var inventory in inventories)
response.Add(String.Format(" #{0}: {1}", inventory.Key, inventory.Value.Title + "§8"));
response.Add(CMDDesc);
return String.Join("\n", response);
}
} }
else return "Please enable inventoryhandling in config to use this command.";
return String.Join("\n", response.ToArray());
} }
} }
} }

View file

@ -12,8 +12,12 @@ namespace MinecraftClient.Commands
public override string Run(McTcpClient handler, string command, Dictionary<string, object> localVars) public override string Run(McTcpClient handler, string command, Dictionary<string, object> localVars)
{ {
handler.UseItemOnHand(); if (handler.GetInventoryEnabled())
return "Use an item"; {
handler.UseItemOnHand();
return "Used an item";
}
else return "Please enable inventoryhandling in config to use this command.";
} }
} }
} }

View file

@ -20,44 +20,34 @@ namespace MinecraftClient.Inventory
/// </summary> /// </summary>
public int Count; public int Count;
/// <summary>
/// Slot ID in the parent inventory
/// </summary>
/// <remarks>-1 means currently being dragged by mouse</remarks>
public int SlotID;
/// <summary> /// <summary>
/// Item Metadata /// Item Metadata
/// </summary> /// </summary>
public Dictionary<string, object> NBT; public Dictionary<string, object> NBT;
/// <summary> /// <summary>
/// Create an item with Type ID, Count and Slot ID /// Create an item with Type ID, Count and Metadata
/// </summary> /// </summary>
/// <param name="ID">Item Type ID</param> /// <param name="ID">Item Type ID</param>
/// <param name="Count">Item Count</param> /// <param name="Count">Item Count</param>
/// <param name="SlotID">Item Slot ID in parent inventory</param> /// <param name="NBT">Item Metadata</param>
public Item(int id, int count, int slotID) public Item(int id, int count, Dictionary<string, object> nbt)
{ {
this.Type = (ItemType)id; this.Type = (ItemType)id;
this.Count = count; this.Count = count;
this.SlotID = slotID; this.NBT = nbt;
this.NBT = new Dictionary<string, object>();
} }
/// <summary> /// <summary>
/// Create an item with Type ID, Count, Slot ID and Metadata /// Check if the item slot is empty
/// </summary> /// </summary>
/// <param name="ID">Item Type ID</param> /// <returns>TRUE if the item is empty</returns>
/// <param name="Count">Item Count</param> public bool IsEmpty
/// <param name="SlotID">Item Slot ID in parent inventory</param>
/// <param name="NBT">Item Metadata</param>
public Item(int id, int count, int slotID, Dictionary<string, object> nbt)
{ {
this.Type = (ItemType)id; get
this.Count = count; {
this.SlotID = slotID; return Type == ItemType.Air || Count == 0;
this.NBT = nbt; }
} }
} }
} }

View file

@ -27,7 +27,7 @@ namespace MinecraftClient
private readonly List<ChatBot> bots = new List<ChatBot>(); private readonly List<ChatBot> bots = new List<ChatBot>();
private static readonly List<ChatBot> botsOnHold = new List<ChatBot>(); private static readonly List<ChatBot> botsOnHold = new List<ChatBot>();
private static List<Container> inventories = new List<Container>(); private static Dictionary<int, Container> inventories = new Dictionary<int, Container>();
private readonly Dictionary<string, List<ChatBot>> registeredBotPluginChannels = new Dictionary<string, List<ChatBot>>(); private readonly Dictionary<string, List<ChatBot>> registeredBotPluginChannels = new Dictionary<string, List<ChatBot>>();
private readonly List<string> registeredServerPluginChannels = new List<String>(); private readonly List<string> registeredServerPluginChannels = new List<String>();
@ -53,7 +53,6 @@ namespace MinecraftClient
private string username; private string username;
private string uuid; private string uuid;
private string sessionid; private string sessionid;
private Container playerInventory = new Container(ContainerType.PlayerInventory);
private DateTime lastKeepAlive; private DateTime lastKeepAlive;
private object lastKeepAliveLock = new object(); private object lastKeepAliveLock = new object();
@ -136,6 +135,12 @@ namespace MinecraftClient
inventoryHandlingEnabled = Settings.InventoryHandling; inventoryHandlingEnabled = Settings.InventoryHandling;
entityHandlingEnabled = Settings.EntityHandling; entityHandlingEnabled = Settings.EntityHandling;
if (inventoryHandlingEnabled)
{
inventories.Clear();
inventories[0] = new Container(0, ContainerType.PlayerInventory, "Player Inventory");
}
bool retry = false; bool retry = false;
this.sessionid = sessionID; this.sessionid = sessionID;
this.uuid = uuid; this.uuid = uuid;
@ -550,7 +555,6 @@ namespace MinecraftClient
inventoryHandlingEnabled = false; inventoryHandlingEnabled = false;
inventoryHandlingRequested = false; inventoryHandlingRequested = false;
inventories.Clear(); inventories.Clear();
playerInventory = null;
} }
return true; return true;
} }
@ -592,13 +596,34 @@ namespace MinecraftClient
} }
} }
/// <summary>
/// Get all inventories. ID 0 is the player inventory.
/// </summary>
/// <returns>All inventories</returns>
public Dictionary<int, Container> GetInventories()
{
return inventories;
}
/// <summary>
/// Get client player's inventory items
/// </summary>
/// <param name="inventoryID">Window ID of the requested inventory</param>
/// <returns> Item Dictionary indexed by Slot ID (Check wiki.vg for slot ID)</returns>
public Container GetInventory(int inventoryID)
{
if (inventories.ContainsKey(inventoryID))
return inventories[inventoryID];
return null;
}
/// <summary> /// <summary>
/// Get client player's inventory items /// Get client player's inventory items
/// </summary> /// </summary>
/// <returns> Item Dictionary indexed by Slot ID (Check wiki.vg for slot ID)</returns> /// <returns> Item Dictionary indexed by Slot ID (Check wiki.vg for slot ID)</returns>
public Container GetPlayerInventory() public Container GetPlayerInventory()
{ {
return playerInventory; return GetInventory(0);
} }
/// <summary> /// <summary>
@ -765,19 +790,14 @@ namespace MinecraftClient
/// When an inventory is opened /// When an inventory is opened
/// </summary> /// </summary>
/// <param name="inventory">Location to reach</param> /// <param name="inventory">Location to reach</param>
public void OnInventoryOpen(Container inventory) public void OnInventoryOpen(int inventoryID, Container inventory)
{ {
//TODO: Handle Inventory inventories[inventoryID] = inventory;
if (!inventories.Contains(inventory))
{
inventories.Add(inventory);
}
if (Settings.DebugMessages) if (inventoryID != 0)
{ {
ConsoleIO.WriteLineFormatted("§8An Inventory opened: " + inventory.Type + " - " + inventory.Title); ConsoleIO.WriteLogLine("Inventory # " + inventoryID + " opened: " + inventory.Title);
foreach (var item in inventory.Items) ConsoleIO.WriteLogLine("Use /inventory to interact with it.");
ConsoleIO.WriteLineFormatted("§8 - Slot " + item.Key + ": " + item.Value.Type + " x" + item.Value.Count);
} }
} }
@ -785,64 +805,42 @@ namespace MinecraftClient
/// When an inventory is close /// When an inventory is close
/// </summary> /// </summary>
/// <param name="inventoryID">Location to reach</param> /// <param name="inventoryID">Location to reach</param>
public void OnInventoryClose(byte inventoryID) public void OnInventoryClose(int inventoryID)
{ {
for (int i = 0; i < inventories.Count; i++) if (inventories.ContainsKey(inventoryID))
{ inventories.Remove(inventoryID);
Container inventory = inventories[i];
if (inventory == null) continue; if (inventoryID != 0)
if (inventory.Type == Container.GetContainerType(inventoryID)) ConsoleIO.WriteLogLine("Inventory # " + inventoryID + " closed.");
{
inventories.Remove(inventory);
return;
}
}
} }
/// <summary> /// <summary>
/// When received window items from server. /// When received window items from server.
/// </summary> /// </summary>
/// <param name="type">Inventory type</param> /// <param name="inventoryID">Inventory ID</param>
/// <param name="itemList">Item list, key = slot ID, value = Item information</param> /// <param name="itemList">Item list, key = slot ID, value = Item information</param>
public void OnWindowItems(int type, Dictionary<int, Inventory.Item> itemList) public void OnWindowItems(byte inventoryID, Dictionary<int, Inventory.Item> itemList)
{ {
// 0 is player inventory if (inventories.ContainsKey(inventoryID))
if (type == 0) inventories[inventoryID].Items = itemList;
playerInventory.Items = itemList;
if (Settings.DebugMessages)
{
ConsoleIO.WriteLineFormatted("§8Received Window of type " + type);
foreach (var item in itemList)
ConsoleIO.WriteLineFormatted("§8 - Slot " + item.Key + ": " + item.Value.Type + " x" + item.Value.Count);
}
}
/// <summary>
/// When a slot is cleared inside window items
/// </summary>
/// <param name="WindowID">Window ID</param>
/// <param name="SlotID">Slot ID</param>
public void OnSlotClear(byte WindowID, short SlotID)
{
if (WindowID == 0 && playerInventory.Items.ContainsKey(SlotID))
{
playerInventory.Items.Remove(SlotID);
}
} }
/// <summary> /// <summary>
/// When a slot is set inside window items /// When a slot is set inside window items
/// </summary> /// </summary>
/// <param name="WindowID">Window ID</param> /// <param name="inventoryID">Window ID</param>
/// <param name="SlotID">Slot ID</param> /// <param name="slotID">Slot ID</param>
/// <param name="ItemID">Item ID</param> /// <param name="item">Item (may be null for empty slot)</param>
/// <param name="Count">Item Count</param> public void OnSetSlot(byte inventoryID, short slotID, Item item)
/// <param name="NBT">Item Metadata</param>
public void OnSetSlot(byte WindowID, short SlotID, int ItemID, byte Count, Dictionary<string, object> NBT)
{ {
if (WindowID == 0) if (inventories.ContainsKey(inventoryID))
{ {
playerInventory.Items[SlotID] = new Inventory.Item(ItemID, Count, SlotID, NBT); if (item == null || item.IsEmpty)
{
if (inventories[inventoryID].Items.ContainsKey(slotID))
inventories[inventoryID].Items.Remove(slotID);
}
else inventories[inventoryID].Items[slotID] = item;
} }
} }
@ -1312,7 +1310,35 @@ namespace MinecraftClient
/// <returns>TRUE if the item was successfully used</returns> /// <returns>TRUE if the item was successfully used</returns>
public bool UseItemOnHand() public bool UseItemOnHand()
{ {
return handler.SendUseItemPacket(0); return handler.SendUseItem(0);
}
/// <summary>
/// Click a slot in the specified window
/// </summary>
/// <returns>TRUE if the slot was successfully clicked</returns>
public bool ClickWindowSlot(int windowId, int slotId)
{
Item item = null;
if (inventories.ContainsKey(windowId) && inventories[windowId].Items.ContainsKey(slotId))
item = inventories[windowId].Items[slotId];
return handler.SendClickWindow(windowId, slotId, item);
}
/// <summary>
/// Close the specified inventory window
/// </summary>
/// <param name="windowId">Window ID</param>
/// <returns>TRUE if the window was successfully closed</returns>
public bool CloseInventory(int windowId)
{
if (windowId != 0 && inventories.ContainsKey(windowId))
{
inventories.Remove(windowId);
return handler.SendCloseWindow(windowId);
}
return false;
} }
/// <summary> /// <summary>
@ -1320,10 +1346,10 @@ namespace MinecraftClient
/// </summary> /// </summary>
/// <param name="EntityID"></param> /// <param name="EntityID"></param>
/// <param name="type">0: interact, 1: attack, 2: interact at</param> /// <param name="type">0: interact, 1: attack, 2: interact at</param>
/// <returns></returns> /// <returns>TRUE if interaction succeeded</returns>
public bool InteractEntity(int EntityID, int type) public bool InteractEntity(int EntityID, int type)
{ {
return handler.SendInteractEntityPacket(EntityID, type); return handler.SendInteractEntity(EntityID, type);
} }
/// <summary> /// <summary>
@ -1342,7 +1368,7 @@ namespace MinecraftClient
/// Change active slot in the player inventory /// Change active slot in the player inventory
/// </summary> /// </summary>
/// <param name="slot">Slot to activate (0 to 8)</param> /// <param name="slot">Slot to activate (0 to 8)</param>
/// <returns></returns> /// <returns>TRUE if the slot was changed</returns>
public bool ChangeSlot(short slot) public bool ChangeSlot(short slot)
{ {
if (slot >= 0 && slot <= 8) if (slot >= 0 && slot <= 8)

View file

@ -5,6 +5,7 @@ using System.Text;
using System.Net.Sockets; using System.Net.Sockets;
using MinecraftClient.Mapping; using MinecraftClient.Mapping;
using MinecraftClient.Crypto; using MinecraftClient.Crypto;
using MinecraftClient.Inventory;
namespace MinecraftClient.Protocol.Handlers namespace MinecraftClient.Protocol.Handlers
{ {
@ -319,6 +320,40 @@ namespace MinecraftClient.Protocol.Handlers
return ReadNextNbt(cache, true); return ReadNextNbt(cache, true);
} }
/// <summary>
/// Read a single item slot from a cache of byte and remove it from the cache
/// </summary>
/// <param name="item">Item</param>
/// <returns>The item that was read or NULL for an empty slot</returns>
public Item ReadNextItemSlot(List<byte> cache)
{
List<byte> slotData = new List<byte>();
if (protocolversion > Protocol18Handler.MC113Version)
{
// MC 1.13 and greater
bool itemPresent = ReadNextBool(cache);
if (itemPresent)
{
int itemID = ReadNextVarInt(cache);
byte itemCount = ReadNextByte(cache);
Dictionary<string, object> NBT = ReadNextNbt(cache);
return new Item(itemID, itemCount, NBT);
}
else return null;
}
else
{
// MC 1.12.2 and lower
short itemID = ReadNextShort(cache);
if (itemID == -1)
return null;
byte itemCount = ReadNextByte(cache);
short itemDamage = ReadNextShort(cache);
Dictionary<string, object> NBT = ReadNextNbt(cache);
return new Item(itemID, itemCount, NBT);
}
}
/// <summary> /// <summary>
/// Read an uncompressed Named Binary Tag blob and remove it from the cache (internal) /// Read an uncompressed Named Binary Tag blob and remove it from the cache (internal)
/// </summary> /// </summary>
@ -395,6 +430,163 @@ namespace MinecraftClient.Protocol.Handlers
} }
} }
/// <summary>
/// Build an uncompressed Named Binary Tag blob for sending over the network
/// </summary>
/// <param name="nbt">Dictionary to encode as Nbt</param>
/// <returns>Byte array for this NBT tag</returns>
public byte[] GetNbt(Dictionary<string, object> nbt)
{
return GetNbt(nbt, true);
}
/// <summary>
/// Build an uncompressed Named Binary Tag blob for sending over the network (internal)
/// </summary>
/// <param name="nbt">Dictionary to encode as Nbt</param>
/// <param name="root">TRUE if starting a new NBT tag, FALSE if processing a nested NBT tag</param>
/// <returns>Byte array for this NBT tag</returns>
private byte[] GetNbt(Dictionary<string, object> nbt, bool root)
{
if (nbt == null || nbt.Count == 0)
return new byte[] { 0 }; // TAG_End
List<byte> bytes = new List<byte>();
if (root)
{
bytes.Add(10); // TAG_Compound
}
foreach (var item in nbt)
{
byte fieldType;
byte[] fieldNameLength = GetUShort((ushort)item.Key.Length);
byte[] fieldName = Encoding.ASCII.GetBytes(item.Key);
byte[] fieldData = GetNbtField(item.Value, out fieldType);
bytes.AddRange(fieldNameLength);
bytes.AddRange(fieldName);
bytes.Add(fieldType);
bytes.AddRange(fieldData);
}
bytes.Add(0); // TAG_End
return bytes.ToArray();
}
/// <summary>
/// Convert a single object into its NBT representation (internal)
/// </summary>
/// <param name="obj">Object to convert</param>
/// <param name="fieldType">Field type for the passed object</param>
/// <returns>Binary data for the passed object</returns>
private byte[] GetNbtField(object obj, out byte fieldType)
{
if (obj is byte)
{
fieldType = 1; // TAG_Byte
return new[] { (byte)obj };
}
else if (obj is short)
{
fieldType = 2; // TAG_Short
return GetShort((short)obj);
}
else if (obj is int)
{
fieldType = 3; // TAG_Int
return GetInt((int)obj);
}
else if (obj is long)
{
fieldType = 4; // TAG_Long
return GetLong((long)obj);
}
else if (obj is float)
{
fieldType = 5; // TAG_Float
return GetFloat((float)obj);
}
else if (obj is double)
{
fieldType = 6; // TAG_Double
return GetDouble((double)obj);
}
else if (obj is byte[])
{
fieldType = 7; // TAG_Byte_Array
return (byte[])obj;
}
else if (obj is string)
{
fieldType = 8; // TAG_String
byte[] stringBytes = Encoding.UTF8.GetBytes((string)obj);
return ConcatBytes(GetUShort((ushort)stringBytes.Length), stringBytes);
}
else if (obj is object[])
{
fieldType = 9; // TAG_List
List<object> list = new List<object>((object[])obj);
int arrayLengthTotal = list.Count;
// Treat empty list as TAG_Byte, length 0
if (arrayLengthTotal == 0)
return ConcatBytes(new[] { (byte)1 }, GetInt(0));
// Encode first list item, retain its type
byte firstItemType;
string firstItemTypeString = list[0].GetType().Name;
byte[] firstItemBytes = GetNbtField(list[0], out firstItemType);
list.RemoveAt(0);
// Encode further list items, check they have the same type
byte subsequentItemType;
List<byte> subsequentItemsBytes = new List<byte>();
foreach (object item in list)
{
subsequentItemsBytes.AddRange(GetNbtField(item, out subsequentItemType));
if (subsequentItemType != firstItemType)
throw new System.IO.InvalidDataException(
"GetNbt: Cannot encode object[] list with mixed types: " + firstItemTypeString + ", " + item.GetType().Name + " into NBT!");
}
// Build NBT list: type, length, item array
return ConcatBytes(new[] { firstItemType }, GetInt(arrayLengthTotal), firstItemBytes, subsequentItemsBytes.ToArray());
}
else if (obj is Dictionary<string, object>)
{
fieldType = 10; // TAG_Compound
return GetNbt((Dictionary<string, object>)obj, false);
}
else if (obj is int[])
{
fieldType = 11; // TAG_Int_Array
int[] srcIntList = (int[])obj;
List<byte> encIntList = new List<byte>();
encIntList.AddRange(GetInt(srcIntList.Length));
foreach (int item in srcIntList)
encIntList.AddRange(GetInt(item));
return encIntList.ToArray();
}
else if (obj is long[])
{
fieldType = 12; // TAG_Long_Array
long[] srcLongList = (long[])obj;
List<byte> encLongList = new List<byte>();
encLongList.AddRange(GetInt(srcLongList.Length));
foreach (long item in srcLongList)
encLongList.AddRange(GetLong(item));
return encLongList.ToArray();
}
else
{
throw new System.IO.InvalidDataException("GetNbt: Cannot encode data type " + obj.GetType().Name + " into NBT!");
}
}
/// <summary> /// <summary>
/// Build an integer for sending over the network /// Build an integer for sending over the network
/// </summary> /// </summary>
@ -412,6 +604,30 @@ namespace MinecraftClient.Protocol.Handlers
return bytes.ToArray(); return bytes.ToArray();
} }
/// <summary>
/// Get byte array representing a long integer
/// </summary>
/// <param name="number">Long to process</param>
/// <returns>Array ready to send</returns>
public byte[] GetLong(long number)
{
byte[] theLong = BitConverter.GetBytes(number);
Array.Reverse(theLong);
return theLong;
}
/// <summary>
/// Get byte array representing an integer
/// </summary>
/// <param name="number">Integer to process</param>
/// <returns>Array ready to send</returns>
public byte[] GetInt(int number)
{
byte[] theInt = BitConverter.GetBytes(number);
Array.Reverse(theInt);
return theInt;
}
/// <summary> /// <summary>
/// Get byte array representing a short /// Get byte array representing a short
/// </summary> /// </summary>
@ -424,6 +640,18 @@ namespace MinecraftClient.Protocol.Handlers
return theShort; return theShort;
} }
/// <summary>
/// Get byte array representing an unsigned short
/// </summary>
/// <param name="number">Short to process</param>
/// <returns>Array ready to send</returns>
public byte[] GetUShort(ushort number)
{
byte[] theShort = BitConverter.GetBytes(number);
Array.Reverse(theShort);
return theShort;
}
/// <summary> /// <summary>
/// Get byte array representing a double /// Get byte array representing a double
/// </summary> /// </summary>
@ -448,7 +676,6 @@ namespace MinecraftClient.Protocol.Handlers
return theFloat; return theFloat;
} }
/// <summary> /// <summary>
/// Get byte array with length information prepended to it /// Get byte array with length information prepended to it
/// </summary> /// </summary>
@ -473,7 +700,6 @@ namespace MinecraftClient.Protocol.Handlers
public byte[] GetString(string text) public byte[] GetString(string text)
{ {
byte[] bytes = Encoding.UTF8.GetBytes(text); byte[] bytes = Encoding.UTF8.GetBytes(text);
return ConcatBytes(GetVarInt(bytes.Length), bytes); return ConcatBytes(GetVarInt(bytes.Length), bytes);
} }
@ -499,6 +725,42 @@ namespace MinecraftClient.Protocol.Handlers
return locationBytes; return locationBytes;
} }
/// <summary>
/// Get a byte array representing the given item as an item slot
/// </summary>
/// <param name="item">Item</param>
/// <returns>Item slot representation</returns>
public byte[] GetItemSlot(Item item)
{
List<byte> slotData = new List<byte>();
if (protocolversion > Protocol18Handler.MC113Version)
{
// MC 1.13 and greater
if (item == null || item.IsEmpty)
slotData.Add(0); // No item
else
{
slotData.Add(1); // Item is present
slotData.AddRange(GetVarInt((int)item.Type));
slotData.Add((byte)item.Count);
slotData.AddRange(GetNbt(item.NBT));
}
}
else
{
// MC 1.12.2 and lower
if (item == null || item.IsEmpty)
slotData.AddRange(GetShort(-1));
else
{
slotData.AddRange(GetShort((short)item.Type));
slotData.Add((byte)item.Count);
slotData.AddRange(GetNbt(item.NBT));
}
}
return slotData.ToArray();
}
/// <summary> /// <summary>
/// Easily append several byte arrays /// Easily append several byte arrays
/// </summary> /// </summary>

View file

@ -23,6 +23,8 @@ namespace MinecraftClient.Protocol.Handlers
HeldItemChange, HeldItemChange,
InteractEntity, InteractEntity,
UseItem, UseItem,
ClickWindow,
CloseWindow,
PlayerBlockPlacement PlayerBlockPlacement
} }
} }

View file

@ -8,6 +8,7 @@ using MinecraftClient.Crypto;
using MinecraftClient.Proxy; using MinecraftClient.Proxy;
using System.Security.Cryptography; using System.Security.Cryptography;
using MinecraftClient.Mapping; using MinecraftClient.Mapping;
using MinecraftClient.Inventory;
namespace MinecraftClient.Protocol.Handlers namespace MinecraftClient.Protocol.Handlers
{ {
@ -641,22 +642,32 @@ namespace MinecraftClient.Protocol.Handlers
return false; //Currently not implemented return false; //Currently not implemented
} }
public bool SendInteractEntityPacket(int EntityID, int type) public bool SendInteractEntity(int EntityID, int type)
{ {
return false; //Currently not implemented return false; //Currently not implemented
} }
public bool SendInteractEntityPacket(int EntityID, int type, float X, float Y, float Z, int hand) public bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z, int hand)
{ {
return false; //Currently not implemented return false; //Currently not implemented
} }
public bool SendInteractEntityPacket(int EntityID, int type, float X, float Y, float Z) public bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z)
{ {
return false; //Currently not implemented return false; //Currently not implemented
} }
public bool SendUseItemPacket(int hand) public bool SendUseItem(int hand)
{
return false; //Currently not implemented
}
public bool SendClickWindow(int windowId, int slotId, Item item)
{
return false; //Currently not implemented
}
public bool SendCloseWindow(int windowId)
{ {
return false; //Currently not implemented return false; //Currently not implemented
} }

View file

@ -41,6 +41,7 @@ namespace MinecraftClient.Protocol.Handlers
private bool autocomplete_received = false; private bool autocomplete_received = false;
private int autocomplete_transaction_id = 0; private int autocomplete_transaction_id = 0;
private readonly List<string> autocomplete_result = new List<string>(); private readonly List<string> autocomplete_result = new List<string>();
private readonly Dictionary<int, short> window_actions = new Dictionary<int, short>();
private bool login_phase = true; private bool login_phase = true;
private int protocolversion; private int protocolversion;
private int currentDimension; private int currentDimension;
@ -497,23 +498,27 @@ namespace MinecraftClient.Protocol.Handlers
case PacketIncomingType.OpenWindow: case PacketIncomingType.OpenWindow:
if (handler.GetInventoryEnabled()) if (handler.GetInventoryEnabled())
{ {
if (protocolversion < MC114Version) // packet changed at 1.14 if (protocolversion < MC114Version)
{ {
// MC 1.13 or lower
byte windowID = dataTypes.ReadNextByte(packetData); byte windowID = dataTypes.ReadNextByte(packetData);
string type = dataTypes.ReadNextString(packetData).Replace("minecraft:", "").ToUpper(); string type = dataTypes.ReadNextString(packetData).Replace("minecraft:", "").ToUpper();
ContainerTypeOld inventoryType = (ContainerTypeOld)Enum.Parse(typeof(ContainerTypeOld), type); ContainerTypeOld inventoryType = (ContainerTypeOld)Enum.Parse(typeof(ContainerTypeOld), type);
string title = dataTypes.ReadNextString(packetData); string title = dataTypes.ReadNextString(packetData);
byte slots = dataTypes.ReadNextByte(packetData); byte slots = dataTypes.ReadNextByte(packetData);
Container inventory = new Container(windowID, inventoryType, title); Container inventory = new Container(windowID, inventoryType, ChatParser.ParseText(title));
handler.OnInventoryOpen(inventory); window_actions[windowID] = 0;
handler.OnInventoryOpen(windowID, inventory);
} }
else else
{ {
int WindowID = dataTypes.ReadNextVarInt(packetData); // MC 1.14 or greater
int WindowType = dataTypes.ReadNextVarInt(packetData); int windowID = dataTypes.ReadNextVarInt(packetData);
int windowType = dataTypes.ReadNextVarInt(packetData);
string title = dataTypes.ReadNextString(packetData); string title = dataTypes.ReadNextString(packetData);
Container inventory = new Container(WindowID, WindowType, title); Container inventory = new Container(windowID, windowType, ChatParser.ParseText(title));
handler.OnInventoryOpen(inventory); window_actions[windowID] = 0;
handler.OnInventoryOpen(windowID, inventory);
} }
} }
break; break;
@ -521,103 +526,32 @@ namespace MinecraftClient.Protocol.Handlers
if (handler.GetInventoryEnabled()) if (handler.GetInventoryEnabled())
{ {
byte windowID = dataTypes.ReadNextByte(packetData); byte windowID = dataTypes.ReadNextByte(packetData);
window_actions[windowID] = 0;
handler.OnInventoryClose(windowID); handler.OnInventoryClose(windowID);
} }
break; break;
case PacketIncomingType.WindowItems: case PacketIncomingType.WindowItems:
if (handler.GetInventoryEnabled()) if (handler.GetInventoryEnabled())
{ {
// MC 1.12.2 or lower byte windowId = dataTypes.ReadNextByte(packetData);
if (protocolversion < MC113Version) short elements = dataTypes.ReadNextShort(packetData);
Dictionary<int, Item> inventorySlots = new Dictionary<int, Item>();
for (short slotId = 0; slotId < elements; slotId++)
{ {
byte id = dataTypes.ReadNextByte(packetData); Item item = dataTypes.ReadNextItemSlot(packetData);
short elements = dataTypes.ReadNextShort(packetData); if (item != null)
Dictionary<int, Item> itemsList = new Dictionary<int, Item>(); // index is SlotID inventorySlots[slotId] = item;
for (int i = 0; i < elements; i++)
{
short itemID = dataTypes.ReadNextShort(packetData);
if (itemID == -1) continue;
byte itemCount = dataTypes.ReadNextByte(packetData);
short itemDamage = dataTypes.ReadNextShort(packetData);
Dictionary<string, object> NBT = new Dictionary<string, object>();
//TODO: Add to the dictionary for the inventory its in using the id
if (packetData.ToArray().Count() > 0)
{
NBT = dataTypes.ReadNextNbt(packetData);
}
Item item = new Item(itemID, itemCount, itemDamage, NBT);
itemsList.Add(i, item);
}
handler.OnWindowItems(id, itemsList);
}
else
{
// MC 1.13 after
byte id = dataTypes.ReadNextByte(packetData);
short elements = dataTypes.ReadNextShort(packetData);
Dictionary<int, Item> itemsList = new Dictionary<int, Item>(); // index is SlotID
for (int i = 0; i < elements; i++)
{
bool haveItem = dataTypes.ReadNextBool(packetData);
if (haveItem)
{
int itemID = dataTypes.ReadNextVarInt(packetData);
byte itemCount = dataTypes.ReadNextByte(packetData);
dataTypes.ReadNextNbt(packetData);
Item item = new Item(itemID, itemCount, i);
itemsList.Add(i, item);
}
}
handler.OnWindowItems(id, itemsList);
} }
handler.OnWindowItems(windowId, inventorySlots);
} }
break; break;
case PacketIncomingType.SetSlot: case PacketIncomingType.SetSlot:
if(handler.GetInventoryEnabled()) if (handler.GetInventoryEnabled())
{ {
// MC 1.12.2 or lower byte windowID = dataTypes.ReadNextByte(packetData);
if (protocolversion < MC113Version) short slotID = dataTypes.ReadNextShort(packetData);
{ Item item = dataTypes.ReadNextItemSlot(packetData);
byte WindowID = dataTypes.ReadNextByte(packetData); handler.OnSetSlot(windowID, slotID, item);
short SlotID = dataTypes.ReadNextShort(packetData);
short ItemID = dataTypes.ReadNextShort(packetData);
if (ItemID == -1)
{
handler.OnSlotClear(WindowID, SlotID);
}
else
{
byte Count = dataTypes.ReadNextByte(packetData);
short itemDamage = dataTypes.ReadNextShort(packetData); // useless so ignored
Dictionary<string, object> NBT = new Dictionary<string, object>();
//TODO: Add to the dictionary for the inventory its in using the id
if (packetData.ToArray().Count() > 0)
{
NBT = dataTypes.ReadNextNbt(packetData);
}
handler.OnSetSlot(WindowID, SlotID, ItemID, Count, NBT);
}
}
else
{
// MC 1.13 after
byte WindowID = dataTypes.ReadNextByte(packetData);
short SlotID = dataTypes.ReadNextShort(packetData);
bool Present = dataTypes.ReadNextBool(packetData);
if (Present)
{
int ItemID = dataTypes.ReadNextVarInt(packetData);
byte Count = dataTypes.ReadNextByte(packetData);
Dictionary<string, object> NBT = dataTypes.ReadNextNbt(packetData);
handler.OnSetSlot(WindowID, SlotID, ItemID, Count, NBT);
}
else
{
handler.OnSlotClear(WindowID, SlotID);
}
}
} }
break; break;
case PacketIncomingType.ResourcePackSend: case PacketIncomingType.ResourcePackSend:
@ -1285,7 +1219,7 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="EntityID"></param> /// <param name="EntityID"></param>
/// <param name="type"></param> /// <param name="type"></param>
/// <returns></returns> /// <returns></returns>
public bool SendInteractEntityPacket(int EntityID, int type) public bool SendInteractEntity(int EntityID, int type)
{ {
try try
{ {
@ -1300,7 +1234,7 @@ namespace MinecraftClient.Protocol.Handlers
catch (ObjectDisposedException) { return false; } catch (ObjectDisposedException) { return false; }
} }
// TODO: Interact at block location (e.g. chest minecart) // TODO: Interact at block location (e.g. chest minecart)
public bool SendInteractEntityPacket(int EntityID, int type, float X, float Y, float Z, int hand) public bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z, int hand)
{ {
try try
{ {
@ -1318,12 +1252,12 @@ namespace MinecraftClient.Protocol.Handlers
catch (System.IO.IOException) { return false; } catch (System.IO.IOException) { return false; }
catch (ObjectDisposedException) { return false; } catch (ObjectDisposedException) { return false; }
} }
public bool SendInteractEntityPacket(int EntityID, int type, float X, float Y, float Z) public bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z)
{ {
return false; return false;
} }
public bool SendUseItemPacket(int hand) public bool SendUseItem(int hand)
{ {
try try
{ {
@ -1370,5 +1304,45 @@ namespace MinecraftClient.Protocol.Handlers
catch (System.IO.IOException) { return false; } catch (System.IO.IOException) { return false; }
catch (ObjectDisposedException) { return false; } catch (ObjectDisposedException) { return false; }
} }
public bool SendClickWindow(int windowId, int slotId, Item item)
{
try
{
short actionNumber = (short)(window_actions[windowId] + 1);
window_actions[windowId] = actionNumber;
List<byte> packet = new List<byte>();
packet.Add((byte)windowId);
packet.AddRange(dataTypes.GetShort((short)slotId));
packet.Add(0); // Left mouse click
packet.AddRange(dataTypes.GetShort(actionNumber));
// Operation mode = 0 (default)
if (protocolversion >= MC19Version)
packet.AddRange(dataTypes.GetVarInt(0));
else packet.Add(0);
packet.AddRange(dataTypes.GetItemSlot(item));
SendPacket(PacketOutgoingType.ClickWindow, packet);
return true;
}
catch (SocketException) { return false; }
catch (System.IO.IOException) { return false; }
catch (ObjectDisposedException) { return false; }
}
public bool SendCloseWindow(int windowId)
{
try
{
SendPacket(PacketOutgoingType.CloseWindow, new[] { (byte)windowId });
return true;
}
catch (SocketException) { return false; }
catch (System.IO.IOException) { return false; }
catch (ObjectDisposedException) { return false; }
}
} }
} }

View file

@ -306,6 +306,8 @@ namespace MinecraftClient.Protocol.Handlers
case PacketOutgoingType.HeldItemChange: return 0x17; case PacketOutgoingType.HeldItemChange: return 0x17;
case PacketOutgoingType.InteractEntity: return 0x02; case PacketOutgoingType.InteractEntity: return 0x02;
case PacketOutgoingType.TeleportConfirm: throw new InvalidOperationException("Teleport confirm is not supported in protocol " + protocol); case PacketOutgoingType.TeleportConfirm: throw new InvalidOperationException("Teleport confirm is not supported in protocol " + protocol);
case PacketOutgoingType.ClickWindow: return 0x0E;
case PacketOutgoingType.CloseWindow: return 0x0D;
} }
} }
else if (protocol <= Protocol18Handler.MC1112Version) // MC 1.9, 1,10 and 1.11 else if (protocol <= Protocol18Handler.MC1112Version) // MC 1.9, 1,10 and 1.11
@ -324,6 +326,8 @@ namespace MinecraftClient.Protocol.Handlers
case PacketOutgoingType.TeleportConfirm: return 0x00; case PacketOutgoingType.TeleportConfirm: return 0x00;
case PacketOutgoingType.HeldItemChange: return 0x17; case PacketOutgoingType.HeldItemChange: return 0x17;
case PacketOutgoingType.InteractEntity: return 0x0A; case PacketOutgoingType.InteractEntity: return 0x0A;
case PacketOutgoingType.ClickWindow: return 0x07;
case PacketOutgoingType.CloseWindow: return 0x08;
} }
} }
else if (protocol <= Protocol18Handler.MC112Version) // MC 1.12 else if (protocol <= Protocol18Handler.MC112Version) // MC 1.12
@ -342,6 +346,8 @@ namespace MinecraftClient.Protocol.Handlers
case PacketOutgoingType.TeleportConfirm: return 0x00; case PacketOutgoingType.TeleportConfirm: return 0x00;
case PacketOutgoingType.HeldItemChange: return 0x1A; case PacketOutgoingType.HeldItemChange: return 0x1A;
case PacketOutgoingType.InteractEntity: return 0x0B; case PacketOutgoingType.InteractEntity: return 0x0B;
case PacketOutgoingType.ClickWindow: return 0x07;
case PacketOutgoingType.CloseWindow: return 0x08;
} }
} }
else if (protocol <= Protocol18Handler.MC1122Version) // 1.12.2 else if (protocol <= Protocol18Handler.MC1122Version) // 1.12.2
@ -360,6 +366,8 @@ namespace MinecraftClient.Protocol.Handlers
case PacketOutgoingType.TeleportConfirm: return 0x00; case PacketOutgoingType.TeleportConfirm: return 0x00;
case PacketOutgoingType.HeldItemChange: return 0x1F; case PacketOutgoingType.HeldItemChange: return 0x1F;
case PacketOutgoingType.InteractEntity: return 0x0A; case PacketOutgoingType.InteractEntity: return 0x0A;
case PacketOutgoingType.ClickWindow: return 0x07;
case PacketOutgoingType.CloseWindow: return 0x08;
} }
} }
else if (protocol < Protocol18Handler.MC114Version) // MC 1.13 to 1.13.2 else if (protocol < Protocol18Handler.MC114Version) // MC 1.13 to 1.13.2
@ -378,6 +386,8 @@ namespace MinecraftClient.Protocol.Handlers
case PacketOutgoingType.TeleportConfirm: return 0x00; case PacketOutgoingType.TeleportConfirm: return 0x00;
case PacketOutgoingType.HeldItemChange: return 0x21; case PacketOutgoingType.HeldItemChange: return 0x21;
case PacketOutgoingType.InteractEntity: return 0x0D; case PacketOutgoingType.InteractEntity: return 0x0D;
case PacketOutgoingType.ClickWindow: return 0x08;
case PacketOutgoingType.CloseWindow: return 0x09;
} }
} }
else // MC 1.14 to 1.15 else // MC 1.14 to 1.15
@ -398,6 +408,8 @@ namespace MinecraftClient.Protocol.Handlers
case PacketOutgoingType.InteractEntity: return 0x0E; case PacketOutgoingType.InteractEntity: return 0x0E;
case PacketOutgoingType.UseItem: return 0x2D; case PacketOutgoingType.UseItem: return 0x2D;
case PacketOutgoingType.PlayerBlockPlacement: return 0x2C; case PacketOutgoingType.PlayerBlockPlacement: return 0x2C;
case PacketOutgoingType.ClickWindow: return 0x09;
case PacketOutgoingType.CloseWindow: return 0x0A;
} }
} }

View file

@ -4,6 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using MinecraftClient.Crypto; using MinecraftClient.Crypto;
using MinecraftClient.Mapping; using MinecraftClient.Mapping;
using MinecraftClient.Inventory;
namespace MinecraftClient.Protocol namespace MinecraftClient.Protocol
{ {
@ -98,7 +99,7 @@ namespace MinecraftClient.Protocol
/// <param name="EntityID">Entity ID to interact with</param> /// <param name="EntityID">Entity ID to interact with</param>
/// <param name="type">Type of interaction (0: interact, 1: attack, 2: interact at)</param> /// <param name="type">Type of interaction (0: interact, 1: attack, 2: interact at)</param>
/// <returns>True if packet was successfully sent</returns> /// <returns>True if packet was successfully sent</returns>
bool SendInteractEntityPacket(int EntityID, int type); bool SendInteractEntity(int EntityID, int type);
/// <summary> /// <summary>
/// Send an entity interaction packet to the server. /// Send an entity interaction packet to the server.
@ -110,7 +111,7 @@ namespace MinecraftClient.Protocol
/// <param name="Z">Z coordinate for "interact at"</param> /// <param name="Z">Z coordinate for "interact at"</param>
/// <param name="hand">Player hand (0: main hand, 1: off hand)</param> /// <param name="hand">Player hand (0: main hand, 1: off hand)</param>
/// <returns>True if packet was successfully sent</returns> /// <returns>True if packet was successfully sent</returns>
bool SendInteractEntityPacket(int EntityID, int type, float X, float Y, float Z, int hand); bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z, int hand);
/// <summary> /// <summary>
/// Send an entity interaction packet to the server. /// Send an entity interaction packet to the server.
@ -121,14 +122,29 @@ namespace MinecraftClient.Protocol
/// <param name="Y">Y coordinate for "interact at"</param> /// <param name="Y">Y coordinate for "interact at"</param>
/// <param name="Z">Z coordinate for "interact at"</param> /// <param name="Z">Z coordinate for "interact at"</param>
/// <returns>True if packet was successfully sent</returns> /// <returns>True if packet was successfully sent</returns>
bool SendInteractEntityPacket(int EntityID, int type, float X, float Y, float Z); bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z);
/// <summary> /// <summary>
/// Send a use item packet to the server /// Send a use item packet to the server
/// </summary> /// </summary>
/// <param name="hand">0: main hand, 1: off hand</param> /// <param name="hand">0: main hand, 1: off hand</param>
/// <returns>True if packet was successfully sent</returns> /// <returns>True if packet was successfully sent</returns>
bool SendUseItemPacket(int hand); bool SendUseItem(int hand);
/// <summary>
/// Send a click window slot packet to the server
/// </summary>
/// <param name="windowId">Id of the window being clicked</param>
/// <param name="slotId">Id of the clicked slot</param>
/// <param name="item">Item in the clicked slot</param>
/// <returns>True if packet was successfully sent</returns>
bool SendClickWindow(int windowId, int slotId, Item item);
/// <summary>
/// Send a close window packet to the server
/// </summary>
/// <param name="windowId">Id of the window being closed</param>
bool SendCloseWindow(int windowId);
/// <summary> /// <summary>
/// Send player block placement packet to the server /// Send player block placement packet to the server

View file

@ -53,12 +53,12 @@ namespace MinecraftClient.Protocol
/// <summary> /// <summary>
/// Called when an inventory is opened /// Called when an inventory is opened
/// </summary> /// </summary>
void OnInventoryOpen(Container inventory); void OnInventoryOpen(int inventoryID, Container inventory);
/// <summary> /// <summary>
/// Called when an inventory is closed /// Called when an inventory is closed
/// </summary> /// </summary>
void OnInventoryClose(byte inventoryID); void OnInventoryClose(int inventoryID);
/// <summary> /// <summary>
/// Called when the player respawns, which happens on login, respawn and world change. /// Called when the player respawns, which happens on login, respawn and world change.
@ -199,26 +199,17 @@ namespace MinecraftClient.Protocol
/// <summary> /// <summary>
/// Called when inventory items have been received /// Called when inventory items have been received
/// </summary> /// </summary>
/// <param name="type">Inventory type</param> /// <param name="inventoryID">Inventory ID</param>
/// <param name="itemList">Item list</param> /// <param name="itemList">Item list</param>
void OnWindowItems(int type, Dictionary<int, MinecraftClient.Inventory.Item> itemList); void OnWindowItems(byte inventoryID, Dictionary<int, MinecraftClient.Inventory.Item> itemList);
/// <summary>
/// Called when a single slot has been cleared inside an inventory
/// </summary>
/// <param name="WindowID">Inventory ID</param>
/// <param name="SlotID">Slot ID</param>
void OnSlotClear(byte WindowID, short SlotID);
/// <summary> /// <summary>
/// Called when a single slot has been updated inside an inventory /// Called when a single slot has been updated inside an inventory
/// </summary> /// </summary>
/// <param name="WindowID">Inventory ID</param> /// <param name="inventoryID">Window ID</param>
/// <param name="SlotID">Slot ID</param> /// <param name="slotID">Slot ID</param>
/// <param name="ItemID">Item ID</param> /// <param name="item">Item (may be null for empty slot)</param>
/// <param name="Count">Item Count</param> void OnSetSlot(byte inventoryID, short slotID, Item item);
/// <param name="NBT">Item Metadata</param>
void OnSetSlot(byte WindowID, short SlotID, int ItemID, byte Count, Dictionary<string, object> NBT);
/// <summary> /// <summary>
/// Called when the Player entity ID has been received from the server /// Called when the Player entity ID has been received from the server