From 093f5fc57917917678d0ec4173d5add72dcdf1a7 Mon Sep 17 00:00:00 2001 From: steve <50219120+steveb05@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:09:08 +0200 Subject: [PATCH] fix(api): keep every equipment slot inside the array and off old clients The equipment array was written when the game had six slots and was never grown, so the two that arrived since, body in 1.20.5 and saddle in 1.21.5, sit at ordinals six and seven and land outside it. Anyone handing one of those to setItem, getItem or clearSlot got an ArrayIndexOutOfBoundsException, which meant saddling a horse or putting armor on a wolf was not something a caller could express at all. Sizing the array from the enum means the next slot the game adds costs nothing here. Holding a slot is only half of it, because a slot travels in the packet as its ordinal from 1.9 onwards, and a version that predates the slot has no number it could map back to. Sending a saddle to a 1.21.4 client is a seven it will read as garbage. So createPacket now leaves out what the running server version does not know rather than trusting that every slot is safe everywhere, and isSlotSupported is public because a caller that wants to decide something else on the same question should not have to keep its own table of version numbers. It takes the version as an argument so the answer is a pure function of what is asked, with the running version filled in by the overload. The version check it would have used was inverted: verifyVersion threw when the server was newer than the version passed rather than older, so getOffhand raised InvalidVersionException on every server past 1.9 and its single caller had been dead for as long as it existed. --- .../entitylib/extras/VersionChecker.java | 10 ++- .../wrapper/WrapperEntityEquipment.java | 64 +++++++++++++------ 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/api/src/main/java/me/tofaa/entitylib/extras/VersionChecker.java b/api/src/main/java/me/tofaa/entitylib/extras/VersionChecker.java index c381d52..76f1292 100644 --- a/api/src/main/java/me/tofaa/entitylib/extras/VersionChecker.java +++ b/api/src/main/java/me/tofaa/entitylib/extras/VersionChecker.java @@ -1,15 +1,21 @@ package me.tofaa.entitylib.extras; import com.github.retrooper.packetevents.manager.server.ServerVersion; -import me.tofaa.entitylib.EntityLib; +import me.tofaa.entitylib.utils.VersionUtil; public final class VersionChecker { private VersionChecker() {} + /** + * Throws when the running server is older than the version a feature needs. + * + * @param version the oldest server version the feature works on + * @param message the message of the thrown exception + */ public static void verifyVersion(ServerVersion version, String message) { - if (!version.isNewerThanOrEquals(EntityLib.getApi().getPacketEvents().getServerManager().getVersion())) { + if (VersionUtil.isOlderThan(version)) { throw new InvalidVersionException(message); } } diff --git a/api/src/main/java/me/tofaa/entitylib/wrapper/WrapperEntityEquipment.java b/api/src/main/java/me/tofaa/entitylib/wrapper/WrapperEntityEquipment.java index bcf9dda..b0686e7 100644 --- a/api/src/main/java/me/tofaa/entitylib/wrapper/WrapperEntityEquipment.java +++ b/api/src/main/java/me/tofaa/entitylib/wrapper/WrapperEntityEquipment.java @@ -5,6 +5,7 @@ import com.github.retrooper.packetevents.protocol.player.Equipment; import com.github.retrooper.packetevents.protocol.player.EquipmentSlot; import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityEquipment; +import me.tofaa.entitylib.EntityLib; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -21,8 +22,7 @@ public class WrapperEntityEquipment { private final WrapperLivingEntity entity; private boolean notifyChanges = true; - // 0 = main hand, 1 = offhand, 2 = boots, 3 = leggings, 4 = chestplate, 5 = helmet - private final ItemStack[] equipment = new ItemStack[6]; + private final ItemStack[] equipment = new ItemStack[EQUIPMENT_SLOTS.length]; public WrapperEntityEquipment(WrapperLivingEntity entity) { this.entity = entity; @@ -41,37 +41,31 @@ public void clearAll() { } public void setHelmet(@Nullable ItemStack itemStack) { - equipment[5] = itemStack == null ? ItemStack.EMPTY : itemStack; - refresh(); + setItem(EquipmentSlot.HELMET, itemStack); } public void setChestplate(@Nullable ItemStack itemStack) { - equipment[4] = itemStack == null ? ItemStack.EMPTY : itemStack; - refresh(); + setItem(EquipmentSlot.CHEST_PLATE, itemStack); } public void setLeggings(@Nullable ItemStack itemStack) { - equipment[3] = itemStack == null ? ItemStack.EMPTY : itemStack; - refresh(); + setItem(EquipmentSlot.LEGGINGS, itemStack); } public void setBoots(@Nullable ItemStack itemStack) { - equipment[2] = itemStack == null ? ItemStack.EMPTY : itemStack; - refresh(); + setItem(EquipmentSlot.BOOTS, itemStack); } public void setMainHand(@Nullable ItemStack itemStack) { - equipment[0] = itemStack == null ? ItemStack.EMPTY : itemStack; - refresh(); + setItem(EquipmentSlot.MAIN_HAND, itemStack); } public void setOffhand(@Nullable ItemStack itemStack) { - equipment[1] = itemStack == null ? ItemStack.EMPTY : itemStack; - refresh(); + setItem(EquipmentSlot.OFF_HAND, itemStack); } public void setItem(@NotNull EquipmentSlot slot, @Nullable ItemStack itemStack) { - equipment[slot.ordinal()] = itemStack == null ? ItemStack.EMPTY : itemStack; + equipment[slot.ordinal()] = itemStack == null ? ItemStack.EMPTY : itemStack; refresh(); } @@ -108,11 +102,45 @@ public void setItem(@NotNull EquipmentSlot slot, @Nullable ItemStack itemStack) return getItem(EquipmentSlot.OFF_HAND); } + /** + * Whether the running server version knows the given equipment slot. + * + * @param slot the slot to check + * @return true when the slot can be sent to a client of the server version + */ + public static boolean isSlotSupported(@NotNull EquipmentSlot slot) { + return isSlotSupported(slot, EntityLib.getApi().getPacketEvents().getServerManager().getVersion()); + } + + /** + * Whether the given server version knows the given equipment slot. + * A slot travels in the equipment packet as its ordinal, so a slot that the version does not + * have yet has no number a client of that version could map back to it. + * + * @param slot the slot to check + * @param version the server version to check the slot against + * @return true when the slot can be sent to a client of that version + */ + public static boolean isSlotSupported(@NotNull EquipmentSlot slot, @NotNull ServerVersion version) { + switch (slot) { + case OFF_HAND: + return version.isNewerThanOrEquals(ServerVersion.V_1_9); + case BODY: + return version.isNewerThanOrEquals(ServerVersion.V_1_20_5); + case SADDLE: + return version.isNewerThanOrEquals(ServerVersion.V_1_21_5); + default: + return true; + } + } + public WrapperPlayServerEntityEquipment createPacket() { - List equipment = new ArrayList<>(); + ServerVersion version = EntityLib.getApi().getPacketEvents().getServerManager().getVersion(); + List equipment = new ArrayList<>(this.equipment.length); for (int i = 0; i < this.equipment.length; i++) { - ItemStack itemStack = this.equipment[i]; - equipment.add(new Equipment(EQUIPMENT_SLOTS[i], itemStack)); + EquipmentSlot slot = EQUIPMENT_SLOTS[i]; + if (!isSlotSupported(slot, version)) continue; + equipment.add(new Equipment(slot, this.equipment[i])); } return new WrapperPlayServerEntityEquipment( entity.getEntityId(),