port to 26.3

This commit is contained in:
Alexander Medvedev 2026-09-17 19:11:17 +02:00
parent 9d23556460
commit 1e8e203281
65 changed files with 3645 additions and 202 deletions

6
.gitignore vendored
View File

@ -56,3 +56,9 @@ java/.idea/
.vscode/ .vscode/
.DS_Store .DS_Store
# Runtime configs
config/
java/patchbukkit/config/
logs/
java/patchbukkit/logs/

View File

@ -11,7 +11,7 @@ repositories {
dependencies { dependencies {
compileOnly(project(":patchbukkit")) compileOnly(project(":patchbukkit"))
compileOnly("io.papermc.paper:paper-api:26.2.build.117-stable") compileOnly("io.papermc.paper:paper-api:26.3.build.8-alpha")
} }
java { java {

View File

@ -47,7 +47,7 @@ repositories {
} }
dependencies { dependencies {
paperweight.paperDevBundle("26.2.build.117-stable") paperweight.paperDevBundle("26.3.build.8-alpha")
implementation("net.sf.jopt-simple:jopt-simple:6.0-alpha-3") implementation("net.sf.jopt-simple:jopt-simple:6.0-alpha-3")
implementation("org.apache.maven:maven-resolver-provider:3.9.6") implementation("org.apache.maven:maven-resolver-provider:3.9.6")
implementation("org.apache.maven.resolver:maven-resolver-impl:1.9.18") implementation("org.apache.maven.resolver:maven-resolver-impl:1.9.18")
@ -55,13 +55,22 @@ dependencies {
implementation("org.apache.maven.resolver:maven-resolver-transport-http:1.9.18") implementation("org.apache.maven.resolver:maven-resolver-transport-http:1.9.18")
implementation("org.apache.maven.resolver:maven-resolver-util:1.9.18") implementation("org.apache.maven.resolver:maven-resolver-util:1.9.18")
implementation("com.google.protobuf:protobuf-java:$protobufVersion") implementation("com.google.protobuf:protobuf-java:$protobufVersion")
implementation("org.slf4j:slf4j-jdk14:2.0.16") implementation("org.apache.logging.log4j:log4j-slf4j2-impl:2.26.0")
implementation("commons-logging:commons-logging:1.3.5")
implementation("commons-codec:commons-codec:1.18.0")
implementation("commons-collections:commons-collections:3.2.2")
implementation("net.bytebuddy:byte-buddy:1.15.11")
implementation("net.bytebuddy:byte-buddy-agent:1.15.11")
implementation("commons-lang:commons-lang:2.6")
implementation("org.xerial:sqlite-jdbc:3.47.1.0")
implementation("com.mysql:mysql-connector-j:9.1.0")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.platform:junit-platform-launcher")
} }
tasks.named<Test>("test") { tasks.named<Test>("test") {
useJUnitPlatform() useJUnitPlatform()
jvmArgs("-Dnet.bytebuddy.experimental=true")
} }
java { java {
@ -79,6 +88,7 @@ tasks.withType<JavaCompile> {
} }
tasks.named<Jar>("jar") { tasks.named<Jar>("jar") {
isZip64 = true
duplicatesStrategy = DuplicatesStrategy.EXCLUDE duplicatesStrategy = DuplicatesStrategy.EXCLUDE
dependsOn(configurations.compileClasspath) dependsOn(configurations.compileClasspath)

View File

@ -0,0 +1 @@
[19:04:38] [Test worker/INFO]: [MoonriseCommon] Paper is using 8 worker threads, 1 I/O threads

View File

@ -0,0 +1,12 @@
package org.patchbukkit;
public class CraftParticle {
public static org.bukkit.Particle minecraftToBukkit(net.minecraft.core.particles.ParticleType<?> nms) {
return org.bukkit.craftbukkit.CraftParticle.minecraftToBukkit(nms);
}
public static net.minecraft.core.particles.ParticleType<?> bukkitToMinecraft(org.bukkit.Particle bukkit) {
return org.bukkit.craftbukkit.CraftParticle.bukkitToMinecraft(bukkit);
}
}

View File

@ -0,0 +1,26 @@
package org.patchbukkit;
import java.util.UUID;
import net.minecraft.server.level.ServerLevel;
import org.patchbukkit.world.PatchBukkitWorld;
public class CraftWorld extends PatchBukkitWorld {
private ServerLevel handle;
public CraftWorld(UUID uuid) {
super(uuid);
}
public ServerLevel getHandle() {
if (this.handle == null) {
try {
java.lang.reflect.Field f = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
sun.misc.Unsafe unsafe = (sun.misc.Unsafe) f.get(null);
this.handle = (ServerLevel) unsafe.allocateInstance(ServerLevel.class);
} catch (Throwable ignored) {}
}
return this.handle;
}
}

View File

@ -100,11 +100,29 @@ public class PatchBukkitInternalAPIBridge implements InternalAPIBridge {
} }
public @Nullable DamageEffect getDamageEffect(String key) { public @Nullable DamageEffect getDamageEffect(String key) {
return null; if (key == null) return null;
try {
var craft = org.bukkit.craftbukkit.damage.CraftDamageEffect.getById(key);
if (craft != null) return craft;
} catch (Throwable ignored) {}
return switch (key.toLowerCase(java.util.Locale.ROOT)) {
case "hurt" -> () -> org.bukkit.Sound.ENTITY_PLAYER_HURT;
case "thorns" -> () -> org.bukkit.Sound.ENCHANT_THORNS_HIT;
case "drowning" -> () -> org.bukkit.Sound.ENTITY_PLAYER_HURT_DROWN;
case "burning" -> () -> org.bukkit.Sound.ENTITY_PLAYER_HURT_ON_FIRE;
case "poking" -> () -> org.bukkit.Sound.ENTITY_PLAYER_HURT_SWEET_BERRY_BUSH;
case "freezing" -> () -> org.bukkit.Sound.ENTITY_PLAYER_HURT_FREEZE;
default -> () -> org.bukkit.Sound.ENTITY_PLAYER_HURT;
};
} }
public DamageSource.Builder createDamageSourceBuilder(DamageType damageType) { public DamageSource.Builder createDamageSourceBuilder(DamageType damageType) {
return null; try {
return new org.bukkit.craftbukkit.damage.CraftDamageSourceBuilder(damageType);
} catch (Throwable t) {
return null;
}
} }
public PoiType.Occupancy createOccupancy(String type) { public PoiType.Occupancy createOccupancy(String type) {

View File

@ -14,6 +14,9 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import java.lang.reflect.Field;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bukkit.plugin.InvalidDescriptionException; import org.bukkit.plugin.InvalidDescriptionException;
import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
@ -24,6 +27,7 @@ public class PatchBukkitPluginClassLoader
implements ConfiguredPluginClassLoader implements ConfiguredPluginClassLoader
{ {
private static final Logger LOGGER = Logger.getLogger("PatchBukkitPluginClassLoader");
private final PluginDescriptionFile description; private final PluginDescriptionFile description;
private final File dataFolder; private final File dataFolder;
private final File file; private final File file;
@ -31,6 +35,9 @@ public class PatchBukkitPluginClassLoader
static { static {
ClassLoader.registerAsParallelCapable(); ClassLoader.registerAsParallelCapable();
try {
org.patchbukkit.PatchBukkitServer.ensureCraftRegistry();
} catch (Throwable ignored) {}
} }
public static final java.util.Set<PatchBukkitPluginClassLoader> ALL_LOADERS = public static final java.util.Set<PatchBukkitPluginClassLoader> ALL_LOADERS =
@ -57,6 +64,17 @@ public class PatchBukkitPluginClassLoader
libsDir.mkdirs(); libsDir.mkdirs();
} }
// Ensure official Mojang server bytecode is cached and added to classpath
File mojangServerJar = MojangServerProvider.getOrDownloadServerJar(
libsDir,
org.patchbukkit.versioning.Versioning.getCurrentApiVersion()
);
if (mojangServerJar != null && mojangServerJar.exists()) {
try {
addURL(mojangServerJar.toURI().toURL());
} catch (MalformedURLException ignored) {}
}
// Extract and load nested JARs inside plugin JAR // Extract and load nested JARs inside plugin JAR
for (File nestedJar : extractNestedJars(file, libsDir)) { for (File nestedJar : extractNestedJars(file, libsDir)) {
try { try {
@ -281,6 +299,9 @@ public class PatchBukkitPluginClassLoader
@Override @Override
protected Class<?> loadClass(String name, boolean resolve) protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException { throws ClassNotFoundException {
if (name != null && name.indexOf('/') != -1) {
name = name.replace('/', '.');
}
synchronized (getClassLoadingLock(name)) { synchronized (getClassLoadingLock(name)) {
// First, check if already loaded // First, check if already loaded
Class<?> c = findLoadedClass(name); Class<?> c = findLoadedClass(name);
@ -292,7 +313,13 @@ public class PatchBukkitPluginClassLoader
!name.startsWith("java.") && !name.startsWith("java.") &&
!name.startsWith("jdk.") && !name.startsWith("jdk.") &&
!name.startsWith("sun.") && !name.startsWith("sun.") &&
!name.startsWith("javax.") !name.startsWith("javax.") &&
!name.startsWith("org.bukkit.") &&
!name.startsWith("io.papermc.") &&
!name.startsWith("net.minecraft.") &&
!name.startsWith("org.spigotmc.") &&
!name.startsWith("com.destroystokyo.paper.") &&
!name.startsWith("org.patchbukkit.")
) { ) {
try { try {
// Try to find in plugin JAR / libraries first // Try to find in plugin JAR / libraries first
@ -315,9 +342,30 @@ public class PatchBukkitPluginClassLoader
} }
} }
// If not found in JAR (or is a system class), delegate to parent // If not found in JAR (or is a system/server class), delegate to parent
if (c == null) { if (c == null) {
c = getParent().loadClass(name); try {
c = getParent().loadClass(name);
} catch (ClassNotFoundException e) {
String remapped = remapLegacyClass(name);
if (remapped != null) {
try {
c = getParent().loadClass(remapped);
} catch (ClassNotFoundException ignored) {
try {
c = findClass(remapped);
} catch (ClassNotFoundException ignored2) {}
}
}
if (c == null) {
try {
c = findClass(name);
} catch (ClassNotFoundException ignored3) {}
}
if (c == null) {
throw e;
}
}
} }
} }
@ -328,6 +376,64 @@ public class PatchBukkitPluginClassLoader
} }
} }
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> loaded = findLoadedClass(name);
if (loaded != null) return loaded;
String path = name.replace('.', '/').concat(".class");
URL resource = findResource(path);
if (resource != null) {
try (InputStream is = resource.openStream()) {
byte[] raw = is.readAllBytes();
byte[] transformed = BytecodeTransformer.transform(raw);
return defineClass(name, transformed, 0, transformed.length, (java.security.CodeSource) null);
} catch (Throwable t) {
LOGGER.log(Level.FINE, "[PatchBukkit] Failed to define transformed class " + name, t);
}
}
return super.findClass(name);
}
private static String remapLegacyClass(String name) {
if (name == null) return null;
// Missing dot in CraftBukkit package (e.g. from Cloud command framework where CB_PKG_VERSION was empty)
// e.g. org.bukkit.craftbukkitcommand.VanillaCommandWrapper -> org.bukkit.craftbukkit.command.VanillaCommandWrapper
if (name.startsWith("org.bukkit.craftbukkit") && !name.startsWith("org.bukkit.craftbukkit.")) {
return "org.bukkit.craftbukkit." + name.substring("org.bukkit.craftbukkit".length());
}
// 1. Versioned CraftBukkit (e.g. org.bukkit.craftbukkit.v1_20_R3.entity.CraftPlayer -> org.bukkit.craftbukkit.entity.CraftPlayer)
if (name.startsWith("org.bukkit.craftbukkit.")) {
String unversioned = name.replaceFirst("^org\\.bukkit\\.craftbukkit\\.(v[0-9_]+R[0-9]+\\.)?", "org.bukkit.craftbukkit.");
if (!unversioned.equals(name)) {
return unversioned;
}
}
// 2. Legacy pre-1.17 NMS package (net.minecraft.server.v1_XX_RX.XYZ)
if (name.startsWith("net.minecraft.server.v")) {
String simpleName = name.substring(name.lastIndexOf('.') + 1);
return switch (simpleName) {
case "EntityPlayer" -> "net.minecraft.server.level.ServerPlayer";
case "MinecraftServer" -> "net.minecraft.server.MinecraftServer";
case "WorldServer" -> "net.minecraft.server.level.ServerLevel";
case "ItemStack" -> "net.minecraft.world.item.ItemStack";
case "Item" -> "net.minecraft.world.item.Item";
case "Block" -> "net.minecraft.world.level.block.Block";
case "Entity" -> "net.minecraft.world.entity.Entity";
case "NBTTagCompound" -> "net.minecraft.nbt.CompoundTag";
case "Packet" -> "net.minecraft.network.protocol.Packet";
case "PlayerConnection" -> "net.minecraft.server.network.ServerGamePacketListenerImpl";
case "World" -> "net.minecraft.world.level.Level";
default -> null;
};
}
return null;
}
@Override @Override
public PluginMeta getConfiguration() { public PluginMeta getConfiguration() {
return description; return description;

View File

@ -114,6 +114,7 @@ public class PatchBukkitPluginManager implements PluginManager {
plugin.onLoad(); plugin.onLoad();
} catch (Throwable t) { } catch (Throwable t) {
server.getLogger().log(Level.SEVERE, "Error loading " + plugin.getName(), t); server.getLogger().log(Level.SEVERE, "Error loading " + plugin.getName(), t);
t.printStackTrace();
} }
registerPlugin(plugin); registerPlugin(plugin);
return plugin; return plugin;
@ -220,7 +221,11 @@ public class PatchBukkitPluginManager implements PluginManager {
} }
if (!plugin.isEnabled()) { if (!plugin.isEnabled()) {
try { try {
plugin.getPluginLoader().enablePlugin(plugin); if (plugin instanceof org.bukkit.plugin.java.JavaPlugin javaPlugin) {
javaPlugin.setEnabled(true);
} else {
plugin.getPluginLoader().enablePlugin(plugin);
}
} catch (Throwable ex) { } catch (Throwable ex) {
server.getLogger().log(Level.SEVERE, "Error enabling " + plugin.getName() + " (Is it up to date?)", ex); server.getLogger().log(Level.SEVERE, "Error enabling " + plugin.getName() + " (Is it up to date?)", ex);
} }
@ -234,7 +239,11 @@ public class PatchBukkitPluginManager implements PluginManager {
} }
if (plugin.isEnabled()) { if (plugin.isEnabled()) {
try { try {
plugin.getPluginLoader().disablePlugin(plugin); if (plugin instanceof org.bukkit.plugin.java.JavaPlugin javaPlugin) {
javaPlugin.setEnabled(false);
} else {
plugin.getPluginLoader().disablePlugin(plugin);
}
} catch (Throwable ex) { } catch (Throwable ex) {
server.getLogger().log(Level.SEVERE, "Error disabling " + plugin.getName(), ex); server.getLogger().log(Level.SEVERE, "Error disabling " + plugin.getName(), ex);
} }

View File

@ -15,6 +15,7 @@ import java.io.File;
import java.io.PrintStream; import java.io.PrintStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.lang.reflect.Field;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -159,15 +160,14 @@ import patchbukkit.server.WhitelistEntryProto;
@SuppressWarnings({ "deprecation", "removal", "unchecked" }) @SuppressWarnings({ "deprecation", "removal", "unchecked" })
public class PatchBukkitServer implements Server { public class PatchBukkitServer implements Server {
private static final Logger logger = Logger.getLogger("Minecraft");
private static volatile PatchBukkitServer INSTANCE; private static volatile PatchBukkitServer INSTANCE;
public PatchBukkitServer() { public PatchBukkitServer() {
INSTANCE = this; INSTANCE = this;
String name = "PatchBukkit";
try { try {
name = io.papermc.paper.ServerBuildInfo.buildInfo().brandName(); this.serverName = io.papermc.paper.ServerBuildInfo.buildInfo().brandName();
} catch (Throwable ignored) {} } catch (Throwable ignored) {}
this.serverName = name;
syncServerInfo(); syncServerInfo();
} }
@ -189,19 +189,307 @@ public class PatchBukkitServer implements Server {
Class<?> bootstrap = Class.forName("net.minecraft.server.Bootstrap"); Class<?> bootstrap = Class.forName("net.minecraft.server.Bootstrap");
bootstrap.getMethod("bootStrap").invoke(null); bootstrap.getMethod("bootStrap").invoke(null);
} catch (Throwable ignored) {} } catch (Throwable ignored) {}
ensureGlobalConfiguration();
ensureCraftRegistry();
}
public static void ensureGlobalConfiguration() {
try {
Class<?> globalConfigClass = Class.forName("io.papermc.paper.configuration.GlobalConfiguration");
java.lang.reflect.Method getMethod = globalConfigClass.getMethod("get");
if (getMethod.invoke(null) != null) {
return;
}
// Ensure Minecraft Bootstrap is executed before touching any Paper/Mojang configurations
try {
Class<?> sharedConstants = Class.forName("net.minecraft.SharedConstants");
sharedConstants.getMethod("tryDetectVersion").invoke(null);
Class<?> bootstrap = Class.forName("net.minecraft.server.Bootstrap");
bootstrap.getMethod("bootStrap").invoke(null);
} catch (Throwable ignored) {}
// 1. Try initializing with PaperConfigurations
try {
Class<?> paperConfigsClass = Class.forName("io.papermc.paper.configuration.PaperConfigurations");
java.nio.file.Path configDir = java.nio.file.Path.of("config");
if (!java.nio.file.Files.exists(configDir)) {
java.nio.file.Files.createDirectories(configDir);
}
Object configs = paperConfigsClass.getConstructor(java.nio.file.Path.class).newInstance(configDir);
Class<?> registryAccessClass = Class.forName("net.minecraft.core.RegistryAccess");
java.lang.reflect.Field emptyField = registryAccessClass.getField("EMPTY");
Object emptyRegistry = emptyField.get(null);
java.lang.reflect.Method initMethod = paperConfigsClass.getMethod("initializeGlobalConfiguration", registryAccessClass);
initMethod.invoke(configs, emptyRegistry);
} catch (Throwable t) {
// 2. Fallback: instantiate GlobalConfiguration and populate inner configuration parts
try {
Object cfg = globalConfigClass.getConstructor().newInstance();
for (java.lang.reflect.Field field : globalConfigClass.getFields()) {
if (!java.lang.reflect.Modifier.isStatic(field.getModifiers()) && field.get(cfg) == null) {
Class<?> fieldType = field.getType();
try {
java.lang.reflect.Constructor<?> ctor = fieldType.getDeclaredConstructor(globalConfigClass);
ctor.setAccessible(true);
field.set(cfg, ctor.newInstance(cfg));
} catch (NoSuchMethodException e) {
try {
java.lang.reflect.Constructor<?> ctor = fieldType.getDeclaredConstructor();
ctor.setAccessible(true);
field.set(cfg, ctor.newInstance());
} catch (Throwable ignored) {}
}
}
}
java.lang.reflect.Method setMethod = globalConfigClass.getDeclaredMethod("set", globalConfigClass);
setMethod.setAccessible(true);
setMethod.invoke(null, cfg);
} catch (Throwable t2) {
logger.log(Level.WARNING, "[PatchBukkit] Failed to fallback-initialize GlobalConfiguration", t2);
}
}
} catch (ClassNotFoundException ignored) {
// Not a Paper environment
} catch (Throwable t) {
logger.log(Level.WARNING, "[PatchBukkit] Unexpected error initializing GlobalConfiguration", t);
}
}
public static synchronized void ensureCraftRegistry() {
try {
if (org.bukkit.craftbukkit.CraftRegistry.getMinecraftRegistry() != null) {
return;
}
net.minecraft.core.RegistryAccess.Frozen frozenAccess =
net.minecraft.core.RegistryAccess.fromRegistryOfRegistries(net.minecraft.core.registries.BuiltInRegistries.REGISTRY);
org.bukkit.craftbukkit.CraftRegistry.setMinecraftRegistry(frozenAccess);
return;
} catch (IllegalStateException ignored) {
return;
} catch (Throwable ignored) {}
try {
Class<?> craftRegistryClass = Class.forName("org.bukkit.craftbukkit.CraftRegistry");
java.lang.reflect.Method getMinecraftRegistryMethod = craftRegistryClass.getMethod("getMinecraftRegistry");
if (getMinecraftRegistryMethod.invoke(null) != null) {
return;
}
Class<?> regAccessClass = Class.forName("net.minecraft.core.RegistryAccess");
Class<?> builtInRegsClass = Class.forName("net.minecraft.core.registries.BuiltInRegistries");
Object rootRegistry = builtInRegsClass.getField("REGISTRY").get(null);
Object frozenAccess = regAccessClass.getMethod("fromRegistryOfRegistries", Class.forName("net.minecraft.core.Registry")).invoke(null, rootRegistry);
craftRegistryClass.getMethod("setMinecraftRegistry", regAccessClass).invoke(null, frozenAccess);
} catch (IllegalStateException ignored) {
// Already set by concurrent thread
} catch (Throwable t) {
if (logger != null) {
logger.log(Level.WARNING, "[PatchBukkit] Failed to initialize CraftRegistry", t);
} else {
System.err.println("[PatchBukkit] Failed to initialize CraftRegistry: " + t.getMessage());
}
}
}
private static volatile net.minecraft.server.network.ServerConnectionListener SERVER_CONNECTION;
private static volatile net.minecraft.server.dedicated.DedicatedServer DEDICATED_SERVER;
private static volatile net.minecraft.server.dedicated.DedicatedPlayerList DEDICATED_PLAYER_LIST;
private static volatile org.bukkit.craftbukkit.CraftServer CRAFT_SERVER;
public static net.minecraft.server.network.ServerConnectionListener getServerConnection() {
if (SERVER_CONNECTION == null) {
initMinecraftServerInstances();
}
return SERVER_CONNECTION;
}
public static net.minecraft.server.dedicated.DedicatedServer getDedicatedServer() {
if (DEDICATED_SERVER == null) {
initMinecraftServerInstances();
}
return DEDICATED_SERVER;
}
public static net.minecraft.server.dedicated.DedicatedPlayerList getDedicatedPlayerList() {
if (DEDICATED_PLAYER_LIST == null) {
initMinecraftServerInstances();
}
return DEDICATED_PLAYER_LIST;
}
public static org.bukkit.craftbukkit.CraftServer getCraftServer() {
if (CRAFT_SERVER == null) {
initMinecraftServerInstances();
}
return CRAFT_SERVER;
}
private static synchronized void initMinecraftServerInstances() {
if (SERVER_CONNECTION != null) return;
try {
ensureGlobalConfiguration();
ensureCraftRegistry();
Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
sun.misc.Unsafe unsafe = (sun.misc.Unsafe) unsafeField.get(null);
Class<?> dsClass = Class.forName("net.minecraft.server.dedicated.DedicatedServer");
DEDICATED_SERVER = (net.minecraft.server.dedicated.DedicatedServer) unsafe.allocateInstance(dsClass);
SERVER_CONNECTION = new net.minecraft.server.network.ServerConnectionListener(DEDICATED_SERVER);
for (Field f : net.minecraft.server.MinecraftServer.class.getDeclaredFields()) {
if (f.getType().equals(net.minecraft.server.network.ServerConnectionListener.class)) {
f.setAccessible(true);
f.set(DEDICATED_SERVER, SERVER_CONNECTION);
break;
}
}
// Set static MinecraftServer.SERVER singleton
for (Field f : net.minecraft.server.MinecraftServer.class.getDeclaredFields()) {
if (java.lang.reflect.Modifier.isStatic(f.getModifiers()) && net.minecraft.server.MinecraftServer.class.isAssignableFrom(f.getType())) {
try {
unsafe.putObject(unsafe.staticFieldBase(f), unsafe.staticFieldOffset(f), DEDICATED_SERVER);
} catch (Throwable t) {
try {
f.setAccessible(true);
f.set(null, DEDICATED_SERVER);
} catch (Throwable ignored) {}
}
}
}
Class<?> plClass = Class.forName("net.minecraft.server.dedicated.DedicatedPlayerList");
DEDICATED_PLAYER_LIST = (net.minecraft.server.dedicated.DedicatedPlayerList) unsafe.allocateInstance(plClass);
Field playersField = null;
Field playersByUUIDField = null;
Class<?> cur = DEDICATED_PLAYER_LIST.getClass();
while (cur != null) {
for (Field f : cur.getDeclaredFields()) {
if (f.getName().equals("players")) {
playersField = f;
} else if (f.getName().equals("playersByUUID")) {
playersByUUIDField = f;
}
}
cur = cur.getSuperclass();
}
if (playersField != null) {
playersField.setAccessible(true);
playersField.set(DEDICATED_PLAYER_LIST, new java.util.concurrent.CopyOnWriteArrayList<>());
}
if (playersByUUIDField != null) {
playersByUUIDField.setAccessible(true);
playersByUUIDField.set(DEDICATED_PLAYER_LIST, new java.util.concurrent.ConcurrentHashMap<>());
}
for (Field f : net.minecraft.server.MinecraftServer.class.getDeclaredFields()) {
if (net.minecraft.server.players.PlayerList.class.isAssignableFrom(f.getType())) {
f.setAccessible(true);
f.set(DEDICATED_SERVER, DEDICATED_PLAYER_LIST);
break;
}
}
Class<?> pldClass = Class.forName("net.minecraft.world.level.storage.PrimaryLevelData");
net.minecraft.world.level.storage.PrimaryLevelData pld = (net.minecraft.world.level.storage.PrimaryLevelData) unsafe.allocateInstance(pldClass);
for (Field f : pldClass.getDeclaredFields()) {
try {
f.setAccessible(true);
if (f.getName().equals("respawnDimension")) {
f.set(pld, net.minecraft.world.level.Level.OVERWORLD);
} else if (f.getName().equals("respawnData")) {
f.set(pld, net.minecraft.world.level.storage.LevelData.RespawnData.DEFAULT);
}
} catch (Throwable ignored) {}
}
for (Field f : net.minecraft.server.MinecraftServer.class.getDeclaredFields()) {
try {
f.setAccessible(true);
if (f.getName().equals("worldData") || net.minecraft.world.level.storage.WorldData.class.isAssignableFrom(f.getType())) {
f.set(DEDICATED_SERVER, pld);
} else if (f.getName().equals("effectiveRespawnData") || f.getType().equals(net.minecraft.world.level.storage.LevelData.RespawnData.class)) {
f.set(DEDICATED_SERVER, net.minecraft.world.level.storage.LevelData.RespawnData.DEFAULT);
} else if (f.getName().equals("levels")) {
f.set(DEDICATED_SERVER, new java.util.HashMap<>());
}
} catch (Throwable ignored) {}
}
Class<?> csClass = Class.forName("org.bukkit.craftbukkit.CraftServer");
CRAFT_SERVER = (org.bukkit.craftbukkit.CraftServer) unsafe.allocateInstance(csClass);
for (Field f : csClass.getDeclaredFields()) {
try {
f.setAccessible(true);
if (f.getName().equals("console")) {
f.set(CRAFT_SERVER, DEDICATED_SERVER);
} else if (f.getName().equals("playerList")) {
f.set(CRAFT_SERVER, DEDICATED_PLAYER_LIST);
} else if (f.getName().equals("serverName")) {
f.set(CRAFT_SERVER, "Paper");
} else if (f.getName().equals("serverVersion")) {
f.set(CRAFT_SERVER, org.patchbukkit.versioning.Versioning.getBukkitVersion());
} else if (f.getName().equals("bukkitVersion")) {
f.set(CRAFT_SERVER, org.patchbukkit.versioning.Versioning.getBukkitVersion());
} else if (f.getName().equals("logger")) {
f.set(CRAFT_SERVER, logger);
} else if (f.getName().equals("worlds")) {
f.set(CRAFT_SERVER, new java.util.concurrent.ConcurrentHashMap<>());
} else if (f.getName().equals("offlinePlayers")) {
f.set(CRAFT_SERVER, new java.util.concurrent.ConcurrentHashMap<>());
}
} catch (Throwable ignored) {}
}
for (Field f : net.minecraft.server.MinecraftServer.class.getDeclaredFields()) {
if (f.getName().equals("server") || f.getType().getName().contains("CraftServer")) {
f.setAccessible(true);
f.set(DEDICATED_SERVER, CRAFT_SERVER);
break;
}
}
} catch (Throwable t) {
logger.log(Level.WARNING, "[PatchBukkit] Failed to initialize DedicatedServer instances", t);
}
} }
public static PatchBukkitServer initServer() { public static PatchBukkitServer initServer() {
ensureGlobalConfiguration();
ensureCraftRegistry();
getServerConnection();
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
logger.log(Level.SEVERE, "Uncaught exception in thread " + thread.getName(), throwable); logger.log(Level.SEVERE, "Uncaught exception in thread " + thread.getName(), throwable);
}); });
PatchBukkitServer server = getInstance(); PatchBukkitServer server = getInstance();
org.bukkit.Bukkit.setServer(server); if (org.bukkit.Bukkit.getServer() == null) {
org.bukkit.Bukkit.setServer(server);
}
return server; return server;
} }
private final String serverName; public final net.minecraft.server.dedicated.DedicatedServer console = getDedicatedServer();
private final String bukkitVersion = Versioning.getBukkitVersion();
public net.minecraft.server.dedicated.DedicatedServer getServer() {
return console;
}
public net.minecraft.server.dedicated.DedicatedPlayerList getHandle() {
return getDedicatedPlayerList();
}
public net.minecraft.server.network.ServerConnectionListener getConnection() {
return getServerConnection();
}
private String serverName = "Pumpkin";
private String bukkitVersion = Versioning.getBukkitVersion();
private String minecraftVersion = Versioning.getCurrentApiVersion();
private String serverVersion = "git-Paper-117 (MC: " + this.minecraftVersion + ")";
public SimpleCommandMap commandMap = new PatchBukkitCommandMap(this); public SimpleCommandMap commandMap = new PatchBukkitCommandMap(this);
public BukkitScheduler scheduler = new PatchBukkitScheduler(); public BukkitScheduler scheduler = new PatchBukkitScheduler();
public PatchBukkitPluginManager pluginManager = new PatchBukkitPluginManager(this); public PatchBukkitPluginManager pluginManager = new PatchBukkitPluginManager(this);
@ -315,6 +603,18 @@ public class PatchBukkitServer implements Server {
try { try {
ServerInfoResponse info = NativeBridgeFfi.getServerInfo(EmptyRequest.getDefaultInstance()); ServerInfoResponse info = NativeBridgeFfi.getServerInfo(EmptyRequest.getDefaultInstance());
if (info != null) { if (info != null) {
if (info.getServerName() != null && !info.getServerName().isEmpty()) {
this.serverName = info.getServerName();
}
if (info.getBukkitVersion() != null && !info.getBukkitVersion().isEmpty()) {
this.bukkitVersion = info.getBukkitVersion();
}
if (info.getMinecraftVersion() != null && !info.getMinecraftVersion().isEmpty()) {
this.minecraftVersion = info.getMinecraftVersion();
}
if (info.getVersion() != null && !info.getVersion().isEmpty()) {
this.serverVersion = "git-Paper-" + info.getVersion() + " (MC: " + this.minecraftVersion + ")";
}
this.motd = info.getMotd(); this.motd = info.getMotd();
this.ip = info.getIp(); this.ip = info.getIp();
this.port = info.getPort(); this.port = info.getPort();
@ -379,14 +679,20 @@ public class PatchBukkitServer implements Server {
} catch (Throwable ignored) {} } catch (Throwable ignored) {}
} }
private static final PrintStream ORIGINAL_OUT = System.out; public static final PrintStream ORIGINAL_OUT = System.out;
private static final PrintStream ORIGINAL_ERR = System.err; public static final PrintStream ORIGINAL_ERR = System.err;
private static final ThreadLocal<Boolean> IN_LOGGING = ThreadLocal.withInitial(() -> Boolean.FALSE); private static final ThreadLocal<Boolean> IN_LOGGING = ThreadLocal.withInitial(() -> Boolean.FALSE);
private static final Logger logger = Logger.getLogger("Minecraft"); public static boolean isLogging() {
return IN_LOGGING.get();
}
public static void setLogging(boolean logging) {
IN_LOGGING.set(logging);
}
static { static {
configureRootLogger(); configureLogging();
} }
public static final Map<Level, LogLevel> LEVEL_MAP = Map.of( public static final Map<Level, LogLevel> LEVEL_MAP = Map.of(
@ -399,29 +705,57 @@ public class PatchBukkitServer implements Server {
Level.FINEST, LogLevel.FINEST Level.FINEST, LogLevel.FINEST
); );
private static void configureRootLogger() { private static void configureLogging() {
configureLog4j();
configureJulLogger();
redirectSystemStreams();
}
private static void configureLog4j() {
try {
org.apache.logging.log4j.core.LoggerContext context =
(org.apache.logging.log4j.core.LoggerContext) org.apache.logging.log4j.LogManager.getContext(false);
org.apache.logging.log4j.core.config.Configuration config = context.getConfiguration();
org.patchbukkit.log.NativeBridgeLog4jAppender appender =
org.patchbukkit.log.NativeBridgeLog4jAppender.create("NativeBridge");
appender.start();
config.addAppender(appender);
org.apache.logging.log4j.core.config.LoggerConfig rootConfig = config.getRootLogger();
rootConfig.removeAppender("TerminalConsole");
rootConfig.removeAppender("ServerGuiConsole");
rootConfig.removeAppender("Async");
rootConfig.addAppender(appender, org.apache.logging.log4j.Level.ALL, null);
rootConfig.setLevel(org.apache.logging.log4j.Level.ALL);
context.updateLoggers();
} catch (Throwable t) {
ORIGINAL_ERR.println("[PatchBukkit] Failed to initialize Log4j native bridge appender: " + t.getMessage());
}
}
private static void configureJulLogger() {
Logger root = Logger.getLogger(""); Logger root = Logger.getLogger("");
root.setUseParentHandlers(false); root.setUseParentHandlers(false);
for (java.util.logging.Handler h : root.getHandlers()) { for (java.util.logging.Handler h : root.getHandlers()) {
root.removeHandler(h); root.removeHandler(h);
} }
root.setLevel(Level.ALL); root.setLevel(Level.ALL);
root.addHandler(new java.util.logging.Handler() { try {
@Override root.addHandler(new org.bukkit.craftbukkit.util.ForwardLogHandler());
public void publish(java.util.logging.LogRecord record) { } catch (Throwable t) {
if (record == null) return; ORIGINAL_ERR.println("[PatchBukkit] Failed to install ForwardLogHandler, using fallback: " + t.getMessage());
if (IN_LOGGING.get()) return; root.addHandler(new java.util.logging.Handler() {
IN_LOGGING.set(true); @Override
try { public void publish(java.util.logging.LogRecord record) {
LogLevel logLevel = LEVEL_MAP.getOrDefault(record.getLevel(), LogLevel.INFO); if (record == null) return;
String message = formatLogRecord(record); if (IN_LOGGING.get()) return;
String loggerName = record.getLoggerName() != null ? record.getLoggerName() : ""; IN_LOGGING.set(true);
if (loggerName.startsWith("jdk.") || loggerName.startsWith("sun.") || loggerName.startsWith("java.") || loggerName.startsWith("javax.")) {
if (record.getLevel().intValue() < Level.WARNING.intValue()) {
return;
}
}
try { try {
LogLevel logLevel = LEVEL_MAP.getOrDefault(record.getLevel(), LogLevel.INFO);
String message = formatLogRecord(record);
String loggerName = record.getLoggerName() != null ? record.getLoggerName() : "";
NativeBridgeFfi.sendLog( NativeBridgeFfi.sendLog(
SendLogRequest.newBuilder() SendLogRequest.newBuilder()
.setLevel(logLevel) .setLevel(logLevel)
@ -429,20 +763,15 @@ public class PatchBukkitServer implements Server {
.setLoggerName(loggerName) .setLoggerName(loggerName)
.build() .build()
); );
} catch (Throwable t) { } catch (Throwable ignored) {
ORIGINAL_ERR.println("[" + record.getLevel() + "][" + loggerName + "] " + message); } finally {
IN_LOGGING.set(false);
} }
} catch (Throwable t) {
ORIGINAL_ERR.println("[RootLogger Error] Failed to publish record: " + t.getMessage());
} finally {
IN_LOGGING.set(false);
} }
} @Override public void flush() {}
@Override public void flush() {} @Override public void close() {}
@Override public void close() {} });
}); }
redirectSystemStreams();
} }
public static String formatLogRecord(java.util.logging.LogRecord record) { public static String formatLogRecord(java.util.logging.LogRecord record) {
@ -475,8 +804,8 @@ public class PatchBukkitServer implements Server {
private static void redirectSystemStreams() { private static void redirectSystemStreams() {
try { try {
System.setOut(new LoggingPrintStream(ORIGINAL_OUT, Level.INFO, "System.out")); System.setOut(new LoggingPrintStream(ORIGINAL_OUT, Level.INFO, "STDOUT"));
System.setErr(new LoggingPrintStream(ORIGINAL_ERR, Level.SEVERE, "System.err")); System.setErr(new LoggingPrintStream(ORIGINAL_ERR, Level.SEVERE, "STDERR"));
} catch (Throwable t) { } catch (Throwable t) {
ORIGINAL_ERR.println("[PatchBukkit] Failed to redirect System streams: " + t.getMessage()); ORIGINAL_ERR.println("[PatchBukkit] Failed to redirect System streams: " + t.getMessage());
} }
@ -525,8 +854,17 @@ public class PatchBukkitServer implements Server {
if (line.isEmpty() || IN_LOGGING.get()) return; if (line.isEmpty() || IN_LOGGING.get()) return;
IN_LOGGING.set(true); IN_LOGGING.set(true);
try { try {
LogLevel logLevel = LEVEL_MAP.getOrDefault(level, LogLevel.INFO); org.apache.logging.log4j.Logger log4jLogger = org.apache.logging.log4j.LogManager.getLogger(loggerName);
if (level == Level.SEVERE) {
log4jLogger.error(line);
} else if (level == Level.WARNING) {
log4jLogger.warn(line);
} else {
log4jLogger.info(line);
}
} catch (Throwable t) {
try { try {
LogLevel logLevel = LEVEL_MAP.getOrDefault(level, LogLevel.INFO);
NativeBridgeFfi.sendLog( NativeBridgeFfi.sendLog(
SendLogRequest.newBuilder() SendLogRequest.newBuilder()
.setLevel(logLevel) .setLevel(logLevel)
@ -547,9 +885,13 @@ public class PatchBukkitServer implements Server {
} }
public static void registerPlayer(String uuidStr, String name, boolean isOp) { public static void registerPlayer(String uuidStr, String name, boolean isOp) {
registerPlayer(uuidStr, name, isOp, -1);
}
public static void registerPlayer(String uuidStr, String name, boolean isOp, int entityId) {
try { try {
UUID uuid = UUID.fromString(uuidStr); UUID uuid = UUID.fromString(uuidStr);
PatchBukkitPlayer player = new PatchBukkitPlayer(uuid, name); PatchBukkitPlayer player = new org.patchbukkit.entity.CraftPlayer(uuid, name, entityId);
if (isOp) { if (isOp) {
player.setOp(true); player.setOp(true);
} }
@ -635,7 +977,7 @@ public class PatchBukkitServer implements Server {
@Override @Override
public @NotNull String getVersion() { public @NotNull String getVersion() {
return "26.2"; return this.serverVersion;
} }
@Override @Override
@ -645,11 +987,7 @@ public class PatchBukkitServer implements Server {
@Override @Override
public @NotNull String getMinecraftVersion() { public @NotNull String getMinecraftVersion() {
String version = this.bukkitVersion; return this.minecraftVersion;
if (version != null && version.contains("-")) {
return version.split("-")[0];
}
return version != null && !version.equals("Unknown-Version") ? version : "1.21.4";
} }
@Override @Override

View File

@ -35,12 +35,12 @@ public final class PatchBukkitServerBuildInfo implements ServerBuildInfo {
@Override @Override
public String minecraftVersionId() { public String minecraftVersionId() {
return "26.2"; return org.patchbukkit.versioning.Versioning.getCurrentApiVersion();
} }
@Override @Override
public String minecraftVersionName() { public String minecraftVersionName() {
return "26.2"; return org.patchbukkit.versioning.Versioning.getCurrentApiVersion();
} }
@Override @Override

View File

@ -0,0 +1,10 @@
package org.patchbukkit.block.data;
import net.minecraft.world.level.block.state.BlockState;
public class CraftBlockData {
public static CraftBlockData fromData(BlockState data) {
return new CraftBlockData();
}
}

View File

@ -157,7 +157,7 @@ public class PatchBukkitCommandMap extends SimpleCommandMap {
java.util.UUID uuid = java.util.UUID.fromString(senderUuid); java.util.UUID uuid = java.util.UUID.fromString(senderUuid);
sender = org.bukkit.Bukkit.getPlayer(uuid); sender = org.bukkit.Bukkit.getPlayer(uuid);
if (sender == null) { if (sender == null) {
sender = new org.patchbukkit.entity.PatchBukkitPlayer(uuid, senderName != null ? senderName : "Player"); sender = new org.patchbukkit.entity.CraftPlayer(uuid, senderName != null ? senderName : "Player");
} }
if (sender instanceof org.patchbukkit.entity.PatchBukkitPlayer p) { if (sender instanceof org.patchbukkit.entity.PatchBukkitPlayer p) {
p.setOp(isOp); p.setOp(isOp);
@ -179,7 +179,7 @@ public class PatchBukkitCommandMap extends SimpleCommandMap {
java.util.UUID uuid = java.util.UUID.fromString(senderUuid); java.util.UUID uuid = java.util.UUID.fromString(senderUuid);
sender = org.bukkit.Bukkit.getPlayer(uuid); sender = org.bukkit.Bukkit.getPlayer(uuid);
if (sender == null) { if (sender == null) {
sender = new org.patchbukkit.entity.PatchBukkitPlayer(uuid, senderName != null ? senderName : "Player"); sender = new org.patchbukkit.entity.CraftPlayer(uuid, senderName != null ? senderName : "Player");
} }
if (sender instanceof org.patchbukkit.entity.PatchBukkitPlayer p) { if (sender instanceof org.patchbukkit.entity.PatchBukkitPlayer p) {
p.setOp(isOp); p.setOp(isOp);

View File

@ -50,7 +50,8 @@ public class PatchBukkitConsoleCommandSender
@Override @Override
public @NotNull Server getServer() { public @NotNull Server getServer() {
return Bukkit.getServer(); org.bukkit.craftbukkit.CraftServer cs = org.patchbukkit.PatchBukkitServer.getCraftServer();
return cs != null ? cs : Bukkit.getServer();
} }
@Override @Override

View File

@ -0,0 +1,21 @@
package org.patchbukkit.entity;
import java.util.UUID;
import net.minecraft.world.entity.Entity;
public class CraftEntity extends PatchBukkitEntity {
private Entity handle;
public CraftEntity(UUID uuid, String name) {
super(uuid, name);
}
public Entity getHandle() {
return this.handle;
}
public void setHandle(Entity handle) {
this.handle = handle;
}
}

View File

@ -0,0 +1,25 @@
package org.patchbukkit.entity;
import java.util.UUID;
import net.minecraft.server.level.ServerPlayer;
import org.patchbukkit.network.VirtualChannelManager;
public class CraftPlayer extends PatchBukkitPlayer {
public CraftPlayer(UUID uuid, String name) {
super(uuid, name);
}
public CraftPlayer(UUID uuid, String name, int entityId) {
super(uuid, name, entityId);
}
public CraftPlayer(org.patchbukkit.PatchBukkitServer server, ServerPlayer entity) {
super(entity.getUUID(), entity.getScoreboardName(), entity.getId());
}
@Override
public ServerPlayer getHandle() {
return VirtualChannelManager.getInstance().getOrCreateServerPlayer(this);
}
}

View File

@ -86,19 +86,41 @@ public class PatchBukkitEntity implements Entity {
private Location cachedLocation; private Location cachedLocation;
private EntityType entityType = EntityType.UNKNOWN; private EntityType entityType = EntityType.UNKNOWN;
public static Entity create(UUID uuid, EntityType type, Location loc) { public static Entity create(UUID uuid, EntityType type, Location loc, int entityId) {
PatchBukkitEntity entity = new PatchBukkitEntity(uuid, type != null ? type.name() : "entity"); PatchBukkitEntity entity = new PatchBukkitEntity(uuid, type != null ? type.name() : "entity", entityId);
entity.entityType = type != null ? type : EntityType.UNKNOWN; entity.entityType = type != null ? type : EntityType.UNKNOWN;
entity.cachedLocation = loc != null ? loc.clone() : new Location(null, 0, 0, 0); entity.cachedLocation = loc != null ? loc.clone() : new Location(null, 0, 0, 0);
return entity; return entity;
} }
public static Entity create(UUID uuid, EntityType type, Location loc) {
return create(uuid, type, loc, -1);
}
private static final java.util.concurrent.atomic.AtomicInteger NEXT_ENTITY_ID = new java.util.concurrent.atomic.AtomicInteger(1);
protected int entityId;
public PatchBukkitEntity(
UUID uuid,
String name,
int entityId
) {
this.uuid = uuid;
this.name = name;
this.entityId = entityId > 0 ? entityId : -1;
}
public PatchBukkitEntity( public PatchBukkitEntity(
UUID uuid, UUID uuid,
String name String name
) { ) {
this.uuid = uuid; this(uuid, name, -1);
this.name = name; }
public void setEntityId(int entityId) {
if (entityId > 0) {
this.entityId = entityId;
}
} }
@Override @Override
@ -425,8 +447,18 @@ public class PatchBukkitEntity implements Entity {
@Override @Override
public int getEntityId() { public int getEntityId() {
// TODO Auto-generated method stub if (this.entityId <= 0) {
throw new UnsupportedOperationException("Unimplemented method 'getEntityId'"); try {
var resp = NativeBridgeFfi.getEntityId(BridgeUtils.convertUuid(this.uuid));
if (resp != null && resp.getEntityId() > 0) {
this.entityId = resp.getEntityId();
}
} catch (Throwable ignored) {}
}
if (this.entityId <= 0) {
this.entityId = NEXT_ENTITY_ID.incrementAndGet();
}
return this.entityId;
} }
@Override @Override

View File

@ -78,8 +78,12 @@ public class PatchBukkitHumanEntity
private int sleepTicks = 0; private int sleepTicks = 0;
private int enchantmentSeed = 0; private int enchantmentSeed = 0;
public PatchBukkitHumanEntity(UUID uuid, String name, int entityId) {
super(uuid, name, entityId);
}
public PatchBukkitHumanEntity(UUID uuid, String name) { public PatchBukkitHumanEntity(UUID uuid, String name) {
super(uuid, name); this(uuid, name, -1);
} }
@Override @Override

View File

@ -60,9 +60,12 @@ public class PatchBukkitLivingEntity
extends PatchBukkitEntity extends PatchBukkitEntity
implements LivingEntity { implements LivingEntity {
public PatchBukkitLivingEntity(UUID uuid, public PatchBukkitLivingEntity(UUID uuid, String name, int entityId) {
String name) { super(uuid, name, entityId);
super(uuid, name); }
public PatchBukkitLivingEntity(UUID uuid, String name) {
this(uuid, name, -1);
} }
@Override @Override
public double getEyeHeight() { public double getEyeHeight() {

View File

@ -124,12 +124,23 @@ public class PatchBukkitPlayer extends PatchBukkitHumanEntity implements Player
private int expCooldown = 0; private int expCooldown = 0;
private TriState flyingFallDamage = TriState.NOT_SET; private TriState flyingFallDamage = TriState.NOT_SET;
public PatchBukkitPlayer(UUID uuid, String name) { public PatchBukkitPlayer(UUID uuid, String name, int entityId) {
super(uuid, name); super(uuid, name, entityId);
this.displayName = name; this.displayName = name;
this.playerListName = name; this.playerListName = name;
this.firstPlayed = System.currentTimeMillis(); this.firstPlayed = System.currentTimeMillis();
this.lastLogin = System.currentTimeMillis(); this.lastLogin = System.currentTimeMillis();
}
public PatchBukkitPlayer(UUID uuid, String name) {
this(uuid, name, -1);
}
public net.minecraft.server.level.ServerPlayer getHandle() {
return org.patchbukkit.network.VirtualChannelManager.getInstance().getOrCreateServerPlayer(this);
}
{
this.spigot = new Player.Spigot() { this.spigot = new Player.Spigot() {
@Override @Override
public void sendMessage(@NotNull BaseComponent component) { public void sendMessage(@NotNull BaseComponent component) {
@ -2054,6 +2065,11 @@ public class PatchBukkitPlayer extends PatchBukkitHumanEntity implements Player
spawnParticle(particle, x, y, z, count, offsetX, offsetY, offsetZ, extra, data); spawnParticle(particle, x, y, z, count, offsetX, offsetY, offsetZ, extra, data);
} }
@Override
public <T> void spawnParticle(@NotNull Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double extra, double xSpread, double ySpread, @Nullable T data, boolean force, @NotNull Particle.RandomizationType randomizationType) {
spawnParticle(particle, x, y, z, count, offsetX, offsetY, offsetZ, extra, data, force);
}
@Override @Override
public @NotNull AdvancementProgress getAdvancementProgress(@NotNull Advancement advancement) { public @NotNull AdvancementProgress getAdvancementProgress(@NotNull Advancement advancement) {
return new AdvancementProgress() { return new AdvancementProgress() {

View File

@ -146,8 +146,22 @@ public class PatchBukkitEventFactory {
var ev = event.getMapInitialize(); var ev = event.getMapInitialize();
yield createGenericBukkitEvent("org.bukkit.event.server.MapInitializeEvent", ev); yield createGenericBukkitEvent("org.bukkit.event.server.MapInitializeEvent", ev);
} }
case PACKET_RECEIVED -> null; case PACKET_RECEIVED -> {
case PACKET_SENT -> null; var ev = event.getPacketReceived();
if (ev.hasPlayerUuid()) {
java.util.UUID uuid = java.util.UUID.fromString(ev.getPlayerUuid().getValue());
org.patchbukkit.network.VirtualChannelManager.getInstance().handlePacketReceived(uuid, ev.getPacketId(), ev.getPayload().toByteArray());
}
yield null;
}
case PACKET_SENT -> {
var ev = event.getPacketSent();
if (ev.hasPlayerUuid()) {
java.util.UUID uuid = java.util.UUID.fromString(ev.getPlayerUuid().getValue());
org.patchbukkit.network.VirtualChannelManager.getInstance().handlePacketSent(uuid, ev.getPacketId(), ev.getPayload().toByteArray());
}
yield null;
}
case PLUGIN_DISABLE -> { case PLUGIN_DISABLE -> {
var ev = event.getPluginDisable(); var ev = event.getPluginDisable();
var plugin = Bukkit.getPluginManager().getPlugin(ev.getPluginName()); var plugin = Bukkit.getPluginManager().getPlugin(ev.getPluginName());
@ -687,6 +701,9 @@ public class PatchBukkitEventFactory {
var ev = event.getPlayerJoin(); var ev = event.getPlayerJoin();
Player player = getPlayer(ev.getPlayerUuid().getValue()); Player player = getPlayer(ev.getPlayerUuid().getValue());
if (player == null) yield null; if (player == null) yield null;
if (ev.getEntityId() > 0 && player instanceof org.patchbukkit.entity.PatchBukkitEntity pbe) {
pbe.setEntityId(ev.getEntityId());
}
Component msg = ev.getJoinMessage().isEmpty() ? Component.empty() : GsonComponentSerializer.gson().deserialize(ev.getJoinMessage()); Component msg = ev.getJoinMessage().isEmpty() ? Component.empty() : GsonComponentSerializer.gson().deserialize(ev.getJoinMessage());
yield new org.bukkit.event.player.PlayerJoinEvent(player, msg); yield new org.bukkit.event.player.PlayerJoinEvent(player, msg);
} }
@ -701,6 +718,11 @@ public class PatchBukkitEventFactory {
case PLAYER_LEAVE -> { case PLAYER_LEAVE -> {
var ev = event.getPlayerLeave(); var ev = event.getPlayerLeave();
Player player = getPlayer(ev.getPlayerUuid().getValue()); Player player = getPlayer(ev.getPlayerUuid().getValue());
if (ev.hasPlayerUuid()) {
org.patchbukkit.network.VirtualChannelManager.getInstance().removePlayer(
java.util.UUID.fromString(ev.getPlayerUuid().getValue())
);
}
if (player == null) yield null; if (player == null) yield null;
Component msg = ev.getLeaveMessage().isEmpty() ? Component.empty() : GsonComponentSerializer.gson().deserialize(ev.getLeaveMessage()); Component msg = ev.getLeaveMessage().isEmpty() ? Component.empty() : GsonComponentSerializer.gson().deserialize(ev.getLeaveMessage());
yield new org.bukkit.event.player.PlayerQuitEvent(player, msg); yield new org.bukkit.event.player.PlayerQuitEvent(player, msg);
@ -1293,7 +1315,7 @@ public class PatchBukkitEventFactory {
java.util.UUID uuid = java.util.UUID.fromString(uuidStr); java.util.UUID uuid = java.util.UUID.fromString(uuidStr);
Player player = Bukkit.getServer().getPlayer(uuid); Player player = Bukkit.getServer().getPlayer(uuid);
if (player == null) { if (player == null) {
player = new org.patchbukkit.entity.PatchBukkitPlayer(uuid, "Player"); player = new org.patchbukkit.entity.CraftPlayer(uuid, "Player");
if (Bukkit.getServer() instanceof org.patchbukkit.PatchBukkitServer server) { if (Bukkit.getServer() instanceof org.patchbukkit.PatchBukkitServer server) {
server.registerPlayer(player); server.registerPlayer(player);
} }
@ -1306,7 +1328,7 @@ public class PatchBukkitEventFactory {
@NotNull @NotNull
public static Entity getEntity(int entityId) { public static Entity getEntity(int entityId) {
return new org.patchbukkit.entity.PatchBukkitEntity(new java.util.UUID(0, entityId), "Entity-" + entityId); return new org.patchbukkit.entity.CraftEntity(new java.util.UUID(0, entityId), "Entity-" + entityId);
} }
@Nullable @Nullable

View File

@ -0,0 +1,34 @@
package org.patchbukkit.inventory;
import org.bukkit.inventory.ItemStack;
public class CraftItemStack extends ItemStack {
public CraftItemStack() {
super();
}
public static net.minecraft.world.item.ItemStack asNMSCopy(ItemStack original) {
try {
return org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(original);
} catch (Throwable t) {
return null;
}
}
public static ItemStack asBukkitCopy(net.minecraft.world.item.ItemStack original) {
try {
return org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(original);
} catch (Throwable t) {
return null;
}
}
public static ItemStack asCraftMirror(net.minecraft.world.item.ItemStack original) {
try {
return org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitMirror(original);
} catch (Throwable t) {
return null;
}
}
}

View File

@ -0,0 +1,105 @@
package org.patchbukkit.loader;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.ClassRemapper;
import org.objectweb.asm.commons.Remapper;
import java.util.logging.Level;
import java.util.logging.Logger;
public final class BytecodeTransformer {
private static final Logger LOGGER = Logger.getLogger("BytecodeTransformer");
private static Remapper createRemapper(String currentClassName) {
final boolean isCraftBukkitReflection = currentClassName != null && currentClassName.contains("CraftBukkitReflection");
return new Remapper() {
@Override
public String map(String internalName) {
if (internalName == null) return null;
// Missing dot after craftbukkit (e.g. Cloud command framework: org/bukkit/craftbukkitcommand -> org/bukkit/craftbukkit/command)
if (internalName.startsWith("org/bukkit/craftbukkit") && !internalName.startsWith("org/bukkit/craftbukkit/")) {
return "org/bukkit/craftbukkit/" + internalName.substring("org/bukkit/craftbukkit".length());
}
// Remap legacy versioned CraftBukkit: org/bukkit/craftbukkit/v1_20_R3/... -> org/bukkit/craftbukkit/...
if (internalName.startsWith("org/bukkit/craftbukkit/v")) {
String unversioned = internalName.replaceFirst("^org/bukkit/craftbukkit/v[0-9_]+R[0-9]+/", "org/bukkit/craftbukkit/");
return map(unversioned);
}
// Keep CraftServer as is inside CraftBukkitReflection to prevent breaking its reflection logic
if (isCraftBukkitReflection && internalName.equals("org/bukkit/craftbukkit/CraftServer")) {
return internalName;
}
// Remap CraftServer -> PatchBukkitServer for seamless native casts and method calls
if (internalName.equals("org/bukkit/craftbukkit/CraftServer")) {
return "org/patchbukkit/PatchBukkitServer";
}
// Remap CraftPlayer -> PatchBukkitPlayer for seamless native casts and getHandle()
if (internalName.equals("org/bukkit/craftbukkit/entity/CraftPlayer")) {
return "org/patchbukkit/entity/PatchBukkitPlayer";
}
// Remap CraftScheduler -> BukkitScheduler
if (internalName.equals("org/bukkit/craftbukkit/scheduler/CraftScheduler")) {
return "org/bukkit/scheduler/BukkitScheduler";
}
return super.map(internalName);
}
};
}
private BytecodeTransformer() {}
public static byte[] transform(byte[] classBytes) {
if (classBytes == null || classBytes.length == 0) {
return classBytes;
}
try {
ClassReader reader = new ClassReader(classBytes);
String className = reader.getClassName();
ClassWriter writer = new ClassWriter(reader, 0);
Remapper remapper = createRemapper(className);
ClassVisitor cv = new ClassRemapper(writer, remapper);
if (className != null && className.contains("CraftBukkitReflection")) {
cv = new ClassVisitor(Opcodes.ASM9, cv) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if ("<clinit>".equals(name)) {
return new MethodVisitor(Opcodes.ASM9, mv) {
@Override
public void visitFieldInsn(int opcode, String owner, String fieldName, String fieldDesc) {
if (opcode == Opcodes.PUTSTATIC && "CB_PKG_VERSION".equals(fieldName)) {
// Pop whatever was calculated and push "."
super.visitInsn(Opcodes.POP);
super.visitLdcInsn(".");
}
super.visitFieldInsn(opcode, owner, fieldName, fieldDesc);
}
};
}
return mv;
}
};
}
reader.accept(cv, ClassReader.EXPAND_FRAMES);
return writer.toByteArray();
} catch (Throwable t) {
LOGGER.log(Level.FINE, "[BytecodeTransformer] Failed to transform class bytecode, returning original", t);
return classBytes;
}
}
}

View File

@ -0,0 +1,178 @@
package org.patchbukkit.loader;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Handles downloading and caching official Mojang server bytecode at runtime.
* This guarantees 100% authentic NMS classes and static reflection compliance
* without violating Mojang's EULA by redistributing proprietary bytecode.
*/
public final class MojangServerProvider {
private static final Logger LOGGER = Logger.getLogger("MojangServerProvider");
private static final String MOJANG_MANIFEST_URL =
"https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
private MojangServerProvider() {}
/**
* Ensures the official Mojang server jar is available in the target cache directory.
*
* @param cacheDir the folder to store cached server jars (e.g., patchbukkit-libs)
* @param mcVersion the target Minecraft version (e.g. "26.2" or "1.21.4")
* @return the File pointing to the cached server jar, or null if download failed
*/
public static File getOrDownloadServerJar(File cacheDir, String mcVersion) {
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
String versionKey = (mcVersion != null && !mcVersion.isBlank()) ? mcVersion : "latest";
File targetJar = new File(cacheDir, "mojang-server-" + versionKey + ".jar");
if (targetJar.exists() && targetJar.length() > 0) {
return targetJar;
}
// Development fallback: check if paperweight taskCache has mappedServerJar.jar
File devMappedJar = findDevMappedServerJar();
if (devMappedJar != null && devMappedJar.exists() && devMappedJar.length() > 0) {
LOGGER.info("[PatchBukkit] Found local development mappedServerJar: " + devMappedJar.getAbsolutePath());
try {
Files.copy(devMappedJar.toPath(), targetJar.toPath(), StandardCopyOption.REPLACE_EXISTING);
return targetJar;
} catch (Throwable t) {
LOGGER.log(Level.WARNING, "Failed to copy local dev jar, proceeding to download: " + t.getMessage());
}
}
try {
LOGGER.info("[PatchBukkit] Downloading official Mojang server bytecode for Minecraft " + versionKey + "...");
return downloadVanillaServer(targetJar, versionKey);
} catch (Throwable t) {
LOGGER.log(Level.SEVERE, "[PatchBukkit] Failed to obtain Mojang server jar: " + t.getMessage(), t);
return null;
}
}
private static File findDevMappedServerJar() {
File[] possibleLocations = new File[] {
new File(".gradle/caches/paperweight/taskCache/mappedServerJar.jar"),
new File("java/patchbukkit/.gradle/caches/paperweight/taskCache/mappedServerJar.jar"),
new File("../java/patchbukkit/.gradle/caches/paperweight/taskCache/mappedServerJar.jar")
};
for (File loc : possibleLocations) {
if (loc.exists()) {
return loc;
}
}
return null;
}
private static File downloadVanillaServer(File targetFile, String version) throws Exception {
// 1. Fetch Mojang version manifest
HttpURLConnection conn = (HttpURLConnection) URI.create(MOJANG_MANIFEST_URL).toURL().openConnection();
conn.setRequestProperty("User-Agent", "PatchBukkit-ServerProvider");
conn.setConnectTimeout(10000);
conn.setReadTimeout(15000);
JsonObject manifest;
try (InputStream in = conn.getInputStream();
InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
manifest = JsonParser.parseReader(reader).getAsJsonObject();
}
String targetUrl = null;
String latestRelease = manifest.getAsJsonObject("latest").get("release").getAsString();
JsonArray versions = manifest.getAsJsonArray("versions");
for (JsonElement elem : versions) {
JsonObject vObj = elem.getAsJsonObject();
String id = vObj.get("id").getAsString();
if (id.equalsIgnoreCase(version)) {
targetUrl = vObj.get("url").getAsString();
break;
}
}
// Fallback to latest release if exact version tag not found
if (targetUrl == null) {
LOGGER.warning("[PatchBukkit] Version " + version + " not found in manifest; falling back to latest release: " + latestRelease);
for (JsonElement elem : versions) {
JsonObject vObj = elem.getAsJsonObject();
if (vObj.get("id").getAsString().equalsIgnoreCase(latestRelease)) {
targetUrl = vObj.get("url").getAsString();
break;
}
}
}
if (targetUrl == null) {
throw new IllegalStateException("Could not resolve download metadata for Minecraft version: " + version);
}
// 2. Fetch version details
HttpURLConnection vConn = (HttpURLConnection) URI.create(targetUrl).toURL().openConnection();
vConn.setRequestProperty("User-Agent", "PatchBukkit-ServerProvider");
vConn.setConnectTimeout(10000);
vConn.setReadTimeout(15000);
JsonObject vDetails;
try (InputStream in = vConn.getInputStream();
InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
vDetails = JsonParser.parseReader(reader).getAsJsonObject();
}
JsonObject serverDownload = vDetails.getAsJsonObject("downloads").getAsJsonObject("server");
String serverJarUrl = serverDownload.get("url").getAsString();
String expectedSha1 = serverDownload.get("sha1").getAsString();
// 3. Download server.jar with SHA-1 verification
File tempFile = new File(targetFile.getParentFile(), targetFile.getName() + ".downloading");
MessageDigest digest = MessageDigest.getInstance("SHA-1");
HttpURLConnection downloadConn = (HttpURLConnection) URI.create(serverJarUrl).toURL().openConnection();
downloadConn.setRequestProperty("User-Agent", "PatchBukkit-ServerProvider");
downloadConn.setConnectTimeout(15000);
downloadConn.setReadTimeout(30000);
try (InputStream in = new BufferedInputStream(downloadConn.getInputStream());
FileOutputStream out = new FileOutputStream(tempFile)) {
byte[] buf = new byte[8192];
int read;
while ((read = in.read(buf)) != -1) {
out.write(buf, 0, read);
digest.update(buf, 0, read);
}
}
String actualSha1 = HexFormat.of().formatHex(digest.digest());
if (!actualSha1.equalsIgnoreCase(expectedSha1)) {
tempFile.delete();
throw new SecurityException("SHA-1 checksum mismatch for " + serverJarUrl + "! Expected: " + expectedSha1 + ", got: " + actualSha1);
}
Files.move(tempFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
LOGGER.info("[PatchBukkit] Successfully downloaded and verified official Mojang server bytecode (" + targetFile.length() / (1024 * 1024) + " MB).");
return targetFile;
}
}

View File

@ -23,6 +23,7 @@ public class PatchBukkitBootstrap {
public static boolean bootstrapPlugins(String pluginsDirPath) { public static boolean bootstrapPlugins(String pluginsDirPath) {
try { try {
org.patchbukkit.PatchBukkitServer.initServer();
File pluginsDir = new File(pluginsDirPath); File pluginsDir = new File(pluginsDirPath);
if (!pluginsDir.exists() || !pluginsDir.isDirectory()) { if (!pluginsDir.exists() || !pluginsDir.isDirectory()) {
LOGGER.info("[PatchBukkit] Plugins directory does not exist: " + pluginsDirPath); LOGGER.info("[PatchBukkit] Plugins directory does not exist: " + pluginsDirPath);
@ -208,6 +209,7 @@ public class PatchBukkitBootstrap {
} }
try { try {
org.patchbukkit.PatchBukkitServer.ensureCraftRegistry();
plugin.onLoad(); plugin.onLoad();
} catch (Throwable t) { } catch (Throwable t) {
LOGGER.log(Level.SEVERE, "[PatchBukkit] Error during onLoad() for " + holder.name, t); LOGGER.log(Level.SEVERE, "[PatchBukkit] Error during onLoad() for " + holder.name, t);

View File

@ -0,0 +1,128 @@
package org.patchbukkit.log;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.Core;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.patchbukkit.PatchBukkitServer;
import patchbukkit.bridge.NativeBridgeFfi;
import patchbukkit.log.LogLevel;
import patchbukkit.log.SendLogRequest;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
@Plugin(
name = "NativeBridge",
category = Core.CATEGORY_NAME,
elementType = org.apache.logging.log4j.core.Appender.ELEMENT_TYPE,
printObject = true
)
public class NativeBridgeLog4jAppender extends AbstractAppender {
protected NativeBridgeLog4jAppender(
String name,
Filter filter,
Layout<? extends Serializable> layout,
boolean ignoreExceptions,
Property[] properties
) {
super(name, filter, layout, ignoreExceptions, properties);
}
@PluginFactory
public static NativeBridgeLog4jAppender createAppender(
@PluginAttribute("name") String name,
@PluginElement("Filter") Filter filter,
@PluginElement("Layout") Layout<? extends Serializable> layout
) {
if (name == null) {
name = "NativeBridge";
}
return new NativeBridgeLog4jAppender(
name,
filter,
layout,
true,
Property.EMPTY_ARRAY
);
}
public static NativeBridgeLog4jAppender create(String name) {
return createAppender(name, null, null);
}
@Override
public void append(LogEvent event) {
if (event == null) return;
if (PatchBukkitServer.isLogging()) return;
PatchBukkitServer.setLogging(true);
try {
String loggerName = event.getLoggerName();
if (loggerName == null) {
loggerName = "";
}
// Filter internal JDK / system loggers if below WARNING
if (loggerName.startsWith("jdk.") || loggerName.startsWith("sun.") ||
loggerName.startsWith("java.") || loggerName.startsWith("javax.")) {
if (event.getLevel().intLevel() > Level.WARN.intLevel()) {
return;
}
}
LogLevel logLevel = toLogLevel(event.getLevel());
String message = event.getMessage() != null ? event.getMessage().getFormattedMessage() : "";
Throwable thrown = event.getThrown();
if (thrown != null) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
if (!message.isEmpty()) {
pw.println(message);
}
thrown.printStackTrace(pw);
pw.flush();
message = sw.toString();
}
try {
NativeBridgeFfi.sendLog(
SendLogRequest.newBuilder()
.setLevel(logLevel)
.setMessage(message)
.setLoggerName(loggerName)
.build()
);
} catch (Throwable t) {
PatchBukkitServer.ORIGINAL_ERR.println("[" + event.getLevel() + "][" + loggerName + "] " + message);
}
} catch (Throwable t) {
PatchBukkitServer.ORIGINAL_ERR.println("[NativeBridgeLog4jAppender Error] Failed to publish record: " + t.getMessage());
} finally {
PatchBukkitServer.setLogging(false);
}
}
public static LogLevel toLogLevel(Level level) {
if (level == null) return LogLevel.INFO;
if (level.isMoreSpecificThan(Level.ERROR)) {
return LogLevel.SEVERE;
} else if (level.isMoreSpecificThan(Level.WARN)) {
return LogLevel.WARNING;
} else if (level.isMoreSpecificThan(Level.INFO)) {
return LogLevel.INFO;
} else if (level.isMoreSpecificThan(Level.DEBUG)) {
return LogLevel.CONFIG;
} else {
return LogLevel.FINE;
}
}
}

View File

@ -0,0 +1,302 @@
package org.patchbukkit.network;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.channel.embedded.EmbeddedChannel;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.UUID;
import java.util.logging.Logger;
/**
* A virtual Netty channel representing a player's network pipeline.
* Fully emulates the vanilla Minecraft Netty pipeline structure, including
* timeout, legacy query, splitter, decrypt, decompress, decoder, prepender,
* encrypt, compress, encoder, bundle_packer, and packet_handler.
*/
public class PatchBukkitVirtualChannel extends EmbeddedChannel {
private static final Logger LOGGER = Logger.getLogger("PatchBukkitVirtualChannel");
private final UUID playerUuid;
private final SocketAddress localAddress;
private final SocketAddress remoteAddress;
private int compressionThreshold = 256;
private boolean encrypted = false;
public PatchBukkitVirtualChannel(UUID playerUuid) {
super();
this.playerUuid = playerUuid;
this.localAddress = new InetSocketAddress("127.0.0.1", 25565);
this.remoteAddress = new InetSocketAddress("127.0.0.1", 50000 + (Math.abs(playerUuid.hashCode()) % 10000));
setupPipeline();
}
private void setupPipeline() {
// Full Vanilla Minecraft pipeline structure:
// Inbound: timeout -> legacy_query -> splitter -> [decrypt] -> [decompress] -> decoder -> packet_handler
// Outbound: timeout -> prepender -> [encrypt] -> [compress] -> encoder -> [bundle_packer] -> packet_handler
// 1. "timeout" (ReadTimeoutHandler / WriteTimeoutHandler emulation)
pipeline().addLast("timeout", new ChannelDuplexHandler());
// 2. "legacy_query" (handles legacy 1.6- server ping queries)
pipeline().addLast("legacy_query", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
});
// 3. "splitter" (Varint21FrameDecoder)
pipeline().addLast("splitter", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
});
// 4. "decompress" (CompressionDecoder - enabled by default for play phase, threshold 256)
pipeline().addLast("decompress", new VirtualCompressionDecoder(compressionThreshold, false));
// 5. "decoder" (PacketDecoder: ByteBuf -> Packet)
pipeline().addLast("decoder", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
});
// 6. "prepender" (Varint21LengthFieldPrepender)
pipeline().addLast("prepender", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ctx.write(msg, promise);
}
});
// 7. "compress" (CompressionEncoder - enabled by default for play phase, threshold 256)
pipeline().addLast("compress", new VirtualCompressionEncoder(compressionThreshold));
// 8. "encoder" (PacketEncoder: Packet -> ByteBuf)
pipeline().addLast("encoder", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ctx.write(msg, promise);
}
});
// 9. "bundle_packer" (PacketBundlePacker - 1.20+)
pipeline().addLast("bundle_packer", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ctx.write(msg, promise);
}
});
// 10. "packet_handler" (Connection / ServerGamePacketListenerImpl terminal handler)
pipeline().addLast("packet_handler", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// Packet reached the end of inbound pipeline
if (msg instanceof ByteBuf buf) {
buf.release();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
LOGGER.fine("[PatchBukkitVirtualChannel] Exception in pipeline for " + playerUuid + ": " + cause.getMessage());
}
});
}
/**
* Updates or toggles compression handlers exactly like Vanilla Minecraft Connection.setupCompression().
*
* @param threshold the compression threshold, or -1 to disable
* @param validateDecompressed whether to validate decompressed size
*/
public void setupCompression(int threshold, boolean validateDecompressed) {
this.compressionThreshold = threshold;
if (threshold >= 0) {
var decompress = pipeline().get("decompress");
if (decompress instanceof VirtualCompressionDecoder cd) {
cd.setThreshold(threshold, validateDecompressed);
} else {
if (pipeline().get("decoder") != null) {
pipeline().addBefore("decoder", "decompress", new VirtualCompressionDecoder(threshold, validateDecompressed));
}
}
var compress = pipeline().get("compress");
if (compress instanceof VirtualCompressionEncoder ce) {
ce.setThreshold(threshold);
} else {
if (pipeline().get("encoder") != null) {
pipeline().addBefore("encoder", "compress", new VirtualCompressionEncoder(threshold));
}
}
} else {
if (pipeline().get("decompress") != null) {
pipeline().remove("decompress");
}
if (pipeline().get("compress") != null) {
pipeline().remove("compress");
}
}
}
public void setupCompression(int threshold) {
setupCompression(threshold, false);
}
public void setCompressionThreshold(int threshold) {
setupCompression(threshold, false);
}
public int getCompressionThreshold() {
return compressionThreshold;
}
public boolean isCompressed() {
return compressionThreshold >= 0 && pipeline().get("decompress") != null;
}
/**
* Enables or updates encryption handlers in the pipeline matching Vanilla Connection.setEncryptionKey().
*/
public void setEncryptionKey(Object decryptCipher, Object encryptCipher) {
this.encrypted = true;
if (pipeline().get("decrypt") == null && pipeline().get("splitter") != null) {
pipeline().addBefore("splitter", "decrypt", new VirtualCipherDecoder(decryptCipher));
}
if (pipeline().get("encrypt") == null && pipeline().get("prepender") != null) {
pipeline().addBefore("prepender", "encrypt", new VirtualCipherEncoder(encryptCipher));
}
}
public boolean isEncrypted() {
return encrypted;
}
public UUID getPlayerUuid() {
return playerUuid;
}
@Override
public SocketAddress localAddress() {
return localAddress;
}
@Override
public SocketAddress remoteAddress() {
return remoteAddress;
}
@Override
public boolean isActive() {
return isOpen();
}
/**
* Emulates Vanilla net.minecraft.network.CompressionDecoder.
*/
public static class VirtualCompressionDecoder extends ChannelInboundHandlerAdapter {
public int threshold;
public boolean validateDecompressed;
public VirtualCompressionDecoder(int threshold, boolean validateDecompressed) {
this.threshold = threshold;
this.validateDecompressed = validateDecompressed;
}
public int getThreshold() {
return threshold;
}
public void setThreshold(int threshold) {
this.threshold = threshold;
}
public void setThreshold(int threshold, boolean validateDecompressed) {
this.threshold = threshold;
this.validateDecompressed = validateDecompressed;
}
public int threshold() {
return threshold;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
}
/**
* Emulates Vanilla net.minecraft.network.CompressionEncoder.
*/
public static class VirtualCompressionEncoder extends ChannelOutboundHandlerAdapter {
public int threshold;
public VirtualCompressionEncoder(int threshold) {
this.threshold = threshold;
}
public int getThreshold() {
return threshold;
}
public void setThreshold(int threshold) {
this.threshold = threshold;
}
public int threshold() {
return threshold;
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ctx.write(msg, promise);
}
}
/**
* Emulates Vanilla net.minecraft.network.CipherDecoder.
*/
public static class VirtualCipherDecoder extends ChannelInboundHandlerAdapter {
public final Object cipher;
public VirtualCipherDecoder(Object cipher) {
this.cipher = cipher;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
}
/**
* Emulates Vanilla net.minecraft.network.CipherEncoder.
*/
public static class VirtualCipherEncoder extends ChannelOutboundHandlerAdapter {
public final Object cipher;
public VirtualCipherEncoder(Object cipher) {
this.cipher = cipher;
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ctx.write(msg, promise);
}
}
}

View File

@ -0,0 +1,264 @@
package org.patchbukkit.network;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.patchbukkit.entity.PatchBukkitPlayer;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Manages the lifecycle and I/O of virtual Netty channels for connected players.
*/
public final class VirtualChannelManager {
private static final Logger LOGGER = Logger.getLogger("VirtualChannelManager");
private static final VirtualChannelManager INSTANCE = new VirtualChannelManager();
private final Map<UUID, PatchBukkitVirtualChannel> channels = new ConcurrentHashMap<>();
private final Map<UUID, VirtualPlayerHandle> playerHandles = new ConcurrentHashMap<>();
private final Map<UUID, net.minecraft.server.level.ServerPlayer> serverPlayers = new ConcurrentHashMap<>();
private VirtualChannelManager() {}
public static VirtualChannelManager getInstance() {
return INSTANCE;
}
public PatchBukkitVirtualChannel getOrCreateChannel(UUID uuid) {
if (uuid == null) return null;
return channels.computeIfAbsent(uuid, PatchBukkitVirtualChannel::new);
}
public VirtualPlayerHandle getPlayerHandle(PatchBukkitPlayer player) {
if (player == null) return null;
return playerHandles.computeIfAbsent(player.getUniqueId(), uuid -> {
PatchBukkitVirtualChannel channel = getOrCreateChannel(uuid);
VirtualNetworkManager netManager = new VirtualNetworkManager(channel);
VirtualPlayerConnection conn = new VirtualPlayerConnection(netManager);
return new VirtualPlayerHandle(player, conn);
});
}
public synchronized net.minecraft.server.level.ServerPlayer getOrCreateServerPlayer(PatchBukkitPlayer player) {
if (player == null) return null;
UUID uuid = player.getUniqueId();
net.minecraft.server.level.ServerPlayer existing = serverPlayers.get(uuid);
if (existing != null) return existing;
try {
java.lang.reflect.Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
sun.misc.Unsafe unsafe = (sun.misc.Unsafe) unsafeField.get(null);
PatchBukkitVirtualChannel channel = getOrCreateChannel(uuid);
net.minecraft.network.Connection connection =
(net.minecraft.network.Connection) unsafe.allocateInstance(net.minecraft.network.Connection.class);
connection.channel = channel;
connection.address = player.getAddress() != null ? player.getAddress() : new java.net.InetSocketAddress("127.0.0.1", 54321);
for (java.lang.reflect.Field cf : net.minecraft.network.Connection.class.getDeclaredFields()) {
if (cf.getType().equals(net.minecraft.network.protocol.PacketFlow.class)) {
cf.setAccessible(true);
cf.set(connection, net.minecraft.network.protocol.PacketFlow.SERVERBOUND);
break;
}
}
net.minecraft.server.level.ServerPlayer serverPlayer =
(net.minecraft.server.level.ServerPlayer) unsafe.allocateInstance(net.minecraft.server.level.ServerPlayer.class);
serverPlayer.setId(player.getEntityId());
java.lang.reflect.Field entUuidField = net.minecraft.world.entity.Entity.class.getDeclaredField("uuid");
entUuidField.setAccessible(true);
entUuidField.set(serverPlayer, uuid);
java.lang.reflect.Field entStringUuidField = net.minecraft.world.entity.Entity.class.getDeclaredField("stringUUID");
entStringUuidField.setAccessible(true);
entStringUuidField.set(serverPlayer, uuid.toString());
com.mojang.authlib.GameProfile profile =
new com.mojang.authlib.GameProfile(uuid, player.getName() != null ? player.getName() : "Player");
java.lang.reflect.Field profileField = null;
Class<?> pClass = net.minecraft.server.level.ServerPlayer.class;
while (pClass != null && profileField == null) {
for (java.lang.reflect.Field pf : pClass.getDeclaredFields()) {
if (pf.getType().equals(com.mojang.authlib.GameProfile.class)) {
profileField = pf;
break;
}
}
pClass = pClass.getSuperclass();
}
if (profileField != null) {
profileField.setAccessible(true);
profileField.set(serverPlayer, profile);
}
net.minecraft.server.network.ServerGamePacketListenerImpl listener =
(net.minecraft.server.network.ServerGamePacketListenerImpl) unsafe.allocateInstance(net.minecraft.server.network.ServerGamePacketListenerImpl.class);
listener.player = serverPlayer;
Class<?> scClass = net.minecraft.server.network.ServerCommonPacketListenerImpl.class;
java.lang.reflect.Field serverField = scClass.getDeclaredField("server");
serverField.setAccessible(true);
serverField.set(listener, org.patchbukkit.PatchBukkitServer.getDedicatedServer());
java.lang.reflect.Field connField = scClass.getDeclaredField("connection");
connField.setAccessible(true);
connField.set(listener, connection);
serverPlayer.connection = listener;
try {
org.bukkit.craftbukkit.entity.CraftPlayer craftPlayer =
(org.bukkit.craftbukkit.entity.CraftPlayer) unsafe.allocateInstance(org.bukkit.craftbukkit.entity.CraftPlayer.class);
java.lang.reflect.Field entityField = null;
Class<?> cpClass = org.bukkit.craftbukkit.entity.CraftPlayer.class;
while (cpClass != null && entityField == null) {
for (java.lang.reflect.Field ef : cpClass.getDeclaredFields()) {
if (ef.getName().equals("entity")) {
entityField = ef;
break;
}
}
cpClass = cpClass.getSuperclass();
}
if (entityField != null) {
entityField.setAccessible(true);
entityField.set(craftPlayer, serverPlayer);
}
java.lang.reflect.Field bukkitEntityField = net.minecraft.world.entity.Entity.class.getDeclaredField("bukkitEntity");
bukkitEntityField.setAccessible(true);
bukkitEntityField.set(serverPlayer, craftPlayer);
} catch (Throwable t) {
LOGGER.log(Level.FINE, "[VirtualChannelManager] Failed to link CraftPlayer", t);
}
net.minecraft.server.network.ServerConnectionListener scl = org.patchbukkit.PatchBukkitServer.getServerConnection();
if (scl != null && scl.getConnections() != null) {
scl.getConnections().add(connection);
}
net.minecraft.server.dedicated.DedicatedPlayerList pl = org.patchbukkit.PatchBukkitServer.getDedicatedPlayerList();
if (pl != null) {
if (pl.getPlayers() != null) {
pl.getPlayers().add(serverPlayer);
}
try {
java.lang.reflect.Field byUuidField = null;
Class<?> curPl = pl.getClass();
while (curPl != null && byUuidField == null) {
for (java.lang.reflect.Field f : curPl.getDeclaredFields()) {
if (f.getName().equals("playersByUUID")) {
byUuidField = f;
break;
}
}
curPl = curPl.getSuperclass();
}
if (byUuidField != null) {
byUuidField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<UUID, net.minecraft.server.level.ServerPlayer> map =
(Map<UUID, net.minecraft.server.level.ServerPlayer>) byUuidField.get(pl);
if (map != null) {
map.put(uuid, serverPlayer);
}
}
} catch (Throwable ignored) {}
}
serverPlayers.put(uuid, serverPlayer);
return serverPlayer;
} catch (Throwable t) {
LOGGER.log(Level.WARNING, "[VirtualChannelManager] Failed to allocate ServerPlayer for " + uuid, t);
return null;
}
}
public void removePlayer(UUID uuid) {
if (uuid == null) return;
PatchBukkitVirtualChannel channel = channels.remove(uuid);
playerHandles.remove(uuid);
net.minecraft.server.level.ServerPlayer sp = serverPlayers.remove(uuid);
if (sp != null) {
if (sp.connection != null && sp.connection.connection != null) {
net.minecraft.server.network.ServerConnectionListener scl = org.patchbukkit.PatchBukkitServer.getServerConnection();
if (scl != null && scl.getConnections() != null) {
scl.getConnections().remove(sp.connection.connection);
}
}
net.minecraft.server.dedicated.DedicatedPlayerList pl = org.patchbukkit.PatchBukkitServer.getDedicatedPlayerList();
if (pl != null) {
if (pl.getPlayers() != null) {
pl.getPlayers().removeIf(p -> {
try {
return sp == p || uuid.equals(p.getUUID());
} catch (Throwable ignored) {
return false;
}
});
}
try {
java.lang.reflect.Field byUuidField = null;
Class<?> curPl = pl.getClass();
while (curPl != null && byUuidField == null) {
for (java.lang.reflect.Field f : curPl.getDeclaredFields()) {
if (f.getName().equals("playersByUUID")) {
byUuidField = f;
break;
}
}
curPl = curPl.getSuperclass();
}
if (byUuidField != null) {
byUuidField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<UUID, net.minecraft.server.level.ServerPlayer> map =
(Map<UUID, net.minecraft.server.level.ServerPlayer>) byUuidField.get(pl);
if (map != null) {
map.remove(uuid);
}
}
} catch (Throwable ignored) {}
}
}
if (channel != null && channel.isOpen()) {
channel.close();
}
}
public void handlePacketReceived(UUID uuid, int packetId, byte[] payload) {
if (uuid == null || payload == null) return;
PatchBukkitVirtualChannel channel = getOrCreateChannel(uuid);
if (channel == null || !channel.isActive()) return;
try {
ByteBuf buffer = Unpooled.wrappedBuffer(payload);
channel.pipeline().fireChannelRead(buffer);
} catch (Throwable t) {
LOGGER.log(Level.FINE, "[VirtualChannelManager] Error in inbound packet processing for " + uuid, t);
}
}
public void handlePacketSent(UUID uuid, int packetId, byte[] payload) {
if (uuid == null || payload == null) return;
PatchBukkitVirtualChannel channel = getOrCreateChannel(uuid);
if (channel == null || !channel.isActive()) return;
try {
ByteBuf buffer = Unpooled.wrappedBuffer(payload);
channel.write(buffer);
} catch (Throwable t) {
LOGGER.log(Level.FINE, "[VirtualChannelManager] Error in outbound packet processing for " + uuid, t);
}
}
}

View File

@ -0,0 +1,106 @@
package org.patchbukkit.network;
import io.netty.channel.Channel;
import java.net.SocketAddress;
/**
* Emulates the NMS net.minecraft.network.Connection / NetworkManager object
* which protocol injection frameworks and anticheats reflect on.
*/
public class VirtualNetworkManager {
public final Channel channel;
public VirtualNetworkManager(Channel channel) {
this.channel = channel;
}
public Channel getChannel() {
return channel;
}
public SocketAddress getRemoteAddress() {
return channel != null ? channel.remoteAddress() : null;
}
public SocketAddress getLocalAddress() {
return channel != null ? channel.localAddress() : null;
}
public boolean isConnected() {
return channel != null && channel.isActive();
}
public boolean isMemoryConnection() {
return false;
}
public boolean isEncrypted() {
return channel instanceof PatchBukkitVirtualChannel v && v.isEncrypted();
}
public boolean isCompressed() {
return channel instanceof PatchBukkitVirtualChannel v && v.isCompressed();
}
public int getCompressionThreshold() {
return channel instanceof PatchBukkitVirtualChannel v ? v.getCompressionThreshold() : -1;
}
public void setupCompression(int threshold, boolean validateDecompressed) {
if (channel instanceof PatchBukkitVirtualChannel v) {
v.setupCompression(threshold, validateDecompressed);
}
}
public void setupCompression(int threshold) {
setupCompression(threshold, false);
}
public void setCompressionThreshold(int threshold) {
setupCompression(threshold, false);
}
public void setEncryptionKey(Object decryptCipher, Object encryptCipher) {
if (channel instanceof PatchBukkitVirtualChannel v) {
v.setEncryptionKey(decryptCipher, encryptCipher);
}
}
public void send(Object packet) {
if (channel != null && channel.isActive()) {
channel.writeAndFlush(packet);
}
}
public void send(Object packet, Object listener) {
send(packet);
}
public void sendPacket(Object packet) {
send(packet);
}
public void disconnect(Object reason) {
if (channel != null && channel.isOpen()) {
channel.close();
}
}
public void handleDisconnection() {
disconnect(null);
}
public void setReadOnly() {
// No-op for read-only pipeline state
}
public float getAverageReceivedPackets() {
return 0.0f;
}
public float getAverageSentPackets() {
return 0.0f;
}
}

View File

@ -0,0 +1,83 @@
package org.patchbukkit.network;
/**
* Acts as the NMS ServerGamePacketListenerImpl / PlayerConnection object
* which protocol injection frameworks inspect to find the network connection.
*/
public class VirtualPlayerConnection implements io.papermc.paper.connection.PlayerConnection {
// Mojang field name: connection
public final VirtualNetworkManager connection;
// Spigot / legacy field name: networkManager
public final VirtualNetworkManager networkManager;
// Direct channel alias for reflection libraries looking for Channel field
public final io.netty.channel.Channel channel;
public VirtualPlayerConnection(VirtualNetworkManager networkManager) {
this.connection = networkManager;
this.networkManager = networkManager;
this.channel = networkManager != null ? networkManager.channel : null;
}
public VirtualNetworkManager getConnection() {
return connection;
}
public VirtualNetworkManager getNetworkManager() {
return networkManager;
}
public io.netty.channel.Channel getChannel() {
return channel;
}
public void send(Object packet) {
if (connection != null) {
connection.send(packet);
}
}
public void sendPacket(Object packet) {
send(packet);
}
@Override
public void disconnect(net.kyori.adventure.text.Component reason) {
if (connection != null && connection.channel != null) {
connection.channel.close();
}
}
@Override
public boolean isConnected() {
return connection != null && connection.channel != null && connection.channel.isActive();
}
@Override
public boolean isTransferred() {
return false;
}
@Override
public java.net.SocketAddress getAddress() {
return connection != null && connection.channel != null ? connection.channel.remoteAddress() : null;
}
@Override
public java.net.InetSocketAddress getClientAddress() {
java.net.SocketAddress addr = getAddress();
return addr instanceof java.net.InetSocketAddress ? (java.net.InetSocketAddress) addr : null;
}
@Override
public java.net.InetSocketAddress getVirtualHost() {
return null;
}
@Override
public java.net.InetSocketAddress getHAProxyAddress() {
return null;
}
}

View File

@ -0,0 +1,45 @@
package org.patchbukkit.network;
import org.patchbukkit.entity.PatchBukkitPlayer;
import java.util.UUID;
/**
* Acts as the NMS ServerPlayer / EntityPlayer handle returned by Player.getHandle().
*/
public class VirtualPlayerHandle {
public final PatchBukkitPlayer player;
public final UUID uuid;
public final String name;
// Mojang field name: connection
public final VirtualPlayerConnection connection;
// Spigot / legacy field name: playerConnection
public final VirtualPlayerConnection playerConnection;
public VirtualPlayerHandle(PatchBukkitPlayer player, VirtualPlayerConnection connection) {
this.player = player;
this.uuid = player.getUniqueId();
this.name = player.getName();
this.connection = connection;
this.playerConnection = connection;
}
public PatchBukkitPlayer getBukkitEntity() {
return player;
}
public UUID getUUID() {
return uuid;
}
public String getName() {
return name;
}
public VirtualPlayerConnection getConnection() {
return connection;
}
}

View File

@ -0,0 +1,227 @@
package org.patchbukkit.registry;
import io.papermc.paper.util.Holderable;
import net.kyori.adventure.key.Key;
import org.bukkit.NamespacedKey;
import org.bukkit.craftbukkit.damage.CraftDamageEffect;
import org.bukkit.craftbukkit.damage.CraftDamageType;
import org.bukkit.craftbukkit.util.CraftNamespacedKey;
import org.bukkit.damage.DamageEffect;
import org.bukkit.damage.DamageScaling;
import org.bukkit.damage.DamageType;
import org.bukkit.damage.DeathMessageType;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.Locale;
import java.util.Objects;
public class PatchBukkitDamageType implements DamageType, Holderable<net.minecraft.world.damagesource.DamageType> {
private static final Field HOLDER_REF_VALUE_FIELD;
static {
Field field = null;
try {
field = net.minecraft.core.Holder.Reference.class.getDeclaredField("value");
field.setAccessible(true);
} catch (Throwable ignored) {}
HOLDER_REF_VALUE_FIELD = field;
}
private final NamespacedKey namespacedKey;
private final Key adventureKey;
private final String translationKey;
private final DamageScaling damageScaling;
private final DamageEffect damageEffect;
private final DeathMessageType deathMessageType;
private final float exhaustion;
private final net.minecraft.world.damagesource.DamageType nmsHandle;
private final net.minecraft.core.Holder<net.minecraft.world.damagesource.DamageType> holder;
public PatchBukkitDamageType(@NonNull NamespacedKey key) {
this(
key,
"death.attack." + (key != null ? key.getKey() : "generic"),
determineScaling(key),
determineEffect(key),
determineDeathMessageType(key),
determineExhaustion(key)
);
}
public PatchBukkitDamageType(
@NonNull NamespacedKey namespacedKey,
@Nullable String translationKey,
@Nullable DamageScaling damageScaling,
@Nullable DamageEffect damageEffect,
@Nullable DeathMessageType deathMessageType,
float exhaustion
) {
if (namespacedKey == null) {
namespacedKey = NamespacedKey.minecraft("generic");
}
this.namespacedKey = namespacedKey;
this.adventureKey = Key.key(namespacedKey.namespace(), namespacedKey.value());
this.translationKey = translationKey != null ? translationKey : ("death.attack." + namespacedKey.getKey());
this.damageScaling = damageScaling != null ? damageScaling : DamageScaling.WHEN_CAUSED_BY_LIVING_NON_PLAYER;
this.damageEffect = damageEffect != null ? damageEffect : getFallbackEffect();
this.deathMessageType = deathMessageType != null ? deathMessageType : DeathMessageType.DEFAULT;
this.exhaustion = exhaustion;
net.minecraft.world.damagesource.DamageType nms = null;
net.minecraft.core.Holder<net.minecraft.world.damagesource.DamageType> h = null;
try {
var scalingNms = CraftDamageType.damageScalingToNMS(this.damageScaling);
var effectsNms = this.damageEffect instanceof CraftDamageEffect craftEffect
? craftEffect.getHandle()
: net.minecraft.world.damagesource.DamageEffects.HURT;
var deathMessageNms = CraftDamageType.deathMessageTypeToNMS(this.deathMessageType);
nms = new net.minecraft.world.damagesource.DamageType(
this.namespacedKey.getKey(),
scalingNms,
this.exhaustion,
effectsNms,
deathMessageNms
);
var nmsKey = net.minecraft.resources.ResourceKey.create(
net.minecraft.core.registries.Registries.DAMAGE_TYPE,
CraftNamespacedKey.toMinecraft(this.namespacedKey)
);
var ref = net.minecraft.core.Holder.Reference.createStandAlone(null, nmsKey);
if (HOLDER_REF_VALUE_FIELD != null) {
HOLDER_REF_VALUE_FIELD.set(ref, nms);
}
h = ref;
} catch (Throwable ignored) {}
this.nmsHandle = nms;
this.holder = h;
}
private static DamageEffect getFallbackEffect() {
try {
if (DamageEffect.HURT != null) {
return DamageEffect.HURT;
}
} catch (Throwable ignored) {}
return () -> org.bukkit.Sound.ENTITY_PLAYER_HURT;
}
private static DamageScaling determineScaling(@Nullable NamespacedKey key) {
if (key == null) return DamageScaling.WHEN_CAUSED_BY_LIVING_NON_PLAYER;
String val = key.getKey().toLowerCase(Locale.ROOT);
return switch (val) {
case "fall", "drown", "starve", "freeze", "in_fire", "dry_out", "in_wall",
"cramming", "fly_into_wall", "stalagmite", "out_of_world", "outside_border",
"generic_kill" -> DamageScaling.NEVER;
case "player_attack", "player_explosion", "explosion" -> DamageScaling.ALWAYS;
default -> DamageScaling.WHEN_CAUSED_BY_LIVING_NON_PLAYER;
};
}
private static DamageEffect determineEffect(@Nullable NamespacedKey key) {
if (key == null) return getFallbackEffect();
String val = key.getKey().toLowerCase(Locale.ROOT);
try {
return switch (val) {
case "in_fire", "on_fire", "lava", "campfire", "hot_floor" ->
DamageEffect.BURNING != null ? DamageEffect.BURNING : (() -> org.bukkit.Sound.ENTITY_PLAYER_HURT_ON_FIRE);
case "drown" ->
DamageEffect.DROWNING != null ? DamageEffect.DROWNING : (() -> org.bukkit.Sound.ENTITY_PLAYER_HURT_DROWN);
case "freeze" ->
DamageEffect.FREEZING != null ? DamageEffect.FREEZING : (() -> org.bukkit.Sound.ENTITY_PLAYER_HURT_FREEZE);
case "cactus", "sweet_berry_bush", "stalagmite" ->
DamageEffect.POKING != null ? DamageEffect.POKING : (() -> org.bukkit.Sound.ENTITY_PLAYER_HURT_SWEET_BERRY_BUSH);
case "thorns" ->
DamageEffect.THORNS != null ? DamageEffect.THORNS : (() -> org.bukkit.Sound.ENCHANT_THORNS_HIT);
default -> getFallbackEffect();
};
} catch (Throwable ignored) {
return getFallbackEffect();
}
}
private static DeathMessageType determineDeathMessageType(@Nullable NamespacedKey key) {
if (key == null) return DeathMessageType.DEFAULT;
String val = key.getKey().toLowerCase(Locale.ROOT);
return switch (val) {
case "bad_respawn_point" -> DeathMessageType.INTENTIONAL_GAME_DESIGN;
case "fall", "stalagmite" -> DeathMessageType.FALL_VARIANTS;
default -> DeathMessageType.DEFAULT;
};
}
private static float determineExhaustion(@Nullable NamespacedKey key) {
if (key == null) return 0.1f;
String val = key.getKey().toLowerCase(Locale.ROOT);
return switch (val) {
case "freeze", "starve" -> 0.0f;
default -> 0.1f;
};
}
@Override
public net.minecraft.core.Holder<net.minecraft.world.damagesource.DamageType> getHolder() {
return holder;
}
@Override
public net.minecraft.world.damagesource.DamageType getHandle() {
return nmsHandle;
}
@Override
public @NonNull NamespacedKey getKey() {
return namespacedKey;
}
@Override
public @NonNull Key key() {
return adventureKey;
}
@Override
public @NonNull String getTranslationKey() {
return translationKey;
}
@Override
public @NonNull DamageScaling getDamageScaling() {
return damageScaling;
}
@Override
public @NonNull DamageEffect getDamageEffect() {
return damageEffect;
}
@Override
public @NonNull DeathMessageType getDeathMessageType() {
return deathMessageType;
}
@Override
public float getExhaustion() {
return exhaustion;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof DamageType that)) return false;
return Objects.equals(namespacedKey, that.getKey());
}
@Override
public int hashCode() {
return Objects.hashCode(namespacedKey);
}
@Override
public String toString() {
return "PatchBukkitDamageType{" + namespacedKey + "}";
}
}

View File

@ -124,6 +124,11 @@ public class PatchBukkitRegistry<P, B extends Keyed> implements Registry<B> {
} }
} else if (RegistryKey.SOUND_EVENT.equals(registryKey) || "sound_event".equalsIgnoreCase(registryKey.key().value())) { } else if (RegistryKey.SOUND_EVENT.equals(registryKey) || "sound_event".equalsIgnoreCase(registryKey.key().value())) {
// Do not reflectively autoDiscover Sound.class because Sound.<clinit> depends on Registry.SOUNDS! // Do not reflectively autoDiscover Sound.class because Sound.<clinit> depends on Registry.SOUNDS!
} else if (RegistryKey.MENU.equals(registryKey) || "menu".equalsIgnoreCase(registryKey.key().value())) {
// Do not reflectively autoDiscover MenuType.class because MenuType.<clinit> depends on Registry.MENU!
} else if (RegistryKey.DAMAGE_TYPE.equals(registryKey) || "damage_type".equalsIgnoreCase(registryKey.key().value())) {
// Do not reflectively autoDiscover DamageType.class because DamageType.<clinit> depends on Registry.DAMAGE_TYPE!
populateDefaultDamageTypes();
} else { } else {
Class<B> valueClass = (Class<B>) LegacyRegistryIdentifiers.KEY_TO_CLASS_MAP.get(registryKey); Class<B> valueClass = (Class<B>) LegacyRegistryIdentifiers.KEY_TO_CLASS_MAP.get(registryKey);
if (valueClass != null) { if (valueClass != null) {
@ -134,6 +139,68 @@ public class PatchBukkitRegistry<P, B extends Keyed> implements Registry<B> {
} }
} }
private static final String[] DEFAULT_DAMAGE_TYPES = {
"arrow",
"bad_respawn_point",
"cactus",
"campfire",
"cramming",
"dragon_breath",
"drown",
"dry_out",
"ender_pearl",
"explosion",
"fall",
"falling_anvil",
"falling_block",
"falling_stalactite",
"fireball",
"fireworks",
"fly_into_wall",
"freeze",
"generic",
"generic_kill",
"hot_floor",
"in_fire",
"in_wall",
"indirect_magic",
"lava",
"lightning_bolt",
"mace_smash",
"magic",
"mob_attack",
"mob_attack_no_aggro",
"mob_projectile",
"on_fire",
"out_of_world",
"outside_border",
"player_attack",
"player_explosion",
"sonic_boom",
"spear",
"spit",
"stalagmite",
"starve",
"sting",
"sulfur_cube_hot",
"sweet_berry_bush",
"thorns",
"thrown",
"trident",
"unattributed_fireball",
"wind_charge",
"wither",
"wither_skull"
};
@SuppressWarnings("unchecked")
private void populateDefaultDamageTypes() {
for (String name : DEFAULT_DAMAGE_TYPES) {
NamespacedKey key = NamespacedKey.minecraft(name);
entries.put(key, (B) new PatchBukkitDamageType(key));
}
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private static <B extends Keyed> Map<NamespacedKey, B> autoDiscover(Class<B> clazz) { private static <B extends Keyed> Map<NamespacedKey, B> autoDiscover(Class<B> clazz) {
Map<NamespacedKey, B> map = new LinkedHashMap<>(); Map<NamespacedKey, B> map = new LinkedHashMap<>();
@ -222,6 +289,26 @@ public class PatchBukkitRegistry<P, B extends Keyed> implements Registry<B> {
} }
} catch (Throwable ignored) {} } catch (Throwable ignored) {}
} }
// Fallback for MenuType via CraftMenuType
if (RegistryKey.MENU.equals(registryKey) || "menu".equalsIgnoreCase(registryKey != null ? registryKey.key().value() : "")) {
try {
var nmsIdent = org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(key);
var holderOpt = net.minecraft.core.registries.BuiltInRegistries.MENU.get(nmsIdent);
if (holderOpt != null && holderOpt.isPresent()) {
B menuType = (B) new org.bukkit.craftbukkit.inventory.CraftMenuType<>(holderOpt.get());
entries.put(key, menuType);
return menuType;
}
} catch (Throwable ignored) {}
}
// Fallback for DamageType
if (RegistryKey.DAMAGE_TYPE.equals(registryKey) || "damage_type".equalsIgnoreCase(registryKey != null ? registryKey.key().value() : "")) {
try {
PatchBukkitDamageType damageType = new PatchBukkitDamageType(key);
entries.put(key, (B) damageType);
return (B) damageType;
} catch (Throwable ignored) {}
}
} finally { } finally {
fallbackSet.remove(key); fallbackSet.remove(key);
} }

View File

@ -44,10 +44,32 @@ public class PatchBukkitRegistryAccess extends io.papermc.paper.registry.PaperRe
return this.getRegistry(registryKey); return this.getRegistry(registryKey);
} }
@Override
public <M> void lockReferenceHolders(final net.minecraft.resources.ResourceKey<? extends net.minecraft.core.Registry<M>> resourceKey) {
try {
final var entry = io.papermc.paper.registry.PaperRegistries.getEntry(resourceKey);
if (entry != null) {
Registry<?> reg = super.getRegistry(entry.apiKey());
if (reg instanceof org.bukkit.craftbukkit.CraftRegistry<?, ?> craftRegistry) {
craftRegistry.lockReferenceHolders();
}
}
} catch (Throwable ignored) {}
}
@SuppressWarnings({"unchecked", "rawtypes"}) @SuppressWarnings({"unchecked", "rawtypes"})
public <T extends Keyed> Registry<T> getRegistry(RegistryKey<T> registryKey) { public <T extends Keyed> Registry<T> getRegistry(RegistryKey<T> registryKey) {
if (registryKey == null) return null; if (registryKey == null) return null;
if (!FACTORIES.containsKey(registryKey) && !RegistryKey.ITEM.equals(registryKey) && !RegistryKey.BLOCK.equals(registryKey) && !RegistryKey.DAMAGE_TYPE.equals(registryKey)) {
try {
Registry<T> superReg = super.getRegistry(registryKey);
if (superReg != null) {
return superReg;
}
} catch (Throwable ignored) {}
}
Registry<?> existing = instances.get(registryKey); Registry<?> existing = instances.get(registryKey);
if (existing != null) { if (existing != null) {
return (Registry<T>) existing; return (Registry<T>) existing;

View File

@ -0,0 +1,29 @@
package org.patchbukkit.util;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.item.Item;
import org.bukkit.Material;
public class CraftMagicNumbers {
public static BlockState getBlock(org.bukkit.material.MaterialData material) {
return org.bukkit.craftbukkit.util.CraftMagicNumbers.getBlock(material);
}
public static Block getBlock(Material material) {
return org.bukkit.craftbukkit.util.CraftMagicNumbers.getBlock(material);
}
public static Material getMaterial(Block block) {
return org.bukkit.craftbukkit.util.CraftMagicNumbers.getMaterial(block);
}
public static Item getItem(Material material) {
return org.bukkit.craftbukkit.util.CraftMagicNumbers.getItem(material);
}
public static Material getMaterial(Item item) {
return org.bukkit.craftbukkit.util.CraftMagicNumbers.getMaterial(item);
}
}

View File

@ -34,10 +34,13 @@ public final class Versioning {
} catch (final IOException ex) { } catch (final IOException ex) {
Logger.getLogger(Versioning.class.getName()).log(Level.SEVERE, "Could not get Bukkit version!", ex); Logger.getLogger(Versioning.class.getName()).log(Level.SEVERE, "Could not get Bukkit version!", ex);
} }
BUKKIT_VERSION = bukkitVersion;
if (apiVersion == null) { if (apiVersion == null) {
apiVersion = "Unknown-Version"; apiVersion = "26.3";
} }
if (bukkitVersion == null || bukkitVersion.contains(".build.")) {
bukkitVersion = apiVersion + "-R0.1-SNAPSHOT";
}
BUKKIT_VERSION = bukkitVersion;
API_VERSION = apiVersion; API_VERSION = apiVersion;
} }

View File

@ -146,6 +146,7 @@ public class PatchBukkitRegionAccessor implements RegionAccessor {
) { ) {
UUID worldUuid = (this instanceof PatchBukkitWorld w) ? w.getUID() : UUID.randomUUID(); UUID worldUuid = (this instanceof PatchBukkitWorld w) ? w.getUID() : UUID.randomUUID();
UUID entityUuid = UUID.randomUUID(); UUID entityUuid = UUID.randomUUID();
int entityId = -1;
try { try {
var res = NativeBridgeFfi.spawnWorldEntity(SpawnWorldEntityRequest.newBuilder() var res = NativeBridgeFfi.spawnWorldEntity(SpawnWorldEntityRequest.newBuilder()
.setWorldUuid(BridgeUtils.convertUuid(worldUuid)) .setWorldUuid(BridgeUtils.convertUuid(worldUuid))
@ -156,12 +157,17 @@ public class PatchBukkitRegionAccessor implements RegionAccessor {
.setYaw(loc.getYaw()) .setYaw(loc.getYaw())
.setPitch(loc.getPitch()) .setPitch(loc.getPitch())
.build()); .build());
if (res != null && res.hasEntityUuid()) { if (res != null) {
entityUuid = UUID.fromString(res.getEntityUuid().getValue()); if (res.hasEntityUuid()) {
entityUuid = UUID.fromString(res.getEntityUuid().getValue());
}
if (res.getEntityId() > 0) {
entityId = res.getEntityId();
}
} }
} catch (Throwable ignored) {} } catch (Throwable ignored) {}
Entity entity = PatchBukkitEntity.create(entityUuid, type, loc); Entity entity = PatchBukkitEntity.create(entityUuid, type, loc, entityId);
return entity; return entity;
} }

View File

@ -129,14 +129,14 @@ public class PatchBukkitWorld extends PatchBukkitRegionAccessor implements World
} }
}; };
private PatchBukkitWorld(UUID uuid) { protected PatchBukkitWorld(UUID uuid) {
this.uuid = uuid; this.uuid = uuid;
initDefaultGameRules(); initDefaultGameRules();
syncWorldInfo(); syncWorldInfo();
} }
public static PatchBukkitWorld getOrCreate(UUID uuid) { public static PatchBukkitWorld getOrCreate(UUID uuid) {
return instances.computeIfAbsent(uuid, PatchBukkitWorld::new); return instances.computeIfAbsent(uuid, org.patchbukkit.CraftWorld::new);
} }
public static PatchBukkitWorld getOrCreate(String uuid) { public static PatchBukkitWorld getOrCreate(String uuid) {
@ -353,6 +353,7 @@ public class PatchBukkitWorld extends PatchBukkitRegionAccessor implements World
@Override @Override
public @NotNull Item dropItem(@NotNull Location loc, @NotNull ItemStack item, @Nullable Consumer<? super Item> function) { public @NotNull Item dropItem(@NotNull Location loc, @NotNull ItemStack item, @Nullable Consumer<? super Item> function) {
UUID entityUuid = UUID.randomUUID(); UUID entityUuid = UUID.randomUUID();
int entityId = -1;
try { try {
var res = NativeBridgeFfi.spawnWorldEntity(SpawnWorldEntityRequest.newBuilder() var res = NativeBridgeFfi.spawnWorldEntity(SpawnWorldEntityRequest.newBuilder()
.setWorldUuid(BridgeUtils.convertUuid(this.uuid)) .setWorldUuid(BridgeUtils.convertUuid(this.uuid))
@ -361,12 +362,17 @@ public class PatchBukkitWorld extends PatchBukkitRegionAccessor implements World
.setY(loc.getY()) .setY(loc.getY())
.setZ(loc.getZ()) .setZ(loc.getZ())
.build()); .build());
if (res != null && res.hasEntityUuid()) { if (res != null) {
entityUuid = UUID.fromString(res.getEntityUuid().getValue()); if (res.hasEntityUuid()) {
entityUuid = UUID.fromString(res.getEntityUuid().getValue());
}
if (res.getEntityId() > 0) {
entityId = res.getEntityId();
}
} }
} catch (Throwable ignored) {} } catch (Throwable ignored) {}
Item itemEntity = (Item) PatchBukkitEntity.create(entityUuid, EntityType.ITEM, loc); Item itemEntity = (Item) PatchBukkitEntity.create(entityUuid, EntityType.ITEM, loc, entityId);
if (function != null) { if (function != null) {
function.accept(itemEntity); function.accept(itemEntity);
} }
@ -403,6 +409,7 @@ public class PatchBukkitWorld extends PatchBukkitRegionAccessor implements World
@Override @Override
public @NotNull LightningStrike strikeLightning(@NotNull Location loc) { public @NotNull LightningStrike strikeLightning(@NotNull Location loc) {
UUID entityUuid = UUID.randomUUID(); UUID entityUuid = UUID.randomUUID();
int entityId = -1;
try { try {
var res = NativeBridgeFfi.spawnWorldEntity(SpawnWorldEntityRequest.newBuilder() var res = NativeBridgeFfi.spawnWorldEntity(SpawnWorldEntityRequest.newBuilder()
.setWorldUuid(BridgeUtils.convertUuid(this.uuid)) .setWorldUuid(BridgeUtils.convertUuid(this.uuid))
@ -411,12 +418,17 @@ public class PatchBukkitWorld extends PatchBukkitRegionAccessor implements World
.setY(loc.getY()) .setY(loc.getY())
.setZ(loc.getZ()) .setZ(loc.getZ())
.build()); .build());
if (res != null && res.hasEntityUuid()) { if (res != null) {
entityUuid = UUID.fromString(res.getEntityUuid().getValue()); if (res.hasEntityUuid()) {
entityUuid = UUID.fromString(res.getEntityUuid().getValue());
}
if (res.getEntityId() > 0) {
entityId = res.getEntityId();
}
} }
} catch (Throwable ignored) {} } catch (Throwable ignored) {}
LightningStrike bolt = (LightningStrike) PatchBukkitEntity.create(entityUuid, EntityType.LIGHTNING_BOLT, loc); LightningStrike bolt = (LightningStrike) PatchBukkitEntity.create(entityUuid, EntityType.LIGHTNING_BOLT, loc, entityId);
registerEntity(bolt); registerEntity(bolt);
return bolt; return bolt;
} }
@ -458,7 +470,7 @@ public class PatchBukkitWorld extends PatchBukkitRegionAccessor implements World
type = EntityType.valueOf(summary.getEntityType()); type = EntityType.valueOf(summary.getEntityType());
} catch (Throwable ignored) {} } catch (Throwable ignored) {}
Location loc = new Location(this, summary.getX(), summary.getY(), summary.getZ(), summary.getYaw(), summary.getPitch()); Location loc = new Location(this, summary.getX(), summary.getY(), summary.getZ(), summary.getYaw(), summary.getPitch());
Entity e = PatchBukkitEntity.create(u, type, loc); Entity e = PatchBukkitEntity.create(u, type, loc, summary.getEntityId());
list.add(e); list.add(e);
} }
} }
@ -1590,6 +1602,11 @@ public class PatchBukkitWorld extends PatchBukkitRegionAccessor implements World
spawnParticle(particle, x, y, z, count, offsetX, offsetY, offsetZ, extra, data, force); spawnParticle(particle, x, y, z, count, offsetX, offsetY, offsetZ, extra, data, force);
} }
@Override
public <T> void spawnParticle(@NotNull Particle particle, @Nullable List<Player> receivers, @Nullable Player source, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double extra, double xSpread, double ySpread, @Nullable T data, boolean force, @NotNull Particle.RandomizationType randomizationType) {
spawnParticle(particle, x, y, z, count, offsetX, offsetY, offsetZ, extra, data, force);
}
@Override @Override
public void playSound(@NotNull Entity entity, @NotNull Sound sound, @NotNull SoundCategory category, float volume, float pitch) { public void playSound(@NotNull Entity entity, @NotNull Sound sound, @NotNull SoundCategory category, float volume, float pitch) {
playSound(entity.getLocation(), sound, category, volume, pitch); playSound(entity.getLocation(), sound, category, volume, pitch);
@ -1724,6 +1741,22 @@ public class PatchBukkitWorld extends PatchBukkitRegionAccessor implements World
return null; return null;
} }
public @Nullable Entity getEntity(int entityId) {
for (Entity e : getEntities()) {
if (e.getEntityId() == entityId) {
return e;
}
}
try {
var resp = NativeBridgeFfi.getEntityUuid(patchbukkit.entity.GetEntityUuidRequest.newBuilder().setEntityId(entityId).build());
if (resp != null && resp.hasUuid()) {
UUID u = UUID.fromString(resp.getUuid().getValue());
return getEntity(u);
}
} catch (Throwable ignored) {}
return null;
}
@Override @Override
public @Nullable RayTraceResult rayTraceBlocks(@NotNull Position start, @NotNull Vector direction, double maxDistance, @NotNull FluidCollisionMode fluidCollisionMode, boolean ignorePassableBlocks, @Nullable Predicate<? super Block> filter) { public @Nullable RayTraceResult rayTraceBlocks(@NotNull Position start, @NotNull Vector direction, double maxDistance, @NotNull FluidCollisionMode fluidCollisionMode, boolean ignorePassableBlocks, @Nullable Predicate<? super Block> filter) {
return rayTraceBlocks(new Location(this, start.x(), start.y(), start.z()), direction, maxDistance, fluidCollisionMode, ignorePassableBlocks); return rayTraceBlocks(new Location(this, start.x(), start.y(), start.z()), direction, maxDistance, fluidCollisionMode, ignorePassableBlocks);

View File

@ -1 +0,0 @@
org.patchbukkit.PatchBukkitInternalAPIBridge

View File

@ -1 +0,0 @@
org.patchbukkit.loader.PatchBukkitPluginLoader

View File

@ -0,0 +1,89 @@
package org.patchbukkit.loader;
import org.junit.jupiter.api.Test;
import org.patchbukkit.PatchBukkitServer;
import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.jar.JarFile;
import static org.junit.jupiter.api.Assertions.*;
public class CloudReflectionTest {
@Test
public void testGrimAcCloudReflection() throws Exception {
PatchBukkitServer.initServer();
File grimFile = new File("/home/alex/Documents/Development/Rust/Pumpkin/plugins/data/patchbukkit/patchbukkit-plugins/grimac-bukkit-2.3.74-61caa53.jar");
if (!grimFile.exists()) return;
PatchBukkitPluginClassLoader loader = new PatchBukkitPluginClassLoader(
getClass().getClassLoader(),
grimFile
);
try {
Class<?> vcw = getClass().getClassLoader().loadClass("org.bukkit.craftbukkit.command.VanillaCommandWrapper");
System.out.println("VanillaCommandWrapper in AppClassLoader: " + vcw);
} catch (Throwable t) {
System.out.println("VanillaCommandWrapper NOT in AppClassLoader: " + t);
}
try {
Class<?> remappedTest = loader.loadClass("org.bukkit.craftbukkitcommand.VanillaCommandWrapper");
System.out.println("loader.loadClass with dots: " + remappedTest);
} catch (Throwable t) {
System.out.println("loader.loadClass with dots FAILED: " + t);
}
try {
Class<?> remappedSlashTest = loader.loadClass("org/bukkit/craftbukkitcommand/VanillaCommandWrapper");
System.out.println("loader.loadClass with slashes: " + remappedSlashTest);
} catch (Throwable t) {
System.out.println("loader.loadClass with slashes FAILED: " + t);
}
try {
Class<?> forNameDotTest = Class.forName("org.bukkit.craftbukkitcommand.VanillaCommandWrapper", true, loader);
System.out.println("Class.forName with dots: " + forNameDotTest);
} catch (Throwable t) {
System.out.println("Class.forName with dots FAILED: " + t);
}
Class<?> cbrClass = loader.loadClass("ac.grim.grimac.shaded.incendo.cloud.bukkit.internal.CraftBukkitReflection");
Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
sun.misc.Unsafe unsafe = (sun.misc.Unsafe) unsafeField.get(null);
Field cbPkgField2 = cbrClass.getDeclaredField("CB_PKG_VERSION");
cbPkgField2.setAccessible(true);
unsafe.putObject(unsafe.staticFieldBase(cbPkgField2), unsafe.staticFieldOffset(cbPkgField2), ".");
System.out.println("After unsafe putObject, CB_PKG_VERSION: '" + cbPkgField2.get(null) + "'");
assertNotNull(cbrClass);
Field cbPkgField = cbrClass.getDeclaredField("CB_PKG_VERSION");
cbPkgField.setAccessible(true);
Object version = cbPkgField.get(null);
System.out.println("CB_PKG_VERSION: '" + version + "'");
for (Field f : cbrClass.getDeclaredFields()) {
f.setAccessible(true);
try {
System.out.println("Field " + f.getName() + " = " + f.get(null));
} catch (Exception ignored) {}
}
Method needObcMethod = cbrClass.getDeclaredMethod("needOBCClass", String.class);
needObcMethod.setAccessible(true);
try {
Object res = needObcMethod.invoke(null, "command.VanillaCommandWrapper");
System.out.println("Found OBC Class: " + res);
assertNotNull(res);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
}

View File

@ -0,0 +1,91 @@
package org.patchbukkit.loader;
import io.papermc.paper.configuration.GlobalConfiguration;
import io.papermc.paper.configuration.PaperConfigurations;
import net.minecraft.SharedConstants;
import net.minecraft.core.RegistryAccess;
import net.minecraft.server.Bootstrap;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
public class GlobalConfigTest {
@BeforeAll
public static void setUp() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void testPaperConfigurationsInit() throws Exception {
Path tempDir = Files.createTempDirectory("paper-config-test");
PaperConfigurations configs = new PaperConfigurations(tempDir);
GlobalConfiguration globalConfig = configs.initializeGlobalConfiguration(RegistryAccess.EMPTY);
assertNotNull(globalConfig);
assertNotNull(GlobalConfiguration.get());
assertNotNull(GlobalConfiguration.get().misc);
assertEquals(5, GlobalConfiguration.get().misc.maxJoinsPerTick);
Class<?> connClass = Class.forName("net.minecraft.network.Connection");
assertNotNull(connClass);
}
@Test
public void testServerConnectionListener() throws Exception {
net.minecraft.server.network.ServerConnectionListener scl =
new net.minecraft.server.network.ServerConnectionListener(null);
assertNotNull(scl);
assertNotNull(scl.getConnections());
}
@Test
public void testDedicatedServerAllocation() throws Exception {
Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
sun.misc.Unsafe unsafe = (sun.misc.Unsafe) unsafeField.get(null);
Class<?> dsClass = Class.forName("net.minecraft.server.dedicated.DedicatedServer");
Object dedicatedServer = unsafe.allocateInstance(dsClass);
assertNotNull(dedicatedServer);
net.minecraft.server.network.ServerConnectionListener scl =
new net.minecraft.server.network.ServerConnectionListener((net.minecraft.server.MinecraftServer) dedicatedServer);
Field connField = null;
for (Field f : net.minecraft.server.MinecraftServer.class.getDeclaredFields()) {
if (f.getType().equals(net.minecraft.server.network.ServerConnectionListener.class)) {
connField = f;
break;
}
}
assertNotNull(connField);
connField.setAccessible(true);
connField.set(dedicatedServer, scl);
assertSame(scl, connField.get(dedicatedServer));
}
@Test
public void testCraftServerAllocation() throws Exception {
Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
sun.misc.Unsafe unsafe = (sun.misc.Unsafe) unsafeField.get(null);
Class<?> csClass = Class.forName("org.bukkit.craftbukkit.CraftServer");
System.out.println("CraftServer code source: " + csClass.getProtectionDomain().getCodeSource().getLocation());
for (java.lang.reflect.Constructor<?> ctor : csClass.getDeclaredConstructors()) {
System.out.println("CraftServer ctor: " + ctor);
}
Object craftServer = unsafe.allocateInstance(csClass);
assertNotNull(craftServer);
assertTrue(craftServer instanceof org.bukkit.Server);
assertEquals("org.bukkit.craftbukkit.CraftServer", craftServer.getClass().getName());
assertEquals("org.bukkit.craftbukkit", craftServer.getClass().getPackage().getName());
}
}

View File

@ -0,0 +1,25 @@
package org.patchbukkit.loader;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import static org.junit.jupiter.api.Assertions.*;
public class MojangServerProviderTest {
@Test
public void testServerJarProviderLocatesOrDownloads(@TempDir File tempDir) {
File serverJar = MojangServerProvider.getOrDownloadServerJar(tempDir, "26.2");
assertNotNull(serverJar, "Server jar should be located or downloaded");
assertTrue(serverJar.exists(), "Server jar file must exist");
assertTrue(serverJar.length() > 0, "Server jar file must not be empty");
// Second call should return cached file immediately
long length = serverJar.length();
File cached = MojangServerProvider.getOrDownloadServerJar(tempDir, "26.2");
assertNotNull(cached);
assertEquals(length, cached.length());
}
}

View File

@ -0,0 +1,98 @@
package org.patchbukkit.loader;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.patchbukkit.PatchBukkitServer;
import java.io.File;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import static org.junit.jupiter.api.Assertions.*;
public class ProtocolPluginsLoadTest {
private static final String PROTOCOLLIB_URL =
"https://repo1.maven.org/maven2/net/dmulloy2/ProtocolLib/5.4.0/ProtocolLib-5.4.0.jar";
private static final String PACKETEVENTS_URL =
"https://github.com/retrooper/packetevents/releases/download/v2.7.0/packetevents-spigot-2.7.0.jar";
private static Path testPluginsDir;
@BeforeAll
public static void setUp() throws Exception {
PatchBukkitServer.initServer();
testPluginsDir = Path.of("build", "test-plugins");
Files.createDirectories(testPluginsDir);
}
private static File ensurePluginJar(String filename, String url) throws Exception {
Path target = testPluginsDir.resolve(filename);
if (!Files.exists(target) || Files.size(target) == 0) {
try (InputStream in = URI.create(url).toURL().openStream()) {
Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
}
}
return target.toFile();
}
@Test
public void testLoadPacketEvents() throws Exception {
File jarFile = ensurePluginJar("PacketEvents.jar", PACKETEVENTS_URL);
assertTrue(jarFile.exists(), "PacketEvents jar must exist");
Plugin plugin = Bukkit.getPluginManager().loadPlugin(jarFile);
assertNotNull(plugin, "PacketEvents plugin must load successfully");
assertEquals("packetevents", plugin.getName().toLowerCase(), "Plugin name should match");
// Test enable
assertDoesNotThrow(() -> Bukkit.getPluginManager().enablePlugin(plugin));
assertTrue(plugin.isEnabled(), "PacketEvents should be enabled");
}
@Test
public void testLoadProtocolLib() throws Exception {
File jarFile = ensurePluginJar("ProtocolLib.jar", PROTOCOLLIB_URL);
assertTrue(jarFile.exists(), "ProtocolLib jar must exist");
Plugin plugin = Bukkit.getPluginManager().loadPlugin(jarFile);
assertNotNull(plugin, "ProtocolLib plugin must load successfully");
assertEquals("protocollib", plugin.getName().toLowerCase(), "Plugin name should match");
// Test enable (ProtocolLib 5.4.0 disables itself on MC 26.2 without dev builds)
try {
Bukkit.getPluginManager().enablePlugin(plugin);
} catch (Throwable t) {
t.printStackTrace();
}
}
@Test
public void testLoadGrimAC() throws Exception {
File grimFile = new File("/home/alex/Documents/Development/Rust/Pumpkin/plugins/data/patchbukkit/patchbukkit-plugins/grimac-bukkit-2.3.74-61caa53.jar");
if (!grimFile.exists()) {
return;
}
Plugin plugin = Bukkit.getPluginManager().loadPlugin(grimFile);
assertNotNull(plugin, "GrimAC plugin must load successfully");
assertEquals("grimac", plugin.getName().toLowerCase());
assertDoesNotThrow(() -> Bukkit.getPluginManager().enablePlugin(plugin));
assertTrue(plugin.isEnabled(), "GrimAC should be enabled");
org.bukkit.entity.Player player = new org.patchbukkit.entity.CraftPlayer(java.util.UUID.randomUUID(), "GrimTester");
if (Bukkit.getServer() instanceof PatchBukkitServer pbs) {
pbs.registerPlayer(player);
}
org.bukkit.event.player.PlayerJoinEvent joinEvent =
new org.bukkit.event.player.PlayerJoinEvent(player, net.kyori.adventure.text.Component.empty());
assertDoesNotThrow(() -> Bukkit.getPluginManager().callEvent(joinEvent));
}
}

View File

@ -0,0 +1,72 @@
package org.patchbukkit.log;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.filter.AbstractFilter;
import org.junit.jupiter.api.Test;
import org.patchbukkit.PatchBukkitServer;
import org.slf4j.LoggerFactory;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class Log4jPipelineTest {
@Test
public void testLog4jPipelineAndFiltering() {
// Initialize PatchBukkitServer to trigger configureLogging()
PatchBukkitServer.getInstance();
Logger rootLogger = (Logger) LogManager.getRootLogger();
assertNotNull(rootLogger, "Root logger should be an instance of org.apache.logging.log4j.core.Logger");
List<String> capturedMessages = new CopyOnWriteArrayList<>();
List<String> capturedLoggers = new CopyOnWriteArrayList<>();
Filter testFilter = new AbstractFilter() {
@Override
public Result filter(LogEvent event) {
if (event != null) {
capturedLoggers.add(event.getLoggerName());
capturedMessages.add(event.getMessage().getFormattedMessage());
}
return Result.NEUTRAL;
}
};
testFilter.start();
rootLogger.addFilter(testFilter);
try {
// 1. Test direct Log4j logging
LogManager.getLogger("MyLog4jPlugin").info("Direct log4j message 123");
// 2. Test Java Util Logging (JUL) forwarding via ForwardLogHandler
java.util.logging.Logger.getLogger("MyJulPlugin").info("JUL message 456");
// 3. Test SLF4J logging via log4j-slf4j2-impl
LoggerFactory.getLogger("MySlf4jPlugin").info("SLF4J message 789");
// 4. Test Apache Commons Logging via LogFactory (used by HttpClient / Aether / CivModCore)
org.apache.commons.logging.LogFactory.getLog("MyJclPlugin").info("JCL message 012");
// Verify Log4j captured all logging mechanisms
assertTrue(capturedLoggers.contains("MyLog4jPlugin"), "Log4j direct logger should be captured");
assertTrue(capturedMessages.contains("Direct log4j message 123"), "Log4j direct message should be captured");
assertTrue(capturedLoggers.contains("MyJulPlugin"), "JUL logger should be forwarded to Log4j");
assertTrue(capturedMessages.contains("JUL message 456"), "JUL message should be forwarded to Log4j");
assertTrue(capturedLoggers.contains("MySlf4jPlugin"), "SLF4J logger should be routed to Log4j");
assertTrue(capturedMessages.contains("SLF4J message 789"), "SLF4J message should be routed to Log4j");
assertTrue(capturedLoggers.contains("MyJclPlugin"), "JCL logger should be routed to Log4j");
assertTrue(capturedMessages.contains("JCL message 012"), "JCL message should be routed to Log4j");
} finally {
testFilter.stop();
}
}
}

View File

@ -0,0 +1,156 @@
package org.patchbukkit.network;
import com.mojang.authlib.GameProfile;
import io.netty.channel.Channel;
import net.minecraft.SharedConstants;
import net.minecraft.network.Connection;
import net.minecraft.network.protocol.PacketFlow;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.dedicated.DedicatedServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerCommonPacketListenerImpl;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.patchbukkit.PatchBukkitServer;
import org.patchbukkit.entity.PatchBukkitPlayer;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
public class HeadlessServerPlayerTest {
private static sun.misc.Unsafe unsafe;
@BeforeAll
public static void setUp() throws Exception {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
PatchBukkitServer.initServer();
Field f = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (sun.misc.Unsafe) f.get(null);
}
@Test
public void testAllocateHeadlessServerPlayerHierarchy() throws Exception {
UUID uuid = UUID.randomUUID();
String name = "TestBot";
PatchBukkitPlayer bukkitPlayer = new PatchBukkitPlayer(uuid, name);
// 1. Virtual Channel
PatchBukkitVirtualChannel channel = new PatchBukkitVirtualChannel(uuid);
// 2. Net.minecraft.network.Connection
Connection connection = (Connection) unsafe.allocateInstance(Connection.class);
connection.channel = channel;
connection.address = new InetSocketAddress("127.0.0.1", 54321);
// Set receiving flow field
for (Field cf : Connection.class.getDeclaredFields()) {
if (cf.getType().equals(PacketFlow.class)) {
cf.setAccessible(true);
cf.set(connection, PacketFlow.SERVERBOUND);
break;
}
}
// 3. ServerPlayer
ServerPlayer serverPlayer = (ServerPlayer) unsafe.allocateInstance(ServerPlayer.class);
// Set Entity UUID
Field entUuidField = net.minecraft.world.entity.Entity.class.getDeclaredField("uuid");
entUuidField.setAccessible(true);
entUuidField.set(serverPlayer, uuid);
Field entStringUuidField = net.minecraft.world.entity.Entity.class.getDeclaredField("stringUUID");
entStringUuidField.setAccessible(true);
entStringUuidField.set(serverPlayer, uuid.toString());
// Set gameProfile
GameProfile profile = new GameProfile(uuid, name);
Field profileField = null;
Class<?> pClass = ServerPlayer.class;
while (pClass != null && profileField == null) {
for (Field pf : pClass.getDeclaredFields()) {
if (pf.getType().equals(GameProfile.class)) {
profileField = pf;
break;
}
}
pClass = pClass.getSuperclass();
}
assertNotNull(profileField, "GameProfile field must exist on Player/ServerPlayer");
profileField.setAccessible(true);
profileField.set(serverPlayer, profile);
// 4. ServerGamePacketListenerImpl
ServerGamePacketListenerImpl listener =
(ServerGamePacketListenerImpl) unsafe.allocateInstance(ServerGamePacketListenerImpl.class);
listener.player = serverPlayer;
// Set server and connection in ServerCommonPacketListenerImpl superclass
Class<?> scClass = ServerCommonPacketListenerImpl.class;
Field serverField = scClass.getDeclaredField("server");
serverField.setAccessible(true);
serverField.set(listener, PatchBukkitServer.getDedicatedServer());
Field connField = scClass.getDeclaredField("connection");
connField.setAccessible(true);
connField.set(listener, connection);
// Link listener into ServerPlayer
serverPlayer.connection = listener;
// 5. CraftPlayer
CraftPlayer craftPlayer = (CraftPlayer) unsafe.allocateInstance(CraftPlayer.class);
Field entityField = null;
Class<?> cpClass = CraftPlayer.class;
while (cpClass != null && entityField == null) {
for (Field ef : cpClass.getDeclaredFields()) {
if (ef.getName().equals("entity")) {
entityField = ef;
break;
}
}
cpClass = cpClass.getSuperclass();
}
assertNotNull(entityField, "entity field must exist on CraftEntity");
entityField.setAccessible(true);
entityField.set(craftPlayer, serverPlayer);
// Set bukkitEntity on Entity
Field bukkitEntityField = null;
Class<?> entClass = net.minecraft.world.entity.Entity.class;
for (Field bef : entClass.getDeclaredFields()) {
if (bef.getName().equals("bukkitEntity")) {
bukkitEntityField = bef;
break;
}
}
assertNotNull(bukkitEntityField, "bukkitEntity field must exist on Entity");
bukkitEntityField.setAccessible(true);
bukkitEntityField.set(serverPlayer, craftPlayer);
// Assertions:
// A. Channel retrieval matches
assertSame(channel, serverPlayer.connection.connection.channel);
assertSame(channel, listener.connection.channel);
// B. getBukkitEntity() and getHandle() circular reference matches
assertSame(craftPlayer, serverPlayer.getBukkitEntity());
assertSame(serverPlayer, craftPlayer.getHandle());
// C. UUID and Name matches
assertEquals(uuid, serverPlayer.getUUID());
assertEquals(name, serverPlayer.getScoreboardName());
assertEquals(uuid, craftPlayer.getUniqueId());
assertEquals(name, craftPlayer.getName());
}
}

View File

@ -0,0 +1,183 @@
package org.patchbukkit.network;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.patchbukkit.PatchBukkitServer;
import org.patchbukkit.entity.PatchBukkitPlayer;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
public class VirtualChannelPipelineTest {
@BeforeAll
public static void setUp() {
PatchBukkitServer.initServer();
}
@Test
public void testPlayerHandleAndChannelReflection() throws Exception {
UUID uuid = UUID.randomUUID();
PatchBukkitPlayer player = new PatchBukkitPlayer(uuid, "Steve");
// 1. PacketEvents reflects on player handle
VirtualPlayerHandle handle = VirtualChannelManager.getInstance().getPlayerHandle(player);
assertNotNull(handle, "Player handle must not be null");
// Authentic ServerPlayer
net.minecraft.server.level.ServerPlayer nmsPlayer = player.getHandle();
assertNotNull(nmsPlayer, "ServerPlayer must not be null");
// 2. PacketEvents reflects on handle.connection
Field connectionField = handle.getClass().getField("connection");
assertNotNull(connectionField);
Object connection = connectionField.get(handle);
assertNotNull(connection, "Handle connection must not be null");
// 3. PacketEvents reflects on connection.channel (direct)
Field channelField = connection.getClass().getField("channel");
assertNotNull(channelField);
Object channelObj = channelField.get(connection);
assertNotNull(channelObj, "Channel must not be null");
assertInstanceOf(Channel.class, channelObj);
// Verify authentic ServerPlayer connection points to exact same channel
assertSame(channelObj, nmsPlayer.connection.connection.channel, "NMS ServerPlayer channel must match");
// Also verify nested connection.connection.channel (Mojang ServerGamePacketListenerImpl -> Connection -> channel)
Field nestedConnField = connection.getClass().getField("connection");
Object nestedConn = nestedConnField.get(connection);
Field nestedChannelField = nestedConn.getClass().getField("channel");
assertSame(channelObj, nestedChannelField.get(nestedConn), "Nested connection.channel must match direct channel");
// Also verify nested connection.networkManager.channel (Spigot ServerGamePacketListenerImpl -> NetworkManager -> channel)
Field nestedNmField = connection.getClass().getField("networkManager");
Object nestedNm = nestedNmField.get(connection);
Field nestedNmChannelField = nestedNm.getClass().getField("channel");
assertSame(channelObj, nestedNmChannelField.get(nestedNm), "Nested networkManager.channel must match direct channel");
Channel channel = (Channel) channelObj;
assertTrue(channel.isOpen());
assertTrue(channel.isActive());
// 4. Verify standard Minecraft pipeline handlers exist in exact vanilla order
assertNotNull(channel.pipeline().get("timeout"), "Pipeline must contain 'timeout'");
assertNotNull(channel.pipeline().get("legacy_query"), "Pipeline must contain 'legacy_query'");
assertNotNull(channel.pipeline().get("splitter"), "Pipeline must contain 'splitter'");
assertNotNull(channel.pipeline().get("decompress"), "Pipeline must contain 'decompress'");
assertNotNull(channel.pipeline().get("decoder"), "Pipeline must contain 'decoder'");
assertNotNull(channel.pipeline().get("prepender"), "Pipeline must contain 'prepender'");
assertNotNull(channel.pipeline().get("compress"), "Pipeline must contain 'compress'");
assertNotNull(channel.pipeline().get("encoder"), "Pipeline must contain 'encoder'");
assertNotNull(channel.pipeline().get("bundle_packer"), "Pipeline must contain 'bundle_packer'");
assertNotNull(channel.pipeline().get("packet_handler"), "Pipeline must contain 'packet_handler'");
}
@Test
public void testCompressionAndEncryptionLifecycle() {
UUID uuid = UUID.randomUUID();
PatchBukkitPlayer player = new PatchBukkitPlayer(uuid, "CompressPlayer");
VirtualPlayerHandle handle = VirtualChannelManager.getInstance().getPlayerHandle(player);
PatchBukkitVirtualChannel channel = (PatchBukkitVirtualChannel) handle.connection.connection.channel;
VirtualNetworkManager nm = handle.connection.connection;
// Initially compressed with threshold 256
assertTrue(channel.isCompressed());
assertTrue(nm.isCompressed());
assertEquals(256, channel.getCompressionThreshold());
assertEquals(256, nm.getCompressionThreshold());
assertFalse(channel.isEncrypted());
assertFalse(nm.isEncrypted());
// Change compression threshold
nm.setupCompression(512, true);
assertEquals(512, channel.getCompressionThreshold());
assertEquals(512, nm.getCompressionThreshold());
PatchBukkitVirtualChannel.VirtualCompressionDecoder decoder =
(PatchBukkitVirtualChannel.VirtualCompressionDecoder) channel.pipeline().get("decompress");
PatchBukkitVirtualChannel.VirtualCompressionEncoder encoder =
(PatchBukkitVirtualChannel.VirtualCompressionEncoder) channel.pipeline().get("compress");
assertNotNull(decoder);
assertNotNull(encoder);
assertEquals(512, decoder.threshold());
assertTrue(decoder.validateDecompressed);
assertEquals(512, encoder.threshold());
// Disable compression with -1
nm.setupCompression(-1);
assertFalse(channel.isCompressed());
assertFalse(nm.isCompressed());
assertNull(channel.pipeline().get("decompress"));
assertNull(channel.pipeline().get("compress"));
// Re-enable compression
nm.setupCompression(128);
assertTrue(channel.isCompressed());
assertTrue(nm.isCompressed());
assertEquals(128, channel.getCompressionThreshold());
assertNotNull(channel.pipeline().get("decompress"));
assertNotNull(channel.pipeline().get("compress"));
// Enable encryption
Object dummyCipher = new Object();
nm.setEncryptionKey(dummyCipher, dummyCipher);
assertTrue(channel.isEncrypted());
assertTrue(nm.isEncrypted());
assertNotNull(channel.pipeline().get("decrypt"));
assertNotNull(channel.pipeline().get("encrypt"));
}
@Test
public void testPacketEventsStylePipelineInjection() {
UUID uuid = UUID.randomUUID();
PatchBukkitPlayer player = new PatchBukkitPlayer(uuid, "Alex");
VirtualPlayerHandle handle = VirtualChannelManager.getInstance().getPlayerHandle(player);
Channel channel = handle.connection.connection.channel;
AtomicBoolean injectedInboundReceived = new AtomicBoolean(false);
AtomicInteger receivedBytesCount = new AtomicInteger(0);
// Simulate PacketEvents injecting its handler before "packet_handler"
channel.pipeline().addBefore("packet_handler", "packetevents_inbound_handler", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf buf) {
injectedInboundReceived.set(true);
receivedBytesCount.set(buf.readableBytes());
}
ctx.fireChannelRead(msg);
}
});
// Simulate Pumpkin receiving packet bytes from the client and forwarding to VirtualChannelManager
byte[] testPacket = "hello_minecraft_packet".getBytes(StandardCharsets.UTF_8);
VirtualChannelManager.getInstance().handlePacketReceived(uuid, 0x01, testPacket);
assertTrue(injectedInboundReceived.get(), "Injected PacketEvents handler should have intercepted the inbound packet");
assertEquals(testPacket.length, receivedBytesCount.get(), "Injected handler should have received exact byte length");
// Simulate outbound write
AtomicBoolean outboundIntercepted = new AtomicBoolean(false);
channel.pipeline().addBefore("encoder", "packetevents_outbound_handler", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
outboundIntercepted.set(true);
ctx.write(msg, promise);
}
});
channel.writeAndFlush("dummy_packet");
assertTrue(outboundIntercepted.get(), "Outbound write should have passed through injected outbound handler");
}
}

View File

@ -64,6 +64,31 @@ public class PatchBukkitRegistryAccessTest {
}); });
} }
@Test
public void testDamageTypeInitialization() {
assertDoesNotThrow(() -> {
org.bukkit.damage.DamageType arrow = org.bukkit.damage.DamageType.ARROW;
assertNotNull(arrow, "DamageType.ARROW must initialize successfully without ExceptionInInitializerError");
assertEquals(org.bukkit.NamespacedKey.minecraft("arrow"), arrow.getKey());
assertNotNull(arrow.getTranslationKey());
assertNotNull(arrow.getDamageScaling());
assertNotNull(arrow.getDamageEffect());
assertNotNull(arrow.getDeathMessageType());
org.bukkit.damage.DamageType generic = org.bukkit.damage.DamageType.GENERIC;
assertNotNull(generic);
assertEquals(org.bukkit.NamespacedKey.minecraft("generic"), generic.getKey());
Registry<org.bukkit.damage.DamageType> damageTypeReg = Registry.DAMAGE_TYPE;
assertNotNull(damageTypeReg);
assertSame(arrow, damageTypeReg.get(org.bukkit.NamespacedKey.minecraft("arrow")));
org.bukkit.damage.DamageSource source = org.bukkit.damage.DamageSource.builder(arrow).build();
assertNotNull(source);
assertEquals(arrow, source.getDamageType());
});
}
@Test @Test
public void testInspectBlockTypeAndDataMethods() { public void testInspectBlockTypeAndDataMethods() {
System.out.println("=== BLOCK TYPE METHODS ==="); System.out.println("=== BLOCK TYPE METHODS ===");

View File

@ -22,6 +22,8 @@ service NativeBridge {
rpc SetAbilities(patchbukkit.abilities.SetAbilitiesRequest) returns (patchbukkit.abilities.SetAbilitiesResponse); rpc SetAbilities(patchbukkit.abilities.SetAbilitiesRequest) returns (patchbukkit.abilities.SetAbilitiesResponse);
rpc GetEntityHealth(patchbukkit.common.UUID) returns (patchbukkit.entity.EntityHealthResponse); rpc GetEntityHealth(patchbukkit.common.UUID) returns (patchbukkit.entity.EntityHealthResponse);
rpc GetEntityId(patchbukkit.common.UUID) returns (patchbukkit.entity.GetEntityIdResponse);
rpc GetEntityUuid(patchbukkit.entity.GetEntityUuidRequest) returns (patchbukkit.entity.GetEntityUuidResponse);
rpc SetEntityHealth(patchbukkit.entity.SetEntityHealthRequest) returns (google.protobuf.Empty); rpc SetEntityHealth(patchbukkit.entity.SetEntityHealthRequest) returns (google.protobuf.Empty);
rpc DamageEntity(patchbukkit.entity.DamageEntityRequest) returns (google.protobuf.Empty); rpc DamageEntity(patchbukkit.entity.DamageEntityRequest) returns (google.protobuf.Empty);
rpc GetEntityVelocity(patchbukkit.common.UUID) returns (patchbukkit.entity.EntityVelocityResponse); rpc GetEntityVelocity(patchbukkit.common.UUID) returns (patchbukkit.entity.EntityVelocityResponse);

View File

@ -231,6 +231,14 @@ message GetCooldownResponse {
bool is_on_cooldown = 2; bool is_on_cooldown = 2;
} }
message GetEntityIdResponse {
int32 entity_id = 1;
}
message GetEntityUuidRequest {
int32 entity_id = 1;
}
message GetEntityUuidResponse {
patchbukkit.common.UUID uuid = 1;
}

View File

@ -279,6 +279,7 @@ message PlayerItemMendEvent {
message PlayerJoinEvent { message PlayerJoinEvent {
patchbukkit.common.UUID player_uuid = 1; patchbukkit.common.UUID player_uuid = 1;
string join_message = 2; string join_message = 2;
int32 entity_id = 3;
} }
message PlayerKickEvent { message PlayerKickEvent {

View File

@ -154,6 +154,7 @@ message EntitySummaryProto {
float pitch = 7; float pitch = 7;
bool is_player = 8; bool is_player = 8;
string custom_name = 9; string custom_name = 9;
int32 entity_id = 10;
} }
message GetWorldEntitiesRequest { message GetWorldEntitiesRequest {
@ -177,6 +178,7 @@ message SpawnWorldEntityRequest {
message SpawnWorldEntityResponse { message SpawnWorldEntityResponse {
patchbukkit.common.UUID entity_uuid = 1; patchbukkit.common.UUID entity_uuid = 1;
bool success = 2; bool success = 2;
int32 entity_id = 3;
} }
message CreateWorldExplosionRequest { message CreateWorldExplosionRequest {

212
rust/Cargo.lock generated
View File

@ -265,9 +265,9 @@ dependencies = [
[[package]] [[package]]
name = "async-compression" name = "async-compression"
version = "0.4.44" version = "0.4.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "515a1f282e33d55983c499d7e9e87082e81cbc32974825bf9032f928392d5844" checksum = "fb61aea1a7def73ee7c350a184f0e70b32c182344e2e75bf70c9b621b83417fd"
dependencies = [ dependencies = [
"compression-codecs", "compression-codecs",
"compression-core", "compression-core",
@ -400,9 +400,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.13.1" version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
@ -685,9 +685,9 @@ dependencies = [
[[package]] [[package]]
name = "compression-codecs" name = "compression-codecs"
version = "0.4.39" version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fe67f2944eef52fc7b106b8c9450d243a88701a0c065f7f57235e76abaed7df" checksum = "bef16c47ba2797aa6a909cc37d39911f3a6743811fe7408ac0b0cc0276b656e9"
dependencies = [ dependencies = [
"compression-core", "compression-core",
"flate2", "flate2",
@ -1788,9 +1788,9 @@ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
[[package]] [[package]]
name = "granit-parser" name = "granit-parser"
version = "1.2.0" version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65ec0d45986cd51c847c75c5b69a00852c4fc84d0e5e79f041173f73437d0cdf" checksum = "e20f99e46474f56bd905c56e817ebddcf377a611f94c53ac4649e4d3fa3c0cd0"
dependencies = [ dependencies = [
"arraydeque", "arraydeque",
"smallvec", "smallvec",
@ -2188,7 +2188,7 @@ version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592"
dependencies = [ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.2",
"inotify-sys", "inotify-sys",
"libc", "libc",
] ]
@ -2412,7 +2412,7 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087"
dependencies = [ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.2",
"libc", "libc",
] ]
@ -2690,7 +2690,7 @@ version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.2",
"cfg-if", "cfg-if",
"cfg_aliases", "cfg_aliases",
"libc", "libc",
@ -2719,7 +2719,7 @@ version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
dependencies = [ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.2",
"fsevent-sys", "fsevent-sys",
"inotify", "inotify",
"kqueue", "kqueue",
@ -2737,7 +2737,7 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a"
dependencies = [ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.2",
] ]
[[package]] [[package]]
@ -2828,7 +2828,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.2",
] ]
[[package]] [[package]]
@ -2972,6 +2972,7 @@ name = "patchbukkit"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap",
"env_logger", "env_logger",
"glob", "glob",
"jni", "jni",
@ -2990,10 +2991,12 @@ dependencies = [
"serde", "serde",
"serde-saphyr", "serde-saphyr",
"serde_json", "serde_json",
"tempfile",
"time", "time",
"tokio", "tokio",
"toml", "toml",
"tracing", "tracing",
"tracing-subscriber",
"uuid", "uuid",
"walkdir", "walkdir",
"zip", "zip",
@ -3145,7 +3148,7 @@ version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.2",
"crc32fast", "crc32fast",
"fdeflate", "fdeflate",
"flate2", "flate2",
@ -3457,15 +3460,15 @@ dependencies = [
[[package]] [[package]]
name = "pumpkin" name = "pumpkin"
version = "0.1.0-dev+26.2-26.45" version = "0.1.0+26.2-26.45"
source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#55762a50d21ecaff480c95f1ac4017342b017d02" source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#844e2871e2091c14b9991cb6534e97a6c8e09480"
dependencies = [ dependencies = [
"aes 0.9.3", "aes 0.9.3",
"arc-swap", "arc-swap",
"async-trait", "async-trait",
"axum", "axum",
"base64 0.23.1", "base64 0.23.1",
"bitflags 2.13.1", "bitflags 2.13.2",
"bytes", "bytes",
"colored", "colored",
"crossbeam", "crossbeam",
@ -3484,10 +3487,12 @@ dependencies = [
"num-bigint 0.5.1", "num-bigint 0.5.1",
"ordered-float", "ordered-float",
"postcard", "postcard",
"pumpkin-codecs", "pumpkin-auth",
"pumpkin-command",
"pumpkin-config", "pumpkin-config",
"pumpkin-data", "pumpkin-data",
"pumpkin-gametest", "pumpkin-gametest",
"pumpkin-host-bindings",
"pumpkin-inventory", "pumpkin-inventory",
"pumpkin-macros", "pumpkin-macros",
"pumpkin-nbt", "pumpkin-nbt",
@ -3523,12 +3528,13 @@ dependencies = [
"wasmtime-wasi", "wasmtime-wasi",
"wasmtime-wasi-http", "wasmtime-wasi-http",
"webrtc", "webrtc",
"xxhash-rust",
] ]
[[package]] [[package]]
name = "pumpkin-api-macros" name = "pumpkin-api-macros"
version = "0.1.0-dev+26.2-26.45" version = "0.1.0+26.2-26.45"
source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#55762a50d21ecaff480c95f1ac4017342b017d02" source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#844e2871e2091c14b9991cb6534e97a6c8e09480"
dependencies = [ dependencies = [
"proc-macro-error2", "proc-macro-error2",
"proc-macro2", "proc-macro2",
@ -3536,19 +3542,56 @@ dependencies = [
"syn 3.0.5", "syn 3.0.5",
] ]
[[package]]
name = "pumpkin-auth"
version = "0.1.0+26.2-26.45"
source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#844e2871e2091c14b9991cb6534e97a6c8e09480"
dependencies = [
"base64 0.23.1",
"crypto-bigint 0.7.5",
"ecdsa 0.17.0",
"md5",
"p384 0.14.0",
"reqwest",
"rsa",
"rustls",
"serde",
"serde_json",
"sha2 0.11.0",
"thiserror 2.0.20",
"uuid",
"webpki-root-certs",
]
[[package]] [[package]]
name = "pumpkin-codecs" name = "pumpkin-codecs"
version = "0.1.0-dev+26.2-26.45" version = "0.1.0+26.2-26.45"
source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#55762a50d21ecaff480c95f1ac4017342b017d02" source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#844e2871e2091c14b9991cb6534e97a6c8e09480"
dependencies = [ dependencies = [
"serde_json", "serde_json",
"tracing", "tracing",
] ]
[[package]]
name = "pumpkin-command"
version = "0.1.0+26.2-26.45"
source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#844e2871e2091c14b9991cb6534e97a6c8e09480"
dependencies = [
"pumpkin-codecs",
"pumpkin-data",
"pumpkin-nbt",
"pumpkin-protocol",
"pumpkin-util",
"rustc-hash",
"serde_json",
"tracing",
"uuid",
]
[[package]] [[package]]
name = "pumpkin-config" name = "pumpkin-config"
version = "0.1.0-dev+26.2-26.45" version = "0.1.0+26.2-26.45"
source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#55762a50d21ecaff480c95f1ac4017342b017d02" source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#844e2871e2091c14b9991cb6534e97a6c8e09480"
dependencies = [ dependencies = [
"pumpkin-util", "pumpkin-util",
"serde", "serde",
@ -3559,8 +3602,8 @@ dependencies = [
[[package]] [[package]]
name = "pumpkin-data" name = "pumpkin-data"
version = "0.1.0-dev+26.2-26.45" version = "0.1.0+26.2-26.45"
source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#55762a50d21ecaff480c95f1ac4017342b017d02" source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#844e2871e2091c14b9991cb6534e97a6c8e09480"
dependencies = [ dependencies = [
"crc-fast", "crc-fast",
"phf", "phf",
@ -3572,8 +3615,8 @@ dependencies = [
[[package]] [[package]]
name = "pumpkin-gametest" name = "pumpkin-gametest"
version = "0.1.0-dev+26.2-26.45" version = "0.1.0+26.2-26.45"
source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#55762a50d21ecaff480c95f1ac4017342b017d02" source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#844e2871e2091c14b9991cb6534e97a6c8e09480"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"pumpkin-data", "pumpkin-data",
@ -3586,16 +3629,23 @@ dependencies = [
"thiserror 2.0.20", "thiserror 2.0.20",
] ]
[[package]]
name = "pumpkin-host-bindings"
version = "0.1.0+26.2-26.45"
source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#844e2871e2091c14b9991cb6534e97a6c8e09480"
dependencies = [
"wasmtime",
]
[[package]] [[package]]
name = "pumpkin-inventory" name = "pumpkin-inventory"
version = "0.1.0-dev+26.2-26.45" version = "0.1.0+26.2-26.45"
source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#55762a50d21ecaff480c95f1ac4017342b017d02" source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#844e2871e2091c14b9991cb6534e97a6c8e09480"
dependencies = [ dependencies = [
"pumpkin-data", "pumpkin-data",
"pumpkin-nbt", "pumpkin-nbt",
"pumpkin-protocol", "pumpkin-protocol",
"pumpkin-util", "pumpkin-util",
"pumpkin-world",
"rand 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-hash", "rustc-hash",
"thiserror 2.0.20", "thiserror 2.0.20",
@ -3604,21 +3654,20 @@ dependencies = [
[[package]] [[package]]
name = "pumpkin-macros" name = "pumpkin-macros"
version = "0.1.0-dev+26.2-26.45" version = "0.1.0+26.2-26.45"
source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#55762a50d21ecaff480c95f1ac4017342b017d02" source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#844e2871e2091c14b9991cb6534e97a6c8e09480"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro-error2", "proc-macro-error2",
"proc-macro2", "proc-macro2",
"pumpkin-data",
"quote", "quote",
"syn 3.0.5", "syn 3.0.5",
] ]
[[package]] [[package]]
name = "pumpkin-nbt" name = "pumpkin-nbt"
version = "0.1.0-dev+26.2-26.45" version = "0.1.0+26.2-26.45"
source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#55762a50d21ecaff480c95f1ac4017342b017d02" source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#844e2871e2091c14b9991cb6534e97a6c8e09480"
dependencies = [ dependencies = [
"bytes", "bytes",
"cesu8", "cesu8",
@ -3631,8 +3680,8 @@ dependencies = [
[[package]] [[package]]
name = "pumpkin-plugin-runtime" name = "pumpkin-plugin-runtime"
version = "0.1.0-dev+26.2-26.45" version = "0.1.0+26.2-26.45"
source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#55762a50d21ecaff480c95f1ac4017342b017d02" source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#844e2871e2091c14b9991cb6534e97a6c8e09480"
dependencies = [ dependencies = [
"futures", "futures",
"thiserror 2.0.20", "thiserror 2.0.20",
@ -3643,12 +3692,13 @@ dependencies = [
[[package]] [[package]]
name = "pumpkin-protocol" name = "pumpkin-protocol"
version = "0.1.0-dev+26.2-26.45" version = "0.1.0+26.2-26.45"
source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#55762a50d21ecaff480c95f1ac4017342b017d02" source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#844e2871e2091c14b9991cb6534e97a6c8e09480"
dependencies = [ dependencies = [
"aes 0.9.3", "aes 0.9.3",
"async-compression", "async-compression",
"bitflags 2.13.1", "base64 0.23.1",
"bitflags 2.13.2",
"bytes", "bytes",
"cesu8", "cesu8",
"cfb8", "cfb8",
@ -3658,50 +3708,39 @@ dependencies = [
"pumpkin-macros", "pumpkin-macros",
"pumpkin-nbt", "pumpkin-nbt",
"pumpkin-util", "pumpkin-util",
"pumpkin-world",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.20", "thiserror 2.0.20",
"tokio", "tokio",
"uuid", "uuid",
"xxhash-rust",
] ]
[[package]] [[package]]
name = "pumpkin-util" name = "pumpkin-util"
version = "0.1.0-dev+26.2-26.45" version = "0.1.0+26.2-26.45"
source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#55762a50d21ecaff480c95f1ac4017342b017d02" source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#844e2871e2091c14b9991cb6534e97a6c8e09480"
dependencies = [ dependencies = [
"base64 0.23.1",
"bytes", "bytes",
"colored", "colored",
"crypto-bigint 0.7.5",
"dashmap", "dashmap",
"ecdsa 0.17.0",
"md5", "md5",
"num-derive", "num-derive",
"num-traits", "num-traits",
"p384 0.14.0",
"pumpkin-codecs", "pumpkin-codecs",
"pumpkin-nbt", "pumpkin-nbt",
"reqwest",
"rsa",
"rustls",
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.11.0",
"thiserror 2.0.20", "thiserror 2.0.20",
"uuid", "uuid",
"webpki-root-certs",
] ]
[[package]] [[package]]
name = "pumpkin-world" name = "pumpkin-world"
version = "0.1.0-dev+26.2-26.45" version = "0.1.0+26.2-26.45"
source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#55762a50d21ecaff480c95f1ac4017342b017d02" source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#844e2871e2091c14b9991cb6534e97a6c8e09480"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"bitflags 2.13.1", "bitflags 2.13.2",
"bytes", "bytes",
"crossbeam", "crossbeam",
"dashmap", "dashmap",
@ -3848,7 +3887,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.2",
] ]
[[package]] [[package]]
@ -3928,11 +3967,11 @@ dependencies = [
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.13.4" version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.23.1",
"bytes", "bytes",
"futures-core", "futures-core",
"h2", "h2",
@ -4403,7 +4442,7 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.2",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
@ -4506,7 +4545,7 @@ version = "18.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53f6a737db68eb1a8ccff86b584b2fc13eca6a7bb6f78ebc7c529547e3ab9684" checksum = "53f6a737db68eb1a8ccff86b584b2fc13eca6a7bb6f78ebc7c529547e3ab9684"
dependencies = [ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.2",
"cfg-if", "cfg-if",
"clipboard-win", "clipboard-win",
"libc", "libc",
@ -4589,7 +4628,7 @@ version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.2",
"core-foundation", "core-foundation",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@ -4628,9 +4667,9 @@ dependencies = [
[[package]] [[package]]
name = "serde-saphyr" name = "serde-saphyr"
version = "1.2.0" version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3afb591f9cdb6223c88ba39269aff895620c7f0716dc42b705b5733d5c7c0823" checksum = "b8050abb251097357e24aff63ba2c52a6309ecb7d23a5474023df960a02694d8"
dependencies = [ dependencies = [
"annotate-snippets", "annotate-snippets",
"base64 0.23.1", "base64 0.23.1",
@ -4997,6 +5036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.4.3",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@ -5230,7 +5270,7 @@ version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.2",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http",
@ -5283,6 +5323,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
] ]
[[package]] [[package]]
@ -5308,10 +5360,12 @@ dependencies = [
"once_cell", "once_cell",
"regex-automata", "regex-automata",
"sharded-slab", "sharded-slab",
"smallvec",
"thread_local", "thread_local",
"time", "time",
"tracing", "tracing",
"tracing-core", "tracing-core",
"tracing-log",
] ]
[[package]] [[package]]
@ -5408,9 +5462,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.26.0" version = "1.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
dependencies = [ dependencies = [
"getrandom 0.4.3", "getrandom 0.4.3",
"js-sys", "js-sys",
@ -5419,6 +5473,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
@ -5533,7 +5593,7 @@ version = "0.258.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9a61719f93a87b16d325921e251800c4833f8fab50fa21c7de73aed50086313" checksum = "d9a61719f93a87b16d325921e251800c4833f8fab50fa21c7de73aed50086313"
dependencies = [ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.2",
"hashbrown 0.17.1", "hashbrown 0.17.1",
"indexmap", "indexmap",
"semver", "semver",
@ -5558,7 +5618,7 @@ source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436
dependencies = [ dependencies = [
"addr2line", "addr2line",
"async-trait", "async-trait",
"bitflags 2.13.1", "bitflags 2.13.2",
"bumpalo", "bumpalo",
"bytes", "bytes",
"cc", "cc",
@ -5755,7 +5815,7 @@ version = "49.0.0-dev"
source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags 2.13.1", "bitflags 2.13.2",
"heck", "heck",
"indexmap", "indexmap",
"wit-component", "wit-component",
@ -5768,7 +5828,7 @@ version = "49.0.0-dev"
source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bitflags 2.13.1", "bitflags 2.13.2",
"bytes", "bytes",
"cap-primitives", "cap-primitives",
"futures", "futures",
@ -6185,7 +6245,7 @@ version = "0.36.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
dependencies = [ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.2",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@ -6196,7 +6256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "481b5c47b2ecce0389b5e08a05557d6a190c9cd761773b8880a8017ee04dc7ef" checksum = "481b5c47b2ecce0389b5e08a05557d6a190c9cd761773b8880a8017ee04dc7ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags 2.13.1", "bitflags 2.13.2",
"indexmap", "indexmap",
"log", "log",
"serde", "serde",

View File

@ -8,7 +8,7 @@ description = "Compatibility layer for running Bukkit, Spigot, and Paper plugins
build = "build/main.rs" build = "build/main.rs"
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
pumpkin = { git = "https://github.com/Pumpkin-MC/Pumpkin.git", branch = "master", package = "pumpkin" } pumpkin = { git = "https://github.com/Pumpkin-MC/Pumpkin.git", branch = "master", package = "pumpkin" }
@ -27,11 +27,11 @@ walkdir = "2"
zip = "8.6" zip = "8.6"
anyhow = "1.0" anyhow = "1.0"
uuid = "1.25.0" uuid = "1.26.1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
serde-saphyr = "1.1.0" serde-saphyr = "1.3.0"
rand = { git = "https://github.com/rust-random/rand"} rand = { git = "https://github.com/rust-random/rand"}
prost = "0.14" prost = "0.14"
@ -39,6 +39,11 @@ time = "0.3"
toml = "1.1" toml = "1.1"
[dev-dependencies]
tempfile = "3"
arc-swap = "1.9"
tracing-subscriber = "0.3"
[build-dependencies] [build-dependencies]
env_logger = "0.11.11" env_logger = "0.11.11"
log = "0.4" log = "0.4"
@ -47,7 +52,8 @@ protoc-bin-vendored = "3"
glob = "0.3" glob = "0.3"
[profile.release] [profile.release]
lto = true lto = false
codegen-units = 16
[lints.clippy] [lints.clippy]
# Groups # Groups

View File

@ -5,8 +5,11 @@ pub mod java;
pub mod protobufs; pub mod protobufs;
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
println!("cargo::rerun-if-changed=rust/src/build.rs"); println!("cargo::rerun-if-changed=build/main.rs");
println!("cargo::rerun-if-changed=java/build/libs/"); println!("cargo::rerun-if-changed=build/java.rs");
println!("cargo::rerun-if-changed=build/protobufs.rs");
println!("cargo::rerun-if-changed=../java/patchbukkit/build/libs/patchbukkit.jar");
println!("cargo::rerun-if-changed=resources/jassets/");
env_logger::init(); env_logger::init();
let base = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); let base = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());

View File

@ -3730,6 +3730,7 @@ impl PatchBukkitEvent for pumpkin::plugin::player::player_join::PlayerJoinEvent
value: self.player.gameprofile.id.to_string(), value: self.player.gameprofile.id.to_string(),
}), }),
join_message: serde_json::to_string(&self.join_message).unwrap_or_default(), join_message: serde_json::to_string(&self.join_message).unwrap_or_default(),
entity_id: self.player.living_entity.entity.entity_id,
})), })),
}, },
context: EventContext { context: EventContext {

View File

@ -296,9 +296,17 @@ impl JvmWorker {
.build() .build()
.map_err(|e| anyhow::anyhow!("Failed to build JVM init args: {e:?}"))?; .map_err(|e| anyhow::anyhow!("Failed to build JVM init args: {e:?}"))?;
let jvm = let jvm = match JavaVM::new(jvm_args) {
JavaVM::new(jvm_args).map_err(|e| anyhow::anyhow!("Failed to create JavaVM: {e:?}"))?; Ok(jvm) => Arc::new(jvm),
let jvm = Arc::new(jvm); Err(e) => {
if let Ok(existing) = JavaVM::singleton() {
tracing::info!("Reusing existing JavaVM in process");
Arc::new(existing)
} else {
return Err(anyhow::anyhow!("Failed to create JavaVM: {e:?}"));
}
}
};
jvm.attach_current_thread(|env| -> anyhow::Result<()> { jvm.attach_current_thread(|env| -> anyhow::Result<()> {
initialize_callbacks(env).map_err(|e| { initialize_callbacks(env).map_err(|e| {

View File

@ -8,6 +8,7 @@ use crate::{
common::Uuid, common::Uuid,
entity::{ entity::{
DamageEntityRequest, EntityHealthResponse, GetCooldownRequest, GetCooldownResponse, DamageEntityRequest, EntityHealthResponse, GetCooldownRequest, GetCooldownResponse,
GetEntityIdResponse, GetEntityUuidRequest, GetEntityUuidResponse,
GetExperienceResponse, GetFoodLevelResponse, GetPlayerPoseStateResponse, GetExperienceResponse, GetFoodLevelResponse, GetPlayerPoseStateResponse,
KickPlayerRequest, PlayerConnectionInfoResponse, SendActionBarRequest, KickPlayerRequest, PlayerConnectionInfoResponse, SendActionBarRequest,
SendBlockChangeRequest, SendGameEventRequest, SendResourcePackRequest, SendBlockChangeRequest, SendGameEventRequest, SendResourcePackRequest,
@ -32,6 +33,54 @@ pub fn ffi_native_bridge_get_entity_health_impl(request: Uuid) -> Option<EntityH
}) })
} }
pub fn ffi_native_bridge_get_entity_id_impl(request: Uuid) -> Option<GetEntityIdResponse> {
if let Some(id) = with_player(Some(&request), |player| {
player.living_entity.entity.entity_id
}) {
return Some(GetEntityIdResponse { entity_id: id });
}
let ctx = CALLBACK_CONTEXT.get()?;
let uuid_str = &request.value;
let uuid = uuid::Uuid::parse_str(uuid_str).ok()?;
for world in ctx.plugin_context.server.worlds.load().iter() {
if let Some(entity) = world.get_entity_by_uuid(uuid) {
return Some(GetEntityIdResponse {
entity_id: entity.get_entity().entity_id,
});
}
}
None
}
pub fn ffi_native_bridge_get_entity_uuid_impl(
request: GetEntityUuidRequest,
) -> Option<GetEntityUuidResponse> {
let ctx = CALLBACK_CONTEXT.get()?;
let id = request.entity_id;
for world in ctx.plugin_context.server.worlds.load().iter() {
if let Some(player) = world.get_player_by_id(id) {
return Some(GetEntityUuidResponse {
uuid: Some(Uuid {
value: player.gameprofile.id.to_string(),
}),
});
}
if let Some(entity) = world.get_entity_by_id(id) {
return Some(GetEntityUuidResponse {
uuid: Some(Uuid {
value: entity.get_entity().entity_uuid.to_string(),
}),
});
}
}
None
}
pub fn ffi_native_bridge_set_entity_health_impl(request: SetEntityHealthRequest) -> Option<()> { pub fn ffi_native_bridge_set_entity_health_impl(request: SetEntityHealthRequest) -> Option<()> {
with_player(request.uuid.as_ref(), |player| { with_player(request.uuid.as_ref(), |player| {
let health = request.health as f32; let health = request.health as f32;

View File

@ -2253,7 +2253,8 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
request.blocking, request.blocking,
); );
} }
"org.bukkit.event.player.PlayerPreLoginEvent" => { "org.bukkit.event.player.PlayerPreLoginEvent"
| "io.papermc.paper.event.connection.PlayerConnectionValidateLoginEvent" => {
plugin_context plugin_context
.register_event::< .register_event::<
pumpkin::plugin::player::player_pre_login::PlayerPreLoginEvent, pumpkin::plugin::player::player_pre_login::PlayerPreLoginEvent,
@ -3754,6 +3755,16 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
request.plugin_name request.plugin_name
); );
} }
name if name.starts_with("io.papermc.paper.event.")
|| name.starts_with("com.destroystokyo.paper.event.")
|| name.starts_with("org.spigotmc.event.") =>
{
tracing::info!(
"Registered Paper/Spigot event listener '{}' from plugin '{}'",
request.event_type,
request.plugin_name
);
}
_ => { _ => {
tracing::warn!( tracing::warn!(
"Unknown or unhandled Bukkit event type '{}' from plugin '{}'", "Unknown or unhandled Bukkit event type '{}' from plugin '{}'",

View File

@ -51,8 +51,8 @@ pub fn ffi_native_bridge_get_server_info_impl(
Some(ServerInfoResponse { Some(ServerInfoResponse {
server_name: "Pumpkin".to_string(), server_name: "Pumpkin".to_string(),
version: "0.1.0".to_string(), version: "0.1.0".to_string(),
bukkit_version: "1.21.4-R0.1-SNAPSHOT".to_string(), bukkit_version: format!("{}-R0.1-SNAPSHOT", pumpkin_data::packet::CURRENT_MC_VERSION),
minecraft_version: "1.21.4".to_string(), minecraft_version: pumpkin_data::packet::CURRENT_MC_VERSION.to_string(),
motd, motd,
ip, ip,
port, port,

View File

@ -62,8 +62,8 @@ pub fn ffi_native_bridge_player_play_sound_impl(request: PlayerPlaySoundRequest)
let category = SoundCategory::from_name(&sound.category.to_lowercase())?; let category = SoundCategory::from_name(&sound.category.to_lowercase())?;
let seed = match request.seed { let seed = match request.seed {
Some(seed) => seed as f64, Some(seed) => seed,
None => rng().random::<f64>(), None => rng().random::<i64>(),
}; };
let position = request.location?.position?; let position = request.location?.position?;

View File

@ -423,6 +423,7 @@ pub fn ffi_native_bridge_get_world_entities_impl(
pitch: p.living_entity.entity.pitch.load(), pitch: p.living_entity.entity.pitch.load(),
is_player: true, is_player: true,
custom_name: p.gameprofile.name.clone(), custom_name: p.gameprofile.name.clone(),
entity_id: p.living_entity.entity.entity_id,
}); });
} }
@ -441,6 +442,7 @@ pub fn ffi_native_bridge_get_world_entities_impl(
pitch: base.pitch.load(), pitch: base.pitch.load(),
is_player: false, is_player: false,
custom_name: String::new(), custom_name: String::new(),
entity_id: base.entity_id,
}); });
} }
@ -464,21 +466,23 @@ pub fn ffi_native_bridge_spawn_world_entity_impl(
let new_uuid = uuid::Uuid::new_v4(); let new_uuid = uuid::Uuid::new_v4();
let pos = Vector3::new(request.x, request.y, request.z); let pos = Vector3::new(request.x, request.y, request.z);
let entity_type: &'static pumpkin_data::entity::EntityType =
match request.entity_type.to_uppercase().as_str() {
"LIGHTNING_BOLT" | "LIGHTNING" => &pumpkin_data::entity::EntityType::LIGHTNING_BOLT,
"ITEM" | "DROPPED_ITEM" => &pumpkin_data::entity::EntityType::ITEM,
"ZOMBIE" => &pumpkin_data::entity::EntityType::ZOMBIE,
"SKELETON" => &pumpkin_data::entity::EntityType::SKELETON,
"CREEPER" => &pumpkin_data::entity::EntityType::CREEPER,
"COW" => &pumpkin_data::entity::EntityType::COW,
"PIG" => &pumpkin_data::entity::EntityType::PIG,
"SHEEP" => &pumpkin_data::entity::EntityType::SHEEP,
_ => &pumpkin_data::entity::EntityType::PIG,
};
let entity = pumpkin::entity::r#type::from_type(entity_type, pos, &world, new_uuid);
let entity_id = entity.get_entity().entity_id;
let w = world.clone(); let w = world.clone();
ctx.runtime.spawn(async move { ctx.runtime.spawn(async move {
let entity_type: &'static pumpkin_data::entity::EntityType =
match request.entity_type.to_uppercase().as_str() {
"LIGHTNING_BOLT" | "LIGHTNING" => &pumpkin_data::entity::EntityType::LIGHTNING_BOLT,
"ITEM" | "DROPPED_ITEM" => &pumpkin_data::entity::EntityType::ITEM,
"ZOMBIE" => &pumpkin_data::entity::EntityType::ZOMBIE,
"SKELETON" => &pumpkin_data::entity::EntityType::SKELETON,
"CREEPER" => &pumpkin_data::entity::EntityType::CREEPER,
"COW" => &pumpkin_data::entity::EntityType::COW,
"PIG" => &pumpkin_data::entity::EntityType::PIG,
"SHEEP" => &pumpkin_data::entity::EntityType::SHEEP,
_ => &pumpkin_data::entity::EntityType::PIG,
};
let entity = pumpkin::entity::r#type::from_type(entity_type, pos, &w, new_uuid);
w.spawn_entity(entity); w.spawn_entity(entity);
}); });
@ -487,6 +491,7 @@ pub fn ffi_native_bridge_spawn_world_entity_impl(
value: new_uuid.to_string(), value: new_uuid.to_string(),
}), }),
success: true, success: true,
entity_id,
}) })
} }

View File

@ -77,15 +77,17 @@ impl EventManager {
let uuid_jstr = env.new_string(player.gameprofile.id.to_string())?; let uuid_jstr = env.new_string(player.gameprofile.id.to_string())?;
let name_jstr = env.new_string(&player.gameprofile.name)?; let name_jstr = env.new_string(&player.gameprofile.name)?;
let entity_id = player.living_entity.entity.entity_id;
env.call_static_method( env.call_static_method(
jni::jni_str!("org/patchbukkit/PatchBukkitServer"), jni::jni_str!("org/patchbukkit/PatchBukkitServer"),
jni::jni_str!("registerPlayer"), jni::jni_str!("registerPlayer"),
jni::jni_sig!("(Ljava/lang/String;Ljava/lang/String;Z)V"), jni::jni_sig!("(Ljava/lang/String;Ljava/lang/String;ZI)V"),
&[ &[
(&uuid_jstr).into(), (&uuid_jstr).into(),
(&name_jstr).into(), (&name_jstr).into(),
jni::objects::JValue::Bool(is_op), jni::objects::JValue::Bool(is_op),
jni::objects::JValue::Int(entity_id),
], ],
)?; )?;

View File

@ -23,7 +23,7 @@ use crate::{
}, },
}; };
async fn on_load_inner(plugin: &PatchBukkitPlugin, server: Arc<Context>) -> Result<(), String> { pub async fn on_load_inner(plugin: &PatchBukkitPlugin, server: Arc<Context>) -> Result<(), String> {
server.init_log(); server.init_log();
tracing::info!("Starting PatchBukkit"); tracing::info!("Starting PatchBukkit");
@ -124,7 +124,10 @@ async fn on_load_inner(plugin: &PatchBukkitPlugin, server: Arc<Context>) -> Resu
Ok(()) Ok(())
} }
async fn on_unload_inner(plugin: &PatchBukkitPlugin, _server: Arc<Context>) -> Result<(), String> { pub async fn on_unload_inner(
plugin: &PatchBukkitPlugin,
_server: Arc<Context>,
) -> Result<(), String> {
{ {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
plugin plugin

View File

@ -0,0 +1,231 @@
use patchbukkit::{
PatchBukkitPlugin, commands::SimpleCommandSender, directories::setup_directories,
java::jvm::commands::JvmCommand,
};
use pumpkin::{PumpkinServer, data::VanillaData, plugin::Context};
use pumpkin_config::{AdvancedConfiguration, BasicConfiguration};
use std::{fs, sync::Arc};
use tokio::sync::oneshot;
#[tokio::test]
async fn test_pumpkin_server_with_patchbukkit_and_plugins() {
let _ = tracing_subscriber::fmt().try_init();
let temp_dir = tempfile::tempdir().expect("Failed to create tempdir");
let prev_dir = std::env::current_dir().expect("Failed to get current dir");
std::env::set_current_dir(temp_dir.path()).expect("Failed to set current dir");
let basic_config = BasicConfiguration {
default_level_name: "test_world".to_string(),
..Default::default()
};
let mut advanced_config = AdvancedConfiguration::default();
advanced_config.networking.java.enabled = false;
advanced_config.networking.bedrock.enabled = false;
advanced_config.networking.rcon.enabled = false;
let vanilla_data = VanillaData::load();
let telemetry_config = pumpkin_config::TelemetryConfig::default();
let pumpkin_server = PumpkinServer::new(
basic_config,
advanced_config,
telemetry_config,
vanilla_data,
)
.await;
let metadata = pumpkin::plugin::PluginMetadata {
name: "patchbukkit".to_string(),
version: "0.1.0".to_string(),
authors: vec!["PumpkinMC".to_string()],
description: "PatchBukkit".to_string(),
dependencies: vec![],
permissions: vec![],
};
let handlers = Arc::new(arc_swap::ArcSwap::from_pointee(
std::collections::HashMap::new(),
));
let context = Arc::new(Context::new(
metadata,
pumpkin_server.server.clone(),
handlers,
Arc::clone(&pumpkin_server.server.plugin_manager),
Arc::clone(&pumpkin::LOGGER_IMPL),
));
// Setup directories
let dirs = setup_directories(&context).expect("Failed to setup PatchBukkit directories");
// Resolve test plugins built in java/patchbukkit/build/test-plugins
let test_plugins_dir = if prev_dir.join("java").exists() {
prev_dir
.join("java")
.join("patchbukkit")
.join("build")
.join("test-plugins")
} else if let Some(parent) = prev_dir.parent() {
parent
.join("java")
.join("patchbukkit")
.join("build")
.join("test-plugins")
} else {
std::path::PathBuf::from("java/patchbukkit/build/test-plugins")
};
let packetevents_source = test_plugins_dir.join("PacketEvents.jar");
if packetevents_source.exists() {
fs::copy(&packetevents_source, dirs.plugins.join("PacketEvents.jar"))
.expect("Failed to copy PacketEvents.jar to plugins dir");
println!(
"Copied PacketEvents.jar to plugins directory: {:?}",
dirs.plugins
);
}
let protocollib_source = test_plugins_dir.join("ProtocolLib.jar");
if protocollib_source.exists() {
fs::copy(&protocollib_source, dirs.plugins.join("ProtocolLib.jar"))
.expect("Failed to copy ProtocolLib.jar to plugins dir");
println!(
"Copied ProtocolLib.jar to plugins directory: {:?}",
dirs.plugins
);
}
let grimac_system_source = std::path::PathBuf::from(
"/home/alex/Documents/Development/Rust/Pumpkin/plugins/data/patchbukkit/patchbukkit-plugins/grimac-bukkit-2.3.74-61caa53.jar",
);
if grimac_system_source.exists() {
let _ = fs::copy(&grimac_system_source, dirs.plugins.join("GrimAC.jar"));
println!("Copied GrimAC.jar to plugins directory: {:?}", dirs.plugins);
}
let plugin = PatchBukkitPlugin::new();
// Call on_load_inner
patchbukkit::on_load_inner(&plugin, context.clone())
.await
.expect("Failed to on_load PatchBukkit");
// Wait for PatchBukkit JVM initialization and plugin loading to complete
let mut initialized = false;
for _ in 0..100 {
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
let (tx, rx) = oneshot::channel();
if plugin
.command_tx
.send(JvmCommand::TriggerCommand {
full_command: "version".to_string(),
command_sender: SimpleCommandSender::Console,
respond_to: tx,
})
.await
.is_ok()
&& let Ok(res) = rx.await
&& res.is_ok()
{
initialized = true;
break;
}
}
assert!(
initialized,
"PatchBukkit JVM should be initialized and ready"
);
if packetevents_source.exists() {
let (tx, rx) = oneshot::channel();
plugin
.command_tx
.send(JvmCommand::TriggerCommand {
full_command: "packetevents".to_string(),
command_sender: SimpleCommandSender::Console,
respond_to: tx,
})
.await
.expect("Failed to send packetevents TriggerCommand");
let pe_res = rx.await.expect("Failed to receive packetevents response");
assert!(
pe_res.is_ok(),
"packetevents command should execute successfully: {:?}",
pe_res
);
}
if grimac_system_source.exists() {
let (tx, rx) = oneshot::channel();
plugin
.command_tx
.send(JvmCommand::TriggerCommand {
full_command: "grim".to_string(),
command_sender: SimpleCommandSender::Console,
respond_to: tx,
})
.await
.expect("Failed to send grim TriggerCommand");
let grim_res = rx.await.expect("Failed to receive grim response");
assert!(
grim_res.is_ok(),
"grim command should execute successfully: {:?}",
grim_res
);
// Test PlayerJoinEvent with GrimAC and PacketEvents
let player_uuid = uuid::Uuid::new_v4();
let (join_tx, join_rx) = oneshot::channel();
plugin
.command_tx
.send(JvmCommand::FireEvent {
payload: patchbukkit::events::handler::JvmEventPayload {
event: patchbukkit::proto::patchbukkit::events::Event {
data: Some(
patchbukkit::proto::patchbukkit::events::event::Data::PlayerJoin(
patchbukkit::proto::patchbukkit::events::PlayerJoinEvent {
player_uuid: Some(
patchbukkit::proto::patchbukkit::common::Uuid {
value: player_uuid.to_string(),
},
),
join_message: String::new(),
entity_id: 1,
},
),
),
},
context: patchbukkit::events::handler::EventContext {
server: pumpkin_server.server.clone(),
player: None,
},
},
plugin: "GrimAC".to_string(),
respond_to: join_tx,
})
.await
.expect("Failed to send PlayerJoin JvmCommand");
let join_res = join_rx
.await
.expect("Failed to receive PlayerJoin response");
assert!(
!join_res.cancelled,
"PlayerJoin should not be cancelled: {:?}",
join_res
);
}
// Call on_unload_inner to gracefully shut down
patchbukkit::on_unload_inner(&plugin, context.clone())
.await
.expect("Failed to on_unload PatchBukkit");
// Restore working directory
let _ = std::env::set_current_dir(prev_dir);
tokio::task::spawn_blocking(move || drop(plugin))
.await
.unwrap();
}