namespace Minecraft.Server.FourKit.Inventory; using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; using Minecraft.Server.FourKit.Enchantments; using Minecraft.Server.FourKit.Entity; using Minecraft.Server.FourKit.Inventory.Meta; /// /// Represents a player's inventory, including armor slots and the held item. /// public class PlayerInventory : Inventory { private const int INVENTORY_SIZE = 40; private const int ARMOR_START = 36; private const int QUICKBAR_SIZE = 9; private int _heldItemSlot; internal HumanEntity? _holder; private int[] syncBuffer; private GCHandle syncBufferHandle; internal PlayerInventory() : base("Player", InventoryType.PLAYER, INVENTORY_SIZE) { this.syncBuffer = new int[121]; this.syncBufferHandle = GCHandle.Alloc(this.syncBuffer, GCHandleType.Pinned); } protected internal override void EnsureSynced() { if (_holder == null || NativeBridge.GetPlayerInventory == null) return; int entityId = _holder.getEntityId(); NativeBridge.GetPlayerInventory(entityId, this.syncBufferHandle.AddrOfPinnedObject()); byte[]? metadataBuffer = null; GCHandle? metadataBufferHandle = null; for (int i = 0; i < INVENTORY_SIZE; i++) { int id = this.syncBuffer[i * 3 + 0]; int aux = this.syncBuffer[i * 3 + 1]; int packed = this.syncBuffer[i * 3 + 2]; ushort count = (ushort)((packed >> 8) & 0xFFFF); byte flags = (byte)((packed >> 24) & 0xFF); bool hasMetadata = (flags & 0x1) != 0; _items[i]?.UnbindFromInventory(); if (id > 0 && count > 0) { if (_items[i] == null) { _items[i] = new ItemStack(id, count, (short)aux); } else { _items[i]!.setTypeId(id); _items[i]!.setAmount(count); _items[i]!.setDurability((short)aux); } if (hasMetadata) { var meta = ReadMetaFromNative(entityId, i, metadataBuffer, metadataBufferHandle); if (meta != null) { _items[i]!.setItemMetaInternal(meta); } } _items[i]!.BindToInventory(this, i); } else { _items[i] = null; } } _heldItemSlot = this.syncBuffer[120]; if (metadataBufferHandle.HasValue) metadataBufferHandle.Value.Free(); } /// public override void setItem(int index, ItemStack? item) { base.setItem(index, item); _slotModifiedByPlugin = true; if (_holder != null && NativeBridge.SetPlayerInventorySlot != null && index >= 0 && index < INVENTORY_SIZE) { int id = item?.getTypeId() ?? 0; int count = item?.getAmount() ?? 0; int aux = item?.getDurability() ?? 0; NativeBridge.SetPlayerInventorySlot(_holder.getEntityId(), index, id, count, aux); WriteMetaToNative(_holder.getEntityId(), index, item?.getItemMetaInternal()); } } /// /// Returns all ItemStacks from the armor slots. /// /// An array of ItemStacks for the armor slots. public ItemStack?[] getArmorContents() { EnsureSynced(); var armor = new ItemStack?[4]; for (int i = 0; i < 4; i++) armor[i] = _items[ARMOR_START + i]; return armor; } /// /// Gets the ItemStack in the helmet slot. /// /// The helmet ItemStack. public ItemStack? getHelmet() => getItem(ARMOR_START + 3); /// /// Gets the ItemStack in the chestplate slot. /// /// The chestplate ItemStack. public ItemStack? getChestplate() => getItem(ARMOR_START + 2); /// /// Gets the ItemStack in the leggings slot. /// /// The leggings ItemStack. public ItemStack? getLeggings() => getItem(ARMOR_START + 1); /// /// Gets the ItemStack in the boots slot. /// /// The boots ItemStack. public ItemStack? getBoots() => getItem(ARMOR_START); /// /// Sets all four armor slots at once. /// /// An array of ItemStacks for the armor slots. public void setArmorContents(ItemStack?[] items) { int len = Math.Min(items.Length, 4); for (int i = 0; i < len; i++) setItem(ARMOR_START + i, items[i]); } /// /// Sets the helmet slot. /// /// The ItemStack to set. public void setHelmet(ItemStack? helmet) => setItem(ARMOR_START + 3, helmet); /// /// Sets the chestplate slot. /// /// The ItemStack to set. public void setChestplate(ItemStack? chestplate) => setItem(ARMOR_START + 2, chestplate); /// /// Sets the leggings slot. /// /// The ItemStack to set. public void setLeggings(ItemStack? leggings) => setItem(ARMOR_START + 1, leggings); /// /// Sets the boots slot. /// /// The ItemStack to set. public void setBoots(ItemStack? boots) => setItem(ARMOR_START, boots); /// /// Gets the item the player is currently holding. /// /// The held ItemStack. public ItemStack? getItemInHand() { EnsureSynced(); return getItem(_heldItemSlot); } /// /// Sets the item in the player's hand. /// /// The ItemStack to set. public void setItemInHand(ItemStack? stack) { EnsureSynced(); //we need to sync the current held slot, hate doing this here during a set call setItem(_heldItemSlot, stack); } /// /// Gets the slot number of the currently held item. /// /// The held item slot index (0-8). public int getHeldItemSlot() { EnsureSynced(); return _heldItemSlot; } /// /// Sets the slot number of the currently held item. /// /// The slot index (0-8). public void setHeldItemSlot(int slot) { if (slot < 0 || slot >= QUICKBAR_SIZE) throw new ArgumentException($"Slot must be between 0 and {QUICKBAR_SIZE - 1} inclusive."); _heldItemSlot = slot; if (_holder != null) NativeBridge.SetHeldItemSlot?.Invoke(_holder.getEntityId(), slot); } /// /// Clears all matching items from the inventory. Setting either value /// to -1 will skip its check, while setting both to -1 will clear all /// items in your inventory unconditionally. /// /// The material id to match, or -1 for any. /// The data value to match, or -1 for any. /// The number of stacks cleared. public int clear(int id, int data) { EnsureSynced(); int count = 0; for (int i = 0; i < getSize(); i++) { var item = _items[i]; if (item == null) continue; if (id != -1 && item.getTypeId() != id) continue; if (data != -1 && item.getDurability() != data) continue; setItem(i, null); count++; } return count; } /// /// Gets the holder of this inventory. /// /// The HumanEntity that owns this inventory. public HumanEntity? getHolder() => _holder; private static ItemMeta? ReadMetaFromNative(int entityId, int slot, byte[]? buffer, GCHandle? bufferHandle) { if (NativeBridge.GetItemMeta == null) return null; if (buffer == null) { buffer = new byte[4096]; bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); } int bytesWritten = NativeBridge.GetItemMeta(entityId, slot, bufferHandle.GetValueOrDefault().AddrOfPinnedObject(), buffer!.Length); if (bytesWritten <= 0) return null; int offset = 0; int nameLen = BitConverter.ToInt32(buffer, offset); offset += 4; string? displayName = null; if (nameLen > 0) { displayName = Encoding.UTF8.GetString(buffer, offset, nameLen); offset += nameLen; } int loreCount = 0; if (offset + 4 <= bytesWritten) { loreCount = BitConverter.ToInt32(buffer, offset); offset += 4; } List? lore = null; if (loreCount > 0) { lore = new List(loreCount); for (int i = 0; i < loreCount; i++) { if (offset + 4 > bytesWritten) break; int lineLen = BitConverter.ToInt32(buffer, offset); offset += 4; if (lineLen > 0 && offset + lineLen <= bytesWritten) { lore.Add(Encoding.UTF8.GetString(buffer, offset, lineLen)); offset += lineLen; } else { lore.Add(string.Empty); } } } int enchantCount = 0; if (offset + 4 <= bytesWritten) { enchantCount = BitConverter.ToInt32(buffer, offset); offset += 4; } Dictionary? enchants = null; if (enchantCount > 0) { enchants = new Dictionary(enchantCount); for (int i = 0; i < enchantCount; i++) { if (offset + (4 + 4) > bytesWritten) break; int type = BitConverter.ToInt32(buffer, offset); offset += 4; int level = BitConverter.ToInt32(buffer, offset); offset += 4; enchants.Add((EnchantmentType)type, level); } } if (displayName == null && (lore == null || lore.Count == 0) && (enchants == null || enchants.Count == 0)) return null; var meta = new ItemMeta(); if (displayName != null) meta.setDisplayName(displayName); if (lore != null && lore.Count > 0) meta.setLore(lore); if (enchants != null && enchants.Count > 0) meta.setEnchants(enchants); return meta; } private static void WriteMetaToNative(int entityId, int slot, ItemMeta? meta) { if (NativeBridge.SetItemMeta == null) return; if (meta == null || meta.isEmpty()) { NativeBridge.SetItemMeta(entityId, slot, IntPtr.Zero, 0); return; } using var ms = new System.IO.MemoryStream(512); using var bw = new System.IO.BinaryWriter(ms); if (meta.hasDisplayName()) { byte[] nameBytes = Encoding.UTF8.GetBytes(meta.getDisplayName()); bw.Write(nameBytes.Length); bw.Write(nameBytes); } else { bw.Write(0); } if (meta.hasLore()) { var lore = meta.getLore(); bw.Write(lore.Count); foreach (var line in lore) { byte[] lineBytes = Encoding.UTF8.GetBytes(line ?? string.Empty); bw.Write(lineBytes.Length); bw.Write(lineBytes); } } else { bw.Write(0); } if (meta.hasEnchants()) { var enchants = meta.getEnchants(); bw.Write(enchants.Count); foreach (var enchant in enchants) { bw.Write((int)enchant.Key); bw.Write(enchant.Value); } } else { bw.Write(0); } bw.Flush(); byte[] data = ms.ToArray(); var gh = GCHandle.Alloc(data, GCHandleType.Pinned); try { NativeBridge.SetItemMeta(entityId, slot, gh.AddrOfPinnedObject(), data.Length); } finally { gh.Free(); } } }