From b031064a69ed45bbbba78502931a5ea7275caa9a Mon Sep 17 00:00:00 2001 From: Pablo Herrera Date: Thu, 28 Nov 2024 01:50:53 +0100 Subject: [PATCH] Add support for slotted attributes Signed-off-by: Pablo Herrera --- .../java/tc/oc/pgm/flag/state/Carried.java | 2 +- .../java/tc/oc/pgm/itemmeta/ItemRule.java | 13 +- .../tc/oc/pgm/kits/ApplyItemKitEvent.java | 1 + .../main/java/tc/oc/pgm/kits/ArmorKit.java | 1 + .../src/main/java/tc/oc/pgm/kits/ItemKit.java | 1 + .../java/tc/oc/pgm/kits/KitMatchModule.java | 1 + .../main/java/tc/oc/pgm/kits/KitParser.java | 7 +- core/src/main/java/tc/oc/pgm/kits/Slot.java | 276 ---------- .../tc/oc/pgm/loot/LootableMatchModule.java | 43 +- .../oc/pgm/modules/ItemKeepMatchModule.java | 6 +- .../modern/inventory/ModernAttributeUtil.java | 103 ++++ .../modern/inventory/ModernInventoryUtil.java | 40 -- .../inventory/SpAttributeUtils.java | 83 +++ .../sportpaper/inventory/SpInventoryUtil.java | 40 -- .../pgm/util/attribute/AttributeModifier.java | 100 ---- .../oc/pgm/util/attribute/AttributeUtils.java | 42 ++ .../tc/oc/pgm/util/inventory}/ArmorType.java | 2 +- .../oc/pgm/util/inventory/InventoryUtils.java | 43 -- .../java/tc/oc/pgm/util/inventory/Slot.java | 500 ++++++++++++++++++ .../tc/oc/pgm/util/material/Materials.java | 19 +- .../java/tc/oc/pgm/util/xml/XMLUtils.java | 36 +- 21 files changed, 797 insertions(+), 562 deletions(-) delete mode 100644 core/src/main/java/tc/oc/pgm/kits/Slot.java create mode 100644 platform/platform-modern/src/main/java/tc/oc/pgm/platform/modern/inventory/ModernAttributeUtil.java create mode 100644 platform/platform-sportpaper/src/main/java/tc/oc/pgm/platform/sportpaper/inventory/SpAttributeUtils.java delete mode 100644 util/src/main/java/tc/oc/pgm/util/attribute/AttributeModifier.java create mode 100644 util/src/main/java/tc/oc/pgm/util/attribute/AttributeUtils.java rename {core/src/main/java/tc/oc/pgm/kits => util/src/main/java/tc/oc/pgm/util/inventory}/ArmorType.java (96%) create mode 100644 util/src/main/java/tc/oc/pgm/util/inventory/Slot.java diff --git a/core/src/main/java/tc/oc/pgm/flag/state/Carried.java b/core/src/main/java/tc/oc/pgm/flag/state/Carried.java index 0750355d75..ca7716c6d3 100644 --- a/core/src/main/java/tc/oc/pgm/flag/state/Carried.java +++ b/core/src/main/java/tc/oc/pgm/flag/state/Carried.java @@ -43,13 +43,13 @@ import tc.oc.pgm.flag.event.FlagCaptureEvent; import tc.oc.pgm.flag.event.FlagStateChangeEvent; import tc.oc.pgm.goals.events.GoalEvent; -import tc.oc.pgm.kits.ArmorType; import tc.oc.pgm.kits.Kit; import tc.oc.pgm.score.ScoreMatchModule; import tc.oc.pgm.scoreboard.SidebarMatchModule; import tc.oc.pgm.spawns.events.ParticipantDespawnEvent; import tc.oc.pgm.teams.TeamFactory; import tc.oc.pgm.teams.TeamMatchModule; +import tc.oc.pgm.util.inventory.ArmorType; import tc.oc.pgm.util.named.NameStyle; /** State of a flag when a player has picked it up and is wearing the banner on their head. */ diff --git a/core/src/main/java/tc/oc/pgm/itemmeta/ItemRule.java b/core/src/main/java/tc/oc/pgm/itemmeta/ItemRule.java index 4513c741b1..616f408c0d 100644 --- a/core/src/main/java/tc/oc/pgm/itemmeta/ItemRule.java +++ b/core/src/main/java/tc/oc/pgm/itemmeta/ItemRule.java @@ -1,5 +1,6 @@ package tc.oc.pgm.itemmeta; +import static tc.oc.pgm.util.attribute.AttributeUtils.ATTRIBUTE_UTILS; import static tc.oc.pgm.util.inventory.InventoryUtils.INVENTORY_UTILS; import java.util.EnumSet; @@ -43,14 +44,12 @@ public void apply(ItemStack stack) { InventoryUtils.addEnchantments(meta, this.meta.getEnchants()); - INVENTORY_UTILS.copyAttributeModifiers(meta, this.meta); + ATTRIBUTE_UTILS.copyAttributeModifiers(meta, this.meta); - Set canDestroy = - unionMaterials( - INVENTORY_UTILS.getCanDestroy(meta), INVENTORY_UTILS.getCanDestroy(this.meta)); - Set canPlaceOn = - unionMaterials( - INVENTORY_UTILS.getCanPlaceOn(meta), INVENTORY_UTILS.getCanPlaceOn(this.meta)); + Set canDestroy = unionMaterials( + INVENTORY_UTILS.getCanDestroy(meta), INVENTORY_UTILS.getCanDestroy(this.meta)); + Set canPlaceOn = unionMaterials( + INVENTORY_UTILS.getCanPlaceOn(meta), INVENTORY_UTILS.getCanPlaceOn(this.meta)); if (!canDestroy.isEmpty()) INVENTORY_UTILS.setCanDestroy(meta, canDestroy); if (!canPlaceOn.isEmpty()) INVENTORY_UTILS.setCanPlaceOn(meta, canPlaceOn); diff --git a/core/src/main/java/tc/oc/pgm/kits/ApplyItemKitEvent.java b/core/src/main/java/tc/oc/pgm/kits/ApplyItemKitEvent.java index 515a2643a5..d7560c7cbd 100644 --- a/core/src/main/java/tc/oc/pgm/kits/ApplyItemKitEvent.java +++ b/core/src/main/java/tc/oc/pgm/kits/ApplyItemKitEvent.java @@ -4,6 +4,7 @@ import java.util.*; import org.bukkit.inventory.ItemStack; import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.util.inventory.Slot; /** * Fired when an {@link ItemKit} is applied to a player. The kit can be modified through the diff --git a/core/src/main/java/tc/oc/pgm/kits/ArmorKit.java b/core/src/main/java/tc/oc/pgm/kits/ArmorKit.java index 3145f276b5..475a137877 100644 --- a/core/src/main/java/tc/oc/pgm/kits/ArmorKit.java +++ b/core/src/main/java/tc/oc/pgm/kits/ArmorKit.java @@ -6,6 +6,7 @@ import org.bukkit.inventory.ItemStack; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.kits.tag.ItemModifier; +import tc.oc.pgm.util.inventory.ArmorType; public class ArmorKit extends AbstractKit { public static class ArmorItem { diff --git a/core/src/main/java/tc/oc/pgm/kits/ItemKit.java b/core/src/main/java/tc/oc/pgm/kits/ItemKit.java index 192f03663b..949ee71a5a 100644 --- a/core/src/main/java/tc/oc/pgm/kits/ItemKit.java +++ b/core/src/main/java/tc/oc/pgm/kits/ItemKit.java @@ -12,6 +12,7 @@ import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.kits.tag.ItemModifier; import tc.oc.pgm.util.inventory.InventoryUtils; +import tc.oc.pgm.util.inventory.Slot; public class ItemKit implements KitDefinition { public static final int INFINITE_STACK_SIZE = 99; diff --git a/core/src/main/java/tc/oc/pgm/kits/KitMatchModule.java b/core/src/main/java/tc/oc/pgm/kits/KitMatchModule.java index c1dfadc0f8..04cc5167e9 100644 --- a/core/src/main/java/tc/oc/pgm/kits/KitMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/kits/KitMatchModule.java @@ -29,6 +29,7 @@ import tc.oc.pgm.kits.tag.Grenade; import tc.oc.pgm.kits.tag.ItemTags; import tc.oc.pgm.util.event.ItemTransferEvent; +import tc.oc.pgm.util.inventory.Slot; @ListenerScope(MatchScope.RUNNING) public class KitMatchModule implements MatchModule, Listener { diff --git a/core/src/main/java/tc/oc/pgm/kits/KitParser.java b/core/src/main/java/tc/oc/pgm/kits/KitParser.java index 68bfc065d9..a31a55bcd8 100644 --- a/core/src/main/java/tc/oc/pgm/kits/KitParser.java +++ b/core/src/main/java/tc/oc/pgm/kits/KitParser.java @@ -1,5 +1,6 @@ package tc.oc.pgm.kits; +import static tc.oc.pgm.util.attribute.AttributeUtils.ATTRIBUTE_UTILS; import static tc.oc.pgm.util.inventory.InventoryUtils.INVENTORY_UTILS; import static tc.oc.pgm.util.nms.NMSHacks.NMS_HACKS; @@ -59,8 +60,10 @@ import tc.oc.pgm.teams.TeamFactory; import tc.oc.pgm.teams.Teams; import tc.oc.pgm.util.bukkit.BukkitUtils; +import tc.oc.pgm.util.inventory.ArmorType; import tc.oc.pgm.util.inventory.InventoryUtils; import tc.oc.pgm.util.inventory.ItemMatcher; +import tc.oc.pgm.util.inventory.Slot; import tc.oc.pgm.util.material.ItemMaterialData; import tc.oc.pgm.util.material.MaterialData; import tc.oc.pgm.util.material.Materials; @@ -344,7 +347,7 @@ public SetMultimap parseAttributeModifiers(Element if (attr != null) { for (String modifierText : Splitter.on(";").split(attr.getValue())) { var mod = XMLUtils.parseCompactAttributeModifier(attr, modifierText); - modifiers.put(mod.getKey(), mod.getValue()); + modifiers.put(mod.getLeft(), mod.getRight()); } } @@ -536,7 +539,7 @@ public void parseItemMeta(Element el, ItemMeta meta) throws InvalidXMLException } } - INVENTORY_UTILS.applyAttributeModifiers(parseAttributeModifiers(el), meta); + ATTRIBUTE_UTILS.applyAttributeModifiers(parseAttributeModifiers(el), meta); String customName = el.getAttributeValue("name"); if (customName != null) { diff --git a/core/src/main/java/tc/oc/pgm/kits/Slot.java b/core/src/main/java/tc/oc/pgm/kits/Slot.java deleted file mode 100644 index 634f4aa3fa..0000000000 --- a/core/src/main/java/tc/oc/pgm/kits/Slot.java +++ /dev/null @@ -1,276 +0,0 @@ -package tc.oc.pgm.kits; - -import com.google.common.collect.ContiguousSet; -import com.google.common.collect.DiscreteDomain; -import com.google.common.collect.HashBasedTable; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Range; -import com.google.common.collect.Table; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.Map; -import org.bukkit.Material; -import org.bukkit.entity.HumanEntity; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; -import org.jetbrains.annotations.Nullable; -import tc.oc.pgm.util.inventory.InventoryUtils; - -/** - * Derived from the names found in net.minecraft.server.CommandReplaceItem. If we ever implement - * applying kits to other types of inventories, this should be expanded to include those slot names - * as well. - */ -public abstract class Slot { - - private final String key; - private final int index; // -1 = auto - - private Slot(Class type, String key, int index) { - this.key = key; - this.index = index; - - byIndex.put(type, index, this); - byKey.put(key, this); - } - - /** @return the name of this slot, as used by the /replaceitem command */ - public String getKey() { - return "slot." + key; - } - - /** @return a slot index that can be passed to {@link Inventory#getItem} et al. */ - public int getIndex() { - if (isAuto()) throw new UnsupportedOperationException("The auto-slot has no index"); - return index; - } - - public Range getIndexRange() { - if (isAuto()) { - return getAutoIndexRange(); - } else { - return Range.singleton(getIndex()); - } - } - - protected abstract Range getAutoIndexRange(); - - /** - * @return true if this is the special "auto" slot. Putting a stack in this slot will merge it - * with the inventory by calling {@link Inventory#addItem} - */ - public boolean isAuto() { - return index < 0; - } - - public abstract Inventory getInventory(InventoryHolder holder); - - protected ItemStack addItem(InventoryHolder holder, ItemStack stack) { - return InventoryUtils.placeStack( - getInventory(holder), - ContiguousSet.create(getAutoIndexRange(), DiscreteDomain.integers()), - stack); - } - - /** - * @return the item in this slot of the given holder's inventory, or null if the slot is empty. - * This will never return a stack of {@link Material#AIR}. - */ - public @Nullable ItemStack getItem(InventoryHolder holder) { - ItemStack stack = getInventory(holder).getItem(getIndex()); - return stack == null || stack.getType() == Material.AIR ? null : stack; - } - - /** - * Put the given stack in this slot of the given holder's inventory. - * - * @return a stack of any items that were NOT placed in the inventory, or null if the entire stack - * was placed. This can only be non-null when placing in the auto-slot. - */ - public @Nullable ItemStack putItem(InventoryHolder holder, ItemStack stack) { - if (isAuto()) { - stack = addItem(holder, stack); - return stack.getAmount() > 0 ? stack : null; - } else { - getInventory(holder).setItem(getIndex(), stack); - return null; - } - } - - public void putItem(Inventory inv, ItemStack stack) { - if (isAuto()) { - inv.addItem(stack); - } else { - inv.setItem(getIndex(), stack); - } - } - - public static class Player extends Slot { - protected Player(String key, int index) { - super(Player.class, key, index); - } - - @Override - public Inventory getInventory(InventoryHolder holder) { - if (!(holder instanceof HumanEntity)) - throw new IllegalArgumentException("InventoryHolder " + holder + " is not a player"); - return holder.getInventory(); - } - - @Override - protected Range getAutoIndexRange() { - return Range.closed(0, 39); - } - - /** - * Convert a {@link PlayerInventory} slot index to a {@link Slot} object. Returns null if the - * index is out of range. - */ - public static @Nullable Slot forIndex(int index) { - return byIndex.get(Player.class, index); - } - } - - public static class Hotbar extends Player { - protected Hotbar(String key, int index) { - super(key, index); - } - - @Override - protected Range getAutoIndexRange() { - return Range.closed(0, 8); - } - } - - public static class Pockets extends Player { - protected Pockets(String key, int index) { - super(key, index); - } - - @Override - protected Range getAutoIndexRange() { - return Range.closed(9, 35); - } - } - - public static class Armor extends Player { - private final ArmorType armorType; - - protected Armor(String key, int index, ArmorType armorType) { - super(key, index); - this.armorType = armorType; - byArmorType.put(armorType, this); - } - - @Override - protected Range getAutoIndexRange() { - return Range.closed(36, 39); - } - - public ArmorType getArmorType() { - return armorType; - } - - public static Armor forType(ArmorType armorType) { - return byArmorType.get(armorType); - } - } - - public static class Container extends Slot { - - private Container(String key, int index) { - super(Container.class, key, index); - } - - @Override - protected Range getAutoIndexRange() { - return Range.closed(0, 53); - } - - @Override - public Inventory getInventory(InventoryHolder holder) { - return holder.getInventory(); - } - - public static Slot forIndex(int index) { - return byIndex.get(Container.class, index); - } - } - - public static class EnderChest extends Slot { - protected EnderChest(String key, int index) { - super(EnderChest.class, key, index); - } - - @Override - public Inventory getInventory(InventoryHolder holder) { - if (holder instanceof HumanEntity) { - return ((HumanEntity) holder).getEnderChest(); - } - throw new IllegalArgumentException( - "InventoryHolder " + holder + " does not have an ender chest"); - } - - @Override - protected Range getAutoIndexRange() { - return Range.closed(0, 26); - } - } - - private static final Map byKey = new HashMap<>(); - private static final Table, Integer, Slot> byIndex = - HashBasedTable.create(); - - private static final Map, Class> byInventoryType = - ImmutableMap., Class>builder() - .put(PlayerInventory.class, Player.class) - .put(Inventory.class, Container.class) - .build(); - private static final Map byArmorType = new EnumMap<>(ArmorType.class); - - /** - * Convert a Mojang slot name (used by /replaceitem) to a {@link Slot} object. The "slot." at the - * beginning of the name is optional. Returns null if the name is invalid. - */ - public static @Nullable Slot forKey(String key) { - if (key.startsWith("slot.")) { - key = key.substring("slot.".length()); - } - return byKey.get(key); - } - - public static Slot forInventoryIndex(Inventory inv, int index) { - for (Map.Entry, Class> entry : - byInventoryType.entrySet()) { - if (entry.getKey().isAssignableFrom(inv.getClass())) { - return byIndex.get(entry.getValue(), index); - } - } - throw new UnsupportedOperationException("Unknown inventory type " + inv); - } - - static { - // new Hotbar("hotbar", -1); - for (int i = 0; i < 9; i++) { - new Hotbar("hotbar." + i, i); - } - - // new Pockets("inventory", -1); - // new EnderChest("enderchest", -1); - for (int i = 0; i < 27; i++) { - new Pockets("inventory." + i, 9 + i); - new EnderChest("enderchest." + i, i); - } - - for (int i = 0; i < 54; i++) { - new Container("container." + i, i); - } - - new Armor("armor.feet", 36, ArmorType.BOOTS); - new Armor("armor.legs", 37, ArmorType.LEGGINGS); - new Armor("armor.chest", 38, ArmorType.CHESTPLATE); - new Armor("armor.head", 39, ArmorType.HELMET); - } -} diff --git a/core/src/main/java/tc/oc/pgm/loot/LootableMatchModule.java b/core/src/main/java/tc/oc/pgm/loot/LootableMatchModule.java index 3f0736bc8b..645268d3d8 100644 --- a/core/src/main/java/tc/oc/pgm/loot/LootableMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/loot/LootableMatchModule.java @@ -36,9 +36,9 @@ import tc.oc.pgm.filters.query.BlockQuery; import tc.oc.pgm.filters.query.EntityQuery; import tc.oc.pgm.itemmeta.ItemModifyMatchModule; -import tc.oc.pgm.kits.Slot; import tc.oc.pgm.util.Pair; import tc.oc.pgm.util.collection.InstantMap; +import tc.oc.pgm.util.inventory.Slot; @ListenerScope(value = MatchScope.LOADED) public class LootableMatchModule implements MatchModule, Listener { @@ -62,11 +62,9 @@ public LootableMatchModule( final FilterMatchModule fmm = match.needModule(FilterMatchModule.class); - fillers.forEach(filler -> { - fmm.onRise(Match.class, filler.getRefillTrigger(), m -> this.filledAt - .keySet() - .removeIf(f -> filler.equals(f.getRight()))); - }); + fillers.forEach(filler -> fmm.onRise(Match.class, filler.getRefillTrigger(), m -> this.filledAt + .keySet() + .removeIf(f -> filler.equals(f.getRight())))); } /** @@ -74,22 +72,19 @@ public LootableMatchModule( * InventoryHolder is not something that we should be filling. */ private static @Nullable Predicate filterPredicate(InventoryHolder holder) { - if (holder instanceof DoubleChest) { - final DoubleChest doubleChest = (DoubleChest) holder; + if (holder instanceof DoubleChest doubleChest) { return filter -> !filter .query(new BlockQuery((Chest) doubleChest.getLeftSide())) .isDenied() || !filter.query(new BlockQuery((Chest) doubleChest.getRightSide())).isDenied(); } else if (holder instanceof BlockState) { return filter -> !filter.query(new BlockQuery((BlockState) holder)).isDenied(); - } else if (holder instanceof Player) { - // This happens with crafting inventories, and possibly other transient inventory types - // Pretty sure we never want to fill an inventory held by the player - return null; - } else if (holder instanceof Entity) { + } else if (holder instanceof Entity && !(holder instanceof Player)) { return filter -> !filter.query(new EntityQuery((Entity) holder)).isDenied(); } else { - // If we're not sure what it is, don't fill it + // This happens with crafting inventories, and possibly other transient inventory types + // Pretty sure we never want to fill an inventory held by the player, or one we don't know + // about return null; } } @@ -139,7 +134,7 @@ void fill(MatchPlayer opener, List fillers) { final List coolFillers = fillers.stream() .filter(filler -> filledAt.putUnlessNewer(new Pair<>(this, filler), filler.getRefillInterval()) == null) - .collect(Collectors.toList()); + .toList(); // Find all the Inventories for this Fillable, and build a map of Fillers to the subset // of Inventories that they are allowed to fill, based on the filter of each Filler. @@ -170,7 +165,7 @@ void fill(MatchPlayer opener, List fillers) { for (Inventory inv : inventories) { for (int index = 0; index < inv.getSize(); index++) { if (inv.getItem(index) == null) { - slots.add(new InventorySlot(index, inv)); + slots.add(InventorySlot.fromInventoryIndex(inv, index)); } } } @@ -248,18 +243,22 @@ Stream inventories() { /** A wrapper of a slot that belongs to a specified {@link Inventory} */ private static class InventorySlot { - private final Slot slot; private final Inventory inventory; + private final Slot slot; - private InventorySlot(Slot slot, Inventory inventory) { - assertNotNull(slot, "slot"); + private InventorySlot(Inventory inventory, Slot slot) { assertNotNull(inventory, "inventory"); - this.slot = slot; + assertNotNull(slot, "slot"); this.inventory = inventory; + this.slot = slot; } - private InventorySlot(int index, Inventory inventory) { - this(Slot.forInventoryIndex(inventory, index), inventory); + static InventorySlot fromInventoryIndex(Inventory inventory, int index) { + final Slot slot = Slot.forInventoryIndex(inventory.getClass(), index); + if (slot == null) + throw new IllegalArgumentException( + "Could not determine slot at index " + index + " in inventory " + inventory); + return new InventorySlot(inventory, slot); } private void putItem(ItemStack item) { diff --git a/core/src/main/java/tc/oc/pgm/modules/ItemKeepMatchModule.java b/core/src/main/java/tc/oc/pgm/modules/ItemKeepMatchModule.java index 86dfdfcfa6..87fa19e726 100644 --- a/core/src/main/java/tc/oc/pgm/modules/ItemKeepMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/modules/ItemKeepMatchModule.java @@ -17,7 +17,7 @@ import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.events.ListenerScope; import tc.oc.pgm.events.PlayerPartyChangeEvent; -import tc.oc.pgm.kits.ArmorType; +import tc.oc.pgm.util.inventory.ArmorType; import tc.oc.pgm.util.material.MaterialMatcher; @ListenerScope(MatchScope.RUNNING) @@ -45,8 +45,8 @@ private boolean canKeepArmor(ItemStack stack) { } /** - * NOTE: Must be called before {@link - * tc.oc.pgm.tracker.trackers.DeathTracker#onPlayerDeath(PlayerDeathEvent)} + * NOTE: Must be called before + * {@link tc.oc.pgm.tracker.trackers.DeathTracker#onPlayerDeath(PlayerDeathEvent)} */ @SuppressWarnings("deprecation") @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) diff --git a/platform/platform-modern/src/main/java/tc/oc/pgm/platform/modern/inventory/ModernAttributeUtil.java b/platform/platform-modern/src/main/java/tc/oc/pgm/platform/modern/inventory/ModernAttributeUtil.java new file mode 100644 index 0000000000..725720b7b7 --- /dev/null +++ b/platform/platform-modern/src/main/java/tc/oc/pgm/platform/modern/inventory/ModernAttributeUtil.java @@ -0,0 +1,103 @@ +package tc.oc.pgm.platform.modern.inventory; + +import static tc.oc.pgm.util.platform.Supports.Variant.PAPER; + +import com.google.common.collect.SetMultimap; +import java.util.UUID; +import org.bukkit.NamespacedKey; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeModifier; +import org.bukkit.inventory.EquipmentSlotGroup; +import org.bukkit.inventory.meta.ItemMeta; +import org.jdom2.Element; +import org.jetbrains.annotations.NotNull; +import tc.oc.pgm.util.attribute.AttributeUtils; +import tc.oc.pgm.util.inventory.Slot; +import tc.oc.pgm.util.platform.Supports; +import tc.oc.pgm.util.xml.InvalidXMLException; +import tc.oc.pgm.util.xml.Node; +import tc.oc.pgm.util.xml.XMLUtils; + +@Supports(value = PAPER, minVersion = "1.21.1") +@SuppressWarnings("UnstableApiUsage") +public class ModernAttributeUtil implements AttributeUtils { + + @Override + public AttributeModifier parseModifier(Element el) throws InvalidXMLException { + var key = el.getAttributeValue("key"); + if (key == null) key = UUID.randomUUID().toString(); + double amount = XMLUtils.parseNumber(Node.fromRequiredAttr(el, "amount"), Double.class); + var operation = XMLUtils.parseAttributeOperation(Node.fromAttr(el, "operation")); + var slot = parseSlotGroup(Node.fromAttr(el, "slot")); + return new AttributeModifier(new NamespacedKey("pgm", key), amount, operation, slot); + } + + private EquipmentSlotGroup parseSlotGroup(Node node) throws InvalidXMLException { + if (node == null) return EquipmentSlotGroup.ANY; + String key = node.getValueNormalize(); + var result = EquipmentSlotGroup.getByName(key); + if (result != null) return result; + + var slot = Slot.forKey(key); + if (slot != null && slot.isEquipment()) return slot.toEquipmentSlot().getGroup(); + + throw new InvalidXMLException("Unknown attribute slot '" + key + "'", node); + } + + @Override + public void copyAttributeModifiers(ItemMeta destination, ItemMeta source) { + var modifiers = source.getAttributeModifiers(); + if (modifiers != null) modifiers.forEach(destination::addAttributeModifier); + } + + @Override + public void applyAttributeModifiers( + SetMultimap modifiers, ItemMeta meta) { + for (var entry : modifiers.entries()) { + meta.addAttributeModifier(entry.getKey(), entry.getValue()); + } + } + + @Override + public boolean attributesEqual(ItemMeta meta1, ItemMeta meta2) { + var attributes1 = meta1.getAttributeModifiers(); + var attributes2 = meta2.getAttributeModifiers(); + if (attributes1 == null || attributes2 == null) return false; + + if (!attributes1.keySet().equals(attributes2.keySet())) return false; + + for (Attribute attr : attributes1.keySet()) { + if (modifiersDiffer(attributes1.get(attr), attributes2.get(attr))) return false; + } + return true; + } + + @Override + public SimpleAttributeModifier simplify(AttributeModifier mod) { + return new SimpleModifier(mod.getAmount(), mod.getOperation(), mod.getSlotGroup()); + } + + private record SimpleModifier( + double amount, AttributeModifier.Operation operation, EquipmentSlotGroup group) + implements SimpleAttributeModifier { + @Override + public int compareTo(@NotNull SimpleAttributeModifier obj) { + if (!(obj instanceof SimpleModifier o)) return 0; + int res = operation.ordinal() - o.operation.ordinal(); + if (res != 0) return res; + res = group.toString().compareTo(o.group.toString()); + if (res != 0) return res; + + return Double.compare(amount, o.amount); + } + } + + @Override + public void stripAttributes(ItemMeta meta) { + var attributes = meta.getAttributeModifiers(); + + if (attributes != null && !attributes.isEmpty()) { + attributes.keySet().forEach(meta::removeAttributeModifier); + } + } +} diff --git a/platform/platform-modern/src/main/java/tc/oc/pgm/platform/modern/inventory/ModernInventoryUtil.java b/platform/platform-modern/src/main/java/tc/oc/pgm/platform/modern/inventory/ModernInventoryUtil.java index 4b7419f2fe..fcd7ce1404 100644 --- a/platform/platform-modern/src/main/java/tc/oc/pgm/platform/modern/inventory/ModernInventoryUtil.java +++ b/platform/platform-modern/src/main/java/tc/oc/pgm/platform/modern/inventory/ModernInventoryUtil.java @@ -2,13 +2,10 @@ import static tc.oc.pgm.util.platform.Supports.Variant.PAPER; -import com.google.common.collect.SetMultimap; import java.util.Collection; import java.util.Collections; import java.util.Set; import org.bukkit.Material; -import org.bukkit.attribute.Attribute; -import org.bukkit.attribute.AttributeModifier; import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.entity.Player; import org.bukkit.entity.Villager; @@ -56,43 +53,6 @@ public ItemStack craftItemCopy(ItemStack item) { return CraftItemStack.asCraftCopy(item); } - @Override - public void copyAttributeModifiers(ItemMeta destination, ItemMeta source) { - var modifiers = source.getAttributeModifiers(); - if (modifiers != null) modifiers.forEach(destination::addAttributeModifier); - } - - @Override - public void applyAttributeModifiers( - SetMultimap modifiers, ItemMeta meta) { - for (var entry : modifiers.entries()) { - meta.addAttributeModifier(entry.getKey(), entry.getValue()); - } - } - - @Override - public boolean attributesEqual(ItemMeta meta1, ItemMeta meta2) { - var attributes1 = meta1.getAttributeModifiers(); - var attributes2 = meta2.getAttributeModifiers(); - if (attributes1 == null || attributes2 == null) return false; - - if (!attributes1.keySet().equals(attributes2.keySet())) return false; - - for (Attribute attr : attributes1.keySet()) { - if (modifiersDiffer(attributes1.get(attr), attributes2.get(attr))) return false; - } - return true; - } - - @Override - public void stripAttributes(ItemMeta meta) { - var attributes = meta.getAttributeModifiers(); - - if (attributes != null && !attributes.isEmpty()) { - attributes.keySet().forEach(meta::removeAttributeModifier); - } - } - @Override public EquipmentSlot getUsedHand(PlayerEvent event) { return switch (event) { diff --git a/platform/platform-sportpaper/src/main/java/tc/oc/pgm/platform/sportpaper/inventory/SpAttributeUtils.java b/platform/platform-sportpaper/src/main/java/tc/oc/pgm/platform/sportpaper/inventory/SpAttributeUtils.java new file mode 100644 index 0000000000..a06fc3ef4c --- /dev/null +++ b/platform/platform-sportpaper/src/main/java/tc/oc/pgm/platform/sportpaper/inventory/SpAttributeUtils.java @@ -0,0 +1,83 @@ +package tc.oc.pgm.platform.sportpaper.inventory; + +import static tc.oc.pgm.util.platform.Supports.Variant.SPORTPAPER; + +import com.google.common.collect.SetMultimap; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeModifier; +import org.bukkit.inventory.meta.ItemMeta; +import org.jdom2.Element; +import org.jetbrains.annotations.NotNull; +import tc.oc.pgm.util.attribute.AttributeUtils; +import tc.oc.pgm.util.platform.Supports; +import tc.oc.pgm.util.xml.InvalidXMLException; +import tc.oc.pgm.util.xml.Node; +import tc.oc.pgm.util.xml.XMLUtils; + +@Supports(SPORTPAPER) +public class SpAttributeUtils implements AttributeUtils { + + @Override + public AttributeModifier parseModifier(Element el) throws InvalidXMLException { + double amount = XMLUtils.parseNumber(Node.fromRequiredAttr(el, "amount"), Double.class); + var operation = XMLUtils.parseAttributeOperation(Node.fromAttr(el, "operation")); + return new AttributeModifier("FromXML", amount, operation); + } + + @Override + public void copyAttributeModifiers(ItemMeta destination, ItemMeta source) { + for (String attribute : source.getModifiedAttributes()) { + for (org.bukkit.attribute.AttributeModifier modifier : + source.getAttributeModifiers(attribute)) { + destination.addAttributeModifier(attribute, modifier); + } + } + } + + @Override + public void applyAttributeModifiers( + SetMultimap modifiers, ItemMeta meta) { + for (var entry : modifiers.entries()) { + meta.addAttributeModifier(entry.getKey(), entry.getValue()); + } + } + + @Override + public boolean attributesEqual(ItemMeta meta1, ItemMeta meta2) { + var attributes = meta1.getModifiedAttributes(); + if (!attributes.equals(meta2.getModifiedAttributes())) return false; + + for (String attr : attributes) { + if (modifiersDiffer(meta1.getAttributeModifiers(attr), meta2.getAttributeModifiers(attr))) + return false; + } + return true; + } + + @Override + public SimpleAttributeModifier simplify(AttributeModifier modifier) { + return new SimpleModifier(modifier); + } + + private record SimpleModifier(double amount, AttributeModifier.Operation operation) + implements SimpleAttributeModifier { + public SimpleModifier(AttributeModifier modifier) { + this(modifier.getAmount(), modifier.getOperation()); + } + + @Override + public int compareTo(@NotNull SimpleAttributeModifier obj) { + if (!(obj instanceof SimpleModifier o)) return 0; + int res = operation.ordinal() - o.operation.ordinal(); + if (res != 0) return res; + return Double.compare(amount, o.amount); + } + } + + @Override + public void stripAttributes(ItemMeta meta) { + for (String attr : meta.getModifiedAttributes()) { + meta.getAttributeModifiers(attr).clear(); + } + } +} diff --git a/platform/platform-sportpaper/src/main/java/tc/oc/pgm/platform/sportpaper/inventory/SpInventoryUtil.java b/platform/platform-sportpaper/src/main/java/tc/oc/pgm/platform/sportpaper/inventory/SpInventoryUtil.java index bcf866ca20..ddff93d38f 100644 --- a/platform/platform-sportpaper/src/main/java/tc/oc/pgm/platform/sportpaper/inventory/SpInventoryUtil.java +++ b/platform/platform-sportpaper/src/main/java/tc/oc/pgm/platform/sportpaper/inventory/SpInventoryUtil.java @@ -2,12 +2,9 @@ import static tc.oc.pgm.util.platform.Supports.Variant.SPORTPAPER; -import com.google.common.collect.SetMultimap; import java.util.Collection; import java.util.Set; import org.bukkit.Material; -import org.bukkit.attribute.Attribute; -import org.bukkit.attribute.AttributeModifier; import org.bukkit.craftbukkit.v1_8_R3.inventory.CraftItemStack; import org.bukkit.entity.Player; import org.bukkit.entity.Villager; @@ -49,43 +46,6 @@ public ItemStack craftItemCopy(ItemStack item) { return CraftItemStack.asCraftCopy(item); } - @Override - public void copyAttributeModifiers(ItemMeta destination, ItemMeta source) { - for (String attribute : source.getModifiedAttributes()) { - for (org.bukkit.attribute.AttributeModifier modifier : - source.getAttributeModifiers(attribute)) { - destination.addAttributeModifier(attribute, modifier); - } - } - } - - @Override - public void applyAttributeModifiers( - SetMultimap modifiers, ItemMeta meta) { - for (var entry : modifiers.entries()) { - meta.addAttributeModifier(entry.getKey(), entry.getValue()); - } - } - - @Override - public boolean attributesEqual(ItemMeta meta1, ItemMeta meta2) { - var attributes = meta1.getModifiedAttributes(); - if (!attributes.equals(meta2.getModifiedAttributes())) return false; - - for (String attr : attributes) { - if (modifiersDiffer(meta1.getAttributeModifiers(attr), meta2.getAttributeModifiers(attr))) - return false; - } - return true; - } - - @Override - public void stripAttributes(ItemMeta meta) { - for (String attr : meta.getModifiedAttributes()) { - meta.getAttributeModifiers(attr).clear(); - } - } - @Override public EquipmentSlot getUsedHand(PlayerEvent event) { return EquipmentSlot.HAND; diff --git a/util/src/main/java/tc/oc/pgm/util/attribute/AttributeModifier.java b/util/src/main/java/tc/oc/pgm/util/attribute/AttributeModifier.java deleted file mode 100644 index b541558e9b..0000000000 --- a/util/src/main/java/tc/oc/pgm/util/attribute/AttributeModifier.java +++ /dev/null @@ -1,100 +0,0 @@ -package tc.oc.pgm.util.attribute; - -import static tc.oc.pgm.util.Assert.assertNotNull; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import org.bukkit.configuration.serialization.ConfigurationSerializable; -import org.bukkit.util.NumberConversions; - -/** Concrete implementation of an attribute modifier. */ -public class AttributeModifier implements ConfigurationSerializable { - - private final UUID uuid; - private final String name; - private final double amount; - private final Operation operation; - - public AttributeModifier(String name, double amount, Operation operation) { - this(UUID.randomUUID(), name, amount, operation); - } - - public AttributeModifier(UUID uuid, String name, double amount, Operation operation) { - this.uuid = assertNotNull(uuid, "uuid"); - this.name = assertNotNull(name, "name"); - this.amount = amount; - this.operation = assertNotNull(operation, "operation"); - } - - /** - * Get the unique ID for this modifier. - * - * @return unique id - */ - public UUID getUniqueId() { - return uuid; - } - - /** - * Get the name of this modifier. - * - * @return name - */ - public String getName() { - return name; - } - - /** - * Get the amount by which this modifier will apply its {@link Operation}. - * - * @return modification amount - */ - public double getAmount() { - return amount; - } - - /** - * Get the operation this modifier will apply. - * - * @return operation - */ - public Operation getOperation() { - return operation; - } - - @Override - public Map serialize() { - Map data = new HashMap(); - data.put("uuid", uuid.toString()); - data.put("name", name); - data.put("operation", operation.ordinal()); - data.put("amount", amount); - return data; - } - - public static AttributeModifier deserialize(Map args) { - return new AttributeModifier( - UUID.fromString((String) args.get("uuid")), - (String) args.get("name"), - NumberConversions.toDouble(args.get("amount")), - Operation.values()[NumberConversions.toInt(args.get("operation"))]); - } - - /** Enumerable operation to be applied. */ - public enum Operation { - - /** Adds (or subtracts) the specified amount to the base value. */ - ADD_NUMBER, - /** Adds this scalar of amount to the base value. */ - ADD_SCALAR, - /** Multiply amount by this value, after adding 1 to it. */ - MULTIPLY_SCALAR_1; - - public static Operation fromOpcode(int code) { - if (code < 0) code = 0; - if (code >= values().length) code = values().length - 1; - return values()[code]; - } - } -} diff --git a/util/src/main/java/tc/oc/pgm/util/attribute/AttributeUtils.java b/util/src/main/java/tc/oc/pgm/util/attribute/AttributeUtils.java new file mode 100644 index 0000000000..0fc18aa7c5 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/attribute/AttributeUtils.java @@ -0,0 +1,42 @@ +package tc.oc.pgm.util.attribute; + +import com.google.common.collect.SetMultimap; +import java.util.Collection; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeModifier; +import org.bukkit.inventory.meta.ItemMeta; +import org.jdom2.Element; +import tc.oc.pgm.util.platform.Platform; +import tc.oc.pgm.util.xml.InvalidXMLException; + +public interface AttributeUtils { + AttributeUtils ATTRIBUTE_UTILS = Platform.get(AttributeUtils.class); + + AttributeModifier parseModifier(Element el) throws InvalidXMLException; + + void copyAttributeModifiers(ItemMeta destination, ItemMeta source); + + void applyAttributeModifiers(SetMultimap modifiers, ItemMeta meta); + + boolean attributesEqual(ItemMeta meta1, ItemMeta meta2); + + default boolean modifiersDiffer( + Collection a, Collection b) { + if (a.size() != b.size()) return true; + if (a.isEmpty()) return false; + // Fast case for single modifier + if (a.size() == 1) { + return !simplify(a.iterator().next()).equals(simplify(b.iterator().next())); + } + + var listA = a.stream().map(this::simplify).sorted().toList(); + var listB = b.stream().map(this::simplify).sorted().toList(); + return !listA.equals(listB); + } + + SimpleAttributeModifier simplify(AttributeModifier attributeModifier); + + void stripAttributes(ItemMeta meta); + + interface SimpleAttributeModifier extends Comparable {} +} diff --git a/core/src/main/java/tc/oc/pgm/kits/ArmorType.java b/util/src/main/java/tc/oc/pgm/util/inventory/ArmorType.java similarity index 96% rename from core/src/main/java/tc/oc/pgm/kits/ArmorType.java rename to util/src/main/java/tc/oc/pgm/util/inventory/ArmorType.java index ed6b674de3..7006e66267 100644 --- a/core/src/main/java/tc/oc/pgm/kits/ArmorType.java +++ b/util/src/main/java/tc/oc/pgm/util/inventory/ArmorType.java @@ -1,4 +1,4 @@ -package tc.oc.pgm.kits; +package tc.oc.pgm.util.inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; diff --git a/util/src/main/java/tc/oc/pgm/util/inventory/InventoryUtils.java b/util/src/main/java/tc/oc/pgm/util/inventory/InventoryUtils.java index 5e16b2cae9..5abb313f21 100644 --- a/util/src/main/java/tc/oc/pgm/util/inventory/InventoryUtils.java +++ b/util/src/main/java/tc/oc/pgm/util/inventory/InventoryUtils.java @@ -1,7 +1,6 @@ package tc.oc.pgm.util.inventory; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.SetMultimap; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -10,8 +9,6 @@ import java.util.Map; import java.util.Set; import org.bukkit.Material; -import org.bukkit.attribute.Attribute; -import org.bukkit.attribute.AttributeModifier; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; import org.bukkit.entity.Villager; @@ -25,7 +22,6 @@ import org.bukkit.inventory.meta.PotionMeta; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import tc.oc.pgm.util.bukkit.BukkitUtils; import tc.oc.pgm.util.platform.Platform; @@ -184,45 +180,6 @@ default void setUnbreakable(ItemStack item, boolean unbreakable) { ItemStack craftItemCopy(ItemStack item); - void copyAttributeModifiers(ItemMeta destination, ItemMeta source); - - void applyAttributeModifiers( - SetMultimap modifiers, ItemMeta meta); - - boolean attributesEqual(ItemMeta meta1, ItemMeta meta2); - - default boolean modifiersDiffer( - Collection a, Collection b) { - if (a.size() != b.size()) return true; - if (a.isEmpty()) return false; - // Fast case for single modifier - if (a.size() == 1) { - var modA = a.iterator().next(); - var modB = b.iterator().next(); - return modA.getOperation() != modB.getOperation() || modA.getAmount() != modB.getAmount(); - } - - record SimpleModifier(double amount, AttributeModifier.Operation operation) - implements Comparable { - public SimpleModifier(AttributeModifier modifier) { - this(modifier.getAmount(), modifier.getOperation()); - } - - @Override - public int compareTo(@NotNull SimpleModifier o) { - int res = operation.ordinal() - o.operation.ordinal(); - if (res != 0) return res; - return Double.compare(amount, o.amount); - } - } - - var listA = a.stream().map(SimpleModifier::new).sorted().toList(); - var listB = b.stream().map(SimpleModifier::new).sorted().toList(); - return !listA.equals(listB); - } - - void stripAttributes(ItemMeta meta); - EquipmentSlot getUsedHand(PlayerEvent event); void setCanDestroy(ItemMeta itemMeta, Set materials); diff --git a/util/src/main/java/tc/oc/pgm/util/inventory/Slot.java b/util/src/main/java/tc/oc/pgm/util/inventory/Slot.java new file mode 100644 index 0000000000..c7b8d401a6 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/inventory/Slot.java @@ -0,0 +1,500 @@ +package tc.oc.pgm.util.inventory; + +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Table; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; +import org.bukkit.Material; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.util.material.Materials; +import tc.oc.pgm.util.platform.Platform; + +/** + * Derived from the names found in net.minecraft.server.CommandReplaceItem. If we ever implement + * applying kits to other types of inventories, this should be expanded to include those slot names + * as well. + */ +public abstract class Slot { + static { + all = new HashSet<>(); + byKey = new HashMap<>(); + byIndex = HashBasedTable.create(); + byInventoryType = ImmutableMap., Class>builder() + .put(PlayerInventory.class, Player.class) + .put(Inventory.class, Container.class) + .build(); + + Container.init(); + Player.init(); + EnderChest.init(); + } + + private static final Set all; + private static final Map byKey; + private static final Table, Integer, Slot> byIndex; + private static final Map, Class> byInventoryType; + + /** + * Convert a Mojang slot name (used by /replaceitem) to a {@link Slot} object. The "slot." at the + * beginning of the name is optional. Returns null if the name is invalid. + */ + public static @Nullable Slot forKey(String key) { + if (key.startsWith("slot.")) { + key = key.substring("slot.".length()); + } + return byKey.get(key); + } + + public static @Nullable S forIndex(Class type, int index) { + return (S) byIndex.get(type, index); + } + + public static @Nullable S atPosition(Class type, int column, int row) { + return forIndex(type, row * 9 + column); + } + + public static Class typeForInventory(Class inv) { + for (Map.Entry, Class> entry : + byInventoryType.entrySet()) { + if (entry.getKey().isAssignableFrom(inv)) { + return entry.getValue(); + } + } + throw new IllegalStateException("Weird inventory type " + inv); + } + + public static Stream forInventory(Class invType) { + return all.stream().filter(typeForInventory(invType)::isInstance); + } + + public static @Nullable Slot forInventoryIndex(Class inv, int index) { + return forIndex(typeForInventory(inv), index); + } + + public static @Nullable Slot forViewIndex(InventoryView view, int rawIndex) { + final int cookedIndex = view.convertSlot(rawIndex); + return forInventoryIndex( + (rawIndex == cookedIndex ? view.getTopInventory() : view.getBottomInventory()).getClass(), + cookedIndex); + } + + private final String key; + private final int index; // -1 = auto + + private Slot(Class type, String key, int index) { + this.key = key; + this.index = index; + + all.add(this); + if (key != null) byKey.put(key, this); + if (index >= 0) byIndex.put(type, index, this); + } + + @Override + public String toString() { + return key != null ? getKey() : super.toString(); + } + + /** @return the name of this slot, as used by the /replaceitem command */ + public String getKey() { + return key == null ? null : "slot." + key; + } + + public boolean hasIndex() { + return index >= 0; + } + + /** @return a slot index that can be passed to {@link Inventory#getItem} et al. */ + public int getIndex() { + if (!hasIndex()) throw new UnsupportedOperationException("Slot " + this + " has no index"); + return index; + } + + public int getColumn() { + return getIndex() % 9; + } + + public int getRow() { + return getIndex() / 9; + } + + public int maxStackSize() { + return 64; + } + + public int maxStackSize(Material material) { + return Math.min(maxStackSize(), material.getMaxStackSize()); + } + + public int maxStackSize(ItemStack item) { + return maxStackSize(item.getType()); + } + + public int maxTransferrableIn(ItemStack source) { + return Math.min(source.getAmount(), maxStackSize(source)); + } + + public int maxTransferrableIn(ItemStack source, Inventory inv) { + final ItemStack dest = getItem(inv); + if (Materials.isNothing(dest)) { + return maxTransferrableIn(source); + } else if (dest.isSimilar(source)) { + return Math.min(source.getAmount(), Math.max(0, maxStackSize(dest) - dest.getAmount())); + } else { + return 0; + } + } + + public boolean isEquipment() { + return false; + } + + public EquipmentSlot toEquipmentSlot() { + throw new UnsupportedOperationException("Slot " + this + " is not an equipment slot"); + } + + public Inventory getInventory(InventoryHolder holder) { + return holder.getInventory(); + } + + protected static @Nullable ItemStack airToNull(ItemStack stack) { + return stack == null || stack.getType() == Material.AIR ? null : stack; + } + + public @Nullable ItemStack getItem(InventoryHolder holder) { + return getItem(getInventory(holder)); + } + + public Optional item(InventoryHolder holder) { + return Optional.ofNullable(getItem(holder)); + } + + public void putItem(InventoryHolder holder, ItemStack stack) { + putItem(getInventory(holder), stack); + } + + /** + * @return the item in this slot of the given holder's inventory, or null if the slot is empty. + * This will never return a stack of {@link Material#AIR}. + */ + public @Nullable ItemStack getItem(Inventory inv) { + return airToNull(inv.getItem(getIndex())); + } + + public Optional item(Inventory inv) { + return Optional.ofNullable(getItem(inv)); + } + + public int amount(Inventory inv) { + return Materials.amount(getItem(inv)); + } + + public boolean isEmpty(Inventory inv) { + return amount(inv) == 0; + } + + /** Put the given stack in this slot of the given holder's inventory. */ + public void putItem(Inventory inv, ItemStack stack) { + inv.setItem(getIndex(), airToNull(stack)); + } + + protected PlayerInventory asPlayerInventory(Inventory inv) { + if (inv instanceof PlayerInventory plInv) return plInv; + throw new IllegalArgumentException("Slot " + this + " is player-only inventory slot"); + } + + protected org.bukkit.entity.Player asPlayer(InventoryHolder holder) { + if (holder instanceof org.bukkit.entity.Player player) return player; + throw new IllegalArgumentException("Slot " + this + " is player-only inventory slot"); + } + + public static class Container extends Slot { + static void init() { + for (int i = 0; i < 54; i++) { + new Container("container." + i, i); + } + } + + public static @Nullable Container forIndex(int index) { + return forIndex(Container.class, index); + } + + Container(String key, int index) { + super(Container.class, key, index); + } + } + + public abstract static class Player extends Slot { + static void init() { + Storage.init(); + Equipment.init(); + Cursor.init(); + } + + public static Stream player() { + return Stream.concat( + Stream.concat(Storage.storage(), Equipment.equipment()), Stream.of(Cursor.cursor())); + } + + Player(String key, int index) { + super(Player.class, key, index); + } + + public static @Nullable Player forIndex(int index) { + return forIndex(Player.class, index); + } + + @Override + public @Nullable ItemStack getItem(Inventory generic) { + var inv = asPlayerInventory(generic); + return isEquipment() ? airToNull(inv.getItem(toEquipmentSlot())) : super.getItem(inv); + } + + @Override + public void putItem(Inventory generic, ItemStack stack) { + var inv = asPlayerInventory(generic); + if (isEquipment()) { + inv.setItem(toEquipmentSlot(), stack); + } else { + super.putItem(inv, stack); + } + } + } + + public static class Storage extends Player { + static void init() { + Hotbar.init(); + Pockets.init(); + } + + public static Stream storage() { + return Stream.concat(Hotbar.hotbar(), Pockets.pockets()); + } + + Storage(String key, int index) { + super(key, index); + } + } + + public static class Hotbar extends Storage { + static void init() { + hotbar = new Hotbar[9]; + for (int i = 0; i < 9; i++) { + hotbar[i] = new Hotbar("hotbar." + i, i); + } + } + + private static Hotbar[] hotbar; + + public static Stream hotbar() { + return Stream.of(hotbar); + } + + public static Hotbar forPosition(int pos) { + return (Hotbar) forIndex(pos); + } + + protected Hotbar(String key, int index) { + super(key, index); + } + } + + public static class Pockets extends Storage { + static void init() { + pockets = new Pockets[27]; + for (int i = 0; i < 27; i++) { + pockets[i] = new Pockets("inventory." + i, 9 + i); + } + MainHand.init(); + } + + private static Pockets[] pockets; + + public static Stream pockets() { + return Stream.of(pockets); + } + + protected Pockets(String key, int index) { + super(key, index); + } + } + + public abstract static class Equipment extends Player { + static void init() { + OffHand.init(); + Armor.init(); + } + + public static Stream equipment() { + return Stream.concat(Stream.of(OffHand.offHand()), Armor.armor()); + } + + private final EquipmentSlot equipmentSlot; + + Equipment(String key, int index, EquipmentSlot equipmentSlot) { + super(key, index); + this.equipmentSlot = equipmentSlot; + } + + @Override + public int maxStackSize() { + return 1; + } + + @Override + public boolean isEquipment() { + return true; + } + + @Override + public EquipmentSlot toEquipmentSlot() { + return equipmentSlot; + } + } + + public static class MainHand extends Hotbar { + static void init() { + mainHand = new MainHand(); + } + + private static MainHand mainHand; + + public static MainHand mainHand() { + return mainHand; + } + + MainHand() { + super("weapon.mainhand", -1); + } + + @Override + public boolean isEquipment() { + return true; + } + + @Override + public EquipmentSlot toEquipmentSlot() { + return EquipmentSlot.HAND; + } + } + + public static class OffHand extends Equipment { + static void init() { + if (Platform.isLegacy()) return; + offHand = new OffHand(); + } + + private static OffHand offHand; + + public static OffHand offHand() { + if (Platform.isLegacy()) + throw new UnsupportedOperationException("OffHand is not supported on legacy platform"); + return offHand; + } + + protected OffHand() { + super("weapon.offhand", 40, EquipmentSlot.valueOf("OFF_HAND")); + } + } + + public static class Armor extends Equipment { + static void init() { + new Armor("armor.feet", EquipmentSlot.FEET, ArmorType.BOOTS); + new Armor("armor.legs", EquipmentSlot.LEGS, ArmorType.LEGGINGS); + new Armor("armor.chest", EquipmentSlot.CHEST, ArmorType.CHESTPLATE); + new Armor("armor.head", EquipmentSlot.HEAD, ArmorType.HELMET); + } + + private static final Map byArmorType = new EnumMap<>(ArmorType.class); + + public static Stream armor() { + return byArmorType.values().stream(); + } + + private final ArmorType armorType; + + Armor(String key, EquipmentSlot equipmentSlot, ArmorType armorType) { + super(key, armorType.inventorySlot(), equipmentSlot); + this.armorType = armorType; + byArmorType.put(armorType, this); + } + + public ArmorType getArmorType() { + return armorType; + } + + public static Armor forType(ArmorType armorType) { + return byArmorType.get(armorType); + } + } + + public static class EnderChest extends Slot { + static void init() { + for (int i = 0; i < 27; i++) { + new EnderChest("enderchest." + i, i); + } + } + + EnderChest(String key, int index) { + super(EnderChest.class, key, index); + } + + @Override + public Inventory getInventory(InventoryHolder holder) { + return asPlayer(holder).getEnderChest(); + } + } + + public static class Cursor extends Player { + static void init() { + cursor = new Cursor(); + } + + private static Cursor cursor; + + public static Cursor cursor() { + return cursor; + } + + Cursor() { + super(null, -1); + } + + @Override + public String toString() { + return "cursor"; + } + + @Override + public @Nullable ItemStack getItem(InventoryHolder holder) { + return airToNull(asPlayer(holder).getItemOnCursor()); + } + + @Override + public void putItem(InventoryHolder holder, @Nullable ItemStack stack) { + asPlayer(holder).setItemOnCursor(stack); + } + + @Override + public @Nullable ItemStack getItem(Inventory inv) { + return getItem(asPlayerInventory(inv).getHolder()); + } + + @Override + public void putItem(Inventory inv, ItemStack stack) { + putItem(asPlayerInventory(inv).getHolder(), stack); + } + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/material/Materials.java b/util/src/main/java/tc/oc/pgm/util/material/Materials.java index 2bc2bd3783..2ba3cf86d2 100644 --- a/util/src/main/java/tc/oc/pgm/util/material/Materials.java +++ b/util/src/main/java/tc/oc/pgm/util/material/Materials.java @@ -1,7 +1,7 @@ package tc.oc.pgm.util.material; import static org.bukkit.Material.*; -import static tc.oc.pgm.util.inventory.InventoryUtils.INVENTORY_UTILS; +import static tc.oc.pgm.util.attribute.AttributeUtils.ATTRIBUTE_UTILS; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -9,6 +9,8 @@ import org.bukkit.block.BlockState; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; import tc.oc.pgm.util.bukkit.BukkitUtils; public interface Materials { @@ -113,13 +115,13 @@ static boolean itemsSimilar(ItemStack first, ItemStack second, boolean skipDur) ItemMeta meta2 = hasMeta2 ? second.getItemMeta() : null; if (hasMeta1 && hasMeta2 && meta1.hasAttributeModifiers() && meta2.hasAttributeModifiers()) { - if (INVENTORY_UTILS.attributesEqual(meta1, meta2)) { + if (ATTRIBUTE_UTILS.attributesEqual(meta1, meta2)) { // If attributes match, strip them not to affect the comparison. // This is done because attributes have a random UUID in t hem that make them never equal meta1 = meta1.clone(); meta2 = meta2.clone(); - INVENTORY_UTILS.stripAttributes(meta1); - INVENTORY_UTILS.stripAttributes(meta2); + ATTRIBUTE_UTILS.stripAttributes(meta1); + ATTRIBUTE_UTILS.stripAttributes(meta2); } else { return false; } @@ -191,6 +193,15 @@ static boolean isBucket(Material bucket) { || bucket == Material.MILK_BUCKET; } + static int amount(@Nullable ItemStack stack) { + return stack == null || stack.getType() == Material.AIR ? 0 : stack.getAmount(); + } + + @Contract(value = "null -> true", pure = true) + static boolean isNothing(@Nullable ItemStack stack) { + return amount(stack) == 0; + } + static Material materialInBucket(ItemStack bucket) { return materialInBucket(bucket.getType()); } diff --git a/util/src/main/java/tc/oc/pgm/util/xml/XMLUtils.java b/util/src/main/java/tc/oc/pgm/util/xml/XMLUtils.java index 0b1bbe8b15..ad25955b67 100644 --- a/util/src/main/java/tc/oc/pgm/util/xml/XMLUtils.java +++ b/util/src/main/java/tc/oc/pgm/util/xml/XMLUtils.java @@ -1,5 +1,6 @@ package tc.oc.pgm.util.xml; +import static tc.oc.pgm.util.attribute.AttributeUtils.ATTRIBUTE_UTILS; import static tc.oc.pgm.util.material.MaterialUtils.MATERIAL_UTILS; import com.google.common.base.Predicate; @@ -1075,38 +1076,27 @@ public static AttributeModifier.Operation parseAttributeOperation(Node node, Str public static AttributeModifier.Operation parseAttributeOperation(Node node) throws InvalidXMLException { - return parseAttributeOperation(node, node.getValueNormalize()); - } - - public static AttributeModifier.Operation parseAttributeOperation( - Node node, AttributeModifier.Operation def) throws InvalidXMLException { - return node == null ? def : parseAttributeOperation(node); + return node == null + ? AttributeModifier.Operation.ADD_NUMBER + : parseAttributeOperation(node, node.getValueNormalize()); } - public static Map.Entry + public static Pair parseCompactAttributeModifier(Node node, String text) throws InvalidXMLException { String[] parts = text.split(":"); + if (parts.length != 3) throw new InvalidXMLException("Bad attribute modifier format", node); - if (parts.length != 3) { - throw new InvalidXMLException("Bad attribute modifier format", node); - } - - var attribute = parseAttribute(node, parts[0]); - AttributeModifier.Operation operation = parseAttributeOperation(node, parts[1]); - double amount = parseNumber(node, parts[2], Double.class); - - return new AbstractMap.SimpleImmutableEntry<>( - attribute, new AttributeModifier("FromXML", amount, operation)); + return Pair.of( + parseAttribute(node, parts[0]), + new AttributeModifier( + "FromXML", + parseNumber(node, parts[2], Double.class), + parseAttributeOperation(node, parts[1]))); } public static Pair parseAttributeModifier( Element el) throws InvalidXMLException { - var attribute = parseAttribute(new Node(el)); - double amount = parseNumber(Node.fromRequiredAttr(el, "amount"), Double.class); - AttributeModifier.Operation operation = parseAttributeOperation( - Node.fromAttr(el, "operation"), AttributeModifier.Operation.ADD_NUMBER); - - return new Pair<>(attribute, new AttributeModifier("FromXML", amount, operation)); + return new Pair<>(parseAttribute(new Node(el)), ATTRIBUTE_UTILS.parseModifier(el)); } public static GameMode parseGameMode(Node node, String text) throws InvalidXMLException {