Merge pull request #24 from steveb05/feat/spigot-entityid-provider

Implement spigot entity id provider
This commit is contained in:
Tofaa 2025-02-22 02:12:21 +04:00 committed by GitHub
commit 7df63952c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 163 additions and 0 deletions

View file

@ -0,0 +1,162 @@
package me.tofaa.entitylib.spigot;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.github.retrooper.packetevents.protocol.entity.type.EntityType;
import me.tofaa.entitylib.EntityIdProvider;
import me.tofaa.entitylib.Platform;
import org.bukkit.Bukkit;
import org.bukkit.UnsafeValues;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Field;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Stream;
/**
* Internal {@link EntityIdProvider} for Spigot servers, handling version and platform differences.
*/
public final class SpigotEntityIdProvider implements EntityIdProvider {
/** The platform abstraction. */
private final Platform<JavaPlugin> platform;
/** The supplier for entity IDs, dynamically chosen by {@link #detectIdSupplier()}. */
private final Supplier<Integer> entityIdSupplier ;
/**
* Constructs a new {@code SpigotEntityIdProvider}.
*
* @param platform The platform instance. Must not be null.
*/
public SpigotEntityIdProvider(final @NotNull Platform<JavaPlugin> platform) {
this.platform = platform;
this.entityIdSupplier = detectIdSupplier();
}
@Override
public int provide(@NotNull UUID entityUUID, @NotNull EntityType entityType) {
// UUID and EntityType are unused in Minecraft id generation.
return entityIdSupplier.get();
}
/**
* Detects and configures the appropriate {@link Supplier} for entity IDs based on server version
* and platform.
* Logic:
* <ul>
* <li>Paper (1.16+): Use `{@link UnsafeValues#nextEntityId()}`. (Paper API)</li>
* <li>1.14+: Access `AtomicInteger` field ("entityCount", "d", "c").</li>
* <li>1.8+: Access legacy `int` field "entityCount".</li>
* </ul>
*
* @return A {@link Supplier} instance that provides the next entity ID.
* @throws IllegalStateException if the entity counter field cannot be located or accessed.
*/
private Supplier<Integer> detectIdSupplier() {
final ServerVersion serverVersion = platform.getAPI().getPacketEvents().getServerManager().getVersion();
if (isPaper() && serverVersion.isNewerThanOrEquals(ServerVersion.V_1_16)) {
return Bukkit.getUnsafe()::nextEntityId; // Paper API
}
final Class<?> entityClass = getEntityClass();
if (serverVersion.isNewerThanOrEquals(ServerVersion.V_1_14)) {
final Field entityAtomicField = getField(entityClass, "entityCount", "d", "c"); // Obfuscated names
if (entityAtomicField == null) {
throw new IllegalStateException("Could not find entity counter field");
}
try {
entityAtomicField.setAccessible(true);
final AtomicInteger counter = (AtomicInteger) entityAtomicField.get(null);
return counter::incrementAndGet;
} catch (final Exception exception) {
throw new IllegalStateException("Failed to access entity counter", exception);
}
}
final Field entityLegacyField = getField(entityClass, "entityCount");
if (entityLegacyField == null) {
throw new IllegalStateException("Could not find legacy entity counter field");
}
try {
entityLegacyField.setAccessible(true);
return () -> {
try {
final int entityId = entityLegacyField.getInt(null);
entityLegacyField.setInt(null, entityId + 1);
return entityId;
} catch (final Exception exception) {
throw new IllegalStateException("Failed to modify entity counter", exception);
}
};
} catch (final Exception exception) {
throw new IllegalStateException("Failed to access legacy entity counter", exception);
}
}
/**
* 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`.
* Prior to 1.17, it is located in `net.minecraft.server.[version].Entity`.
*
* @return Resolved `Entity` class.
* @throws IllegalStateException if the `Entity` class cannot be found.
*/
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;
try {
return Class.forName(packagePath + ".Entity");
} catch (final ClassNotFoundException exception) {
throw new IllegalStateException("Could not find Entity class", exception);
}
}
/**
* Utility method to retrieve a declared field from a class by trying multiple names.
* @param clazz The class to examine for the field.
* @param possibleNames An array of potential field names to attempt retrieval with.
* @return The {@link Field} if a matching field is found, otherwise {@code null}.
*/
private static Field getField(final Class<?> clazz, final String... possibleNames) {
for (final String name : possibleNames) {
try {
return clazz.getDeclaredField(name);
} catch (final NoSuchFieldException ignored) {
// Field name not found, proceed to the next
}
}
return null;
}
/**
* Determines if the server environment is Paper by checking for Paper-specific classes.
* @return true if Paper is detected.
*/
private static boolean isPaper() {
return Stream.of(
"com.destroystokyo.paper.PaperConfig",
"io.papermc.paper.configuration.Configuration"
).anyMatch(SpigotEntityIdProvider::hasClass);
}
/**
* Utility to check if a class exists on the classpath.
* @param className Class name to check.
* @return true if class exists.
*/
private static boolean hasClass(final String className) {
try {
Class.forName(className);
return true;
} catch (final ClassNotFoundException ignored) {
return false;
}
}
}

View file

@ -32,6 +32,7 @@ public class SpigotEntityLibPlatform extends AbstractPlatform<JavaPlugin> {
super.setupApi(settings);
this.logger = settings.shouldUsePlatformLogger() ? handle.getLogger() : Logger.getLogger("EntityLib");
this.api = new SpigotEntityLibAPI(this, settings);
this.setEntityIdProvider(new SpigotEntityIdProvider(this));
this.api.onLoad();
this.api.onEnable();
if (settings.shouldUseBstats()) {