Merge pull request #24 from steveb05/feat/spigot-entityid-provider
Implement spigot entity id provider
This commit is contained in:
commit
7df63952c7
2 changed files with 163 additions and 0 deletions
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,6 +32,7 @@ public class SpigotEntityLibPlatform extends AbstractPlatform<JavaPlugin> {
|
||||||
super.setupApi(settings);
|
super.setupApi(settings);
|
||||||
this.logger = settings.shouldUsePlatformLogger() ? handle.getLogger() : Logger.getLogger("EntityLib");
|
this.logger = settings.shouldUsePlatformLogger() ? handle.getLogger() : Logger.getLogger("EntityLib");
|
||||||
this.api = new SpigotEntityLibAPI(this, settings);
|
this.api = new SpigotEntityLibAPI(this, settings);
|
||||||
|
this.setEntityIdProvider(new SpigotEntityIdProvider(this));
|
||||||
this.api.onLoad();
|
this.api.onLoad();
|
||||||
this.api.onEnable();
|
this.api.onEnable();
|
||||||
if (settings.shouldUseBstats()) {
|
if (settings.shouldUseBstats()) {
|
||||||
|
|
Loading…
Reference in a new issue