feat: add FourKit plugin host with dual server build
Adds the FourKit .NET 10 plugin host as a second dedicated server
build flavour alongside the existing vanilla server. Both flavours
build from the same source tree, with FourKit gated by the
MINECRAFT_SERVER_FOURKIT_BUILD preprocessor define.
Build layout:
Minecraft.Server vanilla, no plugin support, no .NET dep
Minecraft.Server.FourKit FourKit-enabled, ships with bundled
.NET 10 self-contained runtime in runtime/
and an empty plugins/ folder
Both produce a Minecraft.Server.exe in their own per-target output
dir. The variant identity lives in the directory name, not the
binary name, so either flavour can be shipped as a drop-in.
Native bridge (Minecraft.Server/FourKit*.{cpp,h}):
* FourKitRuntime: hosts CoreCLR via hostfxr's command-line init API
(the runtime-config API does not support self-contained components)
* FourKitBridge: ~50 Fire* event entry points, with inline no-op
stubs for the standalone build so gameplay code can call them
unconditionally
* FourKitNatives: ~80 native callbacks the managed side invokes
for player/world/inventory mutations
* FourKitMappers: type and enum mapping helpers
Managed plugin host (Minecraft.Server.FourKit/):
* Bukkit-style API: Player, World, Block, Inventory, Command,
Listener, EventHandler attribute, ~54 event classes
* PluginLoader with per-plugin AssemblyLoadContext
* FourKitHost as the [UnmanagedCallersOnly] entry point table
* Runtime resolves plugins relative to the host process so they
always live next to Minecraft.Server.exe regardless of where the
managed assembly itself is loaded from
Engine hooks (Minecraft.Client/, Minecraft.World/):
* Player lifecycle (PreLogin, Login, Join, Quit, Kick, Move,
Teleport, Portal, Death) wired into PendingConnection and
PlayerConnection without disturbing the cipher handshake or
identity-token security flow
* Inventory open/click/drop hooks across every container menu type
* Block place/break/grow/burn/spread/from-to hooks across the
full tile family
* Bed enter/leave, sign change, entity damage/death, ender pearl
teleport hooks
Regression fixes preserved while applying donor diffs:
* ServerPlayer::die() retains the LCE-Revelations hardcore branch
(setGameMode(ADVENTURE) + banPlayerForHardcoreDeath) in both the
FourKit and non-FourKit code paths
* ServerLevel::entityAdded() retains the sub-entity ID reassignment
loop required by the client's handleAddMob offset, fixing Ender
Dragon and Wither boss multi-part hit detection
* LivingEntity::travel() retains the raw Player* cast and the
cached frictionTile, both Revelations perf wins that the donor
silently reverted
* ServerLogger.cpp keeps the file-logging code donor stripped
* PlayerList.cpp end portal transition fix and UIScene_EndPoem
bounds-check are intact
Build system:
* Top-level CMakeLists.txt adds the Minecraft.Server.FourKit
subdirectory and pulls in the new shared cmake/ServerTarget.cmake
helper
* Minecraft.Server/cmake/sources/Common.cmake is now location
independent (uses CMAKE_CURRENT_LIST_DIR) so the source list
can be consumed from either server target's CMakeLists.txt
* The seven FourKit*.cpp/h files live in their own
_MINECRAFT_SERVER_COMMON_SERVER_FOURKIT variable so the
standalone target omits them
* configure-time .NET 10 SDK check fails fast with a clear
download link if the SDK is missing
* global.json pins the SDK to 10.0.100 with latestFeature
rollforward
Sample plugin (samples/HelloPlugin/) demonstrates the loader and
the PlayerJoinEvent listener pattern.
CI:
* nightly.yml builds both server flavours, ships
LCE-Revelations-Server-Win64.zip and
LCE-Revelations-Server-Win64-FourKit.zip, attests both, and
updates release notes for the dual-flavour layout
* pull-request.yml pulls in actions/setup-dotnet so the FourKit
publish step works in PR validation
* All zip artifacts and the client zip are renamed from
LCREWindows64 to LCE-Revelations-{Client,Server}-Win64
Documentation:
* COMPILE.md gets a VS 2022 quick start, .NET 10 prereq section,
server flavours explanation, and a troubleshooting section
* docs/FOURKIT_PORT_RECON.md captures the file-by-file recon that
drove the port
* docs/FOURKIT_PARITY.md is the canonical reference for which
events FourKit fires
Docker:
* docker-compose.dedicated-server.yml MC_RUNTIME_DIR default points
at the vanilla CMake output. The FourKit Docker image is
intentionally NOT shipped yet because hosting .NET 10 self
contained inside Wine has not been smoke-tested
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
namespace Minecraft.Server.FourKit.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the inventory of a Beacon.
|
||||
/// Single slot at index 0.
|
||||
/// </summary>
|
||||
public class BeaconInventory : Inventory
|
||||
{
|
||||
internal BeaconInventory(string title, int size, int entityId)
|
||||
: base(title, InventoryType.BEACON, size, entityId)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the item in the beacon's single slot.
|
||||
/// </summary>
|
||||
/// <returns>The item.</returns>
|
||||
public ItemStack? getBeaconItem() => getItem(0);
|
||||
|
||||
/// <summary>
|
||||
/// Set the item in the beacon's single slot.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to set.</param>
|
||||
public void setBeaconItem(ItemStack? item) => setItem(0, item);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
namespace Minecraft.Server.FourKit.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the inventory of a Double Chest.
|
||||
/// 54 slots total — left side is slots 0–26, right side is slots 27–53.
|
||||
/// </summary>
|
||||
public class DoubleChestInventory : Inventory
|
||||
{
|
||||
private readonly Inventory _left;
|
||||
private readonly Inventory _right;
|
||||
|
||||
internal DoubleChestInventory(string title, int size, int entityId)
|
||||
: base(title, InventoryType.CHEST, size, entityId)
|
||||
{
|
||||
_left = new InventorySlice("Left chest", this, 0, 27);
|
||||
_right = new InventorySlice("Right chest", this, 27, 27);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the left half of this double chest.
|
||||
/// </summary>
|
||||
/// <returns>The left side inventory.</returns>
|
||||
public Inventory getLeftSide() => _left;
|
||||
|
||||
/// <summary>
|
||||
/// Get the right half of this double chest.
|
||||
/// </summary>
|
||||
/// <returns>The right side inventory.</returns>
|
||||
public Inventory getRightSide() => _right;
|
||||
|
||||
// todo: get rid of this class
|
||||
private sealed class InventorySlice : Inventory
|
||||
{
|
||||
private readonly Inventory _parent;
|
||||
private readonly int _offset;
|
||||
|
||||
internal InventorySlice(string name, Inventory parent, int offset, int size)
|
||||
: base(name, InventoryType.CHEST, size)
|
||||
{
|
||||
_parent = parent;
|
||||
_offset = offset;
|
||||
}
|
||||
|
||||
public override ItemStack? getItem(int index)
|
||||
{
|
||||
if (index < 0 || index >= getSize()) return null;
|
||||
return _parent.getItem(_offset + index);
|
||||
}
|
||||
|
||||
public override void setItem(int index, ItemStack? item)
|
||||
{
|
||||
if (index >= 0 && index < getSize())
|
||||
_parent.setItem(_offset + index, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace Minecraft.Server.FourKit.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the inventory of an Enchanting Table.
|
||||
/// Single slot at index 0.
|
||||
/// </summary>
|
||||
public class EnchantingInventory : Inventory
|
||||
{
|
||||
internal EnchantingInventory(string title, int size, int entityId)
|
||||
: base(title, InventoryType.ENCHANTING, size, entityId)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the item being enchanted.
|
||||
/// </summary>
|
||||
/// <returns>The item.</returns>
|
||||
public ItemStack? getEnchantItem() => getItem(0);
|
||||
|
||||
/// <summary>
|
||||
/// Set the item being enchanted.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to set.</param>
|
||||
public void setEnchantItem(ItemStack? item) => setItem(0, item);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
namespace Minecraft.Server.FourKit.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the inventory of a Furnace.
|
||||
/// Slot layout: 0 = smelting input, 1 = fuel, 2 = result.
|
||||
/// </summary>
|
||||
public class FurnaceInventory : Inventory
|
||||
{
|
||||
internal FurnaceInventory(string title, int size, int entityId)
|
||||
: base(title, InventoryType.FURNACE, size, entityId)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current item in the result slot.
|
||||
/// </summary>
|
||||
/// <returns>The item.</returns>
|
||||
public ItemStack? getResult() => getItem(2);
|
||||
|
||||
/// <summary>
|
||||
/// Get the current fuel.
|
||||
/// </summary>
|
||||
/// <returns>The item.</returns>
|
||||
public ItemStack? getFuel() => getItem(1);
|
||||
|
||||
/// <summary>
|
||||
/// Get the item currently smelting.
|
||||
/// </summary>
|
||||
/// <returns>The item.</returns>
|
||||
public ItemStack? getSmelting() => getItem(0);
|
||||
|
||||
/// <summary>
|
||||
/// Set the current fuel.
|
||||
/// </summary>
|
||||
/// <param name="stack">The item.</param>
|
||||
public void setFuel(ItemStack? stack) => setItem(1, stack);
|
||||
|
||||
/// <summary>
|
||||
/// Set the current item in the result slot.
|
||||
/// </summary>
|
||||
/// <param name="stack">The item.</param>
|
||||
public void setResult(ItemStack? stack) => setItem(2, stack);
|
||||
|
||||
/// <summary>
|
||||
/// Set the item currently smelting.
|
||||
/// </summary>
|
||||
/// <param name="stack">The item.</param>
|
||||
public void setSmelting(ItemStack? stack) => setItem(0, stack);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace Minecraft.Server.FourKit.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the inventory of a Horse.
|
||||
/// Slot layout: 0 = saddle, 1 = armor, 2+ = chest slots.
|
||||
/// </summary>
|
||||
public class HorseInventory : Inventory
|
||||
{
|
||||
internal HorseInventory(string title, int size, int entityId)
|
||||
: base(title, InventoryType.CHEST, size < 2 ? 2 : size, entityId)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item in the horse's saddle slot.
|
||||
/// </summary>
|
||||
/// <returns>The saddle item.</returns>
|
||||
public ItemStack? getSaddle() => getItem(0);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the item in the horse's saddle slot.
|
||||
/// </summary>
|
||||
/// <param name="stack">The saddle item.</param>
|
||||
public void setSaddle(ItemStack? stack) => setItem(0, stack);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item in the horse's armor slot.
|
||||
/// </summary>
|
||||
/// <returns>The armor item.</returns>
|
||||
public ItemStack? getArmor() => getItem(1);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the item in the horse's armor slot.
|
||||
/// </summary>
|
||||
/// <param name="stack">The armor item.</param>
|
||||
public void setArmor(ItemStack? stack) => setItem(1, stack);
|
||||
}
|
||||
@@ -0,0 +1,606 @@
|
||||
namespace Minecraft.Server.FourKit.Inventory;
|
||||
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using Minecraft.Server.FourKit.Entity;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an inventory containing items. Behavior relating to
|
||||
/// <see cref="Material.AIR"/> is unspecified.
|
||||
/// </summary>
|
||||
public class Inventory : IEnumerable<ItemStack>
|
||||
{
|
||||
internal static bool _slotModifiedByPlugin;
|
||||
private readonly string _name;
|
||||
private readonly InventoryType _type;
|
||||
internal readonly ItemStack?[] _items;
|
||||
private readonly int _nativeEntityId = -1;
|
||||
|
||||
internal Inventory(string name, InventoryType type, int size)
|
||||
{
|
||||
_name = name;
|
||||
_type = type;
|
||||
_items = new ItemStack?[size];
|
||||
}
|
||||
|
||||
internal Inventory(string name, InventoryType type, int size, int entityId)
|
||||
: this(name, type, size)
|
||||
{
|
||||
_nativeEntityId = entityId;
|
||||
}
|
||||
|
||||
protected internal virtual void EnsureSynced()
|
||||
{
|
||||
if (_nativeEntityId < 0 || NativeBridge.GetContainerContents == null)
|
||||
return;
|
||||
|
||||
int[] buf = new int[_items.Length * 3];
|
||||
var gh = GCHandle.Alloc(buf, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
NativeBridge.GetContainerContents(_nativeEntityId, gh.AddrOfPinnedObject(), _items.Length);
|
||||
}
|
||||
finally
|
||||
{
|
||||
gh.Free();
|
||||
}
|
||||
|
||||
for (int i = 0; i < _items.Length; i++)
|
||||
{
|
||||
int id = buf[i * 3 + 0];
|
||||
int aux = buf[i * 3 + 1];
|
||||
int packed = buf[i * 3 + 2];
|
||||
|
||||
ushort count = (ushort)((packed >> 8) & 0xFFFF);
|
||||
|
||||
//byte flags = (byte)((packed >> 24) & 0xFF);
|
||||
//bool hasMetadata = (flags & 0x1) != 0; //unused here
|
||||
|
||||
_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);
|
||||
}
|
||||
_items[i]!.BindToInventory(this, i); //should we unbind and rebind or just keep the bind?
|
||||
}
|
||||
else
|
||||
{
|
||||
_items[i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the size of the inventory.
|
||||
/// </summary>
|
||||
/// <returns>The size of the inventory.</returns>
|
||||
public int getSize() => _items.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the name of the inventory.
|
||||
/// </summary>
|
||||
/// <returns>The String with the name of the inventory.</returns>
|
||||
public string getName() => _name;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the ItemStack found in the slot at the given index.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the Slot's ItemStack to return.</param>
|
||||
/// <returns>The ItemStack in the slot.</returns>
|
||||
public virtual ItemStack? getItem(int index)
|
||||
{
|
||||
EnsureSynced();
|
||||
if (index < 0 || index >= _items.Length) return null;
|
||||
var item = _items[index];
|
||||
item?.BindToInventory(this, index);
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores the ItemStack at the given index of the inventory.
|
||||
/// </summary>
|
||||
/// <param name="index">The index where to put the ItemStack.</param>
|
||||
/// <param name="item">The ItemStack to set.</param>
|
||||
public virtual void setItem(int index, ItemStack? item)
|
||||
{
|
||||
if (index >= 0 && index < _items.Length)
|
||||
{
|
||||
var old = _items[index];
|
||||
if (old != item)
|
||||
{
|
||||
old?.UnbindFromInventory();
|
||||
item?.BindToInventory(this, index);
|
||||
}
|
||||
_items[index] = item;
|
||||
_slotModifiedByPlugin = true;
|
||||
}
|
||||
|
||||
if (_nativeEntityId >= 0 && NativeBridge.SetContainerSlot != null && index >= 0 && index < _items.Length)
|
||||
{
|
||||
int id = item?.getTypeId() ?? 0;
|
||||
int count = item?.getAmount() ?? 0;
|
||||
int aux = item?.getDurability() ?? 0;
|
||||
NativeBridge.SetContainerSlot(_nativeEntityId, index, id, count, aux);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores the given ItemStacks in the inventory. This will try to fill
|
||||
/// existing stacks and empty slots as well as it can.
|
||||
/// The returned Dictionary contains what it couldn't store, where the key
|
||||
/// is the index of the parameter, and the value is the ItemStack at that
|
||||
/// index of the params parameter.
|
||||
/// </summary>
|
||||
/// <param name="items">The ItemStacks to add.</param>
|
||||
/// <returns>A Dictionary containing items that didn't fit.</returns>
|
||||
public Dictionary<int, ItemStack> addItem(params ItemStack[] items)
|
||||
{
|
||||
EnsureSynced();
|
||||
var leftover = new Dictionary<int, ItemStack>();
|
||||
for (int i = 0; i < items.Length; i++)
|
||||
{
|
||||
var toAdd = items[i];
|
||||
if (toAdd == null) continue;
|
||||
int remaining = toAdd.getAmount();
|
||||
|
||||
for (int slot = 0; slot < _items.Length && remaining > 0; slot++)
|
||||
{
|
||||
var existing = _items[slot];
|
||||
if (existing != null && existing.getType() == toAdd.getType() &&
|
||||
existing.getDurability() == toAdd.getDurability())
|
||||
{
|
||||
int canFit = 64 - existing.getAmount();
|
||||
if (canFit > 0)
|
||||
{
|
||||
int added = Math.Min(canFit, remaining);
|
||||
existing.setAmount(existing.getAmount() + added);
|
||||
setItem(slot, existing);
|
||||
remaining -= added;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int slot = 0; slot < _items.Length && remaining > 0; slot++)
|
||||
{
|
||||
if (_items[slot] == null)
|
||||
{
|
||||
int added = Math.Min(64, remaining);
|
||||
setItem(slot, new ItemStack(toAdd.getType(), added, toAdd.getDurability()));
|
||||
remaining -= added;
|
||||
}
|
||||
}
|
||||
|
||||
if (remaining > 0)
|
||||
leftover[i] = new ItemStack(toAdd.getType(), remaining, toAdd.getDurability());
|
||||
}
|
||||
return leftover;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the given ItemStacks from the inventory.
|
||||
/// It will try to remove 'as much as possible' from the types and amounts
|
||||
/// you give as arguments.
|
||||
/// The returned Dictionary contains what it couldn't remove.
|
||||
/// </summary>
|
||||
/// <param name="items">The ItemStacks to remove.</param>
|
||||
/// <returns>A Dictionary containing items that couldn't be removed.</returns>
|
||||
public Dictionary<int, ItemStack> removeItem(params ItemStack[] items)
|
||||
{
|
||||
EnsureSynced();
|
||||
var leftover = new Dictionary<int, ItemStack>();
|
||||
for (int i = 0; i < items.Length; i++)
|
||||
{
|
||||
var toRemove = items[i];
|
||||
if (toRemove == null) continue;
|
||||
int remaining = toRemove.getAmount();
|
||||
|
||||
for (int slot = 0; slot < _items.Length && remaining > 0; slot++)
|
||||
{
|
||||
var existing = _items[slot];
|
||||
if (existing != null && existing.getType() == toRemove.getType() &&
|
||||
existing.getDurability() == toRemove.getDurability())
|
||||
{
|
||||
int removed = Math.Min(existing.getAmount(), remaining);
|
||||
existing.setAmount(existing.getAmount() - removed);
|
||||
remaining -= removed;
|
||||
if (existing.getAmount() <= 0)
|
||||
setItem(slot, null);
|
||||
else
|
||||
setItem(slot, existing);
|
||||
}
|
||||
}
|
||||
|
||||
if (remaining > 0)
|
||||
leftover[i] = new ItemStack(toRemove.getType(), remaining, toRemove.getDurability());
|
||||
}
|
||||
return leftover;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all ItemStacks from the inventory.
|
||||
/// </summary>
|
||||
/// <returns>An array of ItemStacks from the inventory.</returns>
|
||||
public ItemStack?[] getContents()
|
||||
{
|
||||
EnsureSynced();
|
||||
return (ItemStack?[])_items.Clone();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completely replaces the inventory's contents. Removes all existing
|
||||
/// contents and replaces it with the ItemStacks given in the array.
|
||||
/// </summary>
|
||||
/// <param name="items">A complete replacement for the contents; the length must
|
||||
/// be less than or equal to <see cref="getSize"/>.</param>
|
||||
public void setContents(ItemStack?[] items)
|
||||
{
|
||||
int len = Math.Min(items.Length, _items.Length);
|
||||
for (int i = 0; i < _items.Length; i++)
|
||||
setItem(i, i < len ? items[i] : null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the inventory contains any ItemStacks with the given material id.
|
||||
/// </summary>
|
||||
/// <param name="materialId">The material id to check for.</param>
|
||||
/// <returns><c>true</c> if an ItemStack in this inventory contains the material id.</returns>
|
||||
public bool contains(int materialId)
|
||||
{
|
||||
EnsureSynced();
|
||||
foreach (var item in _items)
|
||||
if (item != null && item.getTypeId() == materialId) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the inventory contains any ItemStacks with the given material.
|
||||
/// </summary>
|
||||
/// <param name="material">The material to check for.</param>
|
||||
/// <returns><c>true</c> if an ItemStack is found with the given Material.</returns>
|
||||
public bool contains(Material material)
|
||||
{
|
||||
EnsureSynced();
|
||||
foreach (var item in _items)
|
||||
if (item != null && item.getType() == material) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the inventory contains any ItemStacks matching the given ItemStack.
|
||||
/// This will only return true if both the type and the amount of the stack match.
|
||||
/// </summary>
|
||||
/// <param name="item">The ItemStack to match against.</param>
|
||||
/// <returns><c>false</c> if item is null, <c>true</c> if any exactly matching ItemStacks were found.</returns>
|
||||
public bool contains(ItemStack? item)
|
||||
{
|
||||
if (item == null) return false;
|
||||
EnsureSynced();
|
||||
foreach (var slot in _items)
|
||||
if (slot != null && slot.getType() == item.getType() &&
|
||||
slot.getAmount() == item.getAmount() &&
|
||||
slot.getDurability() == item.getDurability()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the inventory contains any ItemStacks with the given material id,
|
||||
/// adding to at least the minimum amount specified.
|
||||
/// </summary>
|
||||
/// <param name="materialId">The material id to check for.</param>
|
||||
/// <param name="amount">The minimum amount to look for.</param>
|
||||
/// <returns><c>true</c> if this contains any matching ItemStack with the given material id and amount.</returns>
|
||||
public bool contains(int materialId, int amount)
|
||||
{
|
||||
EnsureSynced();
|
||||
int total = 0;
|
||||
foreach (var item in _items)
|
||||
if (item != null && item.getTypeId() == materialId) total += item.getAmount();
|
||||
return total >= amount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the inventory contains any ItemStacks with the given material,
|
||||
/// adding to at least the minimum amount specified.
|
||||
/// </summary>
|
||||
/// <param name="material">The material to check for.</param>
|
||||
/// <param name="amount">The minimum amount.</param>
|
||||
/// <returns><c>true</c> if enough ItemStacks were found to add to the given amount.</returns>
|
||||
public bool contains(Material material, int amount)
|
||||
{
|
||||
if (amount <= 0) return true;
|
||||
EnsureSynced();
|
||||
int total = 0;
|
||||
foreach (var item in _items)
|
||||
if (item != null && item.getType() == material) total += item.getAmount();
|
||||
return total >= amount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the inventory contains at least the minimum amount specified
|
||||
/// of exactly matching ItemStacks. An ItemStack only counts if both the type
|
||||
/// and the amount of the stack match.
|
||||
/// </summary>
|
||||
/// <param name="item">The ItemStack to match against.</param>
|
||||
/// <param name="amount">How many identical stacks to check for.</param>
|
||||
/// <returns><c>false</c> if item is null, <c>true</c> if amount of exactly matching ItemStacks were found.</returns>
|
||||
public bool contains(ItemStack? item, int amount)
|
||||
{
|
||||
if (item == null) return false;
|
||||
if (amount <= 0) return true;
|
||||
EnsureSynced();
|
||||
int count = 0;
|
||||
foreach (var slot in _items)
|
||||
if (slot != null && slot.getType() == item.getType() &&
|
||||
slot.getAmount() == item.getAmount() &&
|
||||
slot.getDurability() == item.getDurability()) count++;
|
||||
return count >= amount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the inventory contains ItemStacks matching the given ItemStack
|
||||
/// whose amounts sum to at least the minimum amount specified.
|
||||
/// </summary>
|
||||
/// <param name="item">The ItemStack to match against.</param>
|
||||
/// <param name="amount">The minimum amount.</param>
|
||||
/// <returns><c>false</c> if item is null, <c>true</c> if enough ItemStacks were found to add to the given amount.</returns>
|
||||
public bool containsAtLeast(ItemStack? item, int amount)
|
||||
{
|
||||
if (item == null) return false;
|
||||
if (amount <= 0) return true;
|
||||
EnsureSynced();
|
||||
int total = 0;
|
||||
foreach (var slot in _items)
|
||||
if (slot != null && slot.getType() == item.getType() &&
|
||||
slot.getDurability() == item.getDurability())
|
||||
total += slot.getAmount();
|
||||
return total >= amount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a Dictionary with all slots and ItemStacks in the inventory with given material id.
|
||||
/// </summary>
|
||||
/// <param name="materialId">The material id to look for.</param>
|
||||
/// <returns>A Dictionary containing the slot index, ItemStack pairs.</returns>
|
||||
public Dictionary<int, ItemStack> all(int materialId)
|
||||
{
|
||||
EnsureSynced();
|
||||
var result = new Dictionary<int, ItemStack>();
|
||||
for (int i = 0; i < _items.Length; i++)
|
||||
if (_items[i] != null && _items[i]!.getTypeId() == materialId)
|
||||
result[i] = _items[i]!;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a Dictionary with all slots and ItemStacks in the inventory with the given Material.
|
||||
/// </summary>
|
||||
/// <param name="material">The material to look for.</param>
|
||||
/// <returns>A Dictionary containing the slot index, ItemStack pairs.</returns>
|
||||
public Dictionary<int, ItemStack> all(Material material)
|
||||
{
|
||||
EnsureSynced();
|
||||
var result = new Dictionary<int, ItemStack>();
|
||||
for (int i = 0; i < _items.Length; i++)
|
||||
if (_items[i] != null && _items[i]!.getType() == material)
|
||||
result[i] = _items[i]!;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all slots in the inventory containing any ItemStacks with the given ItemStack.
|
||||
/// This will only match slots if both the type and the amount of the stack match.
|
||||
/// </summary>
|
||||
/// <param name="item">The ItemStack to match against.</param>
|
||||
/// <returns>A dictionary from slot indexes to item at index.</returns>
|
||||
public Dictionary<int, ItemStack> all(ItemStack? item)
|
||||
{
|
||||
var result = new Dictionary<int, ItemStack>();
|
||||
if (item == null) return result;
|
||||
EnsureSynced();
|
||||
for (int i = 0; i < _items.Length; i++)
|
||||
if (_items[i] != null && _items[i]!.getType() == item.getType() &&
|
||||
_items[i]!.getAmount() == item.getAmount() &&
|
||||
_items[i]!.getDurability() == item.getDurability())
|
||||
result[i] = _items[i]!;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first slot in the inventory containing an ItemStack with the given material id.
|
||||
/// </summary>
|
||||
/// <param name="materialId">The material id to look for.</param>
|
||||
/// <returns>The slot index of the given material id or -1 if not found.</returns>
|
||||
public int first(int materialId)
|
||||
{
|
||||
EnsureSynced();
|
||||
for (int i = 0; i < _items.Length; i++)
|
||||
if (_items[i] != null && _items[i]!.getTypeId() == materialId) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first slot in the inventory containing an ItemStack with the given material.
|
||||
/// </summary>
|
||||
/// <param name="material">The material to look for.</param>
|
||||
/// <returns>The slot index of the given Material or -1 if not found.</returns>
|
||||
public int first(Material material)
|
||||
{
|
||||
EnsureSynced();
|
||||
for (int i = 0; i < _items.Length; i++)
|
||||
if (_items[i] != null && _items[i]!.getType() == material) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first slot in the inventory containing an ItemStack with the given stack.
|
||||
/// This will only match a slot if both the type and the amount of the stack match.
|
||||
/// </summary>
|
||||
/// <param name="item">The ItemStack to match against.</param>
|
||||
/// <returns>The slot index of the given ItemStack or -1 if not found.</returns>
|
||||
public int first(ItemStack? item)
|
||||
{
|
||||
if (item == null) return -1;
|
||||
EnsureSynced();
|
||||
for (int i = 0; i < _items.Length; i++)
|
||||
if (_items[i] != null && _items[i]!.getType() == item.getType() &&
|
||||
_items[i]!.getAmount() == item.getAmount() &&
|
||||
_items[i]!.getDurability() == item.getDurability()) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first empty Slot.
|
||||
/// </summary>
|
||||
/// <returns>The first empty Slot found, or -1 if no empty slots.</returns>
|
||||
public int firstEmpty()
|
||||
{
|
||||
EnsureSynced();
|
||||
for (int i = 0; i < _items.Length; i++)
|
||||
if (_items[i] == null) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all stacks in the inventory matching the given material id.
|
||||
/// </summary>
|
||||
/// <param name="materialId">The material to remove.</param>
|
||||
public void remove(int materialId)
|
||||
{
|
||||
EnsureSynced();
|
||||
for (int i = 0; i < _items.Length; i++)
|
||||
if (_items[i] != null && _items[i]!.getTypeId() == materialId)
|
||||
setItem(i, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all stacks in the inventory matching the given material.
|
||||
/// </summary>
|
||||
/// <param name="material">The material to remove.</param>
|
||||
public void remove(Material material)
|
||||
{
|
||||
EnsureSynced();
|
||||
for (int i = 0; i < _items.Length; i++)
|
||||
if (_items[i] != null && _items[i]!.getType() == material)
|
||||
setItem(i, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all stacks in the inventory matching the given stack.
|
||||
/// This will only match a slot if both the type and the amount of the stack match.
|
||||
/// </summary>
|
||||
/// <param name="item">The ItemStack to match against.</param>
|
||||
public void remove(ItemStack? item)
|
||||
{
|
||||
if (item == null) return;
|
||||
EnsureSynced();
|
||||
for (int i = 0; i < _items.Length; i++)
|
||||
if (_items[i] != null && _items[i]!.getType() == item.getType() &&
|
||||
_items[i]!.getAmount() == item.getAmount() &&
|
||||
_items[i]!.getDurability() == item.getDurability())
|
||||
setItem(i, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears out a particular slot in the index.
|
||||
/// </summary>
|
||||
/// <param name="index">The index to empty.</param>
|
||||
public void clear(int index)
|
||||
{
|
||||
setItem(index, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears out the whole Inventory.
|
||||
/// </summary>
|
||||
public void clear()
|
||||
{
|
||||
for (int i = 0; i < _items.Length; i++)
|
||||
setItem(i, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the title of this inventory.
|
||||
/// </summary>
|
||||
/// <returns>A String with the title.</returns>
|
||||
public string getTitle() => _name;
|
||||
|
||||
/// <summary>
|
||||
/// Returns what type of inventory this is.
|
||||
/// </summary>
|
||||
/// <returns>The InventoryType representing the type of inventory.</returns>
|
||||
public InventoryType getType() => _type;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of players viewing the inventory.
|
||||
/// </summary>
|
||||
/// <returns>A list of HumanEntities who are viewing this Inventory.</returns>
|
||||
public virtual List<HumanEntity> getViewers()
|
||||
{
|
||||
if (_nativeEntityId < 0 || NativeBridge.GetContainerViewerEntityIds == null)
|
||||
return new List<HumanEntity>();
|
||||
|
||||
int[] ids = new int[64];
|
||||
var ghIds = GCHandle.Alloc(ids, GCHandleType.Pinned);
|
||||
int[] countBuf = new int[1];
|
||||
var ghCount = GCHandle.Alloc(countBuf, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
NativeBridge.GetContainerViewerEntityIds(_nativeEntityId, ghIds.AddrOfPinnedObject(), 64, ghCount.AddrOfPinnedObject());
|
||||
}
|
||||
finally
|
||||
{
|
||||
ghIds.Free();
|
||||
ghCount.Free();
|
||||
}
|
||||
|
||||
int count = countBuf[0];
|
||||
var viewers = new List<HumanEntity>();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var player = FourKit.GetPlayerByEntityId(ids[i]);
|
||||
if (player != null)
|
||||
viewers.Add(player);
|
||||
}
|
||||
return viewers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an iterator starting at the given index.
|
||||
/// </summary>
|
||||
/// <param name="index">The index.</param>
|
||||
/// <returns>An enumerator.</returns>
|
||||
public IEnumerator<ItemStack> iterator(int index)
|
||||
{
|
||||
EnsureSynced();
|
||||
int start = index >= 0 ? index : _items.Length + index;
|
||||
return GetEnumeratorFrom(start);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerator<ItemStack> GetEnumerator()
|
||||
{
|
||||
EnsureSynced();
|
||||
return GetEnumeratorFrom(0);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
private IEnumerator<ItemStack> GetEnumeratorFrom(int start)
|
||||
{
|
||||
for (int i = start; i < _items.Length; i++)
|
||||
if (_items[i] != null)
|
||||
{
|
||||
_items[i]!.BindToInventory(this, i);
|
||||
yield return _items[i]!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Minecraft.Server.FourKit.Inventory;
|
||||
|
||||
public interface InventoryHolder
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the object's inventory.
|
||||
/// </summary>
|
||||
/// <returns>The inventory.</returns>
|
||||
Inventory getInventory();
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
namespace Minecraft.Server.FourKit.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the different types of inventories available.
|
||||
/// </summary>
|
||||
public enum InventoryType
|
||||
{
|
||||
/// <summary>A chest inventory, with 0, 9, 18, 27, 36, 45, or 54 slots of type CONTAINER.</summary>
|
||||
CHEST,
|
||||
/// <summary>A dispenser inventory, with 9 slots of type CONTAINER.</summary>
|
||||
DISPENSER,
|
||||
/// <summary>A dropper inventory, with 9 slots of type CONTAINER.</summary>
|
||||
DROPPER,
|
||||
/// <summary>A furnace inventory, with a RESULT slot, a CRAFTING slot, and a FUEL slot.</summary>
|
||||
FURNACE,
|
||||
/// <summary>A workbench inventory, with 9 CRAFTING slots and a RESULT slot.</summary>
|
||||
WORKBENCH,
|
||||
/// <summary>A player's crafting inventory, with 4 CRAFTING slots and a RESULT slot.</summary>
|
||||
CRAFTING,
|
||||
/// <summary>An enchantment table inventory, with one CRAFTING slot and three enchanting buttons.</summary>
|
||||
ENCHANTING,
|
||||
/// <summary>A brewing stand inventory, with one FUEL slot and three CRAFTING slots.</summary>
|
||||
BREWING,
|
||||
/// <summary>A player's inventory, with 9 QUICKBAR slots, 27 CONTAINER slots, and 4 ARMOR slots.</summary>
|
||||
PLAYER,
|
||||
/// <summary>The creative mode inventory, with only 9 QUICKBAR slots and nothing else.</summary>
|
||||
CREATIVE,
|
||||
/// <summary>The merchant inventory, with 2 TRADE-IN slots, and 1 RESULT slot.</summary>
|
||||
MERCHANT,
|
||||
/// <summary>The ender chest inventory, with 27 slots.</summary>
|
||||
ENDER_CHEST,
|
||||
/// <summary>An anvil inventory, with 2 CRAFTING slots and 1 RESULT slot.</summary>
|
||||
ANVIL,
|
||||
/// <summary>A beacon inventory, with 1 CRAFTING slot.</summary>
|
||||
BEACON,
|
||||
/// <summary>A hopper inventory, with 5 slots of type CONTAINER.</summary>
|
||||
HOPPER
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a slot type within an inventory.
|
||||
/// </summary>
|
||||
public enum SlotType
|
||||
{
|
||||
/// <summary>A result slot in a furnace or crafting inventory.</summary>
|
||||
RESULT,
|
||||
/// <summary>A slot in the crafting matrix, or the input slot in a furnace inventory, the potion slot in the brewing stand, or the enchanting slot.</summary>
|
||||
CRAFTING,
|
||||
/// <summary>An armour slot in the player's inventory.</summary>
|
||||
ARMOR,
|
||||
/// <summary>A regular slot in the container or the player's inventory.</summary>
|
||||
CONTAINER,
|
||||
/// <summary>A slot in the bottom row or quickbar.</summary>
|
||||
QUICKBAR,
|
||||
/// <summary>A pseudo-slot representing the area outside the inventory window.</summary>
|
||||
OUTSIDE,
|
||||
/// <summary>The fuel slot in a furnace inventory, or the ingredient slot in a brewing stand inventory.</summary>
|
||||
FUEL,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides default size and title information for each <see cref="InventoryType"/>.
|
||||
/// </summary>
|
||||
public static class InventoryTypeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default size for this inventory type.
|
||||
/// </summary>
|
||||
/// <param name="type">The inventory type.</param>
|
||||
/// <returns>The default number of slots.</returns>
|
||||
public static int getDefaultSize(this InventoryType type) => type switch
|
||||
{
|
||||
InventoryType.CHEST => 27,
|
||||
InventoryType.DISPENSER => 9,
|
||||
InventoryType.DROPPER => 9,
|
||||
InventoryType.FURNACE => 3,
|
||||
InventoryType.WORKBENCH => 10,
|
||||
InventoryType.CRAFTING => 5,
|
||||
InventoryType.ENCHANTING => 1,
|
||||
InventoryType.BREWING => 4,
|
||||
InventoryType.PLAYER => 40,
|
||||
InventoryType.CREATIVE => 9,
|
||||
InventoryType.MERCHANT => 3,
|
||||
InventoryType.ENDER_CHEST => 27,
|
||||
InventoryType.ANVIL => 3,
|
||||
InventoryType.BEACON => 1,
|
||||
InventoryType.HOPPER => 5,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default title for this inventory type.
|
||||
/// </summary>
|
||||
/// <param name="type">The inventory type.</param>
|
||||
/// <returns>The default title string.</returns>
|
||||
public static string getDefaultTitle(this InventoryType type) => type switch
|
||||
{
|
||||
InventoryType.CHEST => "Chest",
|
||||
InventoryType.DISPENSER => "Dispenser",
|
||||
InventoryType.DROPPER => "Dropper",
|
||||
InventoryType.FURNACE => "Furnace",
|
||||
InventoryType.WORKBENCH => "Crafting",
|
||||
InventoryType.CRAFTING => "Crafting",
|
||||
InventoryType.ENCHANTING => "Enchant",
|
||||
InventoryType.BREWING => "Brewing",
|
||||
InventoryType.PLAYER => "Player",
|
||||
InventoryType.CREATIVE => "Creative",
|
||||
InventoryType.MERCHANT => "Trading",
|
||||
InventoryType.ENDER_CHEST => "Ender Chest",
|
||||
InventoryType.ANVIL => "Repairing",
|
||||
InventoryType.BEACON => "Beacon",
|
||||
InventoryType.HOPPER => "Item Hopper",
|
||||
_ => "Inventory",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
namespace Minecraft.Server.FourKit.Inventory;
|
||||
|
||||
using Minecraft.Server.FourKit.Entity;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a view linking two inventories and a single player
|
||||
/// (whose inventory may or may not be one of the two).
|
||||
/// </summary>
|
||||
public class InventoryView
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the raw slot ID for clicks outside the inventory window.
|
||||
/// </summary>
|
||||
public static readonly int OUTSIDE = -999;
|
||||
|
||||
private readonly Inventory _topInventory;
|
||||
private readonly Inventory _bottomInventory;
|
||||
private readonly HumanEntity _player;
|
||||
private readonly InventoryType _type;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new InventoryView linking two inventories and a player.
|
||||
/// </summary>
|
||||
/// <param name="topInventory">The upper inventory.</param>
|
||||
/// <param name="bottomInventory">The lower inventory.</param>
|
||||
/// <param name="player">The player viewing.</param>
|
||||
/// <param name="type">The inventory type.</param>
|
||||
public InventoryView(Inventory topInventory, Inventory bottomInventory, HumanEntity player, InventoryType type)
|
||||
{
|
||||
_topInventory = topInventory;
|
||||
_bottomInventory = bottomInventory;
|
||||
_player = player;
|
||||
_type = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the upper inventory involved in this transaction.
|
||||
/// </summary>
|
||||
/// <returns>The inventory.</returns>
|
||||
public virtual Inventory getTopInventory() => _topInventory;
|
||||
|
||||
/// <summary>
|
||||
/// Get the lower inventory involved in this transaction.
|
||||
/// </summary>
|
||||
/// <returns>The inventory.</returns>
|
||||
public virtual Inventory getBottomInventory() => _bottomInventory;
|
||||
|
||||
/// <summary>
|
||||
/// Get the player viewing.
|
||||
/// </summary>
|
||||
/// <returns>The player.</returns>
|
||||
public virtual HumanEntity getPlayer() => _player;
|
||||
|
||||
/// <summary>
|
||||
/// Determine the type of inventory involved in the transaction. This indicates
|
||||
/// the window style being shown. It will never return PLAYER, since that is
|
||||
/// common to all windows.
|
||||
/// </summary>
|
||||
/// <returns>The inventory type.</returns>
|
||||
public virtual InventoryType getType() => _type;
|
||||
|
||||
/// <summary>
|
||||
/// Sets one item in this inventory view by its raw slot ID.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If slot ID -999 is chosen, it may be expected that the item is dropped
|
||||
/// on the ground. This is not required behaviour, however.
|
||||
/// </remarks>
|
||||
/// <param name="slot">The raw slot ID.</param>
|
||||
/// <param name="item">The new item to put in the slot, or null to clear it.</param>
|
||||
public void setItem(int slot, ItemStack? item)
|
||||
{
|
||||
if (slot == OUTSIDE) return;
|
||||
int topSize = _topInventory.getSize();
|
||||
if (slot < topSize)
|
||||
_topInventory.setItem(slot, item);
|
||||
else
|
||||
_bottomInventory.setItem(slot - topSize, item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets one item in this inventory view by its raw slot ID.
|
||||
/// </summary>
|
||||
/// <param name="slot">The raw slot ID.</param>
|
||||
/// <returns>The item currently in the slot.</returns>
|
||||
public ItemStack? getItem(int slot)
|
||||
{
|
||||
if (slot == OUTSIDE) return null;
|
||||
int topSize = _topInventory.getSize();
|
||||
if (slot < topSize)
|
||||
return _topInventory.getItem(slot);
|
||||
return _bottomInventory.getItem(slot - topSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the item on the cursor of one of the viewing players.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to put on the cursor, or null to remove it.</param>
|
||||
public void setCursor(ItemStack? item)
|
||||
{
|
||||
_player.setItemOnCursor(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the item on the cursor of one of the viewing players.
|
||||
/// </summary>
|
||||
/// <returns>The item on the player's cursor, or null if they aren't holding one.</returns>
|
||||
public ItemStack? getCursor()
|
||||
{
|
||||
return _player.getItemOnCursor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a raw slot ID into its local slot ID into whichever of the two
|
||||
/// inventories the slot points to.
|
||||
/// </summary>
|
||||
/// <param name="rawSlot">The raw slot ID.</param>
|
||||
/// <returns>The converted slot ID.</returns>
|
||||
public int convertSlot(int rawSlot)
|
||||
{
|
||||
int topSize = _topInventory.getSize();
|
||||
if (rawSlot < topSize)
|
||||
return rawSlot;
|
||||
return rawSlot - topSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the inventory view.
|
||||
/// </summary>
|
||||
public void close()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check the total number of slots in this view, combining the upper
|
||||
/// and lower inventories.
|
||||
/// </summary>
|
||||
/// <returns>The total size.</returns>
|
||||
public int countSlots()
|
||||
{
|
||||
return _topInventory.getSize() + _bottomInventory.getSize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets an extra property of this inventory if supported by that inventory,
|
||||
/// for example the state of a progress bar.
|
||||
/// </summary>
|
||||
/// <param name="prop">The window property to update.</param>
|
||||
/// <param name="value">The new value for the window property.</param>
|
||||
/// <returns>true if the property was updated successfully.</returns>
|
||||
public bool setProperty(Property prop, int value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the title of this inventory window.
|
||||
/// </summary>
|
||||
/// <returns>The title.</returns>
|
||||
public string getTitle()
|
||||
{
|
||||
return _topInventory.getTitle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents various extra properties of certain inventory windows.
|
||||
/// </summary>
|
||||
public enum Property
|
||||
{
|
||||
/// <summary>The progress of the down-pointing arrow in a brewing inventory.</summary>
|
||||
BREW_TIME,
|
||||
/// <summary>The progress of the flame in a furnace inventory.</summary>
|
||||
BURN_TIME,
|
||||
/// <summary>The progress of the right-pointing arrow in a furnace inventory.</summary>
|
||||
COOK_TIME,
|
||||
/// <summary>In an enchanting inventory, the top button's experience level value.</summary>
|
||||
ENCHANT_BUTTON1,
|
||||
/// <summary>In an enchanting inventory, the middle button's experience level value.</summary>
|
||||
ENCHANT_BUTTON2,
|
||||
/// <summary>In an enchanting inventory, the bottom button's experience level value.</summary>
|
||||
ENCHANT_BUTTON3,
|
||||
/// <summary>How many total ticks the current fuel should last.</summary>
|
||||
TICKS_FOR_CURRENT_FUEL
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="InventoryView.Property"/>.
|
||||
/// </summary>
|
||||
public static class InventoryViewPropertyExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="InventoryType"/> that this property belongs to.
|
||||
/// </summary>
|
||||
/// <param name="prop">The property.</param>
|
||||
/// <returns>The inventory type.</returns>
|
||||
public static InventoryType getType(this InventoryView.Property prop) => prop switch
|
||||
{
|
||||
InventoryView.Property.BREW_TIME => InventoryType.BREWING,
|
||||
InventoryView.Property.BURN_TIME => InventoryType.FURNACE,
|
||||
InventoryView.Property.COOK_TIME => InventoryType.FURNACE,
|
||||
InventoryView.Property.ENCHANT_BUTTON1 => InventoryType.ENCHANTING,
|
||||
InventoryView.Property.ENCHANT_BUTTON2 => InventoryType.ENCHANTING,
|
||||
InventoryView.Property.ENCHANT_BUTTON3 => InventoryType.ENCHANTING,
|
||||
InventoryView.Property.TICKS_FOR_CURRENT_FUEL => InventoryType.FURNACE,
|
||||
_ => InventoryType.CHEST,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the window-property id for this <see cref="InventoryView.Property"/>.
|
||||
/// </summary>
|
||||
/// <param name="prop">The property.</param>
|
||||
/// <returns>The id.</returns>
|
||||
public static int getId(this InventoryView.Property prop) => prop switch
|
||||
{
|
||||
InventoryView.Property.BREW_TIME => 0,
|
||||
InventoryView.Property.BURN_TIME => 0,
|
||||
InventoryView.Property.COOK_TIME => 2,
|
||||
InventoryView.Property.ENCHANT_BUTTON1 => 0,
|
||||
InventoryView.Property.ENCHANT_BUTTON2 => 1,
|
||||
InventoryView.Property.ENCHANT_BUTTON3 => 2,
|
||||
InventoryView.Property.TICKS_FOR_CURRENT_FUEL => 1,
|
||||
_ => -1,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
namespace Minecraft.Server.FourKit.Inventory;
|
||||
|
||||
using Minecraft.Server.FourKit.Inventory.Meta;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack of items.
|
||||
/// </summary>
|
||||
public class ItemStack
|
||||
{
|
||||
private Material _type;
|
||||
private int _amount;
|
||||
private short _durability;
|
||||
private ItemMeta? _meta;
|
||||
internal Inventory? _ownerInventory;
|
||||
internal int _ownerSlot = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ItemStack of the specified material with amount 1.
|
||||
/// </summary>
|
||||
public ItemStack(Material type) : this(type, 1) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ItemStack of the specified material and amount.
|
||||
/// </summary>
|
||||
public ItemStack(Material type, int amount) : this(type, amount, 0) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ItemStack of the specified material, amount and durability.
|
||||
/// </summary>
|
||||
public ItemStack(Material type, int amount, short durability)
|
||||
{
|
||||
_type = type;
|
||||
_amount = amount;
|
||||
_durability = durability;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ItemStack from a raw type ID with amount 1.
|
||||
/// </summary>
|
||||
public ItemStack(int typeId) : this(typeId, 1) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ItemStack from a raw type ID and amount.
|
||||
/// </summary>
|
||||
public ItemStack(int typeId, int amount) : this(typeId, amount, 0) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ItemStack from a raw type ID, amount and durability.
|
||||
/// </summary>
|
||||
public ItemStack(int typeId, int amount, short durability)
|
||||
{
|
||||
_type = Enum.IsDefined(typeof(Material), typeId) ? (Material)typeId : Material.AIR;
|
||||
_amount = amount;
|
||||
_durability = durability;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of this item.
|
||||
/// </summary>
|
||||
/// <returns>Type of the items in this stack.</returns>
|
||||
public Material getType() => _type;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the type of this item.
|
||||
/// </summary>
|
||||
/// <param name="type">New type to set the items in this stack to.</param>
|
||||
public void setType(Material type) { _type = type; SyncToOwner(); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type id for this item.
|
||||
/// </summary>
|
||||
/// <returns>Type Id of the items in this stack.</returns>
|
||||
public int getTypeId() => (int)_type;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the type id for this item.
|
||||
/// </summary>
|
||||
/// <param name="type">New type id to set the items in this stack to.</param>
|
||||
public void setTypeId(int type)
|
||||
{
|
||||
_type = Enum.IsDefined(typeof(Material), type) ? (Material)type : Material.AIR;
|
||||
SyncToOwner();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of items in this stack.
|
||||
/// </summary>
|
||||
/// <returns>Amount of items in this stack.</returns>
|
||||
public int getAmount() => _amount;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the amount of items in this stack.
|
||||
/// </summary>
|
||||
/// <param name="amount">New amount of items in this stack.</param>
|
||||
public void setAmount(int amount) { _amount = amount; SyncToOwner(); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the durability of this item.
|
||||
/// </summary>
|
||||
/// <returns>Durability of this item.</returns>
|
||||
public short getDurability() => _durability;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the durability of this item.
|
||||
/// </summary>
|
||||
/// <param name="durability">Durability of this item.</param>
|
||||
public void setDurability(short durability) { _durability = durability; SyncToOwner(); }
|
||||
|
||||
/// <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();
|
||||
SyncToOwner();
|
||||
return true;
|
||||
}
|
||||
|
||||
internal ItemMeta? getItemMetaInternal() => _meta;
|
||||
|
||||
internal void setItemMetaInternal(ItemMeta? meta) => _meta = meta;
|
||||
|
||||
internal void BindToInventory(Inventory inventory, int slot)
|
||||
{
|
||||
_ownerInventory = inventory;
|
||||
_ownerSlot = slot;
|
||||
}
|
||||
|
||||
internal void UnbindFromInventory()
|
||||
{
|
||||
_ownerInventory = null;
|
||||
_ownerSlot = -1;
|
||||
}
|
||||
|
||||
private void SyncToOwner()
|
||||
{
|
||||
if (_ownerInventory != null && _ownerSlot >= 0)
|
||||
_ownerInventory.setItem(_ownerSlot, this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
namespace Minecraft.Server.FourKit.Inventory.Meta;
|
||||
|
||||
using Minecraft.Server.FourKit.Enchantments;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the metadata of an <see cref="ItemStack"/>, including display name and lore.
|
||||
/// </summary>
|
||||
public class ItemMeta
|
||||
{
|
||||
private string? _displayName;
|
||||
private Dictionary<EnchantmentType, int>? _enchants;
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified enchantment to this item meta.
|
||||
/// </summary>
|
||||
/// <param name="enchantment">Enchantment to add</param>
|
||||
/// <param name="level">Level for the enchantment</param>
|
||||
/// <param name="ignoreLevelRestriction">Indicates the enchantment should be applied, ignoring the level limit</param>
|
||||
/// <returns>true if the item meta changed as a result of this call, false otherwise</returns>
|
||||
public bool addEnchant(EnchantmentType enchantment, int level, bool ignoreLevelRestriction)
|
||||
{
|
||||
if (_enchants == null)
|
||||
_enchants = new Dictionary<EnchantmentType, int>();
|
||||
|
||||
if (!ignoreLevelRestriction)
|
||||
{
|
||||
Enchantment enchant = Enchantment.getByType(enchantment);
|
||||
|
||||
if (enchant.getMaxLevel() < level) return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_enchants.Add(enchantment, level);
|
||||
return true;
|
||||
} catch { }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified enchantment from this item meta.
|
||||
/// </summary>
|
||||
/// <param name="enchantment">Enchantment to remove</param>
|
||||
/// <returns>true if the item meta changed as a result of this call, false otherwise</returns>
|
||||
public bool removeEnchant(EnchantmentType enchantment)
|
||||
{
|
||||
if (!hasEnchant(enchantment)) return false;
|
||||
|
||||
return _enchants.Remove(enchantment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the enchantments in this ItemMeta. Returns an empty map if none.
|
||||
/// </summary>
|
||||
/// <returns>An immutable copy of the enchantments</returns>
|
||||
public Dictionary<EnchantmentType, int> getEnchants() => _enchants != null ? new Dictionary<EnchantmentType, int>(_enchants) { } : new Dictionary<EnchantmentType, int>();
|
||||
|
||||
/// <summary>
|
||||
/// Checks for the level of the specified enchantment.
|
||||
/// </summary>
|
||||
/// <param name="enchantment">Enchantment to check</param>
|
||||
/// <returns>The level that the specified enchantment has, or 0 if none</returns>
|
||||
public int getEnchantLevel(EnchantmentType enchantment)
|
||||
{
|
||||
if (!hasEnchant(enchantment)) return 0;
|
||||
|
||||
return _enchants[enchantment]; //this cant be invalid, we check above
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the specified enchantment conflicts with any enchantments in this ItemMeta.
|
||||
/// </summary>
|
||||
/// <param name="enchantment">Enchantment to test</param>
|
||||
/// <returns>true if the enchantment conflicts, false otherwise</returns>
|
||||
public bool hasConflictingEnchant(EnchantmentType enchantment)
|
||||
{
|
||||
Enchantment enchantmentClass = Enchantment.getByType(enchantment);
|
||||
if (enchantmentClass == null) return false; //this should never happen
|
||||
|
||||
foreach (KeyValuePair<EnchantmentType, int> ench in _enchants)
|
||||
{
|
||||
Enchantment enchClass = Enchantment.getByType(ench.Key);
|
||||
|
||||
if (enchClass == null) continue; //this should never happen
|
||||
|
||||
if (enchClass.conflictsWith(enchantmentClass))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks for existence of the specified enchantment.
|
||||
/// </summary>
|
||||
/// <param name="enchantment">Enchantment to check</param>
|
||||
/// <returns>true if this enchantment exists for this meta</returns>
|
||||
public bool hasEnchant(EnchantmentType enchantment) => hasEnchants() && _enchants.ContainsKey(enchantment);
|
||||
|
||||
/// <summary>
|
||||
/// Checks for the existence of any enchantments.
|
||||
/// </summary>
|
||||
/// <returns>true if an enchantment exists on this meta</returns>
|
||||
public bool hasEnchants() => _enchants != null && _enchants.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the enchantments for this item meta.
|
||||
/// </summary>
|
||||
/// <param name="enchants">The enchantments to set.</param>
|
||||
public void setEnchants(Dictionary<EnchantmentType, int>? enchants)
|
||||
{
|
||||
enchants = enchants != null ? new Dictionary<EnchantmentType, int>(enchants) : null;
|
||||
}
|
||||
|
||||
|
||||
public ItemMeta clone()
|
||||
{
|
||||
var copy = new ItemMeta();
|
||||
copy._displayName = _displayName;
|
||||
copy._enchants = _enchants != null ? new Dictionary<EnchantmentType, int>(_enchants) : null;
|
||||
copy._lore = _lore != null ? new List<string>(_lore) : null;
|
||||
return copy;
|
||||
}
|
||||
|
||||
internal bool isEmpty()
|
||||
{
|
||||
return _displayName == null && (_lore == null || _lore.Count == 0) && (_enchants == null || _enchants.Count == 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a player's inventory, including armor slots and the held item.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all ItemStacks from the armor slots.
|
||||
/// </summary>
|
||||
/// <returns>An array of ItemStacks for the armor slots.</returns>
|
||||
public ItemStack?[] getArmorContents()
|
||||
{
|
||||
EnsureSynced();
|
||||
var armor = new ItemStack?[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
armor[i] = _items[ARMOR_START + i];
|
||||
return armor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ItemStack in the helmet slot.
|
||||
/// </summary>
|
||||
/// <returns>The helmet ItemStack.</returns>
|
||||
public ItemStack? getHelmet() => getItem(ARMOR_START + 3);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ItemStack in the chestplate slot.
|
||||
/// </summary>
|
||||
/// <returns>The chestplate ItemStack.</returns>
|
||||
public ItemStack? getChestplate() => getItem(ARMOR_START + 2);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ItemStack in the leggings slot.
|
||||
/// </summary>
|
||||
/// <returns>The leggings ItemStack.</returns>
|
||||
public ItemStack? getLeggings() => getItem(ARMOR_START + 1);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ItemStack in the boots slot.
|
||||
/// </summary>
|
||||
/// <returns>The boots ItemStack.</returns>
|
||||
public ItemStack? getBoots() => getItem(ARMOR_START);
|
||||
|
||||
/// <summary>
|
||||
/// Sets all four armor slots at once.
|
||||
/// </summary>
|
||||
/// <param name="items">An array of ItemStacks for the armor slots.</param>
|
||||
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]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the helmet slot.
|
||||
/// </summary>
|
||||
/// <param name="helmet">The ItemStack to set.</param>
|
||||
public void setHelmet(ItemStack? helmet) => setItem(ARMOR_START + 3, helmet);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the chestplate slot.
|
||||
/// </summary>
|
||||
/// <param name="chestplate">The ItemStack to set.</param>
|
||||
public void setChestplate(ItemStack? chestplate) => setItem(ARMOR_START + 2, chestplate);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the leggings slot.
|
||||
/// </summary>
|
||||
/// <param name="leggings">The ItemStack to set.</param>
|
||||
public void setLeggings(ItemStack? leggings) => setItem(ARMOR_START + 1, leggings);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the boots slot.
|
||||
/// </summary>
|
||||
/// <param name="boots">The ItemStack to set.</param>
|
||||
public void setBoots(ItemStack? boots) => setItem(ARMOR_START, boots);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item the player is currently holding.
|
||||
/// </summary>
|
||||
/// <returns>The held ItemStack.</returns>
|
||||
public ItemStack? getItemInHand() {
|
||||
EnsureSynced();
|
||||
return getItem(_heldItemSlot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the item in the player's hand.
|
||||
/// </summary>
|
||||
/// <param name="stack">The ItemStack to set.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the slot number of the currently held item.
|
||||
/// </summary>
|
||||
/// <returns>The held item slot index (0-8).</returns>
|
||||
public int getHeldItemSlot()
|
||||
{
|
||||
EnsureSynced();
|
||||
return _heldItemSlot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the slot number of the currently held item.
|
||||
/// </summary>
|
||||
/// <param name="slot">The slot index (0-8).</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="id">The material id to match, or -1 for any.</param>
|
||||
/// <param name="data">The data value to match, or -1 for any.</param>
|
||||
/// <returns>The number of stacks cleared.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the holder of this inventory.
|
||||
/// </summary>
|
||||
/// <returns>The HumanEntity that owns this inventory.</returns>
|
||||
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<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(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<EnchantmentType, int>? enchants = null;
|
||||
if (enchantCount > 0)
|
||||
{
|
||||
enchants = new Dictionary<EnchantmentType, int>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user