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
rust/resources/jassets/* rust/resources/jassets/*
!rust/resources/jassets/j4rs-*
rust/resources/deps/*
# Gradle cache # Gradle cache
.gradle/ .gradle/

View File

@ -47,9 +47,8 @@ repositories {
} }
dependencies { 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("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:maven-resolver-provider:3.9.6")
implementation("org.apache.maven.resolver:maven-resolver-impl:1.9.18") implementation("org.apache.maven.resolver:maven-resolver-impl:1.9.18")
implementation("org.apache.maven.resolver:maven-resolver-connector-basic: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); knownCommands.put(label, command);
registeredDirectly = true; registeredDirectly = true;
} }
command.register(this);
knownCommands.put(fallbackPrefix + ":" + label, command); knownCommands.put(fallbackPrefix + ":" + label, command);
if (command.getAliases() != null) { if (command.getAliases() != null) {

View File

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

View File

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

View File

@ -107,11 +107,13 @@ public class PatchBukkitPluginManager implements PluginManager {
Class<? extends Plugin> pluginClass = jarClass.asSubclass(Plugin.class); Class<? extends Plugin> pluginClass = jarClass.asSubclass(Plugin.class);
Plugin plugin = pluginClass.getDeclaredConstructor().newInstance(); Plugin plugin = pluginClass.getDeclaredConstructor().newInstance();
loader.init((org.bukkit.plugin.java.JavaPlugin) plugin); loader.init((org.bukkit.plugin.java.JavaPlugin) plugin);
try {
org.patchbukkit.loader.PatchBukkitBootstrap.registerPluginCommands(plugin, description);
} catch (Throwable ignored) {}
try { try {
plugin.onLoad(); plugin.onLoad();
} catch (Throwable t) { } catch (Throwable t) {
server.getLogger().severe("Error loading " + plugin.getName() + ": " + t.getMessage()); server.getLogger().log(Level.SEVERE, "Error loading " + plugin.getName(), t);
t.printStackTrace();
} }
registerPlugin(plugin); registerPlugin(plugin);
return plugin; return plugin;
@ -145,7 +147,7 @@ public class PatchBukkitPluginManager implements PluginManager {
result.add(plugin); result.add(plugin);
} }
} catch (Exception e) { } 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 io.papermc.paper.threadedregions.scheduler.RegionScheduler;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.InetAddress; import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path; import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; 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"); private static final Logger logger = Logger.getLogger("Minecraft");
static { static {
@ -201,20 +211,136 @@ public class PatchBukkitServer implements Server {
root.addHandler(new java.util.logging.Handler() { root.addHandler(new java.util.logging.Handler() {
@Override @Override
public void publish(java.util.logging.LogRecord record) { public void publish(java.util.logging.LogRecord record) {
if (record == null) return;
if (IN_LOGGING.get()) return;
IN_LOGGING.set(true);
try { try {
LogLevel logLevel = LEVEL_MAP.getOrDefault(record.getLevel(), LogLevel.INFO); 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( NativeBridgeFfi.sendLog(
SendLogRequest.newBuilder() SendLogRequest.newBuilder()
.setLevel(logLevel) .setLevel(logLevel)
.setMessage(record.getMessage() != null ? record.getMessage() : "") .setMessage(message)
.setLoggerName(record.getLoggerName() != null ? record.getLoggerName() : "") .setLoggerName(loggerName)
.build() .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 flush() {}
@Override public void close() {} @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); 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() { public PatchBukkitEventManager getEventManager() {
return this.pluginManager.getEventManager(); return this.pluginManager.getEventManager();
} }
@ -312,10 +453,11 @@ public class PatchBukkitServer implements Server {
@Override @Override
public @NotNull String getMinecraftVersion() { public @NotNull String getMinecraftVersion() {
// TODO Auto-generated method stub String version = this.bukkitVersion;
throw new UnsupportedOperationException( if (version != null && version.contains("-")) {
"Unimplemented method 'getMinecraftVersion'" return version.split("-")[0];
); }
return version != null && !version.equals("Unknown-Version") ? version : "1.21.4";
} }
@Override @Override
@ -325,56 +467,36 @@ public class PatchBukkitServer implements Server {
@Override @Override
public int getMaxPlayers() { public int getMaxPlayers() {
// TODO Auto-generated method stub return 20;
throw new UnsupportedOperationException(
"Unimplemented method 'getMaxPlayers'"
);
} }
@Override @Override
public void setMaxPlayers(int maxPlayers) { public void setMaxPlayers(int maxPlayers) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException(
"Unimplemented method 'setMaxPlayers'"
);
} }
@Override @Override
public int getPort() { public int getPort() {
// TODO Auto-generated method stub return 25565;
throw new UnsupportedOperationException(
"Unimplemented method 'getPort'"
);
} }
@Override @Override
public int getViewDistance() { public int getViewDistance() {
// TODO Auto-generated method stub return 10;
throw new UnsupportedOperationException(
"Unimplemented method 'getViewDistance'"
);
} }
@Override @Override
public int getSimulationDistance() { public int getSimulationDistance() {
// TODO Auto-generated method stub return 10;
throw new UnsupportedOperationException(
"Unimplemented method 'getSimulationDistance'"
);
} }
@Override @Override
public @NotNull String getIp() { public @NotNull String getIp() {
// TODO Auto-generated method stub return "127.0.0.1";
throw new UnsupportedOperationException("Unimplemented method 'getIp'");
} }
@Override @Override
public @NotNull String getWorldType() { public @NotNull String getWorldType() {
// TODO Auto-generated method stub return "DEFAULT";
throw new UnsupportedOperationException(
"Unimplemented method 'getWorldType'"
);
} }
@Override @Override

View File

@ -79,6 +79,8 @@ public class PatchBukkitCommandMap extends SimpleCommandMap {
registeredDirectly = true; registeredDirectly = true;
} }
command.register(this);
registerVariants(label, command); registerVariants(label, command);
registerVariants(fallbackPrefix + ":" + label, command); registerVariants(fallbackPrefix + ":" + label, command);
if (!clean.isEmpty()) { if (!clean.isEmpty()) {
@ -105,7 +107,8 @@ public class PatchBukkitCommandMap extends SimpleCommandMap {
.setPluginName(fallbackPrefix) .setPluginName(fallbackPrefix)
.build(); .build();
NativeBridgeFfi.registerCommand(request); 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; return registeredDirectly;
@ -140,12 +143,66 @@ public class PatchBukkitCommandMap extends SimpleCommandMap {
try { try {
String[] args = Arrays.copyOfRange(split, 1, split.length); 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) { } catch (Exception ex) {
throw new CommandException("Unhandled exception executing '" + cmdLine + "'", 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 @Override
public void clearCommands() { public void clearCommands() {
knownCommands.clear(); knownCommands.clear();

View File

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

View File

@ -33,6 +33,7 @@ import patchbukkit.events.BlockIgniteEvent;
import patchbukkit.events.ServerCommandEvent; import patchbukkit.events.ServerCommandEvent;
import patchbukkit.events.SignChangeEvent; import patchbukkit.events.SignChangeEvent;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
public class PatchBukkitEventFactory { public class PatchBukkitEventFactory {
@ -44,11 +45,22 @@ public class PatchBukkitEventFactory {
Event event = Event.parseFrom(data); Event event = Event.parseFrom(data);
return createEvent(event); return createEvent(event);
} catch (InvalidProtocolBufferException e) { } catch (InvalidProtocolBufferException e) {
LOGGER.severe("Failed to parse Event: " + e.getMessage()); LOGGER.log(Level.SEVERE, "Failed to parse Event", e);
return null; 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 @Nullable
public static org.bukkit.event.Event createEvent(@NotNull Event event) { public static org.bukkit.event.Event createEvent(@NotNull Event event) {
try { try {
@ -427,7 +439,10 @@ public class PatchBukkitEventFactory {
java.util.UUID uuid = java.util.UUID.fromString(uuidStr); java.util.UUID uuid = java.util.UUID.fromString(uuidStr);
Player player = Bukkit.getServer().getPlayer(uuid); Player player = Bukkit.getServer().getPlayer(uuid);
if (player == null) { if (player == null) {
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; return player;
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {

View File

@ -53,6 +53,8 @@ public class PatchBukkitEventManager {
this.server = server; this.server = server;
} }
private final Set<String> registeredBridgeEvents = java.util.concurrent.ConcurrentHashMap.newKeySet();
public void callEvent(@NotNull Event event) throws IllegalStateException { public void callEvent(@NotNull Event event) throws IllegalStateException {
if (event.isAsynchronous() && this.server.isPrimaryThread()) { if (event.isAsynchronous() && this.server.isPrimaryThread()) {
throw new IllegalStateException(event.getEventName() + " may only be triggered asynchronously."); 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."); 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); callEventJavaOnly(event);
} }
}
/** /**
* Java-only event dispatch for events that don't have Pumpkin equivalents. * 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()) { for (Map.Entry<Class<? extends Event>, Set<RegisteredListener>> entry : this.createRegisteredListeners(listener, plugin).entrySet()) {
this.getEventListeners(this.getRegistrationClass(entry.getKey())).registerAll(entry.getValue()); 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()) { for (RegisteredListener rl : entry.getValue()) {
int priorityOrdinal = Math.min(rl.getPriority().ordinal(), 4); maxPriority = Math.max(maxPriority, Math.min(rl.getPriority().ordinal(), 4));
var request = RegisterEventRequest.newBuilder().setEventType(entry.getKey().getName()).setPluginName(plugin.getName()).setPriority(priorityOrdinal).setBlocking(true).build(); }
var request = RegisterEventRequest.newBuilder()
.setEventType(eventName)
.setPluginName(plugin.getName())
.setPriority(maxPriority)
.setBlocking(true)
.build();
NativeBridgeFfi.registerEvent(request); NativeBridgeFfi.registerEvent(request);
} }
} }
@ -438,10 +172,17 @@ public class PatchBukkitEventManager {
executor = new TimedEventExecutor(executor, plugin, null, event); executor = new TimedEventExecutor(executor, plugin, null, event);
this.getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled)); 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); 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); NativeBridgeFfi.registerEvent(request);
}
} }
@NotNull @NotNull
@ -534,5 +275,6 @@ public class PatchBukkitEventManager {
public void clearEvents() { public void clearEvents() {
HandlerList.unregisterAll(); HandlerList.unregisterAll();
registeredBridgeEvents.clear();
} }
} }

View File

@ -6,6 +6,8 @@ import java.net.URL;
import java.util.*; import java.util.*;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand; import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
@ -17,11 +19,13 @@ import org.patchbukkit.command.PatchBukkitCommandMap;
public class PatchBukkitBootstrap { public class PatchBukkitBootstrap {
private static final Logger LOGGER = Logger.getLogger("PatchBukkitBootstrap");
public static boolean bootstrapPlugins(String pluginsDirPath) { public static boolean bootstrapPlugins(String pluginsDirPath) {
try { try {
File pluginsDir = new File(pluginsDirPath); File pluginsDir = new File(pluginsDirPath);
if (!pluginsDir.exists() || !pluginsDir.isDirectory()) { 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; return true;
} }
@ -29,7 +33,7 @@ public class PatchBukkitBootstrap {
findJarsRecursive(pluginsDir, jarFiles); findJarsRecursive(pluginsDir, jarFiles);
if (jarFiles.isEmpty()) { 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; return true;
} }
@ -42,8 +46,7 @@ public class PatchBukkitBootstrap {
holders.put(holder.name.toLowerCase(Locale.ENGLISH), holder); holders.put(holder.name.toLowerCase(Locale.ENGLISH), holder);
} }
} catch (Throwable t) { } catch (Throwable t) {
System.err.println("[PatchBukkit] Failed to parse plugin JAR " + jarFile.getName() + ": " + t.getMessage()); LOGGER.log(Level.SEVERE, "[PatchBukkit] Failed to parse plugin JAR " + jarFile.getName(), t);
t.printStackTrace();
} }
} }
@ -53,8 +56,7 @@ public class PatchBukkitBootstrap {
try { try {
loadAndRegisterPlugin(holder, holders); loadAndRegisterPlugin(holder, holders);
} catch (Throwable t) { } catch (Throwable t) {
System.err.println("[PatchBukkit] Failed to load plugin " + holder.name + ": " + t.getMessage()); LOGGER.log(Level.SEVERE, "[PatchBukkit] Failed to load plugin " + holder.name, t);
t.printStackTrace();
} }
} }
@ -64,10 +66,9 @@ public class PatchBukkitBootstrap {
if (holder.pluginInstance != null) { if (holder.pluginInstance != null) {
try { try {
pm.enablePlugin(holder.pluginInstance); pm.enablePlugin(holder.pluginInstance);
System.out.println("[PatchBukkit] Enabled plugin: " + holder.name); LOGGER.info("[PatchBukkit] Enabled plugin: " + holder.name);
} catch (Throwable t) { } catch (Throwable t) {
System.err.println("[PatchBukkit] Error enabling plugin " + holder.name + ": " + t.getMessage()); LOGGER.log(Level.SEVERE, "[PatchBukkit] Error enabling plugin " + holder.name, t);
t.printStackTrace();
} }
} }
} }
@ -75,8 +76,7 @@ public class PatchBukkitBootstrap {
return true; return true;
} catch (Throwable t) { } catch (Throwable t) {
System.err.println("[PatchBukkit] Fatal error during bootstrap: " + t.getMessage()); LOGGER.log(Level.SEVERE, "[PatchBukkit] Fatal error during bootstrap", t);
t.printStackTrace();
return false; return false;
} }
} }
@ -210,14 +210,17 @@ public class PatchBukkitBootstrap {
try { try {
plugin.onLoad(); plugin.onLoad();
} catch (Throwable t) { } catch (Throwable t) {
System.err.println("[PatchBukkit] Error during onLoad() for " + holder.name + ": " + t.getMessage()); LOGGER.log(Level.SEVERE, "[PatchBukkit] Error during onLoad() for " + holder.name, t);
t.printStackTrace();
} }
holder.pluginInstance = plugin; 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) { 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) { if (commands != null) {
for (Map.Entry<String, Map<String, Object>> entry : commands.entrySet()) { for (Map.Entry<String, Map<String, Object>> entry : commands.entrySet()) {
String cmdName = entry.getKey(); String cmdName = entry.getKey();
@ -247,8 +250,8 @@ public class PatchBukkitBootstrap {
} }
} }
commandMap.register(cmdName, holder.name, pluginCmd); commandMap.register(cmdName, description.getName(), pluginCmd);
System.out.println("[PatchBukkit] Registered command: /" + cmdName + " for plugin " + holder.name); 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 ThreadLocal<Set<NamespacedKey>> inFallback = ThreadLocal.withInitial(HashSet::new);
private final RegistryKey<B> registryKey; 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) { public PatchBukkitRegistry(RegistryKey<B> registryKey) {
this(null, null, null, registryKey); this(null, null, null, registryKey);
@ -51,19 +54,49 @@ public class PatchBukkitRegistry<P, B extends Keyed> implements Registry<B> {
RegistryKey<B> registryKey RegistryKey<B> registryKey
) { ) {
this.registryKey = registryKey; this.registryKey = registryKey;
if (registryType != null || extractor != null || factory != null) { this.registryType = registryType;
initialize(registryType, extractor, factory); this.extractor = extractor;
this.factory = factory;
}
public void ensureInitialized() {
if (initialized) return;
synchronized (this) {
if (initialized) return;
initialized = true;
initializeInternal();
} }
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public synchronized void initialize( private void initializeInternal() {
RegistryType registryType, if (registryType != null) {
Function<GetRegistryDataResponse, List<P>> extractor, try {
Function<P, B> factory GetRegistryDataRequest request = GetRegistryDataRequest.newBuilder()
) { .setRegistry(registryType)
if (initialized) return; .build();
initialized = true;
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 // Auto-discover pre-registered Paper/Bukkit constants for this registry
if (registryKey != null) { if (registryKey != null) {
@ -89,6 +122,8 @@ public class PatchBukkitRegistry<P, B extends Keyed> implements Registry<B> {
} }
} catch (Throwable ignored) {} } 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 { } else {
Class<B> valueClass = (Class<B>) LegacyRegistryIdentifiers.KEY_TO_CLASS_MAP.get(registryKey); Class<B> valueClass = (Class<B>) LegacyRegistryIdentifiers.KEY_TO_CLASS_MAP.get(registryKey);
if (valueClass != null) { if (valueClass != null) {
@ -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") @SuppressWarnings("unchecked")
@ -162,6 +169,7 @@ public class PatchBukkitRegistry<P, B extends Keyed> implements Registry<B> {
if (key == null) { if (key == null) {
return null; return null;
} }
ensureInitialized();
B value = entries.get(key); B value = entries.get(key);
if (value != null) { if (value != null) {
return value; return value;
@ -228,11 +236,13 @@ public class PatchBukkitRegistry<P, B extends Keyed> implements Registry<B> {
@Override @Override
public boolean hasTag(TagKey<B> key) { public boolean hasTag(TagKey<B> key) {
ensureInitialized();
return key != null && tags.containsKey(key.key().asString()); return key != null && tags.containsKey(key.key().asString());
} }
@Override @Override
public @NonNull Tag<B> getTag(TagKey<B> key) { public @NonNull Tag<B> getTag(TagKey<B> key) {
ensureInitialized();
PatchBukkitTag<B> tag = tags.get(key.key().asString()); PatchBukkitTag<B> tag = tags.get(key.key().asString());
if (tag == null) { if (tag == null) {
throw new NoSuchElementException("Unknown tag: " + key.key().asString()); throw new NoSuchElementException("Unknown tag: " + key.key().asString());
@ -246,25 +256,30 @@ public class PatchBukkitRegistry<P, B extends Keyed> implements Registry<B> {
@Override @Override
public @NonNull Collection<Tag<B>> getTags() { public @NonNull Collection<Tag<B>> getTags() {
ensureInitialized();
return Collections.unmodifiableCollection(tags.values()); return Collections.unmodifiableCollection(tags.values());
} }
@Override @Override
public @NonNull Stream<B> stream() { public @NonNull Stream<B> stream() {
ensureInitialized();
return entries.values().stream(); return entries.values().stream();
} }
public @NonNull Stream<NamespacedKey> keyStream() { public @NonNull Stream<NamespacedKey> keyStream() {
ensureInitialized();
return entries.keySet().stream(); return entries.keySet().stream();
} }
@Override @Override
public int size() { public int size() {
ensureInitialized();
return entries.size(); return entries.size();
} }
@Override @Override
public @NonNull Iterator<B> iterator() { public @NonNull Iterator<B> iterator() {
ensureInitialized();
return Collections.unmodifiableCollection(entries.values()).iterator(); 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; return (Registry<T>) existing;
} }
PatchBukkitRegistry reg = new PatchBukkitRegistry(registryKey); PatchBukkitRegistry reg;
Registry<?> winner = instances.putIfAbsent(registryKey, reg);
if (winner != null) {
return (Registry<T>) winner;
}
try {
RegistryFactory<?, ?> factoryEntry = FACTORIES.get(registryKey); RegistryFactory<?, ?> factoryEntry = FACTORIES.get(registryKey);
if (factoryEntry != null) { if (factoryEntry != null) {
reg.initialize(factoryEntry.registryType(), factoryEntry.extractor(), factoryEntry.factory()); reg = new PatchBukkitRegistry(factoryEntry.registryType(), factoryEntry.extractor(), factoryEntry.factory(), registryKey);
} else { } else {
reg.initialize(null, null, null); reg = new PatchBukkitRegistry(registryKey);
}
} catch (Throwable t) {
// Ignore failure during registry initialization safely
} }
return (Registry<T>) reg; Registry<?> winner = instances.putIfAbsent(registryKey, reg);
return (Registry<T>) (winner != null ? winner : reg);
} }
@SuppressWarnings({"unchecked", "deprecation"}) @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 @Test
public void testInspectBlockTypeAndDataMethods() { public void testInspectBlockTypeAndDataMethods() {
System.out.println("=== BLOCK TYPE METHODS ==="); 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" tokio = "1"
tracing = "0.1" tracing = "0.1"
glob = "0.3.4" 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"] } rust-embed = { version = "8.12.0", features = ["interpolate-folder-path"] }
walkdir = "2" walkdir = "2"
zip = "8.6" zip = "8.6"
@ -39,7 +39,6 @@ prost = "0.14"
toml = "1.1" toml = "1.1"
[build-dependencies] [build-dependencies]
j4rs = { version = "0.25.2", artifact = "cdylib", lib = true }
env_logger = "0.11.11" env_logger = "0.11.11"
log = "0.4" log = "0.4"
prost-build = "0.14" prost-build = "0.14"

View File

@ -1,12 +1,10 @@
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
use j4rs::{JvmBuilder, MavenArtifactRepo, MavenSettings};
pub fn setup_java(base: PathBuf) { pub fn setup_java(base: PathBuf) {
let resources = base.join("resources"); 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(); java_path.pop();
let java_path = java_path.join("java"); let java_path = java_path.join("java");
@ -16,72 +14,15 @@ pub fn setup_java(base: PathBuf) {
.join("libs") .join("libs")
.join("patchbukkit.jar"); .join("patchbukkit.jar");
let jassets = resources.join("jassets"); if !patchbukkit_jar.exists() {
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() {
panic!( 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(); fs::create_dir_all(&jassets).unwrap();
let dest_patchbukkit = jassets.join("patchbukkit.jar");
let cdylib = std::env::var("CARGO_CDYLIB_FILE_J4RS").unwrap(); fs::copy(&patchbukkit_jar, &dest_patchbukkit)
let cdylib = PathBuf::from(cdylib); .map_err(|err| format!("Failed to copy patchbukkit.jar to resources/jassets: {err:?}"))
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:?}"))
.unwrap(); .unwrap();
} }

View File

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

View File

@ -25,8 +25,8 @@ pub struct JavaCommandExecutor {
#[derive(Clone)] #[derive(Clone)]
pub enum SimpleCommandSender { pub enum SimpleCommandSender {
Console, Console,
/// UUID, Name /// UUID, Name, is_op
Player(String, String), Player(String, String, bool),
} }
pub struct AnyCommandNode { pub struct AnyCommandNode {
@ -94,7 +94,8 @@ impl ArgumentConsumer for AnyCommandNode {
Box::pin(async move { Box::pin(async move {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
self.command_tx if let Err(e) = self
.command_tx
.send(JvmCommand::GetCommandTabComplete { .send(JvmCommand::GetCommandTabComplete {
command_sender, command_sender,
full_command: input.to_string(), full_command: input.to_string(),
@ -102,9 +103,15 @@ impl ArgumentConsumer for AnyCommandNode {
location, location,
}) })
.await .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 { fn from(val: &CommandSender) -> Self {
match val { match val {
CommandSender::Console | CommandSender::Rcon(_) | CommandSender::Dummy => Self::Console, 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.id.to_string(),
player.gameprofile.name.clone(), player.gameprofile.name.clone(),
), is_op,
)
}
CommandSender::CommandBlock(_block_entity, _world) => Self::Console, CommandSender::CommandBlock(_block_entity, _world) => Self::Console,
} }
} }
@ -129,6 +141,10 @@ impl CommandExecutor for JavaCommandExecutor {
_server: &'a pumpkin::server::Server, _server: &'a pumpkin::server::Server,
args: &'a pumpkin::command::args::ConsumedArgs<'a>, args: &'a pumpkin::command::args::ConsumedArgs<'a>,
) -> pumpkin::command::CommandResult<'a> { ) -> pumpkin::command::CommandResult<'a> {
if let CommandSender::Player(player) = sender {
crate::java::native_callbacks::utils::cache_player(player.clone());
}
Box::pin(async move { Box::pin(async move {
let full_command = match args.get(ARG_ANY) { let full_command = match args.get(ARG_ANY) {
Some(Arg::Msg(msg)) => { Some(Arg::Msg(msg)) => {
@ -147,15 +163,21 @@ impl CommandExecutor for JavaCommandExecutor {
} }
}; };
let (tx, _rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
self.command_tx if let Err(e) = self
.command_tx
.send(JvmCommand::TriggerCommand { .send(JvmCommand::TriggerCommand {
full_command, full_command,
respond_to: tx, respond_to: tx,
command_sender: sender.into(), command_sender: sender.into(),
}) })
.await .await
.unwrap(); {
tracing::error!("Failed to send command to JVM worker: {e}");
return Ok(0);
}
let _ = rx.await;
Ok(1) 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 base: PathBuf,
pub plugins: PathBuf, pub plugins: PathBuf,
pub plugin_updates: PathBuf, pub plugin_updates: PathBuf,
pub j4rs: PathBuf,
pub jassets: PathBuf, pub jassets: PathBuf,
} }
@ -20,8 +19,7 @@ pub fn setup_directories(server: &Context) -> Result<PatchBukkitDirectories, Str
let plugins = base.join("patchbukkit-plugins"); let plugins = base.join("patchbukkit-plugins");
let plugin_updates = plugins.join("update"); let plugin_updates = plugins.join("update");
let j4rs = base.join("j4rs"); let jassets = base.join("jassets");
let jassets = j4rs.join("jassets");
fs::create_dir_all(&jassets) fs::create_dir_all(&jassets)
.map_err(|err| format!("Failed to create jassets folder: {err:?}"))?; .map_err(|err| format!("Failed to create jassets folder: {err:?}"))?;
@ -58,7 +56,6 @@ pub fn setup_directories(server: &Context) -> Result<PatchBukkitDirectories, Str
base, base,
plugins, plugins,
plugin_updates, plugin_updates,
j4rs,
jassets, jassets,
}) })
} }

View File

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

View File

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

View File

@ -1,6 +1,6 @@
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use j4rs::{InvocationArg, Jvm, JvmBuilder}; use jni::{Env, InitArgsBuilder, JNIVersion, JavaVM};
use pumpkin::plugin::Context; use pumpkin::plugin::Context;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@ -21,7 +21,7 @@ pub struct JvmWorker {
pub plugin_manager: PluginManager, pub plugin_manager: PluginManager,
pub event_manager: EventManager, pub event_manager: EventManager,
pub command_manager: CommandManager, pub command_manager: CommandManager,
jvm: Option<j4rs::Jvm>, jvm: Option<Arc<JavaVM>>,
context: Option<Arc<Context>>, context: Option<Arc<Context>>,
} }
@ -44,21 +44,22 @@ impl JvmWorker {
while let Some(command) = self.command_rx.recv().await { while let Some(command) = self.command_rx.recv().await {
match command { match command {
JvmCommand::Initialize { JvmCommand::Initialize {
j4rs_path, jassets_path,
respond_to, respond_to,
context, context,
runtime_handle,
command_tx, command_tx,
config, config,
} => { } => {
init_callback_context( init_callback_context(
context.clone(), context.clone(),
tokio::runtime::Handle::current(), runtime_handle,
command_tx.clone(), command_tx.clone(),
config, config,
) )
.unwrap(); .unwrap();
self.context = Some(context); self.context = Some(context);
let result = self.initialize_jvm(&j4rs_path); let result = self.initialize_jvm(&jassets_path);
let _ = respond_to.send(result); let _ = respond_to.send(result);
} }
JvmCommand::LoadPlugin { JvmCommand::LoadPlugin {
@ -105,38 +106,49 @@ impl JvmWorker {
server, server,
command_tx, command_tx,
} => { } => {
let jvm = match self.jvm { let res = if let Some(ref jvm) = self.jvm {
Some(ref jvm) => jvm, let plugins_dir = plugins_dir.clone();
None => &Jvm::attach_thread().unwrap(), let server = server.clone();
}; let command_tx = command_tx.clone();
jvm.attach_current_thread(|env| -> anyhow::Result<()> {
let _ = respond_to.send( self.plugin_manager.instantiate_all_plugins(
self.plugin_manager env,
.instantiate_all_plugins(
jvm,
&plugins_dir, &plugins_dir,
&server, &server,
command_tx, command_tx,
&mut self.command_manager, &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 } => { JvmCommand::EnableAllPlugins { respond_to } => {
let jvm = match self.jvm { let res = if let Some(ref jvm) = self.jvm {
Some(ref jvm) => jvm, jvm.attach_current_thread(|env| -> anyhow::Result<()> {
None => &Jvm::attach_thread().unwrap(), 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 } => { JvmCommand::DisableAllPlugins { respond_to } => {
let jvm = match self.jvm { let res = if let Some(ref jvm) = self.jvm {
Some(ref jvm) => jvm, jvm.attach_current_thread(|env| -> anyhow::Result<()> {
None => &Jvm::attach_thread().unwrap(), 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 } => { JvmCommand::Shutdown { respond_to } => {
let _ = respond_to.send(self.plugin_manager.unload_all_plugins()); let _ = respond_to.send(self.plugin_manager.unload_all_plugins());
@ -147,14 +159,14 @@ impl JvmWorker {
plugin, plugin,
payload, payload,
} => { } => {
let jvm = match self.jvm {
Some(ref jvm) => jvm,
None => &Jvm::attach_thread().unwrap(),
};
let original_event = payload.event.clone(); let original_event = payload.event.clone();
let cancelled = match self.event_manager.fire_event(jvm, payload, plugin) { let response = if let Some(ref jvm) = self.jvm {
Ok(c) => c, match jvm.attach_current_thread(
|env| -> anyhow::Result<FireEventResponse> {
self.event_manager.fire_event(env, payload, plugin)
},
) {
Ok(resp) => resp,
Err(e) => { Err(e) => {
tracing::error!("Failed to fire event: {e}"); tracing::error!("Failed to fire event: {e}");
FireEventResponse { FireEventResponse {
@ -162,25 +174,32 @@ impl JvmWorker {
data: Some(original_event), data: Some(original_event),
} }
} }
}
} else {
FireEventResponse {
cancelled: false,
data: Some(original_event),
}
}; };
let _ = respond_to.send(cancelled); let _ = respond_to.send(response);
} }
JvmCommand::TriggerCommand { JvmCommand::TriggerCommand {
full_command, full_command,
command_sender, command_sender,
respond_to, respond_to,
} => { } => {
let jvm = match self.jvm { let res = if let Some(ref jvm) = self.jvm {
Some(ref jvm) => jvm, jvm.attach_current_thread(|env| -> anyhow::Result<()> {
None => &Jvm::attach_thread().unwrap(), 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 = let _ = respond_to.send(res);
self.command_manager
.trigger_command(jvm, full_command, command_sender);
let _ = respond_to.send(result);
} }
JvmCommand::GetCommandTabComplete { JvmCommand::GetCommandTabComplete {
command_sender, command_sender,
@ -188,19 +207,26 @@ impl JvmWorker {
respond_to, respond_to,
location, location,
} => { } => {
let jvm = match self.jvm { let res = if let Some(ref jvm) = self.jvm {
Some(ref jvm) => jvm, match jvm.attach_current_thread(|env| -> anyhow::Result<_> {
None => &Jvm::attach_thread().unwrap(), Ok(self.command_manager.get_tab_complete(
}; env,
let result = self.command_manager.get_tab_complete(
jvm,
command_sender, command_sender,
full_command, full_command,
location, 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"); tracing::info!("JVM worker thread exited");
} }
fn initialize_jvm(&mut self, j4rs_path: &PathBuf) -> anyhow::Result<()> { fn initialize_jvm(&mut self, jassets_path: &PathBuf) -> anyhow::Result<()> {
tracing::info!("Initializing JVM with path: {j4rs_path:?}"); tracing::info!("Initializing JVM with assets path: {jassets_path:?}");
let jassets = j4rs_path.join("jassets"); let mut jar_paths = Vec::new();
let mut classpath_entries = Vec::new();
if let Ok(entries) = std::fs::read_dir(&jassets) { for entry in walkdir::WalkDir::new(jassets_path)
for entry in entries.flatten() { .into_iter()
let path = entry.path(); .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()) if let Some(filename) = path.file_name().and_then(|n| n.to_str())
&& filename.ends_with(".jar") && 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 has_patchbukkit = jar_paths.iter().any(|p| {
let mut jvm_builder = jvm_builder.with_base_path(j4rs_path).java_opts(vec![ p.file_name()
j4rs::JavaOpt::new("--enable-native-access=ALL-UNNAMED"), .and_then(|n| n.to_str())
j4rs::JavaOpt::new("--enable-final-field-mutation=ALL-UNNAMED"), .is_some_and(|n| n.contains("patchbukkit"))
j4rs::JavaOpt::new("--add-opens=java.base/java.lang=ALL-UNNAMED"), });
j4rs::JavaOpt::new("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"), if !has_patchbukkit {
j4rs::JavaOpt::new("-Dcom.google.protobuf.useUnsafe=false"), tracing::warn!(
]); "patchbukkit.jar was not found in {:?}. Server classes may fail to load!",
jassets_path
for entry in classpath_entries { );
jvm_builder = jvm_builder.classpath_entry(entry);
} }
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); self.jvm = Some(jvm);
@ -251,12 +321,17 @@ impl JvmWorker {
} }
} }
pub fn setup_patchbukkit_server(jvm: &Jvm) -> anyhow::Result<()> { pub fn setup_patchbukkit_server(env: &mut Env) -> anyhow::Result<()> {
jvm.invoke_static( if let Err(e) = env.call_static_method(
"org.patchbukkit.PatchBukkitServer", jni::jni_str!("org/patchbukkit/PatchBukkitServer"),
"initServer", jni::jni_str!("initServer"),
InvocationArg::empty(), jni::jni_sig!("()Lorg/patchbukkit/PatchBukkitServer;"),
)?; &[],
) {
env.exception_describe();
env.exception_clear();
return Err(anyhow::anyhow!("Failed to setup PatchBukkit server: {e:?}"));
}
Ok(()) 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::{ use crate::proto::patchbukkit::{
abilities::{Abilities, SetAbilitiesRequest}, abilities::{Abilities, SetAbilitiesRequest},
common::Uuid, common::Uuid,
}; };
pub fn ffi_native_bridge_get_abilities_impl(request: Uuid) -> Option<Abilities> { pub fn ffi_native_bridge_get_abilities_impl(request: Uuid) -> Option<Abilities> {
let ctx = CALLBACK_CONTEXT.get()?; with_player(Some(&request), |player| {
let player_uuid = uuid::Uuid::parse_str(&request.value).ok()?; let abilities = match player.abilities.try_lock() {
let player = ctx.plugin_context.server.get_player_by_uuid(player_uuid)?; Ok(guard) => Abilities {
let abilities = tokio::task::block_in_place(|| { invulnerable: guard.invulnerable,
ctx.runtime flying: guard.flying,
.block_on(async { player.abilities.lock().await }) 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,
}); });
break;
Some(Abilities { }
invulnerable: abilities.invulnerable, }
flying: abilities.flying, acquired.unwrap_or(Abilities {
allow_flying: abilities.allow_flying, invulnerable: false,
creative: abilities.creative, flying: false,
allow_modify_world: abilities.allow_modify_world, allow_flying: false,
fly_speed: abilities.fly_speed, creative: false,
walk_speed: abilities.walk_speed, 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> { pub fn ffi_native_bridge_set_abilities_impl(request: SetAbilitiesRequest) -> Option<bool> {
let ctx = CALLBACK_CONTEXT.get()?; let ctx = CALLBACK_CONTEXT.get()?;
let player_uuid = uuid::Uuid::parse_str(&request.uuid?.value).ok()?;
let abilities = request.abilities?; let abilities = request.abilities?;
let player = ctx.plugin_context.server.get_player_by_uuid(player_uuid)?; with_player(request.uuid.as_ref(), |player| {
let player = player.clone();
let mut pumpkin_abilities = tokio::task::block_in_place(|| { ctx.runtime.spawn(async move {
ctx.runtime {
.block_on(async { player.abilities.lock().await }) let mut pumpkin_abilities = player.abilities.lock().await;
});
pumpkin_abilities.invulnerable = abilities.invulnerable; pumpkin_abilities.invulnerable = abilities.invulnerable;
pumpkin_abilities.flying = abilities.flying; pumpkin_abilities.flying = abilities.flying;
pumpkin_abilities.allow_flying = abilities.allow_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.allow_modify_world = abilities.allow_modify_world;
pumpkin_abilities.fly_speed = abilities.fly_speed; pumpkin_abilities.fly_speed = abilities.fly_speed;
pumpkin_abilities.walk_speed = abilities.walk_speed; pumpkin_abilities.walk_speed = abilities.walk_speed;
}
let player = player.clone();
ctx.runtime.spawn(async move {
player.send_abilities_update().await; player.send_abilities_update().await;
}); });
Some(true) Some(true)
})?
} }

View File

@ -3,7 +3,13 @@ use pumpkin_util::permission::{Permission, PermissionDefault};
use crate::{commands::init_java_command, proto::patchbukkit::command::RegisterCommandRequest}; use crate::{commands::init_java_command, proto::patchbukkit::command::RegisterCommandRequest};
pub fn ffi_native_bridge_register_command_impl(request: RegisterCommandRequest) -> Option<()> { 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 cmd_name = request.cmd_name;
let aliases = request.aliases; 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 command_tx = context.command_tx.clone();
let plugin_context = context.plugin_context.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 { context.runtime.spawn(async move {
let mut raw_names: Vec<String> = vec![cmd_name.clone()]; let mut raw_names: Vec<String> = vec![cmd_name.clone()];
raw_names.extend(aliases.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 clean_perm = cmd_name.trim_start_matches('/');
let permission = format!("patchbukkit:command.{clean_perm}"); let permission = format!("patchbukkit:command.{clean_perm}");
let registry = { let registry = plugin_context.server.permission_manager.clone();
let permission_manager = plugin_context.permission_manager.read().await;
permission_manager.registry.clone()
};
let _ = registry.write().await.register_permission(Permission::new( if let Err(e) = registry.register_permission(Permission::new(
&permission, &permission,
&format!("Allows running command `{cmd_name}` from `{plugin_name}`"), &format!("Allows running command `{cmd_name}` from `{plugin_name}`"),
PermissionDefault::Allow, 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(()) Some(())

View File

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

View File

@ -1,12 +1,9 @@
use std::sync::Arc; use std::sync::Arc;
use pumpkin::plugin::EventPriority; use pumpkin::plugin::EventPriority;
use pumpkin::plugin::player::player_join::PlayerJoinEvent;
use pumpkin_util::text::TextComponent;
use crate::events::handler::PatchBukkitEventHandler; use crate::events::handler::PatchBukkitEventHandler;
use crate::java::native_callbacks::CALLBACK_CONTEXT; use crate::java::native_callbacks::CALLBACK_CONTEXT;
use crate::proto::patchbukkit::events::event::Data;
use crate::proto::patchbukkit::events::{ use crate::proto::patchbukkit::events::{
CallEventRequest, CallEventResponse, RegisterEventRequest, 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 command_tx = ctx.command_tx.clone();
let context = ctx.plugin_context.clone(); let context = ctx.plugin_context.clone();
tokio::task::block_in_place(|| {
ctx.runtime.block_on(async {
match request.event_type.as_str() { match request.event_type.as_str() {
"org.bukkit.event.player.PlayerJoinEvent" => { "org.bukkit.event.player.PlayerJoinEvent" => {
context context
@ -47,8 +42,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.player.PlayerQuitEvent" => { "org.bukkit.event.player.PlayerQuitEvent" => {
context context
@ -62,8 +56,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.player.PlayerGameModeChangeEvent" => { "org.bukkit.event.player.PlayerGameModeChangeEvent" => {
context context
@ -77,8 +70,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.player.PlayerInteractEvent" => { "org.bukkit.event.player.PlayerInteractEvent" => {
context context
@ -92,8 +84,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.block.BlockBreakEvent" => { "org.bukkit.event.block.BlockBreakEvent" => {
context context
@ -107,8 +98,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.block.BlockPlaceEvent" => { "org.bukkit.event.block.BlockPlaceEvent" => {
context context
@ -122,8 +112,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.block.SignChangeEvent" => { "org.bukkit.event.block.SignChangeEvent" => {
context context
@ -137,8 +126,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.block.BlockDamageEvent" => { "org.bukkit.event.block.BlockDamageEvent" => {
context context
@ -152,8 +140,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.server.ServerCommandEvent" => { "org.bukkit.event.server.ServerCommandEvent" => {
context context
@ -167,8 +154,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.block.BlockIgniteEvent" => { "org.bukkit.event.block.BlockIgniteEvent" => {
context context
@ -182,8 +168,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.block.BlockGrowEvent" => { "org.bukkit.event.block.BlockGrowEvent" => {
context context
@ -197,8 +182,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.block.BlockFormEvent" => { "org.bukkit.event.block.BlockFormEvent" => {
context context
@ -212,8 +196,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.block.BlockFadeEvent" => { "org.bukkit.event.block.BlockFadeEvent" => {
context context
@ -227,8 +210,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.player.PlayerToggleSneakEvent" => { "org.bukkit.event.player.PlayerToggleSneakEvent" => {
context context
@ -242,8 +224,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.player.PlayerToggleSprintEvent" => { "org.bukkit.event.player.PlayerToggleSprintEvent" => {
context context
@ -257,8 +238,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.player.PlayerToggleFlightEvent" => { "org.bukkit.event.player.PlayerToggleFlightEvent" => {
context context
@ -272,8 +252,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.player.PlayerMoveEvent" => { "org.bukkit.event.player.PlayerMoveEvent" => {
context context
@ -287,10 +266,12 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, 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 context
.register_event::< .register_event::<
pumpkin::plugin::player::player_interact_entity_event::PlayerInteractEntityEvent, 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, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
"org.bukkit.event.player.AsyncPlayerChatEvent" | "org.bukkit.event.player.PlayerChatEvent" => { "org.bukkit.event.player.AsyncPlayerChatEvent"
| "org.bukkit.event.player.PlayerChatEvent" => {
context context
.register_event::< .register_event::<
pumpkin::plugin::player::player_chat::PlayerChatEvent, pumpkin::plugin::player::player_chat::PlayerChatEvent,
@ -317,8 +298,7 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
)), )),
pumpkin_priority, pumpkin_priority,
request.blocking, request.blocking,
) );
.await;
} }
// Player Events // Player Events
"org.bukkit.event.player.PlayerDropItemEvent" "org.bukkit.event.player.PlayerDropItemEvent"
@ -429,383 +409,15 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
_ => { _ => {
tracing::warn!( tracing::warn!(
"Unsupported Bukkit event type '{}' from plugin '{}'", "Unsupported Bukkit event type '{}' from plugin '{}'",
request.event_type, request.plugin_name request.event_type,
request.plugin_name
); );
} }
} }
});
});
Some(()) Some(())
} }
pub fn ffi_native_bridge_call_event_impl(request: CallEventRequest) -> Option<CallEventResponse> { pub fn ffi_native_bridge_call_event_impl(_request: CallEventRequest) -> Option<CallEventResponse> {
let ctx = CALLBACK_CONTEXT.get()?; Some(CallEventResponse { handled: false })
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 })
} }

View File

@ -1,7 +1,6 @@
use pumpkin_data::data_component_impl::EquipmentSlot; use pumpkin_data::data_component_impl::EquipmentSlot;
use pumpkin_data::item::Item; use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack as PumpkinItemStack; use pumpkin_data::item_stack::ItemStack as PumpkinItemStack;
use tokio::task::block_in_place;
use crate::{ use crate::{
java::native_callbacks::{CALLBACK_CONTEXT, utils::with_player}, 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( pub fn ffi_native_bridge_get_player_inventory_impl(
request: Uuid, request: Uuid,
) -> Option<GetPlayerInventoryResponse> { ) -> Option<GetPlayerInventoryResponse> {
let ctx = CALLBACK_CONTEXT.get()?;
with_player(Some(&request), |player| { 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 selected_slot = u32::from(player.inventory.get_selected_slot());
let main_guard = player.inventory.main_inventory.read().await; let main_inventory = player
let main_inventory = main_guard.iter().map(pumpkin_item_to_proto).collect(); .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, helmet, chestplate, leggings, boots) =
let off_hand = pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::OFF_HAND)); if let Ok(eq_guard) = player.inventory.entity_equipment.try_lock() {
let helmet = pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::HEAD)); (
let chestplate = pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::CHEST)); pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::OFF_HAND)),
let leggings = pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::LEGS)); pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::HEAD)),
let boots = pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::FEET)); 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 { GetPlayerInventoryResponse {
main_inventory, main_inventory,
@ -82,8 +93,6 @@ pub fn ffi_native_bridge_get_player_inventory_impl(
boots: Some(boots), boots: Some(boots),
} }
}) })
})
})
} }
pub fn ffi_native_bridge_set_player_inventory_slot_impl( 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 logger = &request.logger_name;
let msg = &request.message; 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() { match request.level() {
LogLevel::Severe => tracing::error!(logger = %logger, "{}", msg), LogLevel::Severe => tracing::error!(logger = %logger, "{}", msg),
LogLevel::Warning => tracing::warn!(logger = %logger, "{}", msg), LogLevel::Warning => tracing::warn!(logger = %logger, "{}", msg),
LogLevel::Info => tracing::info!(logger = %logger, "{}", msg), LogLevel::Info => tracing::info!(logger = %logger, "{}", msg),
LogLevel::Config => tracing::debug!(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::Finer => tracing::trace!(logger = %logger, "{}", msg),
LogLevel::Finest => tracing::trace!(logger = %logger, "{}", msg), LogLevel::Finest => tracing::trace!(logger = %logger, "{}", msg),
LogLevel::Off => {} LogLevel::Off => {}

View File

@ -1,7 +1,6 @@
use std::sync::{Arc, OnceLock}; use std::sync::{Arc, OnceLock};
use anyhow::Result; use anyhow::Result;
use j4rs::Jvm;
use pumpkin::plugin::Context; use pumpkin::plugin::Context;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@ -76,6 +75,6 @@ pub fn init_callback_context(
Ok(()) Ok(())
} }
pub fn initialize_callbacks(jvm: &Jvm) -> Result<()> { pub fn initialize_callbacks(env: &mut jni::Env) -> Result<()> {
initialize_ffi_callbacks(jvm) 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(); let entity = entity.first()?.get_entity();
player player
.client .send_client_packet(&CEntitySoundEffect::new(
.enqueue_packet(&CEntitySoundEffect::new(
IdOr::Id(pumpkin_sound as u16), IdOr::Id(pumpkin_sound as u16),
category, category,
entity.entity_id.into(), 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() } 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> pub fn with_player<F, R>(proto_uuid: Option<&ProtoUuid>, f: F) -> Option<R>
where where
F: FnOnce(Arc<Player>) -> R, F: FnOnce(Arc<Player>) -> R,

View File

@ -100,7 +100,7 @@ pub fn ffi_native_bridge_get_world_border_impl(
.cloned() .cloned()
.or_else(|| worlds.first().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 { Some(crate::proto::patchbukkit::world::WorldBorderData {
center_x: wb.center_x, center_x: wb.center_x,
@ -131,7 +131,8 @@ pub fn ffi_native_bridge_set_world_border_impl(
.cloned() .cloned()
.or_else(|| worlds.first().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_x = border_data.center_x;
wb.center_z = border_data.center_z; wb.center_z = border_data.center_z;
wb.old_diameter = border_data.size; 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.damage_per_block = border_data.damage_per_block as f32;
wb.buffer = border_data.damage_buffer as f32; wb.buffer = border_data.damage_buffer as f32;
wb.portal_teleport_boundary = border_data.max_center_coordinate; wb.portal_teleport_boundary = border_data.max_center_coordinate;
});
Some(()) Some(())
} }

View File

@ -1,26 +1,11 @@
use std::sync::{Arc, Mutex};
use anyhow::Result; use anyhow::Result;
use j4rs::{Instance, InvocationArg, Jvm}; use jni::Env;
use pumpkin::{command::dispatcher::CommandError, plugin::Context}; use pumpkin::command::dispatcher::CommandError;
use pumpkin_protocol::java::client::play::CommandSuggestion; use pumpkin_protocol::java::client::play::CommandSuggestion;
use pumpkin_util::permission::{Permission, PermissionDefault};
use tokio::sync::mpsc;
use crate::{ use crate::{commands::SimpleCommandSender, java::jvm::commands::Location};
commands::{SimpleCommandSender, init_java_command},
config,
java::{
jvm::commands::{JvmCommand, Location},
plugin::manager::Plugin,
},
};
const PATCHBUKKIT_PERMISSION_NAMESPACE: &str = "patchbukkit"; pub struct CommandManager;
pub struct CommandManager {
command_map: Option<Instance>,
}
impl Default for CommandManager { impl Default for CommandManager {
fn default() -> Self { fn default() -> Self {
@ -31,39 +16,17 @@ impl Default for CommandManager {
impl CommandManager { impl CommandManager {
#[must_use] #[must_use]
pub const fn new() -> Self { pub const fn new() -> Self {
Self { command_map: None } Self
}
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())
} }
pub fn get_tab_complete( pub fn get_tab_complete(
&mut self, &mut self,
jvm: &Jvm, env: &mut Env,
sender: SimpleCommandSender, sender: SimpleCommandSender,
full_command: String, full_command: String,
location: Option<Location>, location: Option<Location>,
) -> Result<Option<Vec<CommandSuggestion>>, CommandError> { ) -> 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), Ok(suggestions) => Ok(suggestions),
Err(e) => { Err(e) => {
tracing::warn!("Tab completion failed: {e}"); tracing::warn!("Tab completion failed: {e}");
@ -74,271 +37,106 @@ impl CommandManager {
fn try_tab_complete( fn try_tab_complete(
&mut self, &mut self,
jvm: &Jvm, env: &mut Env,
sender: SimpleCommandSender, sender: SimpleCommandSender,
full_command: String, full_command: String,
location: Option<Location>, location: Option<Location>,
) -> Result<Option<Vec<CommandSuggestion>>> { ) -> Result<Option<Vec<CommandSuggestion>>> {
let command_map = self.get_or_init_command_map(jvm)?; let (uuid_jstr, name_jstr, is_op) = match sender {
SimpleCommandSender::Console => (None, None, true),
let sender = Self::sender_to_jsender(jvm, sender)?; SimpleCommandSender::Player(uuid_str, name, is_op) => (
Some(env.new_string(&uuid_str)?),
let completions = if let Some(location) = location { Some(env.new_string(&name)?),
let world = jvm.invoke_static( is_op,
"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
), ),
permission_default, };
)) {
if e.contains("already registered") { let cmd_jstr = env.new_string(&full_command)?;
tracing::debug!( let (world_jstr, x, y, z) = match location {
"Permission already registered for command {}: {}", Some(loc) => (
cmd_name, Some(env.new_string(loc.world.to_string())?),
e loc.x,
); loc.y,
} else { loc.z,
tracing::warn!( ),
"Failed to register permission for command {}: {:?}", None => (None, 0.0, 0.0, 0.0),
cmd_name, };
e
); 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(Some(suggestions))
Ok(())
} }
pub fn trigger_command( pub fn trigger_command(
&mut self, &mut self,
jvm: &Jvm, env: &mut Env,
full_command: String, full_command: String,
sender: SimpleCommandSender, sender: SimpleCommandSender,
) -> Result<()> { ) -> 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( let result = env.call_static_method(
command_map, jni::jni_str!("org/patchbukkit/command/PatchBukkitCommandMap"),
"dispatch", jni::jni_str!("dispatchRaw"),
jni::jni_sig!("(Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;)Z"),
&[ &[
InvocationArg::from(j_sender), uuid_val,
InvocationArg::try_from(full_command.clone())?, name_val,
jni::objects::JValue::Bool(is_op),
(&cmd_jstr).into(),
], ],
); );
match dispatch_result { match result {
Ok(result) => { Ok(val) => {
let handled: bool = jvm.to_rust(result)?; let handled = val.z()?;
if !handled { if !handled {
tracing::warn!("Command not handled by any Java plugin: {full_command}"); tracing::warn!("Command not handled by any Java plugin: {full_command}");
} }
@ -350,73 +148,16 @@ impl CommandManager {
Ok(()) 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( fn build_permission_node(
cmd_name: &str, cmd_name: &str,
cmd_data: &config::spigot::Command, cmd_data: &crate::config::spigot::Command,
) -> (String, PermissionDefault) { ) -> (String, pumpkin_util::permission::PermissionDefault) {
let clean = cmd_name.trim_start_matches('/'); let clean = cmd_name.trim_start_matches('/');
if let Some(permission) = &cmd_data.permission { if let Some(permission) = &cmd_data.permission {
let perm_node = if permission.contains(':') { let perm_node = if permission.contains(':') {
@ -424,11 +165,14 @@ fn build_permission_node(
} else { } else {
format!("{PATCHBUKKIT_PERMISSION_NAMESPACE}:{permission}") format!("{PATCHBUKKIT_PERMISSION_NAMESPACE}:{permission}")
}; };
(perm_node, PermissionDefault::Allow) (
perm_node,
pumpkin_util::permission::PermissionDefault::Allow,
)
} else { } else {
( (
format!("{PATCHBUKKIT_PERMISSION_NAMESPACE}:command.{clean}"), format!("{PATCHBUKKIT_PERMISSION_NAMESPACE}:command.{clean}"),
PermissionDefault::Allow, pumpkin_util::permission::PermissionDefault::Allow,
) )
} }
} }

View File

@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use j4rs::{Instance, InvocationArg, Jvm}; use jni::Env;
use prost::Message; use prost::Message;
use pumpkin::{entity::player::Player, server::Server}; use pumpkin::{entity::player::Player, server::Server};
@ -10,7 +10,7 @@ use crate::{
proto::patchbukkit::events::{FireEventResponse, event::Data}, proto::patchbukkit::events::{FireEventResponse, event::Data},
}; };
pub struct EventManager {} pub struct EventManager;
impl Default for EventManager { impl Default for EventManager {
fn default() -> Self { fn default() -> Self {
@ -21,144 +21,74 @@ impl Default for EventManager {
impl EventManager { impl EventManager {
#[must_use] #[must_use]
pub const fn new() -> Self { pub const fn new() -> Self {
Self {} Self
} }
pub fn fire_event( pub fn fire_event(
&self, &self,
jvm: &Jvm, env: &mut Env,
payload: JvmEventPayload, payload: JvmEventPayload,
plugin_name: String, plugin_name: String,
) -> Result<FireEventResponse> { ) -> 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 if let Some(ref event) = payload.event.data
&& matches!(event, Data::PlayerJoin(_)) && matches!(event, Data::PlayerJoin(_))
&& let Some(ref player) = payload.context.player && 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 bytes = payload.event.encode_to_vec();
let j_bytes = jvm.create_java_array( let j_bytes = env.byte_array_from_slice(&bytes)?;
"byte", let plugin_name_jstr = env.new_string(&plugin_name)?;
&bytes
.iter() let result = env.call_static_method(
.map(|&b| { jni::jni_str!("org/patchbukkit/events/PatchBukkitEventFactory"),
InvocationArg::try_from(b as i8) jni::jni_str!("fireEventFromBytes"),
.unwrap() jni::jni_sig!("([BLjava/lang/String;)[B"),
.into_primitive() &[(&j_bytes).into(), (&plugin_name_jstr).into()],
.unwrap()
})
.collect::<Vec<_>>(),
)?; )?;
let j_event = jvm.invoke_static( let obj = result.l()?;
"org.patchbukkit.events.PatchBukkitEventFactory", if obj.is_null() {
"createEventFromBytes", return Ok(FireEventResponse {
&[InvocationArg::from(j_bytes)], cancelled: false,
)?; data: Some(payload.event),
});
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"
));
} }
jvm.invoke( let j_byte_array = jni::objects::JByteArray::cast_local(env, obj)?;
&event_manager, let resp_vec = env.convert_byte_array(&j_byte_array)?;
"fireEvent", let response = FireEventResponse::decode(resp_vec.as_slice())?;
&[
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())?;
Ok(response) Ok(response)
} }
pub fn register_player( pub fn register_player(
jvm: &Jvm, env: &mut Env,
patch_server: &Instance,
player: &Arc<Player>, player: &Arc<Player>,
_server: &Arc<Server>, _server: &Arc<Server>,
) -> Result<()> { ) -> Result<()> {
let j_uuid = jvm let player_permission_level = player.permission_lvl.load();
.invoke_static( let is_op = player_permission_level > pumpkin_util::permission::PermissionLvl::Zero
"java.util.UUID", || _server
"fromString", .data
&[InvocationArg::try_from(player.gameprofile.id.to_string())?], .operator_config
) .try_read()
.map_err(|e| format!("Failed to create Java UUID: {e}")) .is_ok_and(|ops| ops.get_entry(&player.gameprofile.id).is_some());
.unwrap();
let j_player = jvm.create_instance( let uuid_jstr = env.new_string(player.gameprofile.id.to_string())?;
"org.patchbukkit.entity.PatchBukkitPlayer", 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), (&uuid_jstr).into(),
InvocationArg::try_from(player.gameprofile.name.clone())?, (&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(()) Ok(())
} }
} }

View File

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

View File

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

View File

@ -13,17 +13,13 @@ pub mod java;
pub mod proto; pub mod proto;
use directories::setup_directories; use directories::setup_directories;
use java::jar::discover_jar_files; use tokio::sync::{mpsc, oneshot};
use tokio::sync::{
mpsc::{self, Receiver},
oneshot,
};
use crate::{ use crate::{
config::patchbukkit::PatchBukkitConfig, config::patchbukkit::PatchBukkitConfig,
java::{ java::{
jvm::{commands::JvmCommand, worker::JvmWorker}, 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}"))?; .map_err(|e| format!("Failed to setup PatchBukkit config: {e}"))?;
// Manage embedded resources // 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(); let (tx, rx) = oneshot::channel();
plugin if let Err(e) = command_tx
.command_tx
.send(JvmCommand::Initialize { .send(JvmCommand::Initialize {
j4rs_path: dirs.j4rs, jassets_path: dirs.jassets.clone(),
respond_to: tx, respond_to: tx,
context: server.clone(), context: server_clone.clone(),
command_tx: plugin.command_tx.clone(), runtime_handle,
command_tx: command_tx.clone(),
config, config,
}) })
.await .await
.map_err(|e| format!("Failed to send command to initialize J4RS: {e}"))?; {
rx.await tracing::error!("Failed to send command to initialize JVM: {e}");
.map_err(|e| format!("Unable to receive response from J4RS initialization: {e}"))? return;
.map_err(|e| format!("Failed to initialize all plugins: {e}"))?;
} }
{ 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(); let (tx, rx) = oneshot::channel();
plugin if let Err(e) = command_tx
.command_tx
.send(JvmCommand::InstantiateAllPlugins { .send(JvmCommand::InstantiateAllPlugins {
plugins_dir: dirs.plugins.clone(), plugins_dir: dirs.plugins.clone(),
respond_to: tx, respond_to: tx,
server: server.clone(), server: server_clone.clone(),
command_tx: plugin.command_tx.clone(), command_tx: command_tx.clone(),
}) })
.await .await
.map_err(|e| format!("Failed to send command to instantiate plugins: {e}"))?; {
rx.await tracing::error!("Failed to send command to instantiate plugins: {e}");
.map_err(|e| format!("Unable to receive response from instantiate plugins: {e}"))? return;
.map_err(|e| format!("Failed to instantiate all plugins: {e}"))?;
} }
{ 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(); let (tx, rx) = oneshot::channel();
plugin if let Err(e) = command_tx
.command_tx
.send(JvmCommand::EnableAllPlugins { respond_to: tx }) .send(JvmCommand::EnableAllPlugins { respond_to: tx })
.await .await
.map_err(|e| format!("Failed to send command to enable all plugins: {e}"))?; {
rx.await tracing::error!("Failed to send command to enable all plugins: {e}");
.map_err(|e| format!("Unable to receive response from enable all plugins: {e}"))? return;
.map_err(|e| format!("Failed to enable all plugins: {e}"))?; }
};
// Register Bukkit plugin loader with Pumpkin so Bukkit/Paper plugins appear in Pumpkin's native /plugins command match rx.await {
let bukkit_loader = Arc::new(crate::java::plugin::loader::BukkitPluginLoader); Ok(Ok(())) => tracing::info!("PatchBukkit background initialization complete"),
let rt = tokio::runtime::Builder::new_current_thread() Ok(Err(e)) => tracing::error!("Failed to enable all plugins: {e}"),
.enable_all() Err(e) => tracing::error!("Unable to receive response from enable all plugins: {e}"),
.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;
} }
}); });
tracing::info!("PatchBukkit loaded successfully");
Ok(()) Ok(())
} }
@ -155,25 +165,36 @@ async fn on_unload(&self, server: Arc<Context>) -> Result<(), String> {
#[plugin_impl] #[plugin_impl]
pub struct PatchBukkitPlugin { pub struct PatchBukkitPlugin {
pub command_tx: mpsc::Sender<JvmCommand>, pub command_tx: mpsc::Sender<JvmCommand>,
pub runtime: Arc<tokio::runtime::Runtime>,
} }
impl PatchBukkitPlugin { impl PatchBukkitPlugin {
#[must_use] #[must_use]
pub fn new() -> Self { 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] let (tx, rx) = mpsc::channel(4096);
pub async fn jvm_thread_task(rx: Receiver<JvmCommand>) {
JvmWorker::new(rx).attach_thread().await;
}
let runtime_clone = runtime.clone();
std::thread::Builder::new() std::thread::Builder::new()
.name("patchbukkit-jvm-worker".to_string()) .name("patchbukkit-jvm-worker".to_string())
.spawn(move || { .spawn(move || {
jvm_thread_task(rx); runtime_clone.block_on(async move {
JvmWorker::new(rx).attach_thread().await;
});
}) })
.unwrap(); .unwrap();
Self { command_tx: tx } Self {
command_tx: tx,
runtime,
}
} }
} }