Add itemmeta, fix some bugs regarding inventory syncing and missing impl (oops)

This commit is contained in:
sylvessa 2026-03-22 20:00:36 -05:00
parent aca05bad7b
commit 91b189e1bd
6 changed files with 467 additions and 7 deletions

View file

@ -581,11 +581,11 @@ public static class FourKitHost
}
[UnmanagedCallersOnly]
public static void SetInventoryCallbacks(IntPtr getPlayerInventory, IntPtr setPlayerInventorySlot, IntPtr getContainerContents, IntPtr setContainerSlot, IntPtr getContainerViewerEntityIds, IntPtr closeContainer, IntPtr openVirtualContainer)
public static void SetInventoryCallbacks(IntPtr getPlayerInventory, IntPtr setPlayerInventorySlot, IntPtr getContainerContents, IntPtr setContainerSlot, IntPtr getContainerViewerEntityIds, IntPtr closeContainer, IntPtr openVirtualContainer, IntPtr getItemMeta, IntPtr setItemMeta, IntPtr setHeldItemSlot)
{
try
{
NativeBridge.SetInventoryCallbacks(getPlayerInventory, setPlayerInventorySlot, getContainerContents, setContainerSlot, getContainerViewerEntityIds, closeContainer, openVirtualContainer);
NativeBridge.SetInventoryCallbacks(getPlayerInventory, setPlayerInventorySlot, getContainerContents, setContainerSlot, getContainerViewerEntityIds, closeContainer, openVirtualContainer, getItemMeta, setItemMeta, setHeldItemSlot);
//ServerLog.Info("fourkit", "Inventory native callbacks registered.");
}
catch (Exception ex)

View file

@ -1,5 +1,7 @@
namespace Minecraft.Server.FourKit.Inventory;
using Minecraft.Server.FourKit.Inventory.Meta;
/// <summary>
/// Represents a stack of items.
/// </summary>
@ -8,6 +10,7 @@ public class ItemStack
private Material _type;
private int _amount;
private short _durability;
private ItemMeta? _meta;
/// <summary>
/// Creates a new ItemStack of the specified material with amount 1.
@ -99,4 +102,31 @@ public class ItemStack
/// </summary>
/// <param name="durability">Durability of this item.</param>
public void setDurability(short durability) => _durability = durability;
/// <summary>
/// Get a copy of this ItemStack's ItemMeta.
/// </summary>
/// <returns>A copy of the current ItemStack's ItemMeta.</returns>
public ItemMeta getItemMeta() => _meta?.clone() ?? new ItemMeta();
/// <summary>
/// Checks to see if any meta data has been defined.
/// </summary>
/// <returns>Returns true if some meta data has been set for this item.</returns>
public bool hasItemMeta() => _meta != null && !_meta.isEmpty();
/// <summary>
/// Set the ItemMeta of this ItemStack.
/// </summary>
/// <param name="itemMeta">New ItemMeta, or null to indicate meta data be cleared.</param>
/// <returns>True if successfully applied ItemMeta.</returns>
public bool setItemMeta(ItemMeta? itemMeta)
{
_meta = itemMeta?.clone();
return true;
}
internal ItemMeta? getItemMetaInternal() => _meta;
internal void setItemMetaInternal(ItemMeta? meta) => _meta = meta;
}

View file

@ -0,0 +1,64 @@
namespace Minecraft.Server.FourKit.Inventory.Meta;
/// <summary>
/// Represents the metadata of an <see cref="ItemStack"/>, including display name and lore.
/// </summary>
public class ItemMeta
{
private string? _displayName;
private List<string>? _lore;
/// <summary>
/// Checks for existence of a display name.
/// </summary>
/// <returns>true if this has a display name.</returns>
public bool hasDisplayName() => _displayName != null;
/// <summary>
/// Gets the display name that is set.
/// Plugins should check that hasDisplayName() returns true before calling this method.
/// </summary>
/// <returns>The display name that is set.</returns>
public string getDisplayName() => _displayName ?? string.Empty;
/// <summary>
/// Sets the display name.
/// </summary>
/// <param name="name">The name to set.</param>
public void setDisplayName(string? name) => _displayName = name;
/// <summary>
/// Checks for existence of lore.
/// </summary>
/// <returns>true if this has lore.</returns>
public bool hasLore() => _lore != null && _lore.Count > 0;
/// <summary>
/// Gets the lore that is set.
/// Plugins should check if hasLore() returns true before calling this method.
/// </summary>
/// <returns>A list of lore that is set.</returns>
public List<string> getLore() => _lore != null ? new List<string>(_lore) : new List<string>();
/// <summary>
/// Sets the lore for this item. Removes lore when given null.
/// </summary>
/// <param name="lore">The lore that will be set.</param>
public void setLore(List<string>? lore)
{
_lore = lore != null ? new List<string>(lore) : null;
}
public ItemMeta clone()
{
var copy = new ItemMeta();
copy._displayName = _displayName;
copy._lore = _lore != null ? new List<string>(_lore) : null;
return copy;
}
internal bool isEmpty()
{
return _displayName == null && (_lore == null || _lore.Count == 0);
}
}

View file

@ -1,7 +1,9 @@
namespace Minecraft.Server.FourKit.Inventory;
using System.Runtime.InteropServices;
using System.Text;
using Minecraft.Server.FourKit.Entity;
using Minecraft.Server.FourKit.Inventory.Meta;
/// <summary>
/// Represents a player's inventory, including armor slots and the held item.
@ -43,9 +45,17 @@ public class PlayerInventory : Inventory
int count = buf[i * 3 + 1];
int aux = buf[i * 3 + 2];
if (id > 0 && count > 0)
_items[i] = new ItemStack(id, count, (short)aux);
{
var stack = new ItemStack(id, count, (short)aux);
var meta = ReadMetaFromNative(entityId, i);
if (meta != null)
stack.setItemMetaInternal(meta);
_items[i] = stack;
}
else
{
_items[i] = null;
}
}
_heldItemSlot = buf[120];
}
@ -62,6 +72,7 @@ public class PlayerInventory : Inventory
int count = item?.getAmount() ?? 0;
int aux = item?.getDurability() ?? 0;
NativeBridge.SetPlayerInventorySlot(_holder.getEntityId(), index, id, count, aux);
WriteMetaToNative(_holder.getEntityId(), index, item?.getItemMetaInternal());
}
}
@ -147,7 +158,11 @@ public class PlayerInventory : Inventory
/// Sets the item in the player's hand.
/// </summary>
/// <param name="stack">The ItemStack to set.</param>
public void setItemInHand(ItemStack? stack) => setItem(_heldItemSlot, stack);
public void setItemInHand(ItemStack? stack)
{
EnsureSynced();
setItem(_heldItemSlot, stack);
}
/// <summary>
/// Gets the slot number of the currently held item.
@ -168,6 +183,8 @@ public class PlayerInventory : Inventory
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);
}
/// <summary>
@ -199,4 +216,128 @@ public class PlayerInventory : Inventory
/// </summary>
/// <returns>The HumanEntity that owns this inventory.</returns>
public HumanEntity? getHolder() => _holder;
private static ItemMeta? ReadMetaFromNative(int entityId, int slot)
{
if (NativeBridge.GetItemMeta == null)
return null;
byte[] buf = new byte[4096];
int bytesWritten;
var gh = GCHandle.Alloc(buf, GCHandleType.Pinned);
try
{
bytesWritten = NativeBridge.GetItemMeta(entityId, slot, gh.AddrOfPinnedObject(), buf.Length);
}
finally
{
gh.Free();
}
if (bytesWritten <= 0)
return null;
int offset = 0;
int nameLen = BitConverter.ToInt32(buf, offset);
offset += 4;
string? displayName = null;
if (nameLen > 0)
{
displayName = Encoding.UTF8.GetString(buf, offset, nameLen);
offset += nameLen;
}
int loreCount = 0;
if (offset + 4 <= bytesWritten)
{
loreCount = BitConverter.ToInt32(buf, offset);
offset += 4;
}
List<string>? lore = null;
if (loreCount > 0)
{
lore = new List<string>(loreCount);
for (int i = 0; i < loreCount; i++)
{
if (offset + 4 > bytesWritten) break;
int lineLen = BitConverter.ToInt32(buf, offset);
offset += 4;
if (lineLen > 0 && offset + lineLen <= bytesWritten)
{
lore.Add(Encoding.UTF8.GetString(buf, offset, lineLen));
offset += lineLen;
}
else
{
lore.Add(string.Empty);
}
}
}
if (displayName == null && (lore == null || lore.Count == 0))
return null;
var meta = new ItemMeta();
if (displayName != null)
meta.setDisplayName(displayName);
if (lore != null && lore.Count > 0)
meta.setLore(lore);
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);
}
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();
}
}
}

View file

@ -108,6 +108,15 @@ internal static class NativeBridge
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeOpenVirtualContainerDelegate(int entityId, int nativeType, IntPtr titleUtf8, int titleByteLen, int slotCount, IntPtr itemsBuf);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate int NativeGetItemMetaDelegate(int entityId, int slot, IntPtr outBuf, int bufSize);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeSetItemMetaDelegate(int entityId, int slot, IntPtr inBuf, int bufSize);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeSetHeldItemSlotDelegate(int entityId, int slot);
internal static NativeDamageDelegate? DamagePlayer;
internal static NativeSetHealthDelegate? SetPlayerHealth;
@ -144,6 +153,9 @@ internal static class NativeBridge
internal static NativeGetContainerViewerEntityIdsDelegate? GetContainerViewerEntityIds;
internal static NativeCloseContainerDelegate? CloseContainer;
internal static NativeOpenVirtualContainerDelegate? OpenVirtualContainer;
internal static NativeGetItemMetaDelegate? GetItemMeta;
internal static NativeSetItemMetaDelegate? SetItemMeta;
internal static NativeSetHeldItemSlotDelegate? SetHeldItemSlot;
internal static void SetCallbacks(IntPtr damage, IntPtr setHealth, IntPtr teleport, IntPtr setGameMode, IntPtr broadcastMessage, IntPtr setFallDistance, IntPtr getPlayerSnapshot, IntPtr sendMessage, IntPtr setWalkSpeed, IntPtr teleportEntity)
{
@ -184,7 +196,7 @@ internal static class NativeBridge
GetPlayerAddress = Marshal.GetDelegateForFunctionPointer<NativeGetPlayerAddressDelegate>(getPlayerAddress);
}
internal static void SetInventoryCallbacks(IntPtr getPlayerInventory, IntPtr setPlayerInventorySlot, IntPtr getContainerContents, IntPtr setContainerSlot, IntPtr getContainerViewerEntityIds, IntPtr closeContainer, IntPtr openVirtualContainer)
internal static void SetInventoryCallbacks(IntPtr getPlayerInventory, IntPtr setPlayerInventorySlot, IntPtr getContainerContents, IntPtr setContainerSlot, IntPtr getContainerViewerEntityIds, IntPtr closeContainer, IntPtr openVirtualContainer, IntPtr getItemMeta, IntPtr setItemMeta, IntPtr setHeldItemSlot)
{
GetPlayerInventory = Marshal.GetDelegateForFunctionPointer<NativeGetPlayerInventoryDelegate>(getPlayerInventory);
SetPlayerInventorySlot = Marshal.GetDelegateForFunctionPointer<NativeSetPlayerInventorySlotDelegate>(setPlayerInventorySlot);
@ -193,5 +205,8 @@ internal static class NativeBridge
GetContainerViewerEntityIds = Marshal.GetDelegateForFunctionPointer<NativeGetContainerViewerEntityIdsDelegate>(getContainerViewerEntityIds);
CloseContainer = Marshal.GetDelegateForFunctionPointer<NativeCloseContainerDelegate>(closeContainer);
OpenVirtualContainer = Marshal.GetDelegateForFunctionPointer<NativeOpenVirtualContainerDelegate>(openVirtualContainer);
GetItemMeta = Marshal.GetDelegateForFunctionPointer<NativeGetItemMetaDelegate>(getItemMeta);
SetItemMeta = Marshal.GetDelegateForFunctionPointer<NativeSetItemMetaDelegate>(setItemMeta);
SetHeldItemSlot = Marshal.GetDelegateForFunctionPointer<NativeSetHeldItemSlotDelegate>(setHeldItemSlot);
}
}

View file

@ -35,6 +35,7 @@
#include "..\Minecraft.World\LightningBolt.h"
#include "..\Minecraft.World\Player.h"
#include "..\Minecraft.World\PlayerAbilitiesPacket.h"
#include "..\Minecraft.World\SetCarriedItemPacket.h"
#include "..\Minecraft.World\SimpleContainer.h"
#include "..\Minecraft.World\Slot.h"
#include "..\Minecraft.World\Tile.h"
@ -141,7 +142,7 @@ typedef void(__stdcall *fn_set_player_callbacks)(void *kickPlayer, void *banPlay
typedef long long(__stdcall *fn_fire_player_drop_item)(int entityId,
int itemId, int itemCount, int itemAux,
int *outItemId, int *outItemCount, int *outItemAux);
typedef void(__stdcall *fn_set_inventory_callbacks)(void *getPlayerInventory, void *setPlayerInventorySlot, void *getContainerContents, void *setContainerSlot, void *getContainerViewerEntityIds, void *closeContainer, void *openVirtualContainer);
typedef void(__stdcall *fn_set_inventory_callbacks)(void *getPlayerInventory, void *setPlayerInventorySlot, void *getContainerContents, void *setContainerSlot, void *getContainerViewerEntityIds, void *closeContainer, void *openVirtualContainer, void *getItemMeta, void *setItemMeta, void *setHeldItemSlot);
typedef int(__stdcall *fn_fire_player_interact)(int entityId, int action,
int itemId, int itemCount, int itemAux,
int clickedX, int clickedY, int clickedZ,
@ -1069,6 +1070,212 @@ static void __cdecl NativeGetContainerViewerEntityIds(int entityId, int *outIds,
*outCount = count;
}
// [nameLen:int32][nameUTF8:bytes][loreCount:int32][lore0Len:int32][lore0UTF8:bytes]
// todo: des muass i no a bissl überarwatn
static int __cdecl NativeGetItemMeta(int entityId, int slot, char *outBuf, int bufSize)
{
auto player = FindPlayer(entityId);
if (!player || !player->inventory)
{
return 0;
}
unsigned int size = player->inventory->getContainerSize();
if (slot < 0 || slot >= (int)size)
{
return 0;
}
auto item = player->inventory->getItem(slot);
if (!item || !item->hasTag())
{
return 0;
}
CompoundTag *tag = item->getTag();
if (!tag || !tag->contains(L"display"))
{
return 0;
}
CompoundTag *display = tag->getCompound(L"display");
bool hasName = display->contains(L"Name");
bool hasLore = display->contains(L"Lore");
if (!hasName && !hasLore)
{
return 0;
}
int offset = 0;
if (hasName)
{
std::wstring wname = display->getString(L"Name");
std::string nameUtf8 = ServerRuntime::StringUtils::WideToUtf8(wname);
int nameLen = (int)nameUtf8.size();
if (offset + 4 + nameLen > bufSize) return 0;
memcpy(outBuf + offset, &nameLen, 4);
offset += 4;
memcpy(outBuf + offset, nameUtf8.data(), nameLen);
offset += nameLen;
}
else
{
int zero = 0;
if (offset + 4 > bufSize) return 0;
memcpy(outBuf + offset, &zero, 4);
offset += 4;
}
if (hasLore)
{
ListTag<StringTag> *lore = (ListTag<StringTag> *)display->getList(L"Lore");
int loreCount = lore->size();
if (offset + 4 > bufSize) return 0;
memcpy(outBuf + offset, &loreCount, 4);
offset += 4;
for (int i = 0; i < loreCount; i++)
{
std::wstring wline = lore->get(i)->data;
std::string lineUtf8 = ServerRuntime::StringUtils::WideToUtf8(wline);
int lineLen = (int)lineUtf8.size();
if (offset + 4 + lineLen > bufSize) return 0;
memcpy(outBuf + offset, &lineLen, 4);
offset += 4;
memcpy(outBuf + offset, lineUtf8.data(), lineLen);
offset += lineLen;
}
}
else
{
int zero = 0;
if (offset + 4 > bufSize) return 0;
memcpy(outBuf + offset, &zero, 4);
offset += 4;
}
return offset;
}
static void __cdecl NativeSetItemMeta(int entityId, int slot, const char *inBuf, int bufSize)
{
auto player = FindPlayer(entityId);
if (!player || !player->inventory)
{
return;
}
unsigned int size = player->inventory->getContainerSize();
if (slot < 0 || slot >= (int)size)
{
return;
}
auto item = player->inventory->getItem(slot);
if (!item)
{
return;
}
if (inBuf == nullptr || bufSize <= 0)
{
item->resetHoverName();
if (item->hasTag())
{
CompoundTag *tag = item->getTag();
if (tag && tag->contains(L"display"))
{
CompoundTag *display = tag->getCompound(L"display");
display->remove(L"Lore");
if (display->isEmpty())
{
tag->remove(L"display");
if (tag->isEmpty())
{
item->setTag(nullptr);
}
}
}
}
return;
}
int offset = 0;
if (offset + 4 > bufSize) return;
int nameLen = 0;
memcpy(&nameLen, inBuf + offset, 4);
offset += 4;
if (nameLen > 0)
{
if (offset + nameLen > bufSize) return;
std::string nameUtf8(inBuf + offset, nameLen);
offset += nameLen;
std::wstring wname = ServerRuntime::StringUtils::Utf8ToWide(nameUtf8);
item->setHoverName(wname);
}
else
{
item->resetHoverName();
}
if (offset + 4 > bufSize) return;
int loreCount = 0;
memcpy(&loreCount, inBuf + offset, 4);
offset += 4;
if (loreCount > 0)
{
if (!item->hasTag()) item->setTag(new CompoundTag());
CompoundTag *tag = item->getTag();
if (!tag->contains(L"display")) tag->putCompound(L"display", new CompoundTag());
CompoundTag *display = tag->getCompound(L"display");
auto *loreList = new ListTag<StringTag>(L"Lore");
for (int i = 0; i < loreCount; i++)
{
if (offset + 4 > bufSize) break;
int lineLen = 0;
memcpy(&lineLen, inBuf + offset, 4);
offset += 4;
std::wstring wline;
if (lineLen > 0 && offset + lineLen <= bufSize)
{
std::string lineUtf8(inBuf + offset, lineLen);
offset += lineLen;
wline = ServerRuntime::StringUtils::Utf8ToWide(lineUtf8);
}
loreList->add(new StringTag(L"", wline));
}
display->put(L"Lore", loreList);
}
else
{
if (item->hasTag())
{
CompoundTag *tag = item->getTag();
if (tag && tag->contains(L"display"))
{
tag->getCompound(L"display")->remove(L"Lore");
}
}
}
}
static void __cdecl NativeSetHeldItemSlot(int entityId, int slot)
{
auto player = FindPlayer(entityId);
if (!player || !player->inventory) return;
if (slot < 0 || slot >= Inventory::getSelectionSize()) return;
player->inventory->selected = slot;
if (player->connection)
player->connection->queueSend(std::make_shared<SetCarriedItemPacket>(slot));
}
static std::wstring FindNet10SystemRoot()
{
// overengineered
@ -1362,7 +1569,10 @@ void Initialize()
(void *)&NativeSetContainerSlot,
(void *)&NativeGetContainerViewerEntityIds,
(void *)&NativeCloseContainer,
(void *)&NativeOpenVirtualContainer);
(void *)&NativeOpenVirtualContainer,
(void *)&NativeGetItemMeta,
(void *)&NativeSetItemMeta,
(void *)&NativeSetHeldItemSlot);
LogInfo("fourkit", "FourKit initialized successfully.");
}