Refactor offline player data access

- Class that handle all Bukkit/NBT conversion of player data
- Ability to read and save the player inventory (more to come later)
This commit is contained in:
2023-02-18 21:32:12 +01:00
parent fb4c62a0bc
commit bf59617e19
8 changed files with 613 additions and 70 deletions

View File

@@ -0,0 +1,199 @@
package fr.pandacube.lib.paper.util;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.HumanEntity;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
public class InventoryWrapper implements Inventory {
private final Inventory base;
public InventoryWrapper(Inventory base) {
this.base = base;
}
@Override
public int getSize() {
return base.getSize();
}
@Override
public int getMaxStackSize() {
return base.getMaxStackSize();
}
@Override
public void setMaxStackSize(int size) {
base.setMaxStackSize(size);
}
@Override
public @Nullable ItemStack getItem(int index) {
return base.getItem(index);
}
@Override
public void setItem(int index, @Nullable ItemStack item) {
base.setItem(index, item);
}
@Override
public @NotNull HashMap<Integer, ItemStack> addItem(@NotNull ItemStack... items) throws IllegalArgumentException {
return base.addItem(items);
}
@Override
public @NotNull HashMap<Integer, ItemStack> removeItem(@NotNull ItemStack... items) throws IllegalArgumentException {
return base.removeItem(items);
}
@Override
public @NotNull HashMap<Integer, ItemStack> removeItemAnySlot(@NotNull ItemStack... items) throws IllegalArgumentException {
return base.removeItemAnySlot(items);
}
@Override
public @Nullable ItemStack @NotNull [] getContents() {
return base.getContents();
}
@Override
public void setContents(@Nullable ItemStack @NotNull [] items) throws IllegalArgumentException {
base.setContents(items);
}
@Override
public @Nullable ItemStack @NotNull [] getStorageContents() {
return base.getStorageContents();
}
@Override
public void setStorageContents(@Nullable ItemStack @NotNull [] items) throws IllegalArgumentException {
base.setStorageContents(items);
}
@Override
public boolean contains(@NotNull Material material) throws IllegalArgumentException {
return base.contains(material);
}
@Override
public boolean contains(@Nullable ItemStack item) {
return base.contains(item);
}
@Override
public boolean contains(@NotNull Material material, int amount) throws IllegalArgumentException {
return base.contains(material, amount);
}
@Override
public boolean contains(@Nullable ItemStack item, int amount) {
return base.contains(item, amount);
}
@Override
public boolean containsAtLeast(@Nullable ItemStack item, int amount) {
return base.containsAtLeast(item, amount);
}
@Override
public @NotNull HashMap<Integer, ? extends ItemStack> all(@NotNull Material material) throws IllegalArgumentException {
return base.all(material);
}
@Override
public @NotNull HashMap<Integer, ? extends ItemStack> all(@Nullable ItemStack item) {
return base.all(item);
}
@Override
public int first(@NotNull Material material) throws IllegalArgumentException {
return base.first(material);
}
@Override
public int first(@NotNull ItemStack item) {
return base.first(item);
}
@Override
public int firstEmpty() {
return base.firstEmpty();
}
@Override
public boolean isEmpty() {
return base.isEmpty();
}
@Override
public void remove(@NotNull Material material) throws IllegalArgumentException {
base.remove(material);
}
@Override
public void remove(@NotNull ItemStack item) {
base.remove(item);
}
@Override
public void clear(int index) {
base.clear(index);
}
@Override
public void clear() {
base.clear();
}
@Override
public int close() {
return base.close();
}
@Override
public @NotNull List<HumanEntity> getViewers() {
return base.getViewers();
}
@Override
public @NotNull InventoryType getType() {
return base.getType();
}
@Override
public @Nullable InventoryHolder getHolder() {
return base.getHolder();
}
@Override
public @Nullable InventoryHolder getHolder(boolean useSnapshot) {
return base.getHolder(useSnapshot);
}
@Override
public @NotNull ListIterator<ItemStack> iterator() {
return base.iterator();
}
@Override
public @NotNull ListIterator<ItemStack> iterator(int index) {
return base.iterator(index);
}
@Override
public @Nullable Location getLocation() {
return base.getLocation();
}
}

View File

@@ -0,0 +1,350 @@
package fr.pandacube.lib.paper.util;
import com.google.common.base.Preconditions;
import fr.pandacube.lib.paper.reflect.wrapper.craftbukkit.CraftItemStack;
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.nbt.CompoundTag;
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.nbt.ListTag;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.HumanEntity;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.function.IntUnaryOperator;
/**
* A wrapper to easily manipulate vanilla player data.
*/
public class PlayerDataWrapper {
public final CompoundTag data;
public PlayerDataWrapper(CompoundTag data) {
this.data = data == null ? new CompoundTag() : data;
}
/**
* Gets a snapshot of the inventory of this player.
* If modified, call the {@link #setInventory(PlayerInventory)} to update the data.
* @return the player inventory
*/
public PlayerInventory getInventory() {
return new DummyPlayerInventory(
getBukkitInventory("Inventory", InventoryType.PLAYER, this::fromNBTtoBukkitInventorySlot),
getHeldItemSlot());
}
private int fromNBTtoBukkitInventorySlot(int nbtSlot) {
// cat nbEl NBTslot bukkitSlot NBT->Bukkit
// items 36el 0-35 ==
// armor 4el start 100 36-39 -100 + 36
// offhnd 1el start 150 40 -150 + 40
if (nbtSlot >= 0 && nbtSlot < 36) { // regular inventory slots
return nbtSlot;
}
if (nbtSlot >= 100 && nbtSlot < 104) { // armor slots
return nbtSlot - 100 + 36;
}
if (nbtSlot == 150) { // second hand
return 40;
}
throw new IllegalArgumentException("Unrecognized NBT player inventory slot " + nbtSlot);
}
public void setInventory(PlayerInventory inv) {
setBukkitInventory("Inventory", inv, this::fromBukkitToNBTInventorySlot);
setHeldItemSlot(inv.getHeldItemSlot());
}
private int fromBukkitToNBTInventorySlot(int bukkitSlot) {
if (bukkitSlot >= 0 && bukkitSlot < 36) { // regular inventory slots
return bukkitSlot;
}
if (bukkitSlot >= 36 && bukkitSlot < 40) { // armor slots
return bukkitSlot + 100 - 36;
}
if (bukkitSlot == 40) { // second hand
return 150;
}
throw new IllegalArgumentException("Unrecognized Bukkit player inventory slot " + bukkitSlot);
}
public Inventory getEnderChest() {
return getBukkitInventory("EnderItems", InventoryType.ENDER_CHEST, IntUnaryOperator.identity());
}
public void setEnderChest(Inventory inv) {
setBukkitInventory("EnderItems", inv, IntUnaryOperator.identity());
}
private int getHeldItemSlot() {
if (!data.contains("SelectedItemSlot"))
return 0;
return data.getInt("SelectedItemSlot");
}
private void setHeldItemSlot(int slot) {
data.putInt("SelectedItemSlot", slot);
}
private Inventory getBukkitInventory(String nbtKey, InventoryType bukkitType, IntUnaryOperator nbtToBukkitSlotConverter) {
Map<Integer, ItemStack> stacks = getRawInvontoryContent(nbtKey);
Inventory inv = Bukkit.createInventory(null, bukkitType);
if (stacks.isEmpty())
return inv;
for (Map.Entry<Integer, ItemStack> is : stacks.entrySet()) {
inv.setItem(nbtToBukkitSlotConverter.applyAsInt(is.getKey()), is.getValue());
}
return inv;
}
private Map<Integer, ItemStack> getRawInvontoryContent(String key) {
if (!data.contains(key, 9)) // type 9 is list
return Map.of();
ListTag list = data.getList(key, 10); // type of list element 10 is CompoundTag
if (list == null)
return Map.of();
Map<Integer, ItemStack> stacks = new TreeMap<>();
for (int i = 0; i < list.size(); i++) {
CompoundTag itemTag = list.getCompound(i);
int nbtSlot = itemTag.getByte("Slot") & 255;
ItemStack is = CraftItemStack.asCraftMirror(itemTag);
if (is != null && !is.getType().isAir()) {
stacks.put(nbtSlot, CraftItemStack.asCraftMirror(itemTag));
}
}
return stacks;
}
private void setBukkitInventory(String nbtKey, Inventory inv, IntUnaryOperator bukkitToNBTSlotconverter) {
Map<Integer, ItemStack> stacks = new TreeMap<>();
if (inv == null) {
setRawInventoryContent(nbtKey, stacks);
}
for (int bukkitSlot = 0; bukkitSlot < inv.getSize(); bukkitSlot++) {
ItemStack is = inv.getItem(bukkitSlot);
int nbtSlot = bukkitToNBTSlotconverter.applyAsInt(bukkitSlot);
stacks.put(nbtSlot, is);
}
setRawInventoryContent(nbtKey, stacks);
}
private void setRawInventoryContent(String key, Map<Integer, ItemStack> stacks) {
ListTag list = new ListTag();
for (Map.Entry<Integer, ItemStack> is : stacks.entrySet()) {
CompoundTag itemTag = new CompoundTag();
CraftItemStack.asNMSCopy(is.getValue()).save(itemTag);
itemTag.putByte("Slot", is.getKey().byteValue());
list.add(list.size(), itemTag);
}
data.put(key, list);
}
private static class DummyPlayerInventory extends InventoryWrapper implements PlayerInventory {
private int heldItemSlot;
public DummyPlayerInventory(Inventory base, int heldItemSlot) {
super(base);
this.heldItemSlot = heldItemSlot;
}
@Override
public @Nullable ItemStack @NotNull [] getStorageContents() {
return Arrays.copyOfRange(getContents(), 0, 36);
}
@Override
public @Nullable ItemStack @NotNull [] getArmorContents() {
return Arrays.copyOfRange(getContents(), 36, 40);
}
@Override
public void setArmorContents(@Nullable ItemStack[] items) {
this.setSlots(items, 36, 4);
}
@Override
public @Nullable ItemStack @NotNull [] getExtraContents() {
return Arrays.copyOfRange(getContents(), 40, getSize());
}
@Override
public void setExtraContents(@Nullable ItemStack[] items) {
this.setSlots(items, 40, getSize() - 40);
}
private void setSlots(ItemStack[] items, int baseSlot, int length) {
if (items == null) {
items = new ItemStack[length];
}
Preconditions.checkArgument(items.length <= length, "items.length must be < %s", length);
for (int i = 0; i < length; i++) {
if (i >= items.length) {
this.setItem(baseSlot + i, null);
} else {
this.setItem(baseSlot + i, items[i]);
}
}
}
@Override
public ItemStack getHelmet() {
return getItem(39);
}
@Override
public void setHelmet(@Nullable ItemStack helmet) {
setItem(39, helmet);
}
@Override
public ItemStack getChestplate() {
return getItem(38);
}
@Override
public void setChestplate(@Nullable ItemStack chestplate) {
setItem(38, chestplate);
}
@Override
public ItemStack getLeggings() {
return getItem(37);
}
@Override
public void setLeggings(@Nullable ItemStack leggings) {
setItem(37, leggings);
}
@Override
public ItemStack getBoots() {
return getItem(36);
}
@Override
public void setBoots(@Nullable ItemStack boots) {
setItem(36, boots);
}
@Override
public void setItem(EquipmentSlot slot, ItemStack item) {
Preconditions.checkArgument(slot != null, "slot must not be null");
switch (slot) {
case HAND:
this.setItemInMainHand(item);
break;
case OFF_HAND:
this.setItemInOffHand(item);
break;
case FEET:
this.setBoots(item);
break;
case LEGS:
this.setLeggings(item);
break;
case CHEST:
this.setChestplate(item);
break;
case HEAD:
this.setHelmet(item);
break;
default:
throw new IllegalArgumentException("Not implemented. This is a bug");
}
}
@Override
public ItemStack getItem(EquipmentSlot slot) {
Preconditions.checkArgument(slot != null, "slot must not be null");
return switch (slot) {
case HAND -> this.getItemInMainHand();
case OFF_HAND -> this.getItemInOffHand();
case FEET -> Objects.requireNonNullElseGet(this.getBoots(), () -> new ItemStack(Material.AIR));
case LEGS -> Objects.requireNonNullElseGet(this.getLeggings(), () -> new ItemStack(Material.AIR));
case CHEST -> Objects.requireNonNullElseGet(this.getChestplate(), () -> new ItemStack(Material.AIR));
case HEAD -> Objects.requireNonNullElseGet(this.getHelmet(), () -> new ItemStack(Material.AIR));
};
}
@Override
public @NotNull ItemStack getItemInMainHand() {
return getItem(heldItemSlot);
}
@Override
public void setItemInMainHand(@Nullable ItemStack item) {
setItem(heldItemSlot, item);
}
@Override
public @NotNull ItemStack getItemInOffHand() {
return getItem(40);
}
@Override
public void setItemInOffHand(@Nullable ItemStack item) {
setItem(40, item);
}
@Override
public @NotNull ItemStack getItemInHand() {
return getItemInMainHand();
}
@Override
public void setItemInHand(@Nullable ItemStack stack) {
setItemInMainHand(stack);
}
@Override
public int getHeldItemSlot() {
return heldItemSlot;
}
@Override
public void setHeldItemSlot(int slot) {
if (slot < 0 || slot > 8)
throw new IllegalArgumentException("Slot is not between 0 and 8 inclusive");
heldItemSlot = slot;
}
@Override
public @Nullable HumanEntity getHolder() {
return null;
}
}
}