many fixes

This commit is contained in:
Alexander Medvedev 2026-08-19 21:44:30 +02:00
parent e1fb278af0
commit 43e65ca412
40 changed files with 2140 additions and 2646 deletions

3
.gitignore vendored
View File

@ -4,9 +4,6 @@
# Rust resources
rust/resources/jassets/*
!rust/resources/jassets/j4rs-*
rust/resources/deps/*
# Gradle cache
.gradle/

View File

@ -47,9 +47,8 @@ repositories {
}
dependencies {
paperweight.paperDevBundle("26.2.build.111-stable")
paperweight.paperDevBundle("26.2.build.112-stable")
implementation("net.sf.jopt-simple:jopt-simple:6.0-alpha-3")
implementation("io.github.astonbitecode:j4rs:0.25.2")
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-connector-basic:1.9.18")

View File

@ -42,6 +42,8 @@ public class SimpleCommandMap implements CommandMap {
knownCommands.put(label, command);
registeredDirectly = true;
}
command.register(this);
knownCommands.put(fallbackPrefix + ":" + label, command);
if (command.getAliases() != null) {

View File

@ -72,7 +72,7 @@ public class PatchBukkitInternalAPIBridge implements InternalAPIBridge {
JavaPlugin plugin,
BooleanSupplier registrationCheck
) {
return null;
return new org.patchbukkit.events.PatchBukkitLifecycleEventManager(plugin, registrationCheck);
}
public String getStatisticCriteriaKey(Statistic statistic) {

View File

@ -8,6 +8,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bukkit.event.Event;
import org.bukkit.event.Listener;
import org.bukkit.plugin.EventExecutor;
@ -24,6 +26,8 @@ import org.patchbukkit.PatchBukkitPluginManager;
@SuppressWarnings({ "deprecation", "removal" })
public class PatchBukkitPluginLoader implements PluginLoader {
private static final Logger LOGGER = Logger.getLogger("PatchBukkitPluginLoader");
public static Plugin createPlugin(
String jarPath,
String mainClass,
@ -33,7 +37,7 @@ public class PatchBukkitPluginLoader implements PluginLoader {
try {
File jarFile = new File(jarPath);
if (!jarFile.exists()) {
System.err.println(
LOGGER.severe(
"[PatchBukkit] Plugin file does not exist: " + jarPath
);
return null;
@ -87,16 +91,17 @@ public class PatchBukkitPluginLoader implements PluginLoader {
pm.registerPlugin(plugin);
}
} catch (Throwable ignored) {}
try {
PatchBukkitBootstrap.registerPluginCommands(plugin, classLoader.getDescription());
} catch (Throwable ignored) {}
try {
plugin.onLoad();
} catch (Throwable t) {
System.err.println("[PatchBukkit] Error during onLoad() for " + mainClass + ": " + t.getMessage());
t.printStackTrace();
LOGGER.log(Level.SEVERE, "[PatchBukkit] Error during onLoad() for " + mainClass, t);
}
return plugin;
} catch (Throwable e) {
System.err.println("[PatchBukkit] Failed to instantiate plugin class " + mainClass + ": " + e.getMessage());
e.printStackTrace();
LOGGER.log(Level.SEVERE, "[PatchBukkit] Failed to instantiate plugin class " + mainClass, e);
return null;
}
}
@ -110,11 +115,11 @@ public class PatchBukkitPluginLoader implements PluginLoader {
return true;
}
}
} catch (Throwable t) {
System.err.println("[PatchBukkit] Failed to enable plugin " + pluginName + ": " + t.getMessage());
t.printStackTrace();
}
return false;
} catch (Throwable t) {
LOGGER.log(Level.SEVERE, "[PatchBukkit] Failed to enable plugin " + pluginName, t);
return false;
}
}
public static boolean disablePlugin(String pluginName) {
@ -126,11 +131,11 @@ public class PatchBukkitPluginLoader implements PluginLoader {
return true;
}
}
} catch (Throwable t) {
System.err.println("[PatchBukkit] Failed to disable plugin " + pluginName + ": " + t.getMessage());
t.printStackTrace();
}
return false;
} catch (Throwable t) {
LOGGER.log(Level.SEVERE, "[PatchBukkit] Failed to disable plugin " + pluginName, t);
return false;
}
}
@Override

View File

@ -107,11 +107,13 @@ public class PatchBukkitPluginManager implements PluginManager {
Class<? extends Plugin> pluginClass = jarClass.asSubclass(Plugin.class);
Plugin plugin = pluginClass.getDeclaredConstructor().newInstance();
loader.init((org.bukkit.plugin.java.JavaPlugin) plugin);
try {
org.patchbukkit.loader.PatchBukkitBootstrap.registerPluginCommands(plugin, description);
} catch (Throwable ignored) {}
try {
plugin.onLoad();
} catch (Throwable t) {
server.getLogger().severe("Error loading " + plugin.getName() + ": " + t.getMessage());
t.printStackTrace();
server.getLogger().log(Level.SEVERE, "Error loading " + plugin.getName(), t);
}
registerPlugin(plugin);
return plugin;
@ -145,7 +147,7 @@ public class PatchBukkitPluginManager implements PluginManager {
result.add(plugin);
}
} catch (Exception e) {
server.getLogger().severe("Could not load plugin '" + file.getName() + "' in folder '" + file.getParent() + "': " + e.getMessage());
server.getLogger().log(Level.SEVERE, "Could not load plugin '" + file.getName() + "' in folder '" + file.getParent() + "'", e);
}
}
}

View File

@ -12,9 +12,15 @@ import io.papermc.paper.threadedregions.scheduler.GlobalRegionScheduler;
import io.papermc.paper.threadedregions.scheduler.RegionScheduler;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -175,6 +181,10 @@ public class PatchBukkitServer implements Server {
}
}
private static final PrintStream ORIGINAL_OUT = System.out;
private static final PrintStream ORIGINAL_ERR = System.err;
private static final ThreadLocal<Boolean> IN_LOGGING = ThreadLocal.withInitial(() -> Boolean.FALSE);
private static final Logger logger = Logger.getLogger("Minecraft");
static {
@ -201,20 +211,136 @@ public class PatchBukkitServer implements Server {
root.addHandler(new java.util.logging.Handler() {
@Override
public void publish(java.util.logging.LogRecord record) {
if (record == null) return;
if (IN_LOGGING.get()) return;
IN_LOGGING.set(true);
try {
LogLevel logLevel = LEVEL_MAP.getOrDefault(record.getLevel(), LogLevel.INFO);
String message = formatLogRecord(record);
String loggerName = record.getLoggerName() != null ? record.getLoggerName() : "";
if (loggerName.startsWith("jdk.") || loggerName.startsWith("sun.") || loggerName.startsWith("java.") || loggerName.startsWith("javax.")) {
if (record.getLevel().intValue() < Level.WARNING.intValue()) {
return;
}
}
try {
NativeBridgeFfi.sendLog(
SendLogRequest.newBuilder()
.setLevel(logLevel)
.setMessage(record.getMessage() != null ? record.getMessage() : "")
.setLoggerName(record.getLoggerName() != null ? record.getLoggerName() : "")
.setMessage(message)
.setLoggerName(loggerName)
.build()
);
} catch (Throwable ignored) {}
} catch (Throwable t) {
ORIGINAL_ERR.println("[" + record.getLevel() + "][" + loggerName + "] " + message);
}
} 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 close() {}
});
redirectSystemStreams();
}
public static String formatLogRecord(java.util.logging.LogRecord record) {
String msg = record.getMessage();
if (msg == null) {
msg = "";
} else if (record.getResourceBundle() != null) {
try {
msg = record.getResourceBundle().getString(msg);
} catch (Exception ignored) {}
}
Object[] params = record.getParameters();
if (params != null && params.length > 0 && msg.contains("{0}")) {
try {
msg = MessageFormat.format(msg, params);
} catch (Exception ignored) {}
}
if (record.getThrown() != null) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
if (!msg.isEmpty()) {
pw.println(msg);
}
record.getThrown().printStackTrace(pw);
pw.flush();
msg = sw.toString();
}
return msg;
}
private static void redirectSystemStreams() {
try {
System.setOut(new LoggingPrintStream(ORIGINAL_OUT, Level.INFO, "System.out"));
System.setErr(new LoggingPrintStream(ORIGINAL_ERR, Level.SEVERE, "System.err"));
} catch (Throwable t) {
ORIGINAL_ERR.println("[PatchBukkit] Failed to redirect System streams: " + t.getMessage());
}
}
private static class LoggingPrintStream extends PrintStream {
private final PrintStream delegate;
private final Level level;
private final String loggerName;
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
public LoggingPrintStream(PrintStream delegate, Level level, String loggerName) {
super(delegate, true);
this.delegate = delegate;
this.level = level;
this.loggerName = loggerName;
}
@Override
public void write(int b) {
delegate.write(b);
if (b == '\n') {
flushBuffer();
} else if (b != '\r') {
buffer.write(b);
}
}
@Override
public void write(byte[] buf, int off, int len) {
delegate.write(buf, off, len);
for (int i = off; i < off + len; i++) {
byte b = buf[i];
if (b == '\n') {
flushBuffer();
} else if (b != '\r') {
buffer.write(b);
}
}
}
private synchronized void flushBuffer() {
if (buffer.size() == 0) return;
String line = buffer.toString(StandardCharsets.UTF_8);
buffer.reset();
if (line.isEmpty() || IN_LOGGING.get()) return;
IN_LOGGING.set(true);
try {
LogLevel logLevel = LEVEL_MAP.getOrDefault(level, LogLevel.INFO);
try {
NativeBridgeFfi.sendLog(
SendLogRequest.newBuilder()
.setLevel(logLevel)
.setMessage(line)
.setLoggerName(loggerName)
.build()
);
} catch (Throwable ignored) {}
} finally {
IN_LOGGING.set(false);
}
}
}
/**
@ -225,6 +351,21 @@ public class PatchBukkitServer implements Server {
this.onlinePlayersByName.put(player.getName().toLowerCase(), player);
}
public static void registerPlayer(String uuidStr, String name, boolean isOp) {
try {
UUID uuid = UUID.fromString(uuidStr);
org.patchbukkit.entity.PatchBukkitPlayer player = new org.patchbukkit.entity.PatchBukkitPlayer(uuid, name);
if (isOp) {
player.setOp(true);
}
if (org.bukkit.Bukkit.getServer() instanceof PatchBukkitServer server) {
server.registerPlayer(player);
}
} catch (Throwable t) {
logger.log(Level.SEVERE, "Failed to register player: " + name, t);
}
}
public PatchBukkitEventManager getEventManager() {
return this.pluginManager.getEventManager();
}
@ -312,10 +453,11 @@ public class PatchBukkitServer implements Server {
@Override
public @NotNull String getMinecraftVersion() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException(
"Unimplemented method 'getMinecraftVersion'"
);
String version = this.bukkitVersion;
if (version != null && version.contains("-")) {
return version.split("-")[0];
}
return version != null && !version.equals("Unknown-Version") ? version : "1.21.4";
}
@Override
@ -325,56 +467,36 @@ public class PatchBukkitServer implements Server {
@Override
public int getMaxPlayers() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException(
"Unimplemented method 'getMaxPlayers'"
);
return 20;
}
@Override
public void setMaxPlayers(int maxPlayers) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException(
"Unimplemented method 'setMaxPlayers'"
);
}
@Override
public int getPort() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException(
"Unimplemented method 'getPort'"
);
return 25565;
}
@Override
public int getViewDistance() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException(
"Unimplemented method 'getViewDistance'"
);
return 10;
}
@Override
public int getSimulationDistance() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException(
"Unimplemented method 'getSimulationDistance'"
);
return 10;
}
@Override
public @NotNull String getIp() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getIp'");
return "127.0.0.1";
}
@Override
public @NotNull String getWorldType() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException(
"Unimplemented method 'getWorldType'"
);
return "DEFAULT";
}
@Override

View File

@ -79,6 +79,8 @@ public class PatchBukkitCommandMap extends SimpleCommandMap {
registeredDirectly = true;
}
command.register(this);
registerVariants(label, command);
registerVariants(fallbackPrefix + ":" + label, command);
if (!clean.isEmpty()) {
@ -105,7 +107,8 @@ public class PatchBukkitCommandMap extends SimpleCommandMap {
.setPluginName(fallbackPrefix)
.build();
NativeBridgeFfi.registerCommand(request);
} catch (Throwable ignored) {
} catch (Throwable t) {
server.getLogger().log(java.util.logging.Level.WARNING, "Failed to register command to native bridge: " + label, t);
}
return registeredDirectly;
@ -140,12 +143,66 @@ public class PatchBukkitCommandMap extends SimpleCommandMap {
try {
String[] args = Arrays.copyOfRange(split, 1, split.length);
return command.execute(sender, rawLabel, args);
String executedLabel = rawLabel.startsWith("//") ? rawLabel : cleanLabel(rawLabel);
return command.execute(sender, executedLabel, args);
} catch (Exception ex) {
throw new CommandException("Unhandled exception executing '" + cmdLine + "'", ex);
}
}
public static boolean dispatchRaw(String senderUuid, String senderName, boolean isOp, String commandLine) {
try {
org.bukkit.command.CommandSender sender;
if (senderUuid != null && !senderUuid.isBlank()) {
java.util.UUID uuid = java.util.UUID.fromString(senderUuid);
sender = org.bukkit.Bukkit.getPlayer(uuid);
if (sender == null) {
sender = new org.patchbukkit.entity.PatchBukkitPlayer(uuid, senderName != null ? senderName : "Player");
}
if (sender instanceof org.patchbukkit.entity.PatchBukkitPlayer p) {
p.setOp(isOp);
}
} else {
sender = org.bukkit.Bukkit.getConsoleSender();
}
return org.bukkit.Bukkit.dispatchCommand(sender, commandLine);
} catch (Throwable t) {
org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "Error dispatching command: " + commandLine, t);
return false;
}
}
public static String[] tabCompleteRaw(String senderUuid, String senderName, boolean isOp, String fullCommand, String worldName, double x, double y, double z) {
try {
org.bukkit.command.CommandSender sender;
if (senderUuid != null && !senderUuid.isBlank()) {
java.util.UUID uuid = java.util.UUID.fromString(senderUuid);
sender = org.bukkit.Bukkit.getPlayer(uuid);
if (sender == null) {
sender = new org.patchbukkit.entity.PatchBukkitPlayer(uuid, senderName != null ? senderName : "Player");
}
if (sender instanceof org.patchbukkit.entity.PatchBukkitPlayer p) {
p.setOp(isOp);
}
} else {
sender = org.bukkit.Bukkit.getConsoleSender();
}
Location loc = null;
if (worldName != null && !worldName.isBlank()) {
org.bukkit.World world = org.patchbukkit.world.PatchBukkitWorld.getOrCreate(worldName);
loc = new Location(world, x, y, z);
}
List<String> list = org.bukkit.Bukkit.getServer().getCommandMap().tabComplete(sender, fullCommand, loc);
if (list == null || list.isEmpty()) {
return new String[0];
}
return list.toArray(new String[0]);
} catch (Throwable t) {
org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "Error during tab completion: " + fullCommand, t);
return new String[0];
}
}
@Override
public void clearCommands() {
knownCommands.clear();

View File

@ -1240,15 +1240,18 @@ public class PatchBukkitPlayer
@Override
public boolean getAllowFlight() {
return NativeBridgeFfi.getAbilities(BridgeUtils.convertUuid(this.uuid)).getAllowFlying();
var abilities = NativeBridgeFfi.getAbilities(BridgeUtils.convertUuid(this.uuid));
return abilities != null && abilities.getAllowFlying();
}
@Override
public void setAllowFlight(boolean flight) {
var abilities = NativeBridgeFfi.getAbilities(BridgeUtils.convertUuid(this.uuid)).toBuilder();
abilities.setAllowFlying(flight);
if (abilities.getFlying()) abilities.setFlying(false);
NativeBridgeFfi.setAbilities(SetAbilitiesRequest.newBuilder().setAbilities(abilities).setUuid(BridgeUtils.convertUuid(this.uuid)).build());
var playerUuid = BridgeUtils.convertUuid(this.uuid);
var abilities = NativeBridgeFfi.getAbilities(playerUuid);
var builder = (abilities != null ? abilities.toBuilder() : patchbukkit.abilities.Abilities.newBuilder());
builder.setAllowFlying(flight);
if (builder.getFlying()) builder.setFlying(false);
NativeBridgeFfi.setAbilities(SetAbilitiesRequest.newBuilder().setAbilities(builder.build()).setUuid(playerUuid).build());
}
@Override
@ -1335,14 +1338,17 @@ public class PatchBukkitPlayer
@Override
public boolean isFlying() {
return NativeBridgeFfi.getAbilities(BridgeUtils.convertUuid(this.uuid)).getFlying();
var abilities = NativeBridgeFfi.getAbilities(BridgeUtils.convertUuid(this.uuid));
return abilities != null && abilities.getFlying();
}
@Override
public void setFlying(boolean value) {
var playerUuid = BridgeUtils.convertUuid(this.getUniqueId());
var abilities = NativeBridgeFfi.getAbilities(playerUuid).toBuilder().setFlying(value).build();
NativeBridgeFfi.setAbilities(SetAbilitiesRequest.newBuilder().setAbilities(abilities).setUuid(playerUuid).build());
var abilities = NativeBridgeFfi.getAbilities(playerUuid);
var builder = (abilities != null ? abilities.toBuilder() : patchbukkit.abilities.Abilities.newBuilder());
builder.setFlying(value);
NativeBridgeFfi.setAbilities(SetAbilitiesRequest.newBuilder().setAbilities(builder.build()).setUuid(playerUuid).build());
}
@Override
@ -1351,8 +1357,10 @@ public class PatchBukkitPlayer
throw new IllegalArgumentException("Speed value " + value + " is not between -1.0 and 1.0");
}
var playerUuid = BridgeUtils.convertUuid(this.getUniqueId());
var abilities = NativeBridgeFfi.getAbilities(playerUuid).toBuilder().setFlySpeed(value).build();
NativeBridgeFfi.setAbilities(SetAbilitiesRequest.newBuilder().setAbilities(abilities).setUuid(playerUuid).build());
var abilities = NativeBridgeFfi.getAbilities(playerUuid);
var builder = (abilities != null ? abilities.toBuilder() : patchbukkit.abilities.Abilities.newBuilder());
builder.setFlySpeed(value);
NativeBridgeFfi.setAbilities(SetAbilitiesRequest.newBuilder().setAbilities(builder.build()).setUuid(playerUuid).build());
}
@Override
@ -1361,18 +1369,22 @@ public class PatchBukkitPlayer
throw new IllegalArgumentException("Speed value " + value + " is not between -1.0 and 1.0");
}
var playerUuid = BridgeUtils.convertUuid(this.getUniqueId());
var abilities = NativeBridgeFfi.getAbilities(playerUuid).toBuilder().setWalkSpeed(value).build();
NativeBridgeFfi.setAbilities(SetAbilitiesRequest.newBuilder().setAbilities(abilities).setUuid(playerUuid).build());
var abilities = NativeBridgeFfi.getAbilities(playerUuid);
var builder = (abilities != null ? abilities.toBuilder() : patchbukkit.abilities.Abilities.newBuilder());
builder.setWalkSpeed(value);
NativeBridgeFfi.setAbilities(SetAbilitiesRequest.newBuilder().setAbilities(builder.build()).setUuid(playerUuid).build());
}
@Override
public float getFlySpeed() {
return NativeBridgeFfi.getAbilities(BridgeUtils.convertUuid(this.uuid)).getFlySpeed();
var abilities = NativeBridgeFfi.getAbilities(BridgeUtils.convertUuid(this.uuid));
return abilities != null ? abilities.getFlySpeed() : 0.05f;
}
@Override
public float getWalkSpeed() {
return NativeBridgeFfi.getAbilities(BridgeUtils.convertUuid(this.uuid)).getWalkSpeed();
var abilities = NativeBridgeFfi.getAbilities(BridgeUtils.convertUuid(this.uuid));
return abilities != null ? abilities.getWalkSpeed() : 0.1f;
}
@Override

View File

@ -33,6 +33,7 @@ import patchbukkit.events.BlockIgniteEvent;
import patchbukkit.events.ServerCommandEvent;
import patchbukkit.events.SignChangeEvent;
import java.util.logging.Level;
import java.util.logging.Logger;
public class PatchBukkitEventFactory {
@ -44,11 +45,22 @@ public class PatchBukkitEventFactory {
Event event = Event.parseFrom(data);
return createEvent(event);
} catch (InvalidProtocolBufferException e) {
LOGGER.severe("Failed to parse Event: " + e.getMessage());
LOGGER.log(Level.SEVERE, "Failed to parse Event", e);
return null;
}
}
public static byte[] fireEventFromBytes(byte[] data, String pluginName) {
org.bukkit.event.Event event = createEventFromBytes(data);
if (event == null) {
return FireEventResponse.newBuilder().setCancelled(false).build().toByteArray();
}
if (Bukkit.getServer() instanceof org.patchbukkit.PatchBukkitServer server) {
server.getEventManager().fireEvent(event, pluginName);
}
return toFireEventResponse(event);
}
@Nullable
public static org.bukkit.event.Event createEvent(@NotNull Event event) {
try {
@ -427,7 +439,10 @@ public class PatchBukkitEventFactory {
java.util.UUID uuid = java.util.UUID.fromString(uuidStr);
Player player = Bukkit.getServer().getPlayer(uuid);
if (player == null) {
LOGGER.warning("EventFactory: Player not found for UUID " + uuidStr);
player = new org.patchbukkit.entity.PatchBukkitPlayer(uuid, "Player");
if (Bukkit.getServer() instanceof org.patchbukkit.PatchBukkitServer server) {
server.registerPlayer(player);
}
}
return player;
} catch (IllegalArgumentException e) {

View File

@ -53,6 +53,8 @@ public class PatchBukkitEventManager {
this.server = server;
}
private final Set<String> registeredBridgeEvents = java.util.concurrent.ConcurrentHashMap.newKeySet();
public void callEvent(@NotNull Event event) throws IllegalStateException {
if (event.isAsynchronous() && this.server.isPrimaryThread()) {
throw new IllegalStateException(event.getEventName() + " may only be triggered asynchronously.");
@ -60,286 +62,8 @@ public class PatchBukkitEventManager {
throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously.");
}
var request = CallEventRequest.newBuilder();
switch (event.getEventName()) {
case "org.bukkit.event.player.PlayerJoinEvent":
var castedEvent = (org.bukkit.event.player.PlayerJoinEvent) event;
request.setEvent(
patchbukkit.events.Event.newBuilder().setPlayerJoin(
PlayerJoinEvent.newBuilder()
.setJoinMessage(castedEvent.joinMessage().toString())
.setPlayerUuid(BridgeUtils.convertUuid(castedEvent.getPlayer().getUniqueId())).build()
).build()
);
break;
case "org.bukkit.event.player.PlayerQuitEvent":
var castedQuit = (org.bukkit.event.player.PlayerQuitEvent) event;
String qMsg = castedQuit.quitMessage() != null ? castedQuit.quitMessage().toString() : "";
request.setEvent(
patchbukkit.events.Event.newBuilder().setPlayerQuit(
PlayerQuitEvent.newBuilder()
.setQuitMessage(qMsg)
.setPlayerUuid(BridgeUtils.convertUuid(castedQuit.getPlayer().getUniqueId())).build()
).build()
);
break;
case "org.bukkit.event.player.PlayerGameModeChangeEvent":
var castedGM = (org.bukkit.event.player.PlayerGameModeChangeEvent) event;
request.setEvent(
patchbukkit.events.Event.newBuilder().setPlayerGamemodeChange(
PlayerGameModeChangeEvent.newBuilder()
.setNewGamemode(castedGM.getNewGameMode().name())
.setPlayerUuid(BridgeUtils.convertUuid(castedGM.getPlayer().getUniqueId())).build()
).build()
);
break;
case "org.bukkit.event.player.PlayerInteractEvent":
var castedInteract = (org.bukkit.event.player.PlayerInteractEvent) event;
var block = castedInteract.getClickedBlock();
int x = block != null ? block.getX() : 0;
int y = block != null ? block.getY() : 0;
int z = block != null ? block.getZ() : 0;
request.setEvent(
patchbukkit.events.Event.newBuilder().setPlayerInteract(
PlayerInteractEvent.newBuilder()
.setAction(castedInteract.getAction().name())
.setClickedX(x)
.setClickedY(y)
.setClickedZ(z)
.setBlockFace(castedInteract.getBlockFace().name())
.setHand(castedInteract.getHand() != null ? castedInteract.getHand().name() : "HAND")
.setPlayerUuid(BridgeUtils.convertUuid(castedInteract.getPlayer().getUniqueId())).build()
).build()
);
break;
case "org.bukkit.event.server.PluginEnableEvent":
var castedEnable = (org.bukkit.event.server.PluginEnableEvent) event;
request.setEvent(
patchbukkit.events.Event.newBuilder().setPluginEnable(
PluginEnableEvent.newBuilder()
.setPluginName(castedEnable.getPlugin().getName()).build()
).build()
);
break;
case "org.bukkit.event.block.SignChangeEvent":
var castedSign = (org.bukkit.event.block.SignChangeEvent) event;
var signBlock = castedSign.getBlock();
var signBuilder = patchbukkit.events.SignChangeEvent.newBuilder()
.setBlockX(signBlock.getX())
.setBlockY(signBlock.getY())
.setBlockZ(signBlock.getZ());
if (castedSign.getPlayer() != null) {
signBuilder.setPlayerUuid(BridgeUtils.convertUuid(castedSign.getPlayer().getUniqueId()));
}
for (String line : castedSign.getLines()) {
signBuilder.addLines(line != null ? line : "");
}
request.setEvent(patchbukkit.events.Event.newBuilder().setSignChange(signBuilder.build()).build());
break;
case "org.bukkit.event.block.BlockDamageEvent":
var castedDamage = (org.bukkit.event.block.BlockDamageEvent) event;
var dmgBlock = castedDamage.getBlock();
var dmgBuilder = patchbukkit.events.BlockDamageEvent.newBuilder()
.setBlockX(dmgBlock.getX())
.setBlockY(dmgBlock.getY())
.setBlockZ(dmgBlock.getZ())
.setInstaBreak(castedDamage.getInstaBreak());
if (castedDamage.getPlayer() != null) {
dmgBuilder.setPlayerUuid(BridgeUtils.convertUuid(castedDamage.getPlayer().getUniqueId()));
}
request.setEvent(patchbukkit.events.Event.newBuilder().setBlockDamage(dmgBuilder.build()).build());
break;
case "org.bukkit.event.block.BlockIgniteEvent":
var castedIgnite = (org.bukkit.event.block.BlockIgniteEvent) event;
var igniteBlock = castedIgnite.getBlock();
var igniteBuilder = patchbukkit.events.BlockIgniteEvent.newBuilder()
.setBlockX(igniteBlock.getX())
.setBlockY(igniteBlock.getY())
.setBlockZ(igniteBlock.getZ())
.setIgniteCause(castedIgnite.getCause() != null ? castedIgnite.getCause().name() : "FLINT_AND_STEEL");
if (castedIgnite.getPlayer() != null) {
igniteBuilder.setPlayerUuid(BridgeUtils.convertUuid(castedIgnite.getPlayer().getUniqueId()));
}
request.setEvent(patchbukkit.events.Event.newBuilder().setBlockIgnite(igniteBuilder.build()).build());
break;
case "org.bukkit.event.block.BlockGrowEvent":
var castedGrow = (org.bukkit.event.block.BlockGrowEvent) event;
var growBlock = castedGrow.getBlock();
request.setEvent(patchbukkit.events.Event.newBuilder().setBlockGrow(
patchbukkit.events.BlockGrowEvent.newBuilder()
.setBlockX(growBlock.getX())
.setBlockY(growBlock.getY())
.setBlockZ(growBlock.getZ())
.setNewBlockType(castedGrow.getNewState().getType().name()).build()
).build());
break;
case "org.bukkit.event.block.BlockFormEvent":
var castedForm = (org.bukkit.event.block.BlockFormEvent) event;
var formBlock = castedForm.getBlock();
request.setEvent(patchbukkit.events.Event.newBuilder().setBlockForm(
patchbukkit.events.BlockFormEvent.newBuilder()
.setBlockX(formBlock.getX())
.setBlockY(formBlock.getY())
.setBlockZ(formBlock.getZ())
.setNewBlockType(castedForm.getNewState().getType().name()).build()
).build());
break;
case "org.bukkit.event.block.BlockFadeEvent":
var castedFade = (org.bukkit.event.block.BlockFadeEvent) event;
var fadeBlock = castedFade.getBlock();
request.setEvent(patchbukkit.events.Event.newBuilder().setBlockFade(
patchbukkit.events.BlockFadeEvent.newBuilder()
.setBlockX(fadeBlock.getX())
.setBlockY(fadeBlock.getY())
.setBlockZ(fadeBlock.getZ())
.setNewBlockType(castedFade.getNewState().getType().name()).build()
).build());
break;
case "org.bukkit.event.server.ServerCommandEvent":
var castedCmd = (org.bukkit.event.server.ServerCommandEvent) event;
request.setEvent(
patchbukkit.events.Event.newBuilder().setServerCommand(
patchbukkit.events.ServerCommandEvent.newBuilder()
.setSenderName(castedCmd.getSender().getName())
.setCommand(castedCmd.getCommand()).build()
).build()
);
break;
case "org.bukkit.event.server.PluginDisableEvent":
var castedDisable = (org.bukkit.event.server.PluginDisableEvent) event;
request.setEvent(
patchbukkit.events.Event.newBuilder().setPluginDisable(
PluginDisableEvent.newBuilder()
.setPluginName(castedDisable.getPlugin().getName()).build()
).build()
);
break;
case "org.bukkit.event.block.BlockBreakEvent":
var castedBreak = (org.bukkit.event.block.BlockBreakEvent) event;
var breakBlock = castedBreak.getBlock();
var bBuilder = BlockBreakEvent.newBuilder()
.setBlockX(breakBlock.getX())
.setBlockY(breakBlock.getY())
.setBlockZ(breakBlock.getZ())
.setBlockType(breakBlock.getType().name())
.setExp(castedBreak.getExpToDrop())
.setDropItems(castedBreak.isDropItems());
if (castedBreak.getPlayer() != null) {
bBuilder.setPlayerUuid(BridgeUtils.convertUuid(castedBreak.getPlayer().getUniqueId()));
}
request.setEvent(patchbukkit.events.Event.newBuilder().setBlockBreak(bBuilder.build()).build());
break;
case "org.bukkit.event.block.BlockPlaceEvent":
var castedPlace = (org.bukkit.event.block.BlockPlaceEvent) event;
var placedBlock = castedPlace.getBlockPlaced();
var againstBlock = castedPlace.getBlockAgainst();
request.setEvent(
patchbukkit.events.Event.newBuilder().setBlockPlace(
BlockPlaceEvent.newBuilder()
.setBlockX(placedBlock.getX())
.setBlockY(placedBlock.getY())
.setBlockZ(placedBlock.getZ())
.setBlockPlacedType(placedBlock.getType().name())
.setBlockAgainstType(againstBlock.getType().name())
.setCanBuild(castedPlace.canBuild())
.setPlayerUuid(BridgeUtils.convertUuid(castedPlace.getPlayer().getUniqueId())).build()
).build()
);
break;
case "org.bukkit.event.player.PlayerToggleSneakEvent":
var castedSneak = (org.bukkit.event.player.PlayerToggleSneakEvent) event;
request.setEvent(
patchbukkit.events.Event.newBuilder().setPlayerToggleSneak(
PlayerToggleSneakEvent.newBuilder()
.setIsSneaking(castedSneak.isSneaking())
.setPlayerUuid(BridgeUtils.convertUuid(castedSneak.getPlayer().getUniqueId())).build()
).build()
);
break;
case "org.bukkit.event.player.PlayerToggleSprintEvent":
var castedSprint = (org.bukkit.event.player.PlayerToggleSprintEvent) event;
request.setEvent(
patchbukkit.events.Event.newBuilder().setPlayerToggleSprint(
PlayerToggleSprintEvent.newBuilder()
.setIsSprinting(castedSprint.isSprinting())
.setPlayerUuid(BridgeUtils.convertUuid(castedSprint.getPlayer().getUniqueId())).build()
).build()
);
break;
case "org.bukkit.event.player.PlayerToggleFlightEvent":
var castedFlight = (org.bukkit.event.player.PlayerToggleFlightEvent) event;
request.setEvent(
patchbukkit.events.Event.newBuilder().setPlayerToggleFlight(
PlayerToggleFlightEvent.newBuilder()
.setIsFlying(castedFlight.isFlying())
.setPlayerUuid(BridgeUtils.convertUuid(castedFlight.getPlayer().getUniqueId())).build()
).build()
);
break;
case "org.bukkit.event.player.PlayerMoveEvent":
var castedMove = (org.bukkit.event.player.PlayerMoveEvent) event;
request.setEvent(
patchbukkit.events.Event.newBuilder().setPlayerMove(
PlayerMoveEvent.newBuilder()
.setPlayerUuid(BridgeUtils.convertUuid(castedMove.getPlayer().getUniqueId()))
.setFrom(BridgeUtils.convertLocation(castedMove.getFrom()))
.setTo(BridgeUtils.convertLocation(castedMove.getTo())).build()
).build()
);
break;
case "org.bukkit.event.player.PlayerInteractEntityEvent":
case "org.bukkit.event.player.PlayerInteractAtEntityEvent":
var castedInteractEntity = (org.bukkit.event.player.PlayerInteractEntityEvent) event;
request.setEvent(
patchbukkit.events.Event.newBuilder().setPlayerInteractEntity(
PlayerInteractEntityEvent.newBuilder()
.setPlayerUuid(BridgeUtils.convertUuid(castedInteractEntity.getPlayer().getUniqueId()))
.setTargetUuid(BridgeUtils.convertUuid(castedInteractEntity.getRightClicked().getUniqueId()))
.setAction("INTERACT")
.setIsSneaking(castedInteractEntity.getPlayer().isSneaking())
.build()
).build()
);
break;
case "org.bukkit.event.entity.EntityDamageByEntityEvent":
var castedDamageByEntity = (org.bukkit.event.entity.EntityDamageByEntityEvent) event;
if (castedDamageByEntity.getDamager() instanceof org.bukkit.entity.Player damagerPlayer) {
request.setEvent(
patchbukkit.events.Event.newBuilder().setPlayerInteractEntity(
PlayerInteractEntityEvent.newBuilder()
.setPlayerUuid(BridgeUtils.convertUuid(damagerPlayer.getUniqueId()))
.setTargetUuid(BridgeUtils.convertUuid(castedDamageByEntity.getEntity().getUniqueId()))
.setAction("ATTACK")
.setIsSneaking(damagerPlayer.isSneaking())
.build()
).build()
);
}
break;
case "org.bukkit.event.player.AsyncPlayerChatEvent":
case "org.bukkit.event.player.PlayerChatEvent":
var castedChat = (org.bukkit.event.player.AsyncPlayerChatEvent) event;
request.setEvent(
patchbukkit.events.Event.newBuilder().setPlayerChat(
PlayerChatEvent.newBuilder()
.setMessage(castedChat.getMessage())
.setFormat(castedChat.getFormat())
.setPlayerUuid(BridgeUtils.convertUuid(castedChat.getPlayer().getUniqueId())).build()
).build()
);
break;
}
var response = NativeBridgeFfi.callEvent(request.build());
boolean handledByPumpkin;
if (response == null) handledByPumpkin = false;
else handledByPumpkin = response.getHandled();
if (!handledByPumpkin) {
// Pumpkin doesn't know this event type, dispatch Java-only
callEventJavaOnly(event);
}
}
/**
* Java-only event dispatch for events that don't have Pumpkin equivalents.
@ -418,9 +142,19 @@ public class PatchBukkitEventManager {
for (Map.Entry<Class<? extends Event>, Set<RegisteredListener>> entry : this.createRegisteredListeners(listener, plugin).entrySet()) {
this.getEventListeners(this.getRegistrationClass(entry.getKey())).registerAll(entry.getValue());
String eventName = entry.getKey().getName();
String key = plugin.getName() + ":" + eventName;
if (registeredBridgeEvents.add(key)) {
int maxPriority = 0;
for (RegisteredListener rl : entry.getValue()) {
int priorityOrdinal = Math.min(rl.getPriority().ordinal(), 4);
var request = RegisterEventRequest.newBuilder().setEventType(entry.getKey().getName()).setPluginName(plugin.getName()).setPriority(priorityOrdinal).setBlocking(true).build();
maxPriority = Math.max(maxPriority, Math.min(rl.getPriority().ordinal(), 4));
}
var request = RegisterEventRequest.newBuilder()
.setEventType(eventName)
.setPluginName(plugin.getName())
.setPriority(maxPriority)
.setBlocking(true)
.build();
NativeBridgeFfi.registerEvent(request);
}
}
@ -438,10 +172,17 @@ public class PatchBukkitEventManager {
executor = new TimedEventExecutor(executor, plugin, null, event);
this.getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled));
String key = plugin.getName() + ":" + event.getName();
if (registeredBridgeEvents.add(key)) {
int priorityOrdinal = Math.min(priority.ordinal(), 4);
var request = RegisterEventRequest.newBuilder().setEventType(event.getName()).setPluginName(plugin.getName()).setPriority(priorityOrdinal).setBlocking(true).build();
var request = RegisterEventRequest.newBuilder()
.setEventType(event.getName())
.setPluginName(plugin.getName())
.setPriority(priorityOrdinal)
.setBlocking(true)
.build();
NativeBridgeFfi.registerEvent(request);
}
}
@NotNull
@ -534,5 +275,6 @@ public class PatchBukkitEventManager {
public void clearEvents() {
HandlerList.unregisterAll();
registeredBridgeEvents.clear();
}
}

View File

@ -6,6 +6,8 @@ import java.net.URL;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.Plugin;
@ -17,11 +19,13 @@ import org.patchbukkit.command.PatchBukkitCommandMap;
public class PatchBukkitBootstrap {
private static final Logger LOGGER = Logger.getLogger("PatchBukkitBootstrap");
public static boolean bootstrapPlugins(String pluginsDirPath) {
try {
File pluginsDir = new File(pluginsDirPath);
if (!pluginsDir.exists() || !pluginsDir.isDirectory()) {
System.out.println("[PatchBukkit] Plugins directory does not exist: " + pluginsDirPath);
LOGGER.info("[PatchBukkit] Plugins directory does not exist: " + pluginsDirPath);
return true;
}
@ -29,7 +33,7 @@ public class PatchBukkitBootstrap {
findJarsRecursive(pluginsDir, jarFiles);
if (jarFiles.isEmpty()) {
System.out.println("[PatchBukkit] No plugin JAR files found in: " + pluginsDirPath);
LOGGER.info("[PatchBukkit] No plugin JAR files found in: " + pluginsDirPath);
return true;
}
@ -42,8 +46,7 @@ public class PatchBukkitBootstrap {
holders.put(holder.name.toLowerCase(Locale.ENGLISH), holder);
}
} catch (Throwable t) {
System.err.println("[PatchBukkit] Failed to parse plugin JAR " + jarFile.getName() + ": " + t.getMessage());
t.printStackTrace();
LOGGER.log(Level.SEVERE, "[PatchBukkit] Failed to parse plugin JAR " + jarFile.getName(), t);
}
}
@ -53,8 +56,7 @@ public class PatchBukkitBootstrap {
try {
loadAndRegisterPlugin(holder, holders);
} catch (Throwable t) {
System.err.println("[PatchBukkit] Failed to load plugin " + holder.name + ": " + t.getMessage());
t.printStackTrace();
LOGGER.log(Level.SEVERE, "[PatchBukkit] Failed to load plugin " + holder.name, t);
}
}
@ -64,10 +66,9 @@ public class PatchBukkitBootstrap {
if (holder.pluginInstance != null) {
try {
pm.enablePlugin(holder.pluginInstance);
System.out.println("[PatchBukkit] Enabled plugin: " + holder.name);
LOGGER.info("[PatchBukkit] Enabled plugin: " + holder.name);
} catch (Throwable t) {
System.err.println("[PatchBukkit] Error enabling plugin " + holder.name + ": " + t.getMessage());
t.printStackTrace();
LOGGER.log(Level.SEVERE, "[PatchBukkit] Error enabling plugin " + holder.name, t);
}
}
}
@ -75,8 +76,7 @@ public class PatchBukkitBootstrap {
return true;
} catch (Throwable t) {
System.err.println("[PatchBukkit] Fatal error during bootstrap: " + t.getMessage());
t.printStackTrace();
LOGGER.log(Level.SEVERE, "[PatchBukkit] Fatal error during bootstrap", t);
return false;
}
}
@ -210,14 +210,17 @@ public class PatchBukkitBootstrap {
try {
plugin.onLoad();
} catch (Throwable t) {
System.err.println("[PatchBukkit] Error during onLoad() for " + holder.name + ": " + t.getMessage());
t.printStackTrace();
LOGGER.log(Level.SEVERE, "[PatchBukkit] Error during onLoad() for " + holder.name, t);
}
holder.pluginInstance = plugin;
registerPluginCommands(plugin, holder.description);
}
public static void registerPluginCommands(Plugin plugin, PluginDescriptionFile description) {
if (plugin == null || description == null) return;
if (Bukkit.getCommandMap() instanceof PatchBukkitCommandMap commandMap) {
Map<String, Map<String, Object>> commands = holder.description.getCommands();
Map<String, Map<String, Object>> commands = description.getCommands();
if (commands != null) {
for (Map.Entry<String, Map<String, Object>> entry : commands.entrySet()) {
String cmdName = entry.getKey();
@ -247,8 +250,8 @@ public class PatchBukkitBootstrap {
}
}
commandMap.register(cmdName, holder.name, pluginCmd);
System.out.println("[PatchBukkit] Registered command: /" + cmdName + " for plugin " + holder.name);
commandMap.register(cmdName, description.getName(), pluginCmd);
System.out.println("[PatchBukkit] Registered command: /" + cmdName + " for plugin " + description.getName());
}
}
}

View File

@ -30,7 +30,10 @@ public class PatchBukkitRegistry<P, B extends Keyed> implements Registry<B> {
private final ThreadLocal<Set<NamespacedKey>> inFallback = ThreadLocal.withInitial(HashSet::new);
private final RegistryKey<B> registryKey;
private boolean initialized = false;
private final RegistryType registryType;
private final Function<GetRegistryDataResponse, List<P>> extractor;
private final Function<P, B> factory;
private volatile boolean initialized = false;
public PatchBukkitRegistry(RegistryKey<B> registryKey) {
this(null, null, null, registryKey);
@ -51,19 +54,49 @@ public class PatchBukkitRegistry<P, B extends Keyed> implements Registry<B> {
RegistryKey<B> registryKey
) {
this.registryKey = registryKey;
if (registryType != null || extractor != null || factory != null) {
initialize(registryType, extractor, factory);
this.registryType = registryType;
this.extractor = extractor;
this.factory = factory;
}
public void ensureInitialized() {
if (initialized) return;
synchronized (this) {
if (initialized) return;
initialized = true;
initializeInternal();
}
}
@SuppressWarnings("unchecked")
public synchronized void initialize(
RegistryType registryType,
Function<GetRegistryDataResponse, List<P>> extractor,
Function<P, B> factory
) {
if (initialized) return;
initialized = true;
private void initializeInternal() {
if (registryType != null) {
try {
GetRegistryDataRequest request = GetRegistryDataRequest.newBuilder()
.setRegistry(registryType)
.build();
GetRegistryDataResponse response = NativeBridgeFfi.getRegistryData(request);
if (response != null && extractor != null) {
List<P> protoEntries = extractor.apply(response);
if (protoEntries != null) {
for (P protoEntry : protoEntries) {
if (protoEntry == null) continue;
try {
B value = factory != null ? factory.apply(protoEntry) : null;
if (value != null && value.getKey() != null) {
entries.put(value.getKey(), value);
}
} catch (Throwable t) {
// Skip invalid entry safely
}
}
}
}
} catch (Throwable t) {
// Ignore FFI or RPC failure safely
}
}
// Auto-discover pre-registered Paper/Bukkit constants for this registry
if (registryKey != null) {
@ -89,6 +122,8 @@ public class PatchBukkitRegistry<P, B extends Keyed> implements Registry<B> {
}
} catch (Throwable ignored) {}
}
} 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!
} else {
Class<B> valueClass = (Class<B>) LegacyRegistryIdentifiers.KEY_TO_CLASS_MAP.get(registryKey);
if (valueClass != null) {
@ -97,34 +132,6 @@ public class PatchBukkitRegistry<P, B extends Keyed> implements Registry<B> {
}
}
}
if (registryType != null) {
try {
GetRegistryDataRequest request = GetRegistryDataRequest.newBuilder()
.setRegistry(registryType)
.build();
GetRegistryDataResponse response = NativeBridgeFfi.getRegistryData(request);
if (response != null && extractor != null) {
List<P> protoEntries = extractor.apply(response);
if (protoEntries != null) {
for (P protoEntry : protoEntries) {
if (protoEntry == null) continue;
try {
B value = factory.apply(protoEntry);
if (value != null && value.getKey() != null) {
entries.put(value.getKey(), value);
}
} catch (Throwable t) {
// Skip invalid entry safely
}
}
}
}
} catch (Throwable t) {
// Ignore FFI or RPC failure safely
}
}
}
@SuppressWarnings("unchecked")
@ -162,6 +169,7 @@ public class PatchBukkitRegistry<P, B extends Keyed> implements Registry<B> {
if (key == null) {
return null;
}
ensureInitialized();
B value = entries.get(key);
if (value != null) {
return value;
@ -228,11 +236,13 @@ public class PatchBukkitRegistry<P, B extends Keyed> implements Registry<B> {
@Override
public boolean hasTag(TagKey<B> key) {
ensureInitialized();
return key != null && tags.containsKey(key.key().asString());
}
@Override
public @NonNull Tag<B> getTag(TagKey<B> key) {
ensureInitialized();
PatchBukkitTag<B> tag = tags.get(key.key().asString());
if (tag == null) {
throw new NoSuchElementException("Unknown tag: " + key.key().asString());
@ -246,25 +256,30 @@ public class PatchBukkitRegistry<P, B extends Keyed> implements Registry<B> {
@Override
public @NonNull Collection<Tag<B>> getTags() {
ensureInitialized();
return Collections.unmodifiableCollection(tags.values());
}
@Override
public @NonNull Stream<B> stream() {
ensureInitialized();
return entries.values().stream();
}
public @NonNull Stream<NamespacedKey> keyStream() {
ensureInitialized();
return entries.keySet().stream();
}
@Override
public int size() {
ensureInitialized();
return entries.size();
}
@Override
public @NonNull Iterator<B> iterator() {
ensureInitialized();
return Collections.unmodifiableCollection(entries.values()).iterator();
}
}

View File

@ -53,24 +53,16 @@ public class PatchBukkitRegistryAccess extends io.papermc.paper.registry.PaperRe
return (Registry<T>) existing;
}
PatchBukkitRegistry reg = new PatchBukkitRegistry(registryKey);
Registry<?> winner = instances.putIfAbsent(registryKey, reg);
if (winner != null) {
return (Registry<T>) winner;
}
try {
PatchBukkitRegistry reg;
RegistryFactory<?, ?> factoryEntry = FACTORIES.get(registryKey);
if (factoryEntry != null) {
reg.initialize(factoryEntry.registryType(), factoryEntry.extractor(), factoryEntry.factory());
reg = new PatchBukkitRegistry(factoryEntry.registryType(), factoryEntry.extractor(), factoryEntry.factory(), registryKey);
} else {
reg.initialize(null, null, null);
}
} catch (Throwable t) {
// Ignore failure during registry initialization safely
reg = new PatchBukkitRegistry(registryKey);
}
return (Registry<T>) reg;
Registry<?> winner = instances.putIfAbsent(registryKey, reg);
return (Registry<T>) (winner != null ? winner : reg);
}
@SuppressWarnings({"unchecked", "deprecation"})

View File

@ -55,6 +55,15 @@ public class PatchBukkitRegistryAccessTest {
});
}
@Test
public void testSoundClassInitialization() {
assertDoesNotThrow(() -> {
org.bukkit.Sound sound = org.bukkit.Sound.ENTITY_EXPERIENCE_ORB_PICKUP;
assertNotNull(sound, "Sound constant must initialize successfully without ExceptionInInitializerError");
assertNotNull(sound.getKey(), "Sound key must not be null");
});
}
@Test
public void testInspectBlockTypeAndDataMethods() {
System.out.println("=== BLOCK TYPE METHODS ===");

1176
rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@ pumpkin-api-macros = { git = "https://github.com/Pumpkin-MC/Pumpkin.git", branch
tokio = "1"
tracing = "0.1"
glob = "0.3.4"
j4rs = { version = "0.25.2" }
jni = { version = "0.22.4", features = ["invocation"] }
rust-embed = { version = "8.12.0", features = ["interpolate-folder-path"] }
walkdir = "2"
zip = "8.6"
@ -39,7 +39,6 @@ prost = "0.14"
toml = "1.1"
[build-dependencies]
j4rs = { version = "0.25.2", artifact = "cdylib", lib = true }
env_logger = "0.11.11"
log = "0.4"
prost-build = "0.14"

View File

@ -1,12 +1,10 @@
use std::{fs, path::PathBuf};
use j4rs::{JvmBuilder, MavenArtifactRepo, MavenSettings};
pub fn setup_java(base: PathBuf) {
let resources = base.join("resources");
let deps = resources.join("deps");
let jassets = resources.join("jassets");
let mut java_path = base.clone();
let mut java_path = base;
java_path.pop();
let java_path = java_path.join("java");
@ -16,72 +14,15 @@ pub fn setup_java(base: PathBuf) {
.join("libs")
.join("patchbukkit.jar");
let jassets = resources.join("jassets");
let mut classpath_entries = Vec::new();
if let Ok(entries) = fs::read_dir(&jassets) {
for entry in entries.flatten() {
let path = entry.path();
if let Some(filename) = path.file_name().and_then(|n| n.to_str())
&& filename.starts_with("j4rs")
&& filename.ends_with(".jar")
{
classpath_entries.push(j4rs::ClasspathEntry::new(&path));
}
}
}
let mut jvm_builder = JvmBuilder::new();
let mut jvm_builder = jvm_builder
.with_maven_settings(MavenSettings::new(vec![MavenArtifactRepo::from(
"papermc::https://repo.papermc.io/repository/maven-public/",
)]))
.skip_setting_native_lib()
.with_base_path(&resources)
.java_opts(vec![
j4rs::JavaOpt::new("--enable-native-access=ALL-UNNAMED"),
j4rs::JavaOpt::new("--enable-final-field-mutation=ALL-UNNAMED"),
j4rs::JavaOpt::new("--add-opens=java.base/java.lang=ALL-UNNAMED"),
j4rs::JavaOpt::new("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"),
j4rs::JavaOpt::new("-Dcom.google.protobuf.useUnsafe=false"),
]);
for entry in classpath_entries {
jvm_builder = jvm_builder.classpath_entry(entry);
}
let _jvm = jvm_builder
.build()
.map_err(|err| format!("jvm failed to init: {err:?}"))
.unwrap();
if !&patchbukkit_jar.exists() {
if !patchbukkit_jar.exists() {
panic!(
"Failed to find patchbukkit.jar, build the java library first by running `gradle build` in the java directory!"
"Failed to find patchbukkit.jar, build the java library first by running `./gradlew build` in the java directory!"
);
}
fs::create_dir_all(&jassets).unwrap();
let cdylib = std::env::var("CARGO_CDYLIB_FILE_J4RS").unwrap();
let cdylib = PathBuf::from(cdylib);
let mut cdylib_to = deps;
fs::create_dir_all(&cdylib_to).unwrap();
let original_name = cdylib.file_name().unwrap().to_string_lossy();
let mut target_name = original_name.split('-').next().unwrap().to_string();
if !target_name.ends_with(".dll")
&& !target_name.ends_with(".so")
&& !target_name.ends_with(".dylib")
&& let Some(ext) = cdylib.extension()
{
target_name = format!("{target_name}.{}", ext.to_string_lossy());
}
cdylib_to.push(target_name);
fs::copy(&cdylib, &cdylib_to)
.map_err(|err| format!("Failed to copy j4rs native lib: {err:?}"))
let dest_patchbukkit = jassets.join("patchbukkit.jar");
fs::copy(&patchbukkit_jar, &dest_patchbukkit)
.map_err(|err| format!("Failed to copy patchbukkit.jar to resources/jassets: {err:?}"))
.unwrap();
}

View File

@ -106,7 +106,6 @@ pub unsafe extern "C" fn {fn_name}(
output_len: *mut usize,
) -> *mut u8 {{
use prost::Message;
tracing::debug!("{fn_name}: called with input_ptr={{:?}}, input_len={{}}", input_ptr, input_len);
if input_ptr.is_null() {{
tracing::warn!("{fn_name}: null input (ptr={{:?}}, len={{}})", input_ptr, input_len);
unsafe {{ *output_len = 0 }};
@ -123,16 +122,13 @@ pub unsafe extern "C" fn {fn_name}(
return std::ptr::null_mut();
}};
let Some(response) = {0}::{fn_name}_impl(request) else {{
tracing::debug!("{fn_name}: impl returned None");
unsafe {{ *output_len = 0 }};
return std::ptr::null_mut();
}};
let encoded = response.encode_to_vec().into_boxed_slice();
let len = encoded.len();
unsafe {{ *output_len = len }};
let ptr = Box::into_raw(encoded) as *mut u8;
tracing::debug!("{fn_name}: returning ptr={{:?}}, len={{}}", ptr, len);
ptr
Box::into_raw(encoded) as *mut u8
}}
"#,
self.impl_module
@ -214,47 +210,61 @@ pub unsafe extern "C" fn ffi_free_bytes(ptr: *mut u8, len: usize) {{
}}
}}
pub fn initialize_ffi_callbacks(jvm: &j4rs::Jvm) -> anyhow::Result<()> {{
use j4rs::InvocationArg;"#
pub fn initialize_ffi_callbacks(env: &mut jni::Env) -> anyhow::Result<()> {{"#
)
.unwrap();
for service in &state.services {
let class_slash = service.java_class.replace('.', "/");
let sig = format!("({})V", "J".repeat(service.methods.len()));
writeln!(
file,
r#"
// Initialize {}
if let Err(e) = jvm.invoke_static(
"{}",
"init",
&["#,
service.java_class, service.java_class
// Initialize {class_slash}
{{
let args = ["#,
)
.unwrap();
for method in &service.methods {
writeln!(file,
" InvocationArg::try_from(crate::proto::{}::{} as *const () as i64)?.into_primitive()?,",
writeln!(
file,
" jni::objects::JValue::Long(crate::proto::{}::{} as *const () as i64),",
service.module_path, method.fn_name
).unwrap();
)
.unwrap();
}
writeln!(
file,
r#" ],
r#" ];
if let Err(e) = env.call_static_method(
jni::jni_str!("{class_slash}"),
jni::jni_str!("init"),
jni::jni_sig!("{sig}"),
&args,
) {{
tracing::error!("Failed to initialize FFI service: {{:?}}", e);
env.exception_describe();
env.exception_clear();
tracing::error!("Failed to initialize FFI service {class_slash}: {{:?}}", e);
return Err(anyhow::anyhow!("Failed to initialize FFI service {class_slash}: {{e:?}}"));
}}
if let Err(e) = jvm.invoke_static(
"{}",
"initFree",
&[InvocationArg::try_from(ffi_free_bytes as *const () as i64)?.into_primitive()?],
if let Err(e) = env.call_static_method(
jni::jni_str!("{class_slash}"),
jni::jni_str!("initFree"),
jni::jni_sig!("(J)V"),
&[jni::objects::JValue::Long(ffi_free_bytes as *const () as i64)],
) {{
tracing::error!("Failed to initialize FFI service initFree: {{:?}}", e);
env.exception_describe();
env.exception_clear();
tracing::error!("Failed to initialize FFI service initFree on {class_slash}: {{:?}}", e);
return Err(anyhow::anyhow!("Failed to initialize FFI service initFree on {class_slash}: {{e:?}}"));
}}
}}"#,
service.java_class
)
.unwrap();
}

View File

@ -25,8 +25,8 @@ pub struct JavaCommandExecutor {
#[derive(Clone)]
pub enum SimpleCommandSender {
Console,
/// UUID, Name
Player(String, String),
/// UUID, Name, is_op
Player(String, String, bool),
}
pub struct AnyCommandNode {
@ -94,7 +94,8 @@ impl ArgumentConsumer for AnyCommandNode {
Box::pin(async move {
let (tx, rx) = oneshot::channel();
self.command_tx
if let Err(e) = self
.command_tx
.send(JvmCommand::GetCommandTabComplete {
command_sender,
full_command: input.to_string(),
@ -102,9 +103,15 @@ impl ArgumentConsumer for AnyCommandNode {
location,
})
.await
.unwrap();
{
tracing::warn!("Failed to send tab complete to JVM worker: {e}");
return Ok(None);
}
rx.await.unwrap()
match rx.await {
Ok(res) => res,
Err(_) => Ok(None),
}
})
}
}
@ -113,10 +120,15 @@ impl From<&CommandSender> for SimpleCommandSender {
fn from(val: &CommandSender) -> Self {
match val {
CommandSender::Console | CommandSender::Rcon(_) | CommandSender::Dummy => Self::Console,
CommandSender::Player(player) => Self::Player(
CommandSender::Player(player) => {
let is_op =
player.permission_lvl.load() >= pumpkin_util::permission::PermissionLvl::Two;
Self::Player(
player.gameprofile.id.to_string(),
player.gameprofile.name.clone(),
),
is_op,
)
}
CommandSender::CommandBlock(_block_entity, _world) => Self::Console,
}
}
@ -129,6 +141,10 @@ impl CommandExecutor for JavaCommandExecutor {
_server: &'a pumpkin::server::Server,
args: &'a pumpkin::command::args::ConsumedArgs<'a>,
) -> pumpkin::command::CommandResult<'a> {
if let CommandSender::Player(player) = sender {
crate::java::native_callbacks::utils::cache_player(player.clone());
}
Box::pin(async move {
let full_command = match args.get(ARG_ANY) {
Some(Arg::Msg(msg)) => {
@ -147,15 +163,21 @@ impl CommandExecutor for JavaCommandExecutor {
}
};
let (tx, _rx) = oneshot::channel();
self.command_tx
let (tx, rx) = oneshot::channel();
if let Err(e) = self
.command_tx
.send(JvmCommand::TriggerCommand {
full_command,
respond_to: tx,
command_sender: sender.into(),
})
.await
.unwrap();
{
tracing::error!("Failed to send command to JVM worker: {e}");
return Ok(0);
}
let _ = rx.await;
Ok(1)
})
}
@ -186,3 +208,23 @@ pub fn init_java_command(
}),
)
}
#[cfg(test)]
mod tests {
use super::*;
use pumpkin::command::dispatcher::CommandDispatcher;
#[test]
fn test_init_java_command_registration() {
let mut dispatcher = CommandDispatcher::default();
let (tx, _rx) = mpsc::channel(100);
let names = vec!["fly".to_string(), "/fly".to_string(), "//fly".to_string()];
let tree = init_java_command("fly", tx, names, "Fly command");
dispatcher.register(tree, "patchbukkit:command.fly");
assert!(dispatcher.get_tree("fly").is_ok());
assert!(dispatcher.get_tree("/fly").is_ok());
assert!(dispatcher.get_tree("//fly").is_ok());
}
}

View File

@ -6,7 +6,6 @@ pub struct PatchBukkitDirectories {
pub base: PathBuf,
pub plugins: PathBuf,
pub plugin_updates: PathBuf,
pub j4rs: PathBuf,
pub jassets: PathBuf,
}
@ -20,8 +19,7 @@ pub fn setup_directories(server: &Context) -> Result<PatchBukkitDirectories, Str
let plugins = base.join("patchbukkit-plugins");
let plugin_updates = plugins.join("update");
let j4rs = base.join("j4rs");
let jassets = j4rs.join("jassets");
let jassets = base.join("jassets");
fs::create_dir_all(&jassets)
.map_err(|err| format!("Failed to create jassets folder: {err:?}"))?;
@ -58,7 +56,6 @@ pub fn setup_directories(server: &Context) -> Result<PatchBukkitDirectories, Str
base,
plugins,
plugin_updates,
j4rs,
jassets,
})
}

View File

@ -1,11 +1,9 @@
use std::marker::PhantomData;
use std::str::FromStr;
use std::sync::Arc;
use pumpkin::entity::player::Player;
use pumpkin::plugin::{BoxFuture, Cancellable, EventHandler, Payload};
use pumpkin::server::Server;
use pumpkin_api_macros::with_runtime;
use tokio::sync::{mpsc, oneshot};
use crate::java::jvm::commands::JvmCommand;
@ -52,10 +50,11 @@ impl PatchBukkitEvent for pumpkin::plugin::player::player_join::PlayerJoinEvent
}
}
fn apply_modifications(&mut self, server: &Arc<Server>, data: Data) -> Option<()> {
if let Data::PlayerJoin(event) = data {
self.join_message = serde_json::from_str(&event.join_message).ok()?;
server.get_player_by_uuid(uuid::Uuid::from_str(&event.player_uuid?.value).ok()?)?;
fn apply_modifications(&mut self, _server: &Arc<Server>, data: Data) -> Option<()> {
if let Data::PlayerJoin(event) = data
&& let Ok(msg) = serde_json::from_str(&event.join_message)
{
self.join_message = msg;
}
Some(())
@ -80,10 +79,11 @@ impl PatchBukkitEvent for pumpkin::plugin::player::player_leave::PlayerLeaveEven
}
}
fn apply_modifications(&mut self, server: &Arc<Server>, data: Data) -> Option<()> {
if let Data::PlayerQuit(event) = data {
self.leave_message = serde_json::from_str(&event.quit_message).ok()?;
server.get_player_by_uuid(uuid::Uuid::from_str(&event.player_uuid?.value).ok()?)?;
fn apply_modifications(&mut self, _server: &Arc<Server>, data: Data) -> Option<()> {
if let Data::PlayerQuit(event) = data
&& let Ok(msg) = serde_json::from_str(&event.quit_message)
{
self.leave_message = msg;
}
Some(())
@ -603,23 +603,51 @@ impl<E: PatchBukkitEvent> PatchBukkitEventHandler<E> {
}
}
#[with_runtime(global)]
impl<E> EventHandler<E> for PatchBukkitEventHandler<E>
where
E: PatchBukkitEvent + Payload + Cancellable + 'static,
{
fn handle<'a>(&'a self, server: &'a Arc<Server>, event: &'a E) -> BoxFuture<'a, ()> {
let command_tx = self.command_tx.clone();
let payload = event.to_payload(server.clone());
if let Some(player) = &payload.context.player {
crate::java::native_callbacks::utils::cache_player(player.clone());
}
Box::pin(async move {
let (tx, rx) = oneshot::channel();
if let Err(e) = command_tx
.send(JvmCommand::FireEvent {
payload,
respond_to: tx,
plugin: self.plugin_name.clone(),
})
.await
{
tracing::error!("Failed to send event to JVM worker: {e}");
return;
}
let _ = rx.await;
})
}
fn handle_blocking<'a>(
&'a self,
server: &'a Arc<Server>,
event: &'a mut E,
) -> BoxFuture<'a, ()> {
let command_tx = self.command_tx.clone();
let payload = event.to_payload(server.clone());
if let Some(player) = &payload.context.player {
crate::java::native_callbacks::utils::cache_player(player.clone());
}
Box::pin(async move {
let (tx, rx) = oneshot::channel();
if let Err(e) = command_tx
.send(JvmCommand::FireEvent {
payload: event.to_payload(server.clone()),
payload,
respond_to: tx,
plugin: self.plugin_name.clone(),
})
@ -632,7 +660,9 @@ where
match rx.await {
Ok(response) => {
event.set_cancelled(response.cancelled);
event.apply_modifications(server, response.data.unwrap().data.unwrap());
if let Some(event_data) = response.data.and_then(|d| d.data) {
let _ = event.apply_modifications(server, event_data);
}
}
Err(_) => {
tracing::warn!("JVM worker dropped response channel for event");

View File

@ -22,9 +22,10 @@ pub enum LoadPluginResult {
pub enum JvmCommand {
Initialize {
j4rs_path: PathBuf,
jassets_path: PathBuf,
respond_to: oneshot::Sender<Result<()>>,
context: Arc<Context>,
runtime_handle: tokio::runtime::Handle,
command_tx: mpsc::Sender<Self>,
config: PatchBukkitConfig,
},

View File

@ -1,6 +1,6 @@
use std::{path::PathBuf, sync::Arc};
use j4rs::{InvocationArg, Jvm, JvmBuilder};
use jni::{Env, InitArgsBuilder, JNIVersion, JavaVM};
use pumpkin::plugin::Context;
use tokio::sync::mpsc;
@ -21,7 +21,7 @@ pub struct JvmWorker {
pub plugin_manager: PluginManager,
pub event_manager: EventManager,
pub command_manager: CommandManager,
jvm: Option<j4rs::Jvm>,
jvm: Option<Arc<JavaVM>>,
context: Option<Arc<Context>>,
}
@ -44,21 +44,22 @@ impl JvmWorker {
while let Some(command) = self.command_rx.recv().await {
match command {
JvmCommand::Initialize {
j4rs_path,
jassets_path,
respond_to,
context,
runtime_handle,
command_tx,
config,
} => {
init_callback_context(
context.clone(),
tokio::runtime::Handle::current(),
runtime_handle,
command_tx.clone(),
config,
)
.unwrap();
self.context = Some(context);
let result = self.initialize_jvm(&j4rs_path);
let result = self.initialize_jvm(&jassets_path);
let _ = respond_to.send(result);
}
JvmCommand::LoadPlugin {
@ -105,38 +106,49 @@ impl JvmWorker {
server,
command_tx,
} => {
let jvm = match self.jvm {
Some(ref jvm) => jvm,
None => &Jvm::attach_thread().unwrap(),
};
let _ = respond_to.send(
self.plugin_manager
.instantiate_all_plugins(
jvm,
let res = if let Some(ref jvm) = self.jvm {
let plugins_dir = plugins_dir.clone();
let server = server.clone();
let command_tx = command_tx.clone();
jvm.attach_current_thread(|env| -> anyhow::Result<()> {
self.plugin_manager.instantiate_all_plugins(
env,
&plugins_dir,
&server,
command_tx,
&mut self.command_manager,
)
.await,
);
})
.map_err(|e| anyhow::anyhow!("Failed to instantiate plugins: {e}"))
} else {
Err(anyhow::anyhow!("JVM is not initialized"))
};
let _ = respond_to.send(res);
}
JvmCommand::EnableAllPlugins { respond_to } => {
let jvm = match self.jvm {
Some(ref jvm) => jvm,
None => &Jvm::attach_thread().unwrap(),
let res = if let Some(ref jvm) = self.jvm {
jvm.attach_current_thread(|env| -> anyhow::Result<()> {
self.plugin_manager.enable_all_plugins(env)
})
.map_err(|e| anyhow::anyhow!("Failed to enable plugins: {e}"))
} else {
Err(anyhow::anyhow!("JVM is not initialized"))
};
let _ = respond_to.send(self.plugin_manager.enable_all_plugins(jvm));
let _ = respond_to.send(res);
}
JvmCommand::DisableAllPlugins { respond_to } => {
let jvm = match self.jvm {
Some(ref jvm) => jvm,
None => &Jvm::attach_thread().unwrap(),
let res = if let Some(ref jvm) = self.jvm {
jvm.attach_current_thread(|env| -> anyhow::Result<()> {
self.plugin_manager.disable_all_plugins(env)
})
.map_err(|e| anyhow::anyhow!("Failed to disable plugins: {e}"))
} else {
Err(anyhow::anyhow!("JVM is not initialized"))
};
let _ = respond_to.send(self.plugin_manager.disable_all_plugins(jvm));
let _ = respond_to.send(res);
}
JvmCommand::Shutdown { respond_to } => {
let _ = respond_to.send(self.plugin_manager.unload_all_plugins());
@ -147,14 +159,14 @@ impl JvmWorker {
plugin,
payload,
} => {
let jvm = match self.jvm {
Some(ref jvm) => jvm,
None => &Jvm::attach_thread().unwrap(),
};
let original_event = payload.event.clone();
let cancelled = match self.event_manager.fire_event(jvm, payload, plugin) {
Ok(c) => c,
let response = if let Some(ref jvm) = self.jvm {
match jvm.attach_current_thread(
|env| -> anyhow::Result<FireEventResponse> {
self.event_manager.fire_event(env, payload, plugin)
},
) {
Ok(resp) => resp,
Err(e) => {
tracing::error!("Failed to fire event: {e}");
FireEventResponse {
@ -162,25 +174,32 @@ impl JvmWorker {
data: Some(original_event),
}
}
}
} else {
FireEventResponse {
cancelled: false,
data: Some(original_event),
}
};
let _ = respond_to.send(cancelled);
let _ = respond_to.send(response);
}
JvmCommand::TriggerCommand {
full_command,
command_sender,
respond_to,
} => {
let jvm = match self.jvm {
Some(ref jvm) => jvm,
None => &Jvm::attach_thread().unwrap(),
let res = if let Some(ref jvm) = self.jvm {
jvm.attach_current_thread(|env| -> anyhow::Result<()> {
self.command_manager
.trigger_command(env, full_command, command_sender)
})
.map_err(|e| anyhow::anyhow!("Failed to trigger command: {e}"))
} else {
Err(anyhow::anyhow!("JVM is not initialized"))
};
let result =
self.command_manager
.trigger_command(jvm, full_command, command_sender);
let _ = respond_to.send(result);
let _ = respond_to.send(res);
}
JvmCommand::GetCommandTabComplete {
command_sender,
@ -188,19 +207,26 @@ impl JvmWorker {
respond_to,
location,
} => {
let jvm = match self.jvm {
Some(ref jvm) => jvm,
None => &Jvm::attach_thread().unwrap(),
};
let result = self.command_manager.get_tab_complete(
jvm,
let res = if let Some(ref jvm) = self.jvm {
match jvm.attach_current_thread(|env| -> anyhow::Result<_> {
Ok(self.command_manager.get_tab_complete(
env,
command_sender,
full_command,
location,
);
))
}) {
Ok(inner_res) => inner_res,
Err(e) => {
tracing::error!("Failed to get tab complete: {e}");
Ok(None)
}
}
} else {
Ok(None)
};
let _ = respond_to.send(result);
let _ = respond_to.send(res);
}
}
}
@ -208,41 +234,85 @@ impl JvmWorker {
tracing::info!("JVM worker thread exited");
}
fn initialize_jvm(&mut self, j4rs_path: &PathBuf) -> anyhow::Result<()> {
tracing::info!("Initializing JVM with path: {j4rs_path:?}");
fn initialize_jvm(&mut self, jassets_path: &PathBuf) -> anyhow::Result<()> {
tracing::info!("Initializing JVM with assets path: {jassets_path:?}");
let jassets = j4rs_path.join("jassets");
let mut classpath_entries = Vec::new();
let mut jar_paths = Vec::new();
if let Ok(entries) = std::fs::read_dir(&jassets) {
for entry in entries.flatten() {
let path = entry.path();
for entry in walkdir::WalkDir::new(jassets_path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
{
let path = entry.path().to_path_buf();
if let Some(filename) = path.file_name().and_then(|n| n.to_str())
&& filename.ends_with(".jar")
{
classpath_entries.push(j4rs::ClasspathEntry::new(&path));
jar_paths.push(path);
}
}
if jar_paths.is_empty() {
tracing::warn!(
"No JAR files found in jassets directory ({:?})",
jassets_path
);
} else {
tracing::info!(
"Found {} JAR entries in jassets: {:?}",
jar_paths.len(),
jar_paths
);
}
let mut jvm_builder = JvmBuilder::new();
let mut jvm_builder = jvm_builder.with_base_path(j4rs_path).java_opts(vec![
j4rs::JavaOpt::new("--enable-native-access=ALL-UNNAMED"),
j4rs::JavaOpt::new("--enable-final-field-mutation=ALL-UNNAMED"),
j4rs::JavaOpt::new("--add-opens=java.base/java.lang=ALL-UNNAMED"),
j4rs::JavaOpt::new("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"),
j4rs::JavaOpt::new("-Dcom.google.protobuf.useUnsafe=false"),
]);
for entry in classpath_entries {
jvm_builder = jvm_builder.classpath_entry(entry);
let has_patchbukkit = jar_paths.iter().any(|p| {
p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.contains("patchbukkit"))
});
if !has_patchbukkit {
tracing::warn!(
"patchbukkit.jar was not found in {:?}. Server classes may fail to load!",
jassets_path
);
}
let jvm = jvm_builder.build()?;
let separator = if cfg!(windows) { ";" } else { ":" };
let classpath = jar_paths
.iter()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>()
.join(separator);
initialize_callbacks(&jvm)?;
let jvm_args = InitArgsBuilder::new()
.version(JNIVersion::V21)
.option(format!("-Djava.class.path={classpath}"))
.option("--enable-native-access=ALL-UNNAMED")
.option("--enable-final-field-mutation=ALL-UNNAMED")
.option("--add-opens=java.base/java.lang=ALL-UNNAMED")
.option("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED")
.option("-Dcom.google.protobuf.useUnsafe=false")
.build()
.map_err(|e| anyhow::anyhow!("Failed to build JVM init args: {e:?}"))?;
setup_patchbukkit_server(&jvm)?;
let jvm =
JavaVM::new(jvm_args).map_err(|e| anyhow::anyhow!("Failed to create JavaVM: {e:?}"))?;
let jvm = Arc::new(jvm);
jvm.attach_current_thread(|env| -> anyhow::Result<()> {
initialize_callbacks(env).map_err(|e| {
tracing::error!("Failed to initialize callbacks: {e:?}");
e
})?;
setup_patchbukkit_server(env).map_err(|e| {
tracing::error!("Failed to setup PatchBukkit server: {e:?}");
e
})?;
Ok(())
})
.map_err(|e| anyhow::anyhow!("Failed to attach thread for JVM initialization: {e:?}"))?;
self.jvm = Some(jvm);
@ -251,12 +321,17 @@ impl JvmWorker {
}
}
pub fn setup_patchbukkit_server(jvm: &Jvm) -> anyhow::Result<()> {
jvm.invoke_static(
"org.patchbukkit.PatchBukkitServer",
"initServer",
InvocationArg::empty(),
)?;
pub fn setup_patchbukkit_server(env: &mut Env) -> anyhow::Result<()> {
if let Err(e) = env.call_static_method(
jni::jni_str!("org/patchbukkit/PatchBukkitServer"),
jni::jni_str!("initServer"),
jni::jni_sig!("()Lorg/patchbukkit/PatchBukkitServer;"),
&[],
) {
env.exception_describe();
env.exception_clear();
return Err(anyhow::anyhow!("Failed to setup PatchBukkit server: {e:?}"));
}
Ok(())
}

View File

@ -1,40 +1,62 @@
use crate::java::native_callbacks::CALLBACK_CONTEXT;
use crate::java::native_callbacks::{CALLBACK_CONTEXT, utils::with_player};
use crate::proto::patchbukkit::{
abilities::{Abilities, SetAbilitiesRequest},
common::Uuid,
};
pub fn ffi_native_bridge_get_abilities_impl(request: Uuid) -> Option<Abilities> {
let ctx = CALLBACK_CONTEXT.get()?;
let player_uuid = uuid::Uuid::parse_str(&request.value).ok()?;
let player = ctx.plugin_context.server.get_player_by_uuid(player_uuid)?;
let abilities = tokio::task::block_in_place(|| {
ctx.runtime
.block_on(async { player.abilities.lock().await })
with_player(Some(&request), |player| {
let abilities = match player.abilities.try_lock() {
Ok(guard) => Abilities {
invulnerable: guard.invulnerable,
flying: guard.flying,
allow_flying: guard.allow_flying,
creative: guard.creative,
allow_modify_world: guard.allow_modify_world,
fly_speed: guard.fly_speed,
walk_speed: guard.walk_speed,
},
Err(_) => {
let mut acquired = None;
for _ in 0..10 {
std::thread::yield_now();
if let Ok(guard) = player.abilities.try_lock() {
acquired = Some(Abilities {
invulnerable: guard.invulnerable,
flying: guard.flying,
allow_flying: guard.allow_flying,
creative: guard.creative,
allow_modify_world: guard.allow_modify_world,
fly_speed: guard.fly_speed,
walk_speed: guard.walk_speed,
});
Some(Abilities {
invulnerable: abilities.invulnerable,
flying: abilities.flying,
allow_flying: abilities.allow_flying,
creative: abilities.creative,
allow_modify_world: abilities.allow_modify_world,
fly_speed: abilities.fly_speed,
walk_speed: abilities.walk_speed,
break;
}
}
acquired.unwrap_or(Abilities {
invulnerable: false,
flying: false,
allow_flying: false,
creative: false,
allow_modify_world: true,
fly_speed: 0.05,
walk_speed: 0.1,
})
}
};
Some(abilities)
})?
}
pub fn ffi_native_bridge_set_abilities_impl(request: SetAbilitiesRequest) -> Option<bool> {
let ctx = CALLBACK_CONTEXT.get()?;
let player_uuid = uuid::Uuid::parse_str(&request.uuid?.value).ok()?;
let abilities = request.abilities?;
let player = ctx.plugin_context.server.get_player_by_uuid(player_uuid)?;
let mut pumpkin_abilities = tokio::task::block_in_place(|| {
ctx.runtime
.block_on(async { player.abilities.lock().await })
});
with_player(request.uuid.as_ref(), |player| {
let player = player.clone();
ctx.runtime.spawn(async move {
{
let mut pumpkin_abilities = player.abilities.lock().await;
pumpkin_abilities.invulnerable = abilities.invulnerable;
pumpkin_abilities.flying = abilities.flying;
pumpkin_abilities.allow_flying = abilities.allow_flying;
@ -42,11 +64,10 @@ pub fn ffi_native_bridge_set_abilities_impl(request: SetAbilitiesRequest) -> Opt
pumpkin_abilities.allow_modify_world = abilities.allow_modify_world;
pumpkin_abilities.fly_speed = abilities.fly_speed;
pumpkin_abilities.walk_speed = abilities.walk_speed;
let player = player.clone();
ctx.runtime.spawn(async move {
}
player.send_abilities_update().await;
});
Some(true)
})?
}

View File

@ -3,7 +3,13 @@ use pumpkin_util::permission::{Permission, PermissionDefault};
use crate::{commands::init_java_command, proto::patchbukkit::command::RegisterCommandRequest};
pub fn ffi_native_bridge_register_command_impl(request: RegisterCommandRequest) -> Option<()> {
let context = super::CALLBACK_CONTEXT.get()?;
let Some(context) = super::CALLBACK_CONTEXT.get() else {
tracing::warn!(
"Failed to register command '{}': CALLBACK_CONTEXT is not initialized",
request.cmd_name
);
return None;
};
let cmd_name = request.cmd_name;
let aliases = request.aliases;
@ -12,6 +18,13 @@ pub fn ffi_native_bridge_register_command_impl(request: RegisterCommandRequest)
let command_tx = context.command_tx.clone();
let plugin_context = context.plugin_context.clone();
tracing::info!(
"Registering Bukkit command '{}' from plugin '{}' (aliases: {:?})",
cmd_name,
plugin_name,
aliases
);
context.runtime.spawn(async move {
let mut raw_names: Vec<String> = vec![cmd_name.clone()];
raw_names.extend(aliases.clone());
@ -34,23 +47,31 @@ pub fn ffi_native_bridge_register_command_impl(request: RegisterCommandRequest)
}
}
let node = init_java_command(cmd_name.clone(), command_tx, names, description);
let node = init_java_command(cmd_name.clone(), command_tx, names.clone(), description);
let clean_perm = cmd_name.trim_start_matches('/');
let permission = format!("patchbukkit:command.{clean_perm}");
let registry = {
let permission_manager = plugin_context.permission_manager.read().await;
permission_manager.registry.clone()
};
let registry = plugin_context.server.permission_manager.clone();
let _ = registry.write().await.register_permission(Permission::new(
if let Err(e) = registry.register_permission(Permission::new(
&permission,
&format!("Allows running command `{cmd_name}` from `{plugin_name}`"),
PermissionDefault::Allow,
));
)) {
tracing::debug!("Permission '{}' registration notice: {}", permission, e);
}
plugin_context.register_command(node, permission).await;
plugin_context
.register_command(node, permission.clone())
.await;
tracing::info!(
"Successfully registered command '{}' with names {:?} and permission '{}'",
cmd_name,
names,
permission
);
});
Some(())

View File

@ -12,12 +12,8 @@ use crate::{
};
pub fn ffi_native_bridge_get_entity_health_impl(request: Uuid) -> Option<EntityHealthResponse> {
let ctx = CALLBACK_CONTEXT.get()?;
with_player(Some(&request), |player| {
let health = tokio::task::block_in_place(|| {
ctx.runtime
.block_on(async { f64::from(player.living_entity.health.load()) })
});
let health = f64::from(player.living_entity.health.load());
EntityHealthResponse {
health,
@ -155,24 +151,22 @@ pub fn ffi_native_bridge_is_op_impl(
let op_lvl = ctx.plugin_context.server.basic_config.op_permission_level;
let mut is_op = perm_lvl >= op_lvl || perm_lvl > pumpkin_util::PermissionLvl::Zero;
if !is_op {
let op_config = tokio::task::block_in_place(|| {
ctx.runtime
.block_on(async { ctx.plugin_context.server.data.operator_config.read().await })
});
if op_config.get_entry(&player_uuid).is_some() {
if !is_op
&& let Ok(op_config) = ctx.plugin_context.server.data.operator_config.try_read()
&& op_config.get_entry(&player_uuid).is_some()
{
is_op = true;
}
}
is_op
})
.unwrap_or_else(|| {
let op_config = tokio::task::block_in_place(|| {
ctx.runtime
.block_on(async { ctx.plugin_context.server.data.operator_config.read().await })
});
op_config.get_entry(&player_uuid).is_some()
ctx.plugin_context
.server
.data
.operator_config
.try_read()
.is_ok_and(|ops| ops.get_entry(&player_uuid).is_some())
});
Some(crate::proto::patchbukkit::entity::IsOpResponse { is_op })

View File

@ -1,12 +1,9 @@
use std::sync::Arc;
use pumpkin::plugin::EventPriority;
use pumpkin::plugin::player::player_join::PlayerJoinEvent;
use pumpkin_util::text::TextComponent;
use crate::events::handler::PatchBukkitEventHandler;
use crate::java::native_callbacks::CALLBACK_CONTEXT;
use crate::proto::patchbukkit::events::event::Data;
use crate::proto::patchbukkit::events::{
CallEventRequest, CallEventResponse, RegisterEventRequest,
};
@ -32,8 +29,6 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
let command_tx = ctx.command_tx.clone();
let context = ctx.plugin_context.clone();
tokio::task::block_in_place(|| {
ctx.runtime.block_on(async {
match request.event_type.as_str() {
"org.bukkit.event.player.PlayerJoinEvent" => {
context
@ -47,8 +42,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.player.PlayerQuitEvent" => {
context
@ -62,8 +56,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.player.PlayerGameModeChangeEvent" => {
context
@ -77,8 +70,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.player.PlayerInteractEvent" => {
context
@ -92,8 +84,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.block.BlockBreakEvent" => {
context
@ -107,8 +98,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.block.BlockPlaceEvent" => {
context
@ -122,8 +112,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.block.SignChangeEvent" => {
context
@ -137,8 +126,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.block.BlockDamageEvent" => {
context
@ -152,8 +140,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.server.ServerCommandEvent" => {
context
@ -167,8 +154,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.block.BlockIgniteEvent" => {
context
@ -182,8 +168,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.block.BlockGrowEvent" => {
context
@ -197,8 +182,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.block.BlockFormEvent" => {
context
@ -212,8 +196,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.block.BlockFadeEvent" => {
context
@ -227,8 +210,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.player.PlayerToggleSneakEvent" => {
context
@ -242,8 +224,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.player.PlayerToggleSprintEvent" => {
context
@ -257,8 +238,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.player.PlayerToggleFlightEvent" => {
context
@ -272,8 +252,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.player.PlayerMoveEvent" => {
context
@ -287,10 +266,12 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.player.PlayerInteractEntityEvent" | "org.bukkit.event.player.PlayerInteractAtEntityEvent" | "org.bukkit.event.entity.EntityDamageByEntityEvent" | "org.bukkit.event.entity.EntityDamageEvent" => {
"org.bukkit.event.player.PlayerInteractEntityEvent"
| "org.bukkit.event.player.PlayerInteractAtEntityEvent"
| "org.bukkit.event.entity.EntityDamageByEntityEvent"
| "org.bukkit.event.entity.EntityDamageEvent" => {
context
.register_event::<
pumpkin::plugin::player::player_interact_entity_event::PlayerInteractEntityEvent,
@ -302,10 +283,10 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
"org.bukkit.event.player.AsyncPlayerChatEvent" | "org.bukkit.event.player.PlayerChatEvent" => {
"org.bukkit.event.player.AsyncPlayerChatEvent"
| "org.bukkit.event.player.PlayerChatEvent" => {
context
.register_event::<
pumpkin::plugin::player::player_chat::PlayerChatEvent,
@ -317,8 +298,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)),
pumpkin_priority,
request.blocking,
)
.await;
);
}
// Player Events
"org.bukkit.event.player.PlayerDropItemEvent"
@ -429,383 +409,15 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
_ => {
tracing::warn!(
"Unsupported Bukkit event type '{}' from plugin '{}'",
request.event_type, request.plugin_name
request.event_type,
request.plugin_name
);
}
}
});
});
Some(())
}
pub fn ffi_native_bridge_call_event_impl(request: CallEventRequest) -> Option<CallEventResponse> {
let ctx = CALLBACK_CONTEXT.get()?;
let event = request.event?;
tracing::debug!("Java calling event {:?}", event);
let context = ctx.plugin_context.clone();
let handled = tokio::task::block_in_place(|| {
ctx.runtime.block_on(async {
match event.data? {
Data::PlayerJoin(player_join_event_data) => {
let uuid =
uuid::Uuid::parse_str(&player_join_event_data.player_uuid?.value).ok()?;
let player = context.server.get_player_by_uuid(uuid)?;
let mut pumpkin_event = PlayerJoinEvent::new(
player,
TextComponent::from_legacy_string(&player_join_event_data.join_message),
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::PlayerQuit(player_quit_event_data) => {
let uuid =
uuid::Uuid::parse_str(&player_quit_event_data.player_uuid?.value).ok()?;
let player = context.server.get_player_by_uuid(uuid)?;
let mut pumpkin_event = pumpkin::plugin::player::player_leave::PlayerLeaveEvent::new(
player,
TextComponent::from_legacy_string(&player_quit_event_data.quit_message),
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::PlayerGamemodeChange(event_data) => {
let uuid =
uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?;
let player = context.server.get_player_by_uuid(uuid)?;
let new_gamemode = match event_data.new_gamemode.to_lowercase().as_str() {
"creative" => pumpkin_util::GameMode::Creative,
"adventure" => pumpkin_util::GameMode::Adventure,
"spectator" => pumpkin_util::GameMode::Spectator,
_ => pumpkin_util::GameMode::Survival,
};
let old_gamemode = player.gamemode.load();
let mut pumpkin_event = pumpkin::plugin::player::player_gamemode_change::PlayerGamemodeChangeEvent::new(
player,
old_gamemode,
new_gamemode,
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::PlayerInteract(event_data) => {
let uuid =
uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?;
let player = context.server.get_player_by_uuid(uuid)?;
let action = match event_data.action.as_str() {
"LEFT_CLICK_BLOCK" => pumpkin::plugin::player::player_interact_event::InteractAction::LeftClickBlock,
"LEFT_CLICK_AIR" => pumpkin::plugin::player::player_interact_event::InteractAction::LeftClickAir,
"RIGHT_CLICK_AIR" => pumpkin::plugin::player::player_interact_event::InteractAction::RightClickAir,
_ => pumpkin::plugin::player::player_interact_event::InteractAction::RightClickBlock,
};
let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.clicked_x, event_data.clicked_y, event_data.clicked_z);
let mut pumpkin_event = pumpkin::plugin::player::player_interact_event::PlayerInteractEvent::new(
&player,
action,
&pumpkin_data::Block::AIR,
Some(block_pos),
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::BlockBreak(event_data) => {
let player = if let Some(uuid_proto) = event_data.player_uuid {
let uuid = uuid::Uuid::parse_str(&uuid_proto.value).ok()?;
context.server.get_player_by_uuid(uuid)
} else {
None
};
let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.block_x, event_data.block_y, event_data.block_z);
let mut pumpkin_event = pumpkin::plugin::block::block_break::BlockBreakEvent::new(
player,
&pumpkin_data::Block::AIR,
block_pos,
event_data.exp,
event_data.drop_items,
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::BlockPlace(event_data) => {
let uuid =
uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?;
let player = context.server.get_player_by_uuid(uuid)?;
let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.block_x, event_data.block_y, event_data.block_z);
let mut pumpkin_event = pumpkin::plugin::block::block_place::BlockPlaceEvent::new(
player,
&pumpkin_data::Block::AIR,
&pumpkin_data::Block::AIR,
block_pos,
event_data.can_build,
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::PlayerToggleSneak(event_data) => {
let uuid =
uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?;
let player = context.server.get_player_by_uuid(uuid)?;
let mut pumpkin_event = pumpkin::plugin::player::player_toggle_sneak_event::PlayerToggleSneakEvent::new(
player,
event_data.is_sneaking,
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::PlayerToggleSprint(event_data) => {
let uuid =
uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?;
let player = context.server.get_player_by_uuid(uuid)?;
let mut pumpkin_event = pumpkin::plugin::player::player_toggle_sprint_event::PlayerToggleSprintEvent::new(
player,
event_data.is_sprinting,
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::PlayerToggleFlight(event_data) => {
let uuid =
uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?;
let player = context.server.get_player_by_uuid(uuid)?;
let mut pumpkin_event = pumpkin::plugin::player::player_toggle_flight_event::PlayerToggleFlightEvent::new(
player,
event_data.is_flying,
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::PlayerMove(event_data) => {
let uuid =
uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?;
let player = context.server.get_player_by_uuid(uuid)?;
let from = event_data
.from
.and_then(|l| l.position)
.map(|p| pumpkin_util::math::vector3::Vector3::new(p.x, p.y, p.z))
.unwrap_or_default();
let to = event_data
.to
.and_then(|l| l.position)
.map(|p| pumpkin_util::math::vector3::Vector3::new(p.x, p.y, p.z))
.unwrap_or_default();
let mut pumpkin_event = pumpkin::plugin::player::player_move::PlayerMoveEvent::new(
player, from, to,
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::PlayerInteractEntity(event_data) => {
let player_uuid =
uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?;
let player = context.server.get_player_by_uuid(player_uuid)?;
let target_uuid =
uuid::Uuid::parse_str(&event_data.target_uuid?.value).ok()?;
let target = context.server.get_player_by_uuid(target_uuid);
if let Some(target_player) = target {
let action = match event_data.action.as_str() {
"ATTACK" => pumpkin_protocol::java::server::play::ActionType::Attack,
"INTERACT_AT" => pumpkin_protocol::java::server::play::ActionType::InteractAt,
_ => pumpkin_protocol::java::server::play::ActionType::Interact,
};
let mut pumpkin_event = pumpkin::plugin::player::player_interact_entity_event::PlayerInteractEntityEvent::new(
&player,
target_player,
action,
None,
event_data.is_sneaking,
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
}
Some(true)
}
Data::PlayerChat(event_data) => {
let uuid =
uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?;
let player = context.server.get_player_by_uuid(uuid)?;
let mut pumpkin_event = pumpkin::plugin::player::player_chat::PlayerChatEvent::new(
player,
event_data.message,
Vec::new(),
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::SignChange(event_data) => {
let uuid =
uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?;
let player = context.server.get_player_by_uuid(uuid)?;
let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.block_x, event_data.block_y, event_data.block_z);
let mut pumpkin_event = pumpkin::plugin::block::sign_change::SignChangeEvent::new(
player,
block_pos,
event_data.lines,
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::BlockDamage(event_data) => {
let player = if let Some(uuid_proto) = event_data.player_uuid {
let uuid = uuid::Uuid::parse_str(&uuid_proto.value).ok()?;
context.server.get_player_by_uuid(uuid)
} else {
None
};
if let Some(player) = player {
let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.block_x, event_data.block_y, event_data.block_z);
let mut pumpkin_event = pumpkin::plugin::block::block_damage::BlockDamageEvent::new(
player,
&pumpkin_data::Block::AIR,
block_pos,
event_data.insta_break,
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
}
Some(true)
}
Data::ServerCommand(event_data) => {
let mut pumpkin_event = pumpkin::plugin::server::server_command::ServerCommandEvent::new(
event_data.command,
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::BlockIgnite(event_data) => {
let player = if let Some(uuid_proto) = event_data.player_uuid {
let uuid = uuid::Uuid::parse_str(&uuid_proto.value).ok()?;
context.server.get_player_by_uuid(uuid)
} else {
None
};
let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.block_x, event_data.block_y, event_data.block_z);
let mut pumpkin_event = pumpkin::plugin::block::block_ignite::BlockIgniteEvent::new(
block_pos,
&pumpkin_data::Block::AIR,
player,
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::BlockGrow(event_data) => {
let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.block_x, event_data.block_y, event_data.block_z);
let world = context.server.worlds.load().first().cloned()?;
let mut pumpkin_event = pumpkin::plugin::block::block_grow::BlockGrowEvent::new(
world,
&pumpkin_data::Block::AIR,
pumpkin_data::BlockStateId::new_or_air(0),
&pumpkin_data::Block::AIR,
pumpkin_data::BlockStateId::new_or_air(0),
block_pos,
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::BlockForm(event_data) => {
let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.block_x, event_data.block_y, event_data.block_z);
let mut pumpkin_event = pumpkin::plugin::block::block_form::BlockFormEvent::new(
block_pos,
&pumpkin_data::Block::AIR,
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::BlockFade(event_data) => {
let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.block_x, event_data.block_y, event_data.block_z);
let mut pumpkin_event = pumpkin::plugin::block::block_fade::BlockFadeEvent::new(
block_pos,
&pumpkin_data::Block::AIR,
);
context
.server
.plugin_manager
.fire(&context.server, &mut pumpkin_event)
.await;
Some(true)
}
Data::PluginEnable(plugin_enable_event) => {
tracing::info!("Plugin enable event for {}", plugin_enable_event.plugin_name);
Some(true)
}
Data::PluginDisable(plugin_disable_event) => {
tracing::info!("Plugin disable event for {}", plugin_disable_event.plugin_name);
Some(true)
}
}
})
})?;
Some(CallEventResponse { handled })
pub fn ffi_native_bridge_call_event_impl(_request: CallEventRequest) -> Option<CallEventResponse> {
Some(CallEventResponse { handled: false })
}

View File

@ -1,7 +1,6 @@
use pumpkin_data::data_component_impl::EquipmentSlot;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack as PumpkinItemStack;
use tokio::task::block_in_place;
use crate::{
java::native_callbacks::{CALLBACK_CONTEXT, utils::with_player},
@ -55,22 +54,34 @@ fn proto_item_to_pumpkin(proto: Option<&ProtoItemStack>) -> PumpkinItemStack {
pub fn ffi_native_bridge_get_player_inventory_impl(
request: Uuid,
) -> Option<GetPlayerInventoryResponse> {
let ctx = CALLBACK_CONTEXT.get()?;
with_player(Some(&request), |player| {
let player = player.clone();
block_in_place(|| {
ctx.runtime.block_on(async {
let selected_slot = u32::from(player.inventory.get_selected_slot());
let main_guard = player.inventory.main_inventory.read().await;
let main_inventory = main_guard.iter().map(pumpkin_item_to_proto).collect();
let main_inventory = player
.inventory
.main_inventory
.try_read()
.map(|main_guard| main_guard.iter().map(pumpkin_item_to_proto).collect())
.unwrap_or_default();
let eq_guard = player.inventory.entity_equipment.lock().await;
let off_hand = pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::OFF_HAND));
let helmet = pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::HEAD));
let chestplate = pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::CHEST));
let leggings = pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::LEGS));
let boots = pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::FEET));
let (off_hand, helmet, chestplate, leggings, boots) =
if let Ok(eq_guard) = player.inventory.entity_equipment.try_lock() {
(
pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::OFF_HAND)),
pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::HEAD)),
pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::CHEST)),
pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::LEGS)),
pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::FEET)),
)
} else {
(
pumpkin_item_to_proto(PumpkinItemStack::EMPTY),
pumpkin_item_to_proto(PumpkinItemStack::EMPTY),
pumpkin_item_to_proto(PumpkinItemStack::EMPTY),
pumpkin_item_to_proto(PumpkinItemStack::EMPTY),
pumpkin_item_to_proto(PumpkinItemStack::EMPTY),
)
};
GetPlayerInventoryResponse {
main_inventory,
@ -82,8 +93,6 @@ pub fn ffi_native_bridge_get_player_inventory_impl(
boots: Some(boots),
}
})
})
})
}
pub fn ffi_native_bridge_set_player_inventory_slot_impl(

View File

@ -4,12 +4,17 @@ pub fn ffi_native_bridge_send_log_impl(request: SendLogRequest) -> Option<()> {
let logger = &request.logger_name;
let msg = &request.message;
// Filter internal JDK / system loggers
if logger.starts_with("jdk.") || logger.starts_with("sun.") || logger.starts_with("javax.") {
return Some(());
}
match request.level() {
LogLevel::Severe => tracing::error!(logger = %logger, "{}", msg),
LogLevel::Warning => tracing::warn!(logger = %logger, "{}", msg),
LogLevel::Info => tracing::info!(logger = %logger, "{}", msg),
LogLevel::Config => tracing::debug!(logger = %logger, "{}", msg),
LogLevel::Fine => tracing::debug!(logger = %logger, "{}", msg),
LogLevel::Fine => tracing::trace!(logger = %logger, "{}", msg),
LogLevel::Finer => tracing::trace!(logger = %logger, "{}", msg),
LogLevel::Finest => tracing::trace!(logger = %logger, "{}", msg),
LogLevel::Off => {}

View File

@ -1,7 +1,6 @@
use std::sync::{Arc, OnceLock};
use anyhow::Result;
use j4rs::Jvm;
use pumpkin::plugin::Context;
use tokio::sync::mpsc;
@ -76,6 +75,6 @@ pub fn init_callback_context(
Ok(())
}
pub fn initialize_callbacks(jvm: &Jvm) -> Result<()> {
initialize_ffi_callbacks(jvm)
pub fn initialize_callbacks(env: &mut jni::Env) -> Result<()> {
initialize_ffi_callbacks(env)
}

View File

@ -38,8 +38,7 @@ pub fn ffi_native_bridge_player_entity_play_sound_impl(
let entity = entity.first()?.get_entity();
player
.client
.enqueue_packet(&CEntitySoundEffect::new(
.send_client_packet(&CEntitySoundEffect::new(
IdOr::Id(pumpkin_sound as u16),
category,
entity.entity_id.into(),

View File

@ -12,6 +12,14 @@ pub fn get_string(str_ptr: *const c_char) -> String {
unsafe { CStr::from_ptr(str_ptr).to_string_lossy().into_owned() }
}
pub fn cache_player(player: Arc<Player>) {
let player_uuid = player.gameprofile.id;
if let Ok(mut write_guard) = PLAYER_HANDLE_CACHE.write() {
let cache = write_guard.get_or_insert_with(HashMap::new);
cache.insert(player_uuid, player);
}
}
pub fn with_player<F, R>(proto_uuid: Option<&ProtoUuid>, f: F) -> Option<R>
where
F: FnOnce(Arc<Player>) -> R,

View File

@ -100,7 +100,7 @@ pub fn ffi_native_bridge_get_world_border_impl(
.cloned()
.or_else(|| worlds.first().cloned())?;
let wb = world.worldborder.blocking_lock();
let wb = world.worldborder.try_lock().ok()?;
Some(crate::proto::patchbukkit::world::WorldBorderData {
center_x: wb.center_x,
@ -131,7 +131,8 @@ pub fn ffi_native_bridge_set_world_border_impl(
.cloned()
.or_else(|| worlds.first().cloned())?;
let mut wb = world.worldborder.blocking_lock();
ctx.runtime.spawn(async move {
let mut wb = world.worldborder.lock().await;
wb.center_x = border_data.center_x;
wb.center_z = border_data.center_z;
wb.old_diameter = border_data.size;
@ -142,6 +143,7 @@ pub fn ffi_native_bridge_set_world_border_impl(
wb.damage_per_block = border_data.damage_per_block as f32;
wb.buffer = border_data.damage_buffer as f32;
wb.portal_teleport_boundary = border_data.max_center_coordinate;
});
Some(())
}

View File

@ -1,26 +1,11 @@
use std::sync::{Arc, Mutex};
use anyhow::Result;
use j4rs::{Instance, InvocationArg, Jvm};
use pumpkin::{command::dispatcher::CommandError, plugin::Context};
use jni::Env;
use pumpkin::command::dispatcher::CommandError;
use pumpkin_protocol::java::client::play::CommandSuggestion;
use pumpkin_util::permission::{Permission, PermissionDefault};
use tokio::sync::mpsc;
use crate::{
commands::{SimpleCommandSender, init_java_command},
config,
java::{
jvm::commands::{JvmCommand, Location},
plugin::manager::Plugin,
},
};
use crate::{commands::SimpleCommandSender, java::jvm::commands::Location};
const PATCHBUKKIT_PERMISSION_NAMESPACE: &str = "patchbukkit";
pub struct CommandManager {
command_map: Option<Instance>,
}
pub struct CommandManager;
impl Default for CommandManager {
fn default() -> Self {
@ -31,39 +16,17 @@ impl Default for CommandManager {
impl CommandManager {
#[must_use]
pub const fn new() -> Self {
Self { command_map: None }
}
pub fn init(&mut self, jvm: &Jvm) -> Result<()> {
let server_instance =
jvm.invoke_static("org.bukkit.Bukkit", "getServer", InvocationArg::empty())?;
let command_map = jvm.invoke(&server_instance, "getCommandMap", InvocationArg::empty())?;
let command_map = jvm.cast(
&command_map,
"org.patchbukkit.command.PatchBukkitCommandMap",
)?;
self.command_map = Some(command_map);
Ok(())
}
fn get_or_init_command_map(&mut self, jvm: &Jvm) -> Result<&Instance> {
if self.command_map.is_none() {
self.init(jvm)?;
}
Ok(self.command_map.as_ref().unwrap())
Self
}
pub fn get_tab_complete(
&mut self,
jvm: &Jvm,
env: &mut Env,
sender: SimpleCommandSender,
full_command: String,
location: Option<Location>,
) -> Result<Option<Vec<CommandSuggestion>>, CommandError> {
match self.try_tab_complete(jvm, sender, full_command, location) {
match self.try_tab_complete(env, sender, full_command, location) {
Ok(suggestions) => Ok(suggestions),
Err(e) => {
tracing::warn!("Tab completion failed: {e}");
@ -74,271 +37,106 @@ impl CommandManager {
fn try_tab_complete(
&mut self,
jvm: &Jvm,
env: &mut Env,
sender: SimpleCommandSender,
full_command: String,
location: Option<Location>,
) -> Result<Option<Vec<CommandSuggestion>>> {
let command_map = self.get_or_init_command_map(jvm)?;
let sender = Self::sender_to_jsender(jvm, sender)?;
let completions = if let Some(location) = location {
let world = jvm.invoke_static(
"org.patchbukkit.world.PatchBukkitWorld",
"getOrCreate",
&[InvocationArg::try_from(location.world.to_string())?],
)?;
let location = match location.rotation {
Some(rotation) => jvm.create_instance(
"org.bukkit.Location",
&[
InvocationArg::try_from(world)?,
InvocationArg::try_from(location.x)?.into_primitive()?,
InvocationArg::try_from(location.y)?.into_primitive()?,
InvocationArg::try_from(location.z)?.into_primitive()?,
InvocationArg::try_from(rotation.yaw)?.into_primitive()?,
InvocationArg::try_from(rotation.pitch)?.into_primitive()?,
],
)?,
None => jvm.create_instance(
"org.bukkit.Location",
&[
InvocationArg::try_from(world)?,
InvocationArg::try_from(location.x)?.into_primitive()?,
InvocationArg::try_from(location.y)?.into_primitive()?,
InvocationArg::try_from(location.z)?.into_primitive()?,
],
)?,
};
jvm.invoke(
command_map,
"tabComplete",
&[
InvocationArg::try_from(sender)?,
InvocationArg::try_from(full_command)?,
InvocationArg::try_from(location)?,
],
)?
} else {
jvm.invoke(
command_map,
"tabComplete",
&[
InvocationArg::try_from(sender)?,
InvocationArg::try_from(full_command)?,
],
)?
};
let completions: Vec<String> = jvm.to_rust(completions)?;
Ok(Some(
completions
.into_iter()
.map(|completion| CommandSuggestion::new(completion, None))
.collect(),
))
}
pub async fn register_command(
&mut self,
jvm: &Jvm,
context: &Arc<Context>,
plugin: &Plugin,
cmd_name: String,
cmd_data: &config::spigot::Command,
command_tx: mpsc::Sender<JvmCommand>,
) -> Result<()> {
let command_map = self.get_or_init_command_map(jvm)?;
let created_cmd = match jvm.invoke_static(
"org.patchbukkit.command.CommandFactory",
"create",
&[
InvocationArg::try_from(&cmd_name)?,
InvocationArg::try_from(&plugin.name)?,
],
) {
Ok(inst) => {
if jvm
.invoke(&inst, "getName", InvocationArg::empty())
.is_err()
{
tracing::error!(
"Failed to create PluginCommand for {}: Java returned null",
cmd_name
);
return Ok(());
}
inst
}
Err(e) => {
tracing::error!("Failed to create PluginCommand for {}: {:?}", cmd_name, e);
return Ok(());
}
};
let j_plugin_cmd = Arc::new(Mutex::new(created_cmd));
tracing::info!("Registering Bukkit command: {}", &cmd_name);
let cmd_aliases: Vec<String> = cmd_data
.aliases
.as_ref()
.map_or_else(Vec::new, |a| a.to_vec());
let mut raw_names: Vec<String> = vec![cmd_name.clone()];
raw_names.extend(cmd_aliases.clone());
let mut names: Vec<String> = Vec::new();
for name in raw_names {
let clean = name.trim_start_matches('/').to_string();
if !clean.is_empty() {
if !names.contains(&clean) {
names.push(clean.clone());
}
let single = format!("/{}", clean);
if !names.contains(&single) {
names.push(single);
}
let double = format!("//{}", clean);
if !names.contains(&double) {
names.push(double);
}
}
}
{
let cmd_lock = j_plugin_cmd.lock().unwrap();
let j_plugin_cmd_owned = jvm.clone_instance(&cmd_lock)?;
if let Some(ref desc) = cmd_data.description {
let _ = jvm.invoke(
&j_plugin_cmd_owned,
"setDescription",
&[InvocationArg::try_from(desc)?],
);
}
if let Some(ref usage) = cmd_data.usage {
let _ = jvm.invoke(
&j_plugin_cmd_owned,
"setUsage",
&[InvocationArg::try_from(usage)?],
);
}
if let Some(ref perm_msg) = cmd_data.permission_message {
let _ = jvm.invoke(
&j_plugin_cmd_owned,
"setPermissionMessage",
&[InvocationArg::try_from(perm_msg)?],
);
}
if let Some(ref perm) = cmd_data.permission {
let _ = jvm.invoke(
&j_plugin_cmd_owned,
"setPermission",
&[InvocationArg::try_from(perm)?],
);
}
if !cmd_aliases.is_empty() {
let j_aliases = jvm.create_java_array(
"java.lang.String",
&cmd_aliases
.iter()
.map(|a| InvocationArg::try_from(a).unwrap())
.collect::<Vec<_>>(),
)?;
let j_list = jvm.invoke_static(
"java.util.Arrays",
"asList",
&[InvocationArg::from(j_aliases)],
)?;
let _ = jvm.invoke(
&j_plugin_cmd_owned,
"setAliases",
&[InvocationArg::from(j_list)],
);
}
let j_command_cast = match jvm.cast(&j_plugin_cmd_owned, "org.bukkit.command.Command") {
Ok(casted) => casted,
Err(_) => j_plugin_cmd_owned,
};
jvm.invoke(
command_map,
"register",
&[
InvocationArg::try_from(&cmd_name)?,
InvocationArg::try_from(&plugin.name)?,
InvocationArg::from(j_command_cast),
],
)?;
}
let node = init_java_command(
cmd_name.clone(),
command_tx.clone(),
names,
cmd_data.description.clone().unwrap_or_default(),
);
let (permission, permission_default) = build_permission_node(&cmd_name, cmd_data);
let registry = {
let permission_manager = context.permission_manager.read().await;
permission_manager.registry.clone()
};
if let Err(e) = registry.write().await.register_permission(Permission::new(
&permission,
&format!(
"Allows running the Bukkit command `{cmd_name}` from `{}`",
plugin.name
let (uuid_jstr, name_jstr, is_op) = match sender {
SimpleCommandSender::Console => (None, None, true),
SimpleCommandSender::Player(uuid_str, name, is_op) => (
Some(env.new_string(&uuid_str)?),
Some(env.new_string(&name)?),
is_op,
),
permission_default,
)) {
if e.contains("already registered") {
tracing::debug!(
"Permission already registered for command {}: {}",
cmd_name,
e
);
} else {
tracing::warn!(
"Failed to register permission for command {}: {:?}",
cmd_name,
e
);
};
let cmd_jstr = env.new_string(&full_command)?;
let (world_jstr, x, y, z) = match location {
Some(loc) => (
Some(env.new_string(loc.world.to_string())?),
loc.x,
loc.y,
loc.z,
),
None => (None, 0.0, 0.0, 0.0),
};
let null_obj = jni::objects::JObject::null();
let uuid_val = uuid_jstr.as_deref().unwrap_or(&null_obj).into();
let name_val = name_jstr.as_deref().unwrap_or(&null_obj).into();
let world_val = world_jstr.as_deref().unwrap_or(&null_obj).into();
let result = env.call_static_method(
jni::jni_str!("org/patchbukkit/command/PatchBukkitCommandMap"),
jni::jni_str!("tabCompleteRaw"),
jni::jni_sig!("(Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;DDD)[Ljava/lang/String;"),
&[
uuid_val,
name_val,
jni::objects::JValue::Bool(is_op),
(&cmd_jstr).into(),
world_val,
jni::objects::JValue::Double(x),
jni::objects::JValue::Double(y),
jni::objects::JValue::Double(z),
],
)?;
let obj = result.l()?;
if obj.is_null() {
return Ok(None);
}
let array = jni::objects::JObjectArray::<jni::objects::JString>::cast_local(env, obj)?;
let len = array.len(env)?;
let mut suggestions = Vec::with_capacity(len);
for i in 0..len {
let elem = array.get_element(env, i)?;
if !elem.is_null() {
let rust_str = elem.try_to_string(env)?;
suggestions.push(CommandSuggestion::new(rust_str, None));
}
}
context.register_command(node, permission).await;
Ok(())
Ok(Some(suggestions))
}
pub fn trigger_command(
&mut self,
jvm: &Jvm,
env: &mut Env,
full_command: String,
sender: SimpleCommandSender,
) -> Result<()> {
let command_map = self.get_or_init_command_map(jvm)?;
let (uuid_jstr, name_jstr, is_op) = match sender {
SimpleCommandSender::Console => (None, None, true),
SimpleCommandSender::Player(uuid_str, name, is_op) => (
Some(env.new_string(&uuid_str)?),
Some(env.new_string(&name)?),
is_op,
),
};
let j_sender = Self::sender_to_jsender(jvm, sender)?;
let cmd_jstr = env.new_string(&full_command)?;
let null_obj = jni::objects::JObject::null();
let uuid_val = uuid_jstr.as_deref().unwrap_or(&null_obj).into();
let name_val = name_jstr.as_deref().unwrap_or(&null_obj).into();
let dispatch_result = jvm.invoke(
command_map,
"dispatch",
let result = env.call_static_method(
jni::jni_str!("org/patchbukkit/command/PatchBukkitCommandMap"),
jni::jni_str!("dispatchRaw"),
jni::jni_sig!("(Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;)Z"),
&[
InvocationArg::from(j_sender),
InvocationArg::try_from(full_command.clone())?,
uuid_val,
name_val,
jni::objects::JValue::Bool(is_op),
(&cmd_jstr).into(),
],
);
match dispatch_result {
Ok(result) => {
let handled: bool = jvm.to_rust(result)?;
match result {
Ok(val) => {
let handled = val.z()?;
if !handled {
tracing::warn!("Command not handled by any Java plugin: {full_command}");
}
@ -350,73 +148,16 @@ impl CommandManager {
Ok(())
}
pub fn sender_to_jsender(jvm: &Jvm, sender: SimpleCommandSender) -> Result<Instance> {
match sender {
SimpleCommandSender::Console => Ok(jvm.invoke_static(
"org.bukkit.Bukkit",
"getConsoleSender",
InvocationArg::empty(),
)?),
SimpleCommandSender::Player(uuid_str, name) => {
let server =
jvm.invoke_static("org.bukkit.Bukkit", "getServer", InvocationArg::empty())?;
let patch_server = jvm.cast(&server, "org.patchbukkit.PatchBukkitServer")?;
let j_uuid = jvm.invoke_static(
"java.util.UUID",
"fromString",
&[InvocationArg::try_from(uuid_str)?],
)?;
let j_player = jvm.invoke(
&patch_server,
"getPlayer",
&[InvocationArg::from(jvm.clone_instance(&j_uuid)?)],
)?;
let is_null: bool = jvm.to_rust(jvm.invoke_static(
"java.util.Objects",
"isNull",
&[InvocationArg::from(jvm.clone_instance(&j_player)?)],
)?)?;
if is_null {
tracing::info!(
"Player not found in Java, creating PatchBukkitPlayer for {}",
name
);
let j_player = jvm.create_instance(
"org.patchbukkit.entity.PatchBukkitPlayer",
&[InvocationArg::from(j_uuid), InvocationArg::try_from(name)?],
)?;
jvm.invoke(
&j_player,
"setOp",
&[InvocationArg::try_from(true)?.into_primitive()?],
)?;
jvm.invoke(
&patch_server,
"registerPlayer",
&[InvocationArg::from(jvm.clone_instance(&j_player)?)],
)?;
Ok(j_player)
} else {
Ok(j_player)
}
}
}
}
}
#[cfg(test)]
const PATCHBUKKIT_PERMISSION_NAMESPACE: &str = "patchbukkit";
#[cfg(test)]
fn build_permission_node(
cmd_name: &str,
cmd_data: &config::spigot::Command,
) -> (String, PermissionDefault) {
cmd_data: &crate::config::spigot::Command,
) -> (String, pumpkin_util::permission::PermissionDefault) {
let clean = cmd_name.trim_start_matches('/');
if let Some(permission) = &cmd_data.permission {
let perm_node = if permission.contains(':') {
@ -424,11 +165,14 @@ fn build_permission_node(
} else {
format!("{PATCHBUKKIT_PERMISSION_NAMESPACE}:{permission}")
};
(perm_node, PermissionDefault::Allow)
(
perm_node,
pumpkin_util::permission::PermissionDefault::Allow,
)
} else {
(
format!("{PATCHBUKKIT_PERMISSION_NAMESPACE}:command.{clean}"),
PermissionDefault::Allow,
pumpkin_util::permission::PermissionDefault::Allow,
)
}
}

View File

@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
use j4rs::{Instance, InvocationArg, Jvm};
use jni::Env;
use prost::Message;
use pumpkin::{entity::player::Player, server::Server};
@ -10,7 +10,7 @@ use crate::{
proto::patchbukkit::events::{FireEventResponse, event::Data},
};
pub struct EventManager {}
pub struct EventManager;
impl Default for EventManager {
fn default() -> Self {
@ -21,144 +21,74 @@ impl Default for EventManager {
impl EventManager {
#[must_use]
pub const fn new() -> Self {
Self {}
Self
}
pub fn fire_event(
&self,
jvm: &Jvm,
env: &mut Env,
payload: JvmEventPayload,
plugin_name: String,
) -> Result<FireEventResponse> {
let server = jvm.invoke_static("org.bukkit.Bukkit", "getServer", InvocationArg::empty())?;
let patch_server = jvm.cast(&server, "org.patchbukkit.PatchBukkitServer")?;
let event_manager = jvm.invoke(&patch_server, "getEventManager", InvocationArg::empty())?;
if let Some(ref event) = payload.event.data
&& matches!(event, Data::PlayerJoin(_))
&& let Some(ref player) = payload.context.player
{
Self::register_player(jvm, &patch_server, player, &payload.context.server)?;
Self::register_player(env, player, &payload.context.server)?;
}
let bytes = payload.event.encode_to_vec();
let j_bytes = jvm.create_java_array(
"byte",
&bytes
.iter()
.map(|&b| {
InvocationArg::try_from(b as i8)
.unwrap()
.into_primitive()
.unwrap()
})
.collect::<Vec<_>>(),
let j_bytes = env.byte_array_from_slice(&bytes)?;
let plugin_name_jstr = env.new_string(&plugin_name)?;
let result = env.call_static_method(
jni::jni_str!("org/patchbukkit/events/PatchBukkitEventFactory"),
jni::jni_str!("fireEventFromBytes"),
jni::jni_sig!("([BLjava/lang/String;)[B"),
&[(&j_bytes).into(), (&plugin_name_jstr).into()],
)?;
let j_event = jvm.invoke_static(
"org.patchbukkit.events.PatchBukkitEventFactory",
"createEventFromBytes",
&[InvocationArg::from(j_bytes)],
)?;
let is_null: bool = jvm.to_rust(jvm.invoke_static(
"java.util.Objects",
"isNull",
&[InvocationArg::from(jvm.clone_instance(&j_event)?)],
)?)?;
if is_null {
return Err(anyhow::anyhow!(
"Failed to create event - factory returned null"
));
let obj = result.l()?;
if obj.is_null() {
return Ok(FireEventResponse {
cancelled: false,
data: Some(payload.event),
});
}
jvm.invoke(
&event_manager,
"fireEvent",
&[
InvocationArg::from(jvm.clone_instance(&j_event)?),
InvocationArg::try_from(plugin_name)?,
],
)?;
let response_bytes: Vec<i8> = jvm.to_rust(jvm.invoke_static(
"org.patchbukkit.events.PatchBukkitEventFactory",
"toFireEventResponse",
&[InvocationArg::from(j_event)],
)?)?;
let response_bytes: Vec<u8> = response_bytes.iter().map(|&b| b as u8).collect();
let response = FireEventResponse::decode(response_bytes.as_slice())?;
let j_byte_array = jni::objects::JByteArray::cast_local(env, obj)?;
let resp_vec = env.convert_byte_array(&j_byte_array)?;
let response = FireEventResponse::decode(resp_vec.as_slice())?;
Ok(response)
}
pub fn register_player(
jvm: &Jvm,
patch_server: &Instance,
env: &mut Env,
player: &Arc<Player>,
_server: &Arc<Server>,
) -> Result<()> {
let j_uuid = jvm
.invoke_static(
"java.util.UUID",
"fromString",
&[InvocationArg::try_from(player.gameprofile.id.to_string())?],
)
.map_err(|e| format!("Failed to create Java UUID: {e}"))
.unwrap();
let player_permission_level = player.permission_lvl.load();
let is_op = player_permission_level > pumpkin_util::permission::PermissionLvl::Zero
|| _server
.data
.operator_config
.try_read()
.is_ok_and(|ops| ops.get_entry(&player.gameprofile.id).is_some());
let j_player = jvm.create_instance(
"org.patchbukkit.entity.PatchBukkitPlayer",
let uuid_jstr = env.new_string(player.gameprofile.id.to_string())?;
let name_jstr = env.new_string(&player.gameprofile.name)?;
env.call_static_method(
jni::jni_str!("org/patchbukkit/PatchBukkitServer"),
jni::jni_str!("registerPlayer"),
jni::jni_sig!("(Ljava/lang/String;Ljava/lang/String;Z)V"),
&[
InvocationArg::from(j_uuid),
InvocationArg::try_from(player.gameprofile.name.clone())?,
(&uuid_jstr).into(),
(&name_jstr).into(),
jni::objects::JValue::Bool(is_op),
],
)?;
let player_permission_level = player.permission_lvl.load();
let is_op = player_permission_level > pumpkin_util::permission::PermissionLvl::Zero
|| tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
_server
.data
.operator_config
.read()
.await
.get_entry(&player.gameprofile.id)
.is_some()
})
});
if is_op {
jvm.invoke(
&j_player,
"setOp",
&[InvocationArg::try_from(true)
.unwrap()
.into_primitive()
.unwrap()],
)?;
}
jvm.invoke(
patch_server,
"registerPlayer",
&[InvocationArg::from(j_player)],
)?;
Ok(())
}
pub fn call_event(&self, jvm: &Jvm, event: Instance) -> Result<()> {
let server = jvm.invoke_static("org.bukkit.Bukkit", "getServer", InvocationArg::empty())?;
let plugin_manager = jvm.invoke(&server, "getPluginManager", InvocationArg::empty())?;
jvm.invoke(
&plugin_manager,
"callEvent",
&[InvocationArg::try_from(event)?],
)?;
Ok(())
}
}

View File

@ -5,7 +5,7 @@ use std::{
};
use anyhow::Result;
use j4rs::{Instance, InvocationArg, Jvm};
use jni::Env;
use pumpkin::plugin::Context;
use tokio::sync::mpsc;
@ -64,12 +64,9 @@ pub struct Plugin {
pub state: PluginState,
/// Data folder for this plugin
pub data_folder: PathBuf,
pub instance: Option<Instance>,
// The registered commands
pub commands: HashMap<String, Command>,
pub listeners: HashMap<String, Instance>,
// Dependency metadata (normalized to lowercase)
pub provides: Vec<String>,
pub depends: Vec<String>,
@ -257,9 +254,7 @@ impl PluginManager {
state: PluginState::Registered,
data_folder: jar_path.as_ref().parent().unwrap().join("data"),
path: jar_path.as_ref().to_path_buf(),
instance: None,
commands,
listeners: HashMap::new(),
provides,
depends: dedupe_names(depends),
@ -306,9 +301,7 @@ impl PluginManager {
state: PluginState::Registered,
data_folder: jar_path.as_ref().parent().unwrap().join("data"),
path: jar_path.as_ref().to_path_buf(),
instance: None,
commands,
listeners: HashMap::new(),
provides,
depends: dedupe_names(depends),
@ -323,30 +316,43 @@ impl PluginManager {
Ok(())
}
pub fn enable_all_plugins(&mut self, _jvm: &Jvm) -> Result<()> {
pub fn enable_all_plugins(&mut self, _env: &mut Env) -> Result<()> {
Ok(())
}
pub fn disable_all_plugins(&mut self, _jvm: &Jvm) -> Result<()> {
pub fn disable_all_plugins(&mut self, _env: &mut Env) -> Result<()> {
Ok(())
}
pub async fn instantiate_all_plugins(
pub fn instantiate_all_plugins(
&mut self,
jvm: &Jvm,
env: &mut Env,
plugin_folder: &Path,
_server: &Arc<Context>,
_command_tx: mpsc::Sender<JvmCommand>,
_command_manager: &mut CommandManager,
) -> Result<()> {
tracing::info!("Bootstrapping Java plugins from {:?}", plugin_folder);
let _ = jvm.invoke_static(
"org.patchbukkit.loader.PatchBukkitBootstrap",
"bootstrapPlugins",
&[InvocationArg::try_from(
&plugin_folder.to_string_lossy().to_string(),
)?],
let path_str = plugin_folder.to_string_lossy();
let j_str = env.new_string(&path_str)?;
let result = env.call_static_method(
jni::jni_str!("org/patchbukkit/loader/PatchBukkitBootstrap"),
jni::jni_str!("bootstrapPlugins"),
jni::jni_sig!("(Ljava/lang/String;)Z"),
&[(&j_str).into()],
);
match result {
Ok(val) => {
if !val.z()? {
tracing::error!("Bootstrap of Java plugins reported errors");
}
}
Err(e) => {
tracing::error!("Failed to bootstrap Java plugins: {:?}", e);
return Err(e.into());
}
}
Ok(())
}

View File

@ -8,39 +8,49 @@ use anyhow::Result;
use rust_embed::Embed;
#[derive(Embed)]
#[folder = "resources/"]
#[folder = "resources/jassets/"]
pub struct Resources;
pub fn setup_j4rs(j4rs_folder: &Path) -> Result<()> {
sync_embedded_resources(j4rs_folder)?;
cleanup_stale_files(j4rs_folder);
pub fn setup_resources(resources_folder: &Path) -> Result<()> {
sync_embedded_resources(resources_folder)?;
cleanup_stale_files(resources_folder);
Ok(())
}
fn cleanup_stale_files(j4rs_folder: &Path) {
fn cleanup_stale_files(resources_folder: &Path) {
let embedded_paths: HashSet<PathBuf> = Resources::iter()
.map(|p| PathBuf::from(p.to_string()))
.collect();
for entry in walkdir::WalkDir::new(j4rs_folder)
for entry in walkdir::WalkDir::new(resources_folder)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
{
let path = entry.path();
if let Ok(rel_path) = path.strip_prefix(j4rs_folder)
if let Ok(rel_path) = path.strip_prefix(resources_folder)
&& !embedded_paths.contains(rel_path)
{
tracing::warn!("Removing stale embedded file: {}", rel_path.display());
let _ = fs::remove_file(path);
}
}
// Clean up any empty subdirectories in resources_folder
for entry in walkdir::WalkDir::new(resources_folder)
.contents_first(true)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_dir() && e.path() != resources_folder)
{
let _ = fs::remove_dir(entry.path());
}
}
pub fn sync_embedded_resources(j4rs_folder: &Path) -> Result<()> {
pub fn sync_embedded_resources(resources_folder: &Path) -> Result<()> {
for resource_path_str in Resources::iter() {
let resource_path = j4rs_folder.join(resource_path_str.to_string());
let resource_path = resources_folder.join(resource_path_str.to_string());
let resource = Resources::get(&resource_path_str).unwrap();
if resource_path.exists() {

View File

@ -13,17 +13,13 @@ pub mod java;
pub mod proto;
use directories::setup_directories;
use java::jar::discover_jar_files;
use tokio::sync::{
mpsc::{self, Receiver},
oneshot,
};
use tokio::sync::{mpsc, oneshot};
use crate::{
config::patchbukkit::PatchBukkitConfig,
java::{
jvm::{commands::JvmCommand, worker::JvmWorker},
resources::setup_j4rs,
resources::setup_resources,
},
};
@ -40,77 +36,91 @@ async fn on_load_inner(plugin: &PatchBukkitPlugin, server: Arc<Context>) -> Resu
.map_err(|e| format!("Failed to setup PatchBukkit config: {e}"))?;
// Manage embedded resources
setup_j4rs(&dirs.j4rs).map_err(|e| format!("Failed to setup J4RS: {e}"))?;
setup_resources(&dirs.jassets).map_err(|e| format!("Failed to setup resources: {e}"))?;
{
let runtime_handle = plugin.runtime.handle().clone();
let command_tx = plugin.command_tx.clone();
let server_clone = server.clone();
// Run JVM initialization and Java plugin bootstrap in a background task
// on PatchBukkit's dedicated runtime so Pumpkin's main startup is never blocked.
plugin.runtime.spawn(async move {
tracing::info!("Initializing JVM in background...");
// 1. Initialize JVM
let (tx, rx) = oneshot::channel();
plugin
.command_tx
if let Err(e) = command_tx
.send(JvmCommand::Initialize {
j4rs_path: dirs.j4rs,
jassets_path: dirs.jassets.clone(),
respond_to: tx,
context: server.clone(),
command_tx: plugin.command_tx.clone(),
context: server_clone.clone(),
runtime_handle,
command_tx: command_tx.clone(),
config,
})
.await
.map_err(|e| format!("Failed to send command to initialize J4RS: {e}"))?;
rx.await
.map_err(|e| format!("Unable to receive response from J4RS initialization: {e}"))?
.map_err(|e| format!("Failed to initialize all plugins: {e}"))?;
{
tracing::error!("Failed to send command to initialize JVM: {e}");
return;
}
{
match rx.await {
Ok(Ok(())) => tracing::info!("JVM initialization completed successfully"),
Ok(Err(e)) => {
tracing::error!("Failed to initialize JVM: {e}");
return;
}
Err(e) => {
tracing::error!("Unable to receive response from JVM initialization: {e}");
return;
}
}
// 2. Instantiate all Java plugins
let (tx, rx) = oneshot::channel();
plugin
.command_tx
if let Err(e) = command_tx
.send(JvmCommand::InstantiateAllPlugins {
plugins_dir: dirs.plugins.clone(),
respond_to: tx,
server: server.clone(),
command_tx: plugin.command_tx.clone(),
server: server_clone.clone(),
command_tx: command_tx.clone(),
})
.await
.map_err(|e| format!("Failed to send command to instantiate plugins: {e}"))?;
rx.await
.map_err(|e| format!("Unable to receive response from instantiate plugins: {e}"))?
.map_err(|e| format!("Failed to instantiate all plugins: {e}"))?;
{
tracing::error!("Failed to send command to instantiate plugins: {e}");
return;
}
{
match rx.await {
Ok(Ok(())) => tracing::info!("Java plugins instantiated successfully"),
Ok(Err(e)) => {
tracing::error!("Failed to instantiate Java plugins: {e}");
return;
}
Err(e) => {
tracing::error!("Unable to receive response from instantiate plugins: {e}");
return;
}
}
// 3. Enable all Java plugins
let (tx, rx) = oneshot::channel();
plugin
.command_tx
if let Err(e) = command_tx
.send(JvmCommand::EnableAllPlugins { respond_to: tx })
.await
.map_err(|e| format!("Failed to send command to enable all plugins: {e}"))?;
rx.await
.map_err(|e| format!("Unable to receive response from enable all plugins: {e}"))?
.map_err(|e| format!("Failed to enable all plugins: {e}"))?;
};
{
tracing::error!("Failed to send command to enable all plugins: {e}");
return;
}
// Register Bukkit plugin loader with Pumpkin so Bukkit/Paper plugins appear in Pumpkin's native /plugins command
let bukkit_loader = Arc::new(crate::java::plugin::loader::BukkitPluginLoader);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| format!("Failed to build Tokio runtime: {e}"))?;
let server_ref = server.clone();
let jar_paths: Vec<_> = discover_jar_files(&dirs.plugins).collect();
rt.block_on(async move {
server_ref
.plugin_manager
.add_loader(&server_ref.server, bukkit_loader)
.await;
for jar_path in &jar_paths {
let _ = server_ref
.plugin_manager
.try_load_plugin(&server_ref.server, jar_path)
.await;
match rx.await {
Ok(Ok(())) => tracing::info!("PatchBukkit background initialization complete"),
Ok(Err(e)) => tracing::error!("Failed to enable all plugins: {e}"),
Err(e) => tracing::error!("Unable to receive response from enable all plugins: {e}"),
}
});
tracing::info!("PatchBukkit loaded successfully");
Ok(())
}
@ -155,25 +165,36 @@ async fn on_unload(&self, server: Arc<Context>) -> Result<(), String> {
#[plugin_impl]
pub struct PatchBukkitPlugin {
pub command_tx: mpsc::Sender<JvmCommand>,
pub runtime: Arc<tokio::runtime::Runtime>,
}
impl PatchBukkitPlugin {
#[must_use]
pub fn new() -> Self {
let (tx, rx) = mpsc::channel(100);
let runtime = Arc::new(
tokio::runtime::Builder::new_multi_thread()
.worker_threads(4)
.thread_name("patchbukkit-worker")
.enable_all()
.build()
.expect("Failed to create PatchBukkit Tokio runtime"),
);
#[tokio::main]
pub async fn jvm_thread_task(rx: Receiver<JvmCommand>) {
JvmWorker::new(rx).attach_thread().await;
}
let (tx, rx) = mpsc::channel(4096);
let runtime_clone = runtime.clone();
std::thread::Builder::new()
.name("patchbukkit-jvm-worker".to_string())
.spawn(move || {
jvm_thread_task(rx);
runtime_clone.block_on(async move {
JvmWorker::new(rx).attach_thread().await;
});
})
.unwrap();
Self { command_tx: tx }
Self {
command_tx: tx,
runtime,
}
}
}