From 7a45be1cdb587822f2472e04ca7e3f9f4f81d063 Mon Sep 17 00:00:00 2001 From: TWME <65117253+TWME-TW@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:33:04 +0800 Subject: [PATCH] fix: Paper 26.2 compatibility for SpigotEntityIdProvider Three fixes for the entity ID provider used on Paper 26.2+: 1. Add reflection guard for UnsafeValues.nextEntityId() - Paper 26.2+ removed this method; verify via reflection before calling - Falls through to AtomicInteger reflection path when absent 2. Fix ArrayIndexOutOfBoundsException in getEntityClass() - Paper 26.2+ has no version suffix in the CraftServer package name - Skip package name parsing entirely for 1.17+ (flattened) servers 3. Fix IllegalAccessException on legacy entity counter - Skip static final fields (e.g. CURRENT_LEVEL) - Add findMutableStaticIntField helper with isFinal check - Fall back to local AtomicInteger when no mutable field is found - Also search for ENTITY_COUNTER in AtomicInteger field names --- .../spigot/SpigotEntityIdProvider.java | 59 ++++++++++++++++--- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/platforms/spigot/src/main/java/me/tofaa/entitylib/spigot/SpigotEntityIdProvider.java b/platforms/spigot/src/main/java/me/tofaa/entitylib/spigot/SpigotEntityIdProvider.java index c1f8cf3..8cf4a1e 100644 --- a/platforms/spigot/src/main/java/me/tofaa/entitylib/spigot/SpigotEntityIdProvider.java +++ b/platforms/spigot/src/main/java/me/tofaa/entitylib/spigot/SpigotEntityIdProvider.java @@ -63,7 +63,14 @@ private Supplier detectIdSupplier() { final ServerVersion serverVersion = platform.getAPI().getPacketEvents().getServerManager().getVersion(); if (isPaper() && serverVersion.isNewerThanOrEquals(ServerVersion.V_1_16)) { - return Bukkit.getUnsafe()::nextEntityId; // Paper API + // Paper removed UnsafeValues.nextEntityId() in API 26.2+. + // Verify the method exists via reflection to avoid NoSuchMethodError. + try { + UnsafeValues.class.getMethod("nextEntityId"); + return Bukkit.getUnsafe()::nextEntityId; // Paper API (pre-26.x) + } catch (final NoSuchMethodException ignored) { + // Method removed — fall through to AtomicInteger reflection path + } } final Class entityClass = getEntityClass(); @@ -79,7 +86,7 @@ private Supplier detectIdSupplier() { private Supplier resolveAtomicSupplier(final Class entityClass) { final Field entityAtomicField = getStaticFieldOfType(entityClass, AtomicInteger.class, - "entityCount", "d", "c", "counter", "nextEntityId"); + "entityCount", "d", "c", "counter", "nextEntityId", "ENTITY_COUNTER"); if (entityAtomicField == null) { return null; } @@ -97,9 +104,13 @@ private Supplier resolveAtomicSupplier(final Class entityClass) { } private Supplier resolveLegacySupplier(final Class entityClass) { - final Field entityLegacyField = getStaticFieldOfType(entityClass, Integer.TYPE, "entityCount", "b"); + // Search for a non-final static int field (entity counters are never final). + final Field entityLegacyField = findMutableStaticIntField(entityClass); if (entityLegacyField == null) { - throw new IllegalStateException("Could not find legacy entity counter field"); + // Last resort: local high-offset counter. Entity ID collision is unlikely + // since the server allocates from 1 upward. + final AtomicInteger fallback = new AtomicInteger(Integer.MAX_VALUE - 100000); + return fallback::incrementAndGet; } entityLegacyField.setAccessible(true); return () -> { @@ -113,6 +124,32 @@ private Supplier resolveLegacySupplier(final Class entityClass) { }; } + /** + * Finds a mutable (non-final) static int field in the given class to use as + * a legacy entity counter. Tries known field names first, then falls back to + * any non-final static int field. + */ + private static Field findMutableStaticIntField(final Class clazz) { + for (final String name : new String[]{"entityCount", "b", "c"}) { + final Field field = getField(clazz, name); + if (field != null + && field.getType() == Integer.TYPE + && Modifier.isStatic(field.getModifiers()) + && !Modifier.isFinal(field.getModifiers())) { + return field; + } + } + // Wildcard fallback: any non-final static int field + for (final Field field : clazz.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) + && field.getType() == Integer.TYPE + && !Modifier.isFinal(field.getModifiers())) { + return field; + } + } + return null; + } + /** * Resolves server's internal `Entity` class, handling version-specific packages. * In Minecraft versions 1.17 and later, the `Entity` class resides in `net.minecraft.world.entity.Entity`. @@ -125,11 +162,19 @@ private Class getEntityClass() { final ServerVersion serverVersion = platform.getAPI().getPacketEvents().getServerManager().getVersion(); final boolean isFlattened = serverVersion.isNewerThanOrEquals(ServerVersion.V_1_17); - final String version = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]; - final String packagePath = isFlattened ? "net.minecraft.world.entity" : "net.minecraft.server." + version; + if (isFlattened) { + try { + return Class.forName("net.minecraft.world.entity.Entity"); + } catch (final ClassNotFoundException exception) { + throw new IllegalStateException("Could not find Entity class", exception); + } + } + // Pre-1.17: versioned package (e.g. net.minecraft.server.v1_16_R3.Entity) + final String[] parts = Bukkit.getServer().getClass().getPackage().getName().split("\\."); + final String ver = parts.length > 3 ? parts[3] : ""; try { - return Class.forName(packagePath + ".Entity"); + return Class.forName("net.minecraft.server." + ver + ".Entity"); } catch (final ClassNotFoundException exception) { throw new IllegalStateException("Could not find Entity class", exception); }