feat: more config & testing stuff
This commit is contained in:
parent
1e8e203281
commit
0ec9711408
84
.gitignore
vendored
84
.gitignore
vendored
@ -1,13 +1,20 @@
|
||||
# Rust
|
||||
/rust/target/
|
||||
/target/
|
||||
**/target/
|
||||
**/*.rs.bk
|
||||
*.pdb
|
||||
|
||||
# Rust resources
|
||||
rust/resources/jassets/*
|
||||
!rust/resources/jassets/.gitkeep
|
||||
|
||||
# Gradle cache
|
||||
.gradle/
|
||||
java/.gradle/
|
||||
**/.gradle/
|
||||
.gradletasknamecache
|
||||
gradle-app.setting
|
||||
|
||||
# Build outputs
|
||||
build/
|
||||
@ -19,22 +26,72 @@ java/protoc-gen-ffi/bin/
|
||||
java/patchbukkit-test-plugin/build/
|
||||
java/patchbukkit-test-plugin/bin/
|
||||
!rust/build/
|
||||
!rust/build/**
|
||||
|
||||
# Examples
|
||||
examples/*/bin
|
||||
examples/*/run
|
||||
# Java compiled classes and JAR packages
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
# Keep Gradle wrapper
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!java/gradle/wrapper/gradle-wrapper.jar
|
||||
!gradle/wrapper/gradle-wrapper.properties
|
||||
!java/gradle/wrapper/gradle-wrapper.properties
|
||||
|
||||
# Never ignore source build dirs if any exist
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
# JVM crash logs
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
# Logs and Crash Reports
|
||||
*.log
|
||||
*.log.gz
|
||||
logs/
|
||||
**/logs/
|
||||
crash-reports/
|
||||
|
||||
# Runtime and Test directories / artifacts
|
||||
patchbukkit/
|
||||
patchbukkit-plugins/
|
||||
patchbukkit-libs/
|
||||
run/
|
||||
**/run/
|
||||
world/
|
||||
world_nether/
|
||||
world_the_end/
|
||||
*.local.nbt
|
||||
|
||||
# Vanilla / Server runtime generated files
|
||||
server.properties
|
||||
eula.txt
|
||||
ops.json
|
||||
whitelist.json
|
||||
banned-ips.json
|
||||
banned-players.json
|
||||
usercache.json
|
||||
version_history.json
|
||||
.console_history
|
||||
|
||||
# Runtime configs
|
||||
config/
|
||||
java/patchbukkit/config/
|
||||
|
||||
# Examples
|
||||
examples/*/bin
|
||||
examples/*/run
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea/
|
||||
java/.idea/
|
||||
**/.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
|
||||
# Eclipse
|
||||
.classpath
|
||||
@ -54,11 +111,20 @@ java/.idea/
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
**/.vscode/
|
||||
*.code-workspace
|
||||
|
||||
# OS and Environment
|
||||
.DS_Store
|
||||
|
||||
# Runtime configs
|
||||
config/
|
||||
java/patchbukkit/config/
|
||||
logs/
|
||||
java/patchbukkit/logs/
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
._*
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
.direnv/
|
||||
.env
|
||||
.env.*
|
||||
!*.example
|
||||
|
||||
@ -27,3 +27,5 @@ tasks.withType<JavaCompile> {
|
||||
"-nowarn"
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -23,10 +23,10 @@ public final class EntityTests {
|
||||
}
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getEntity(UUID) stub", category = TestCategory.ENTITY,
|
||||
expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
@ConformanceTest(name = "Server.getEntity(UUID) returns null for random UUID", category = TestCategory.ENTITY)
|
||||
public void testGetEntity() {
|
||||
Bukkit.getServer().getEntity(UUID.randomUUID());
|
||||
var entity = Bukkit.getServer().getEntity(UUID.randomUUID());
|
||||
assertTrue(entity == null, "Server.getEntity(randomUUID) should be null");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Player flight state and speed getters run cleanly", category = TestCategory.ENTITY)
|
||||
|
||||
@ -33,14 +33,10 @@ public final class RegistryTests {
|
||||
assertNotNull(stream, "Registry.SOUNDS.stream()");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getRegistry(Sound.class) returns registry", category = TestCategory.REGISTRY,
|
||||
expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
@ConformanceTest(name = "Server.getRegistry(Sound.class) returns registry", category = TestCategory.REGISTRY)
|
||||
public void testServerGetRegistry() {
|
||||
// Server.getRegistry() returns null in PatchBukkit — might throw or return null
|
||||
Registry<Sound> reg = Bukkit.getServer().getRegistry(Sound.class);
|
||||
if (reg == null) {
|
||||
throw new UnsupportedOperationException("Server.getRegistry() returned null (not implemented)");
|
||||
}
|
||||
assertNotNull(reg, "Server.getRegistry(Sound.class)");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -102,4 +102,106 @@ public final class ServerTests {
|
||||
assertTrue(cmd == null, "Server.getPluginCommand(unknown) should be null");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getWorlds() returns non-empty list", category = TestCategory.SERVER)
|
||||
public void testGetWorlds() {
|
||||
var worlds = Bukkit.getServer().getWorlds();
|
||||
assertNotNull(worlds, "Server.getWorlds()");
|
||||
assertTrue(!worlds.isEmpty(), "Server.getWorlds() should not be empty");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getMaxPlayers() returns positive int", category = TestCategory.SERVER)
|
||||
public void testGetMaxPlayers() {
|
||||
int max = Bukkit.getServer().getMaxPlayers();
|
||||
assertTrue(max >= 0, "Server.getMaxPlayers() >= 0");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getPort() returns port", category = TestCategory.SERVER)
|
||||
public void testGetPort() {
|
||||
int port = Bukkit.getServer().getPort();
|
||||
assertTrue(port >= 0, "Server.getPort() >= 0");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getIp() returns string", category = TestCategory.SERVER)
|
||||
public void testGetIp() {
|
||||
String ip = Bukkit.getServer().getIp();
|
||||
assertNotNull(ip, "Server.getIp()");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getViewDistance() returns positive int", category = TestCategory.SERVER)
|
||||
public void testGetViewDistance() {
|
||||
int vd = Bukkit.getServer().getViewDistance();
|
||||
assertTrue(vd > 0, "Server.getViewDistance() > 0");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getSimulationDistance() returns positive int", category = TestCategory.SERVER)
|
||||
public void testGetSimulationDistance() {
|
||||
int sd = Bukkit.getServer().getSimulationDistance();
|
||||
assertTrue(sd > 0, "Server.getSimulationDistance() > 0");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getUpdateFolder() returns string", category = TestCategory.SERVER)
|
||||
public void testGetUpdateFolder() {
|
||||
String uf = Bukkit.getServer().getUpdateFolder();
|
||||
assertNotNull(uf, "Server.getUpdateFolder()");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getUpdateFolderFile() returns non-null", category = TestCategory.SERVER)
|
||||
public void testGetUpdateFolderFile() {
|
||||
java.io.File file = Bukkit.getServer().getUpdateFolderFile();
|
||||
assertNotNull(file, "Server.getUpdateFolderFile()");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getConnectionThrottle() returns long", category = TestCategory.SERVER)
|
||||
public void testGetConnectionThrottle() {
|
||||
long throttle = Bukkit.getServer().getConnectionThrottle();
|
||||
assertTrue(throttle >= 0, "Server.getConnectionThrottle() >= 0");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.broadcastMessage() works without throwing", category = TestCategory.SERVER)
|
||||
@SuppressWarnings("deprecation")
|
||||
public void testBroadcastMessage() {
|
||||
int reached = Bukkit.getServer().broadcastMessage("test conformance broadcast");
|
||||
assertTrue(reached >= 0, "broadcastMessage return >= 0");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getOfflinePlayer(UUID) returns non-null", category = TestCategory.SERVER)
|
||||
public void testGetOfflinePlayer() {
|
||||
var op = Bukkit.getServer().getOfflinePlayer(java.util.UUID.randomUUID());
|
||||
assertNotNull(op, "Server.getOfflinePlayer(UUID)");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getBanList() returns non-null", category = TestCategory.SERVER)
|
||||
public void testGetBanList() {
|
||||
var bans = Bukkit.getServer().getBanList(org.bukkit.BanList.Type.NAME);
|
||||
assertNotNull(bans, "Server.getBanList(NAME)");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getOperators() returns non-null set", category = TestCategory.SERVER)
|
||||
public void testGetOperators() {
|
||||
var ops = Bukkit.getServer().getOperators();
|
||||
assertNotNull(ops, "Server.getOperators()");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getWhitelistedPlayers() returns non-null set", category = TestCategory.SERVER)
|
||||
public void testGetWhitelistedPlayers() {
|
||||
var wl = Bukkit.getServer().getWhitelistedPlayers();
|
||||
assertNotNull(wl, "Server.getWhitelistedPlayers()");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.reloadWhitelist() executes without throwing", category = TestCategory.SERVER)
|
||||
public void testReloadWhitelist() {
|
||||
Bukkit.getServer().reloadWhitelist();
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getMotd() returns non-null", category = TestCategory.SERVER)
|
||||
public void testGetMotd() {
|
||||
String motd = Bukkit.getServer().getMotd();
|
||||
assertNotNull(motd, "Server.getMotd()");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getAllowNether() returns boolean", category = TestCategory.SERVER)
|
||||
public void testGetAllowNether() {
|
||||
boolean nether = Bukkit.getServer().getAllowNether();
|
||||
assertTrue(nether || !nether, "Server.getAllowNether()");
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,114 +10,7 @@ import java.util.UUID;
|
||||
|
||||
public final class StubTests {
|
||||
|
||||
@ConformanceTest(name = "Server.getWorlds() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testGetWorlds() {
|
||||
Bukkit.getServer().getWorlds();
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getMaxPlayers() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testGetMaxPlayers() {
|
||||
Bukkit.getServer().getMaxPlayers();
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getPort() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testGetPort() {
|
||||
Bukkit.getServer().getPort();
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getIp() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testGetIp() {
|
||||
Bukkit.getServer().getIp();
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getViewDistance() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testGetViewDistance() {
|
||||
Bukkit.getServer().getViewDistance();
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getSimulationDistance() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testGetSimulationDistance() {
|
||||
Bukkit.getServer().getSimulationDistance();
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getUpdateFolder() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testGetUpdateFolder() {
|
||||
Bukkit.getServer().getUpdateFolder();
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getUpdateFolderFile() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testGetUpdateFolderFile() {
|
||||
Bukkit.getServer().getUpdateFolderFile();
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getConnectionThrottle() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testGetConnectionThrottle() {
|
||||
Bukkit.getServer().getConnectionThrottle();
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.broadcastMessage() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
@SuppressWarnings("deprecation")
|
||||
public void testBroadcastMessage() {
|
||||
Bukkit.getServer().broadcastMessage("test");
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getOfflinePlayer(UUID) throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testGetOfflinePlayer() {
|
||||
Bukkit.getServer().getOfflinePlayer(UUID.randomUUID());
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getBanList() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testGetBanList() {
|
||||
Bukkit.getServer().getBanList(org.bukkit.BanList.Type.NAME);
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getOperators() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testGetOperators() {
|
||||
Bukkit.getServer().getOperators();
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getWhitelistedPlayers() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testGetWhitelistedPlayers() {
|
||||
Bukkit.getServer().getWhitelistedPlayers();
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.reloadWhitelist() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testReloadWhitelist() {
|
||||
Bukkit.getServer().reloadWhitelist();
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.shutdown() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testShutdown() {
|
||||
Bukkit.getServer().shutdown();
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getMotd() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testGetMotd() {
|
||||
Bukkit.getServer().getMotd();
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "Server.getAllowNether() throws UnsupportedOperationException",
|
||||
category = TestCategory.STUBS, expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
||||
public void testGetAllowNether() {
|
||||
Bukkit.getServer().getAllowNether();
|
||||
}
|
||||
|
||||
@ConformanceTest(name = "PaperLib detection classes and PaperLib.isPaper() are available", category = TestCategory.STUBS)
|
||||
public void testPaperLibDetection() throws ClassNotFoundException {
|
||||
|
||||
@ -1 +0,0 @@
|
||||
[19:04:38] [Test worker/INFO]: [MoonriseCommon] Paper is using 8 worker threads, 1 I/O threads
|
||||
@ -22,6 +22,9 @@ import org.eclipse.aether.transport.http.HttpTransporterFactory;
|
||||
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
|
||||
|
||||
public final class LibraryResolver {
|
||||
private static final java.util.logging.Logger LOGGER =
|
||||
java.util.logging.Logger.getLogger("LibraryResolver");
|
||||
|
||||
private LibraryResolver() {}
|
||||
|
||||
public static List<File> resolveLibraries(
|
||||
@ -32,6 +35,14 @@ public final class LibraryResolver {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
try {
|
||||
var config = patchbukkit.bridge.NativeBridgeFfi.getPatchBukkitConfig(patchbukkit.common.EmptyRequest.newBuilder().build());
|
||||
if (config != null && !config.getDownloadDependencies()) {
|
||||
LOGGER.info("[PatchBukkit] Library downloading is disabled in configuration, skipping resolution.");
|
||||
return List.of();
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
|
||||
Set<String> unique = new LinkedHashSet<>();
|
||||
for (String line : coordinates.split("\n")) {
|
||||
String trimmed = line.trim();
|
||||
|
||||
@ -28,6 +28,7 @@ import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
@ -727,21 +728,70 @@ public class PatchBukkitServer implements Server {
|
||||
rootConfig.removeAppender("ServerGuiConsole");
|
||||
rootConfig.removeAppender("Async");
|
||||
|
||||
rootConfig.addAppender(appender, org.apache.logging.log4j.Level.ALL, null);
|
||||
rootConfig.setLevel(org.apache.logging.log4j.Level.ALL);
|
||||
org.apache.logging.log4j.Level targetLevel = getEffectiveLog4jLevel();
|
||||
rootConfig.addAppender(appender, targetLevel, null);
|
||||
rootConfig.setLevel(targetLevel);
|
||||
context.updateLoggers();
|
||||
} catch (Throwable t) {
|
||||
ORIGINAL_ERR.println("[PatchBukkit] Failed to initialize Log4j native bridge appender: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static org.apache.logging.log4j.Level getEffectiveLog4jLevel() {
|
||||
try {
|
||||
var cfg = NativeBridgeFfi.getPatchBukkitConfig(patchbukkit.common.EmptyRequest.newBuilder().build());
|
||||
if (cfg != null) {
|
||||
if (cfg.getDebugBridge()) {
|
||||
return org.apache.logging.log4j.Level.ALL;
|
||||
}
|
||||
String level = cfg.getLogLevel();
|
||||
if (level != null && !level.isBlank()) {
|
||||
return switch (level.toLowerCase(Locale.ENGLISH).trim()) {
|
||||
case "trace" -> org.apache.logging.log4j.Level.TRACE;
|
||||
case "debug" -> org.apache.logging.log4j.Level.DEBUG;
|
||||
case "warn", "warning" -> org.apache.logging.log4j.Level.WARN;
|
||||
case "error", "severe" -> org.apache.logging.log4j.Level.ERROR;
|
||||
case "off" -> org.apache.logging.log4j.Level.OFF;
|
||||
case "all" -> org.apache.logging.log4j.Level.ALL;
|
||||
default -> org.apache.logging.log4j.Level.INFO;
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
return org.apache.logging.log4j.Level.INFO;
|
||||
}
|
||||
|
||||
public static Level getEffectiveJulLevel() {
|
||||
try {
|
||||
var cfg = NativeBridgeFfi.getPatchBukkitConfig(patchbukkit.common.EmptyRequest.newBuilder().build());
|
||||
if (cfg != null) {
|
||||
if (cfg.getDebugBridge()) {
|
||||
return Level.ALL;
|
||||
}
|
||||
String level = cfg.getLogLevel();
|
||||
if (level != null && !level.isBlank()) {
|
||||
return switch (level.toLowerCase(Locale.ENGLISH).trim()) {
|
||||
case "trace" -> Level.FINEST;
|
||||
case "debug" -> Level.FINE;
|
||||
case "warn", "warning" -> Level.WARNING;
|
||||
case "error", "severe" -> Level.SEVERE;
|
||||
case "off" -> Level.OFF;
|
||||
case "all" -> Level.ALL;
|
||||
default -> Level.INFO;
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
return Level.INFO;
|
||||
}
|
||||
|
||||
private static void configureJulLogger() {
|
||||
Logger root = Logger.getLogger("");
|
||||
root.setUseParentHandlers(false);
|
||||
for (java.util.logging.Handler h : root.getHandlers()) {
|
||||
root.removeHandler(h);
|
||||
}
|
||||
root.setLevel(Level.ALL);
|
||||
root.setLevel(getEffectiveJulLevel());
|
||||
try {
|
||||
root.addHandler(new org.bukkit.craftbukkit.util.ForwardLogHandler());
|
||||
} catch (Throwable t) {
|
||||
|
||||
@ -71,8 +71,13 @@ public class PatchBukkitUnsafeValues implements UnsafeValues {
|
||||
public boolean isSupportedApiVersion(String apiVersion) {
|
||||
if (apiVersion == null) return false;
|
||||
final ApiVersion toCheck = ApiVersion.getOrCreateVersion(apiVersion);
|
||||
var minimumApi = NativeBridgeFfi.getPatchBukkitConfig(EmptyRequest.newBuilder().build()).getMinimumSupportedPluginApi();
|
||||
final ApiVersion minimumVersion = ApiVersion.getOrCreateVersion(minimumApi);
|
||||
String minimumApi = null;
|
||||
try {
|
||||
minimumApi = NativeBridgeFfi.getPatchBukkitConfig(EmptyRequest.newBuilder().build()).getMinimumSupportedPluginApi();
|
||||
} catch (Throwable ignored) {}
|
||||
final ApiVersion minimumVersion = (minimumApi == null || minimumApi.isEmpty() || "0.0.0".equals(minimumApi))
|
||||
? ApiVersion.getOrCreateVersion("1.13")
|
||||
: ApiVersion.getOrCreateVersion(minimumApi);
|
||||
|
||||
return !toCheck.isNewerThan(ApiVersion.CURRENT) && !toCheck.isOlderThan(minimumVersion);
|
||||
}
|
||||
|
||||
@ -297,13 +297,17 @@ public class PatchBukkitEntity implements Entity {
|
||||
public @NotNull Location getLocation() {
|
||||
try {
|
||||
var location = NativeBridgeFfi.getLocation(BridgeUtils.convertUuid(this.uuid));
|
||||
if (location != null && location.hasWorld() && location.hasPosition()) {
|
||||
if (location != null && location.hasWorld() && location.hasPosition() && location.getWorld().hasUuid()) {
|
||||
var world = PatchBukkitWorld.getOrCreate(BridgeUtils.convertUuid(location.getWorld().getUuid()));
|
||||
var position = location.getPosition();
|
||||
return new Location(world, position.getX(), position.getY(), position.getZ(), location.getYaw(), location.getPitch());
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
return this.cachedLocation != null ? this.cachedLocation.clone() : new Location(null, 0, 0, 0);
|
||||
Location fallback = this.cachedLocation != null ? this.cachedLocation.clone() : new Location(null, 0, 0, 0);
|
||||
if (fallback.getWorld() == null && !Bukkit.getWorlds().isEmpty()) {
|
||||
fallback.setWorld(Bukkit.getWorlds().get(0));
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -387,9 +391,17 @@ public class PatchBukkitEntity implements Entity {
|
||||
|
||||
@Override
|
||||
public @NotNull World getWorld() {
|
||||
try {
|
||||
var location = NativeBridgeFfi.getLocation(BridgeUtils.convertUuid(this.uuid));
|
||||
if (location != null && location.hasWorld() && location.getWorld().hasUuid()) {
|
||||
return PatchBukkitWorld.getOrCreate(BridgeUtils.convertUuid(location.getWorld().getUuid()));
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
if (this.cachedLocation != null && this.cachedLocation.getWorld() != null) {
|
||||
return this.cachedLocation.getWorld();
|
||||
}
|
||||
return Bukkit.getWorlds().isEmpty() ? null : Bukkit.getWorlds().get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRotation(float yaw, float pitch) {
|
||||
|
||||
@ -38,12 +38,32 @@ public class PatchBukkitBootstrap {
|
||||
return true;
|
||||
}
|
||||
|
||||
Set<String> disabledPlugins = new HashSet<>();
|
||||
boolean debugBridge = false;
|
||||
try {
|
||||
var config = patchbukkit.bridge.NativeBridgeFfi.getPatchBukkitConfig(patchbukkit.common.EmptyRequest.newBuilder().build());
|
||||
if (config != null) {
|
||||
for (String d : config.getDisabledPluginsList()) {
|
||||
disabledPlugins.add(d.toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
debugBridge = config.getDebugBridge();
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
|
||||
if (debugBridge && !disabledPlugins.isEmpty()) {
|
||||
LOGGER.info("[PatchBukkit] Disabled plugins from config: " + disabledPlugins);
|
||||
}
|
||||
|
||||
Map<String, PluginHolder> holders = new LinkedHashMap<>();
|
||||
|
||||
for (File jarFile : jarFiles) {
|
||||
try {
|
||||
PluginHolder holder = parsePluginHolder(jarFile);
|
||||
if (holder != null) {
|
||||
if (disabledPlugins.contains(holder.name.toLowerCase(Locale.ENGLISH))) {
|
||||
LOGGER.info("[PatchBukkit] Plugin '" + holder.name + "' is disabled in configuration, skipping.");
|
||||
continue;
|
||||
}
|
||||
holders.put(holder.name.toLowerCase(Locale.ENGLISH), holder);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
|
||||
@ -17,7 +17,10 @@ public class CloudReflectionTest {
|
||||
public void testGrimAcCloudReflection() throws Exception {
|
||||
PatchBukkitServer.initServer();
|
||||
|
||||
File grimFile = new File("/home/alex/Documents/Development/Rust/Pumpkin/plugins/data/patchbukkit/patchbukkit-plugins/grimac-bukkit-2.3.74-61caa53.jar");
|
||||
File grimFile = new File("build/test-plugins/GrimAC.jar");
|
||||
if (!grimFile.exists()) {
|
||||
grimFile = new File("java/patchbukkit/build/test-plugins/GrimAC.jar");
|
||||
}
|
||||
if (!grimFile.exists()) return;
|
||||
|
||||
PatchBukkitPluginClassLoader loader = new PatchBukkitPluginClassLoader(
|
||||
|
||||
@ -75,7 +75,7 @@ public class ProtocolPluginsLoadTest {
|
||||
|
||||
@Test
|
||||
public void testLoadGrimAC() throws Exception {
|
||||
File grimFile = new File("/home/alex/Documents/Development/Rust/Pumpkin/plugins/data/patchbukkit/patchbukkit-plugins/grimac-bukkit-2.3.74-61caa53.jar");
|
||||
File grimFile = testPluginsDir.resolve("GrimAC.jar").toFile();
|
||||
if (!grimFile.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -9,4 +9,9 @@ option java_package = "patchbukkit.config";
|
||||
|
||||
message GetPatchBukkitConfigResponse {
|
||||
string minimum_supported_plugin_api = 1;
|
||||
repeated string disabled_plugins = 2;
|
||||
bool download_dependencies = 3;
|
||||
bool warn_unimplemented_api = 4;
|
||||
bool debug_bridge = 5;
|
||||
string log_level = 6;
|
||||
}
|
||||
|
||||
@ -2,22 +2,135 @@ use std::{fs, path::Path};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct PatchBukkitConfig {
|
||||
#[serde(default)]
|
||||
pub settings: SettingsConfig,
|
||||
#[serde(default)]
|
||||
pub jvm: JvmConfig,
|
||||
#[serde(default)]
|
||||
pub plugins: PluginsConfig,
|
||||
#[serde(default)]
|
||||
pub libraries: LibrariesConfig,
|
||||
#[serde(default)]
|
||||
pub diagnostics: DiagnosticsConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct SettingsConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub minimum_supported_plugin_api: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct JvmConfig {
|
||||
/// Maximum heap memory allocated to the JVM (e.g. "2G", "1024M")
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_heap: Option<String>,
|
||||
/// Initial heap memory allocated to the JVM (e.g. "512M")
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub initial_heap: Option<String>,
|
||||
/// Additional JVM command-line flags
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub extra_args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct PluginsConfig {
|
||||
/// Directory where Bukkit/Paper plugins are located
|
||||
#[serde(default = "default_plugins_dir")]
|
||||
pub directory: String,
|
||||
/// List of plugin names to skip loading (case-insensitive)
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub disabled: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_plugins_dir() -> String {
|
||||
"patchbukkit-plugins".to_string()
|
||||
}
|
||||
|
||||
impl Default for PluginsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
directory: default_plugins_dir(),
|
||||
disabled: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LibrariesConfig {
|
||||
/// Whether to automatically download declared Maven libraries for plugins
|
||||
#[serde(default = "default_true")]
|
||||
pub download_dependencies: bool,
|
||||
}
|
||||
|
||||
const fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for LibrariesConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
download_dependencies: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DiagnosticsConfig {
|
||||
/// Minimum log level ("info", "warn", "error", "debug", "trace")
|
||||
#[serde(default = "default_log_level")]
|
||||
pub log_level: String,
|
||||
/// Log warnings when plugins call unimplemented Bukkit API methods
|
||||
#[serde(default = "default_true")]
|
||||
pub warn_unimplemented_api: bool,
|
||||
/// Enable verbose bridge and event logging
|
||||
#[serde(default)]
|
||||
pub debug_bridge: bool,
|
||||
}
|
||||
|
||||
fn default_log_level() -> String {
|
||||
"info".to_string()
|
||||
}
|
||||
|
||||
impl Default for DiagnosticsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
log_level: default_log_level(),
|
||||
warn_unimplemented_api: true,
|
||||
debug_bridge: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PatchBukkitConfig {
|
||||
pub const DEFAULT: PatchBukkitConfig = PatchBukkitConfig {
|
||||
settings: SettingsConfig {
|
||||
minimum_supported_plugin_api: None,
|
||||
},
|
||||
jvm: JvmConfig {
|
||||
max_heap: None,
|
||||
initial_heap: None,
|
||||
extra_args: Vec::new(),
|
||||
},
|
||||
plugins: PluginsConfig {
|
||||
directory: String::new(),
|
||||
disabled: Vec::new(),
|
||||
},
|
||||
libraries: LibrariesConfig {
|
||||
download_dependencies: true,
|
||||
},
|
||||
diagnostics: DiagnosticsConfig {
|
||||
log_level: String::new(),
|
||||
warn_unimplemented_api: true,
|
||||
debug_bridge: false,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn parse<S: AsRef<str>>(config: S) -> Result<Self, toml::de::Error> {
|
||||
@ -27,11 +140,126 @@ impl PatchBukkitConfig {
|
||||
pub fn get_or_create<P: AsRef<Path>>(config_path: P) -> anyhow::Result<Self> {
|
||||
let config_path = config_path.as_ref();
|
||||
if !config_path.exists() {
|
||||
let default = PatchBukkitConfig::DEFAULT;
|
||||
std::fs::write(config_path, &toml::to_string_pretty(&default)?)?;
|
||||
let default = PatchBukkitConfig::default();
|
||||
let template = r#"# PatchBukkit Configuration File
|
||||
|
||||
[settings]
|
||||
# Minimum Bukkit/Paper plugin API version required (e.g. "1.20"). Leave unset for all versions.
|
||||
# minimum-supported-plugin-api = "1.20"
|
||||
|
||||
[jvm]
|
||||
# Maximum and initial heap memory allocated to the embedded JVM (e.g. "2G", "512M")
|
||||
# max-heap = "2G"
|
||||
# initial-heap = "512M"
|
||||
|
||||
# Additional JVM arguments passed on startup
|
||||
extra-args = []
|
||||
|
||||
[plugins]
|
||||
# Directory where plugin JARs are located
|
||||
directory = "patchbukkit-plugins"
|
||||
|
||||
# List of plugin names to skip loading (case-insensitive)
|
||||
disabled = []
|
||||
|
||||
[libraries]
|
||||
# Automatically download declared Maven libraries for plugins
|
||||
download-dependencies = true
|
||||
|
||||
[diagnostics]
|
||||
# Minimum log level: "info", "warn", "error", "debug", "trace" (default: "info")
|
||||
log-level = "info"
|
||||
|
||||
# Log warnings when plugins call unimplemented Bukkit API methods
|
||||
warn-unimplemented-api = true
|
||||
|
||||
# Enable verbose logging for bridge and FFI events
|
||||
debug-bridge = false
|
||||
"#;
|
||||
std::fs::write(config_path, template)?;
|
||||
return Ok(default);
|
||||
}
|
||||
let config = PatchBukkitConfig::parse(fs::read_to_string(config_path)?)?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_empty_toml() {
|
||||
let config = PatchBukkitConfig::parse("").unwrap();
|
||||
assert_eq!(config.plugins.directory, "patchbukkit-plugins");
|
||||
assert!(config.plugins.disabled.is_empty());
|
||||
assert!(config.libraries.download_dependencies);
|
||||
assert_eq!(config.diagnostics.log_level, "info");
|
||||
assert!(config.diagnostics.warn_unimplemented_api);
|
||||
assert!(!config.diagnostics.debug_bridge);
|
||||
assert_eq!(config.jvm.max_heap, None);
|
||||
assert_eq!(config.jvm.initial_heap, None);
|
||||
assert!(config.jvm.extra_args.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_legacy_toml() {
|
||||
let toml_str = r#"
|
||||
[settings]
|
||||
minimum-supported-plugin-api = "1.20"
|
||||
"#;
|
||||
let config = PatchBukkitConfig::parse(toml_str).unwrap();
|
||||
assert_eq!(
|
||||
config.settings.minimum_supported_plugin_api.as_deref(),
|
||||
Some("1.20")
|
||||
);
|
||||
assert_eq!(config.plugins.directory, "patchbukkit-plugins");
|
||||
assert_eq!(config.diagnostics.log_level, "info");
|
||||
assert!(config.libraries.download_dependencies);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_full_toml() {
|
||||
let toml_str = r#"
|
||||
[settings]
|
||||
minimum-supported-plugin-api = "1.20.4"
|
||||
|
||||
[jvm]
|
||||
max-heap = "4G"
|
||||
initial-heap = "1G"
|
||||
extra-args = ["-XX:+UseG1GC", "-Dcustom.prop=true"]
|
||||
|
||||
[plugins]
|
||||
directory = "plugins"
|
||||
disabled = ["ProblematicPlugin", "OldPlugin"]
|
||||
|
||||
[libraries]
|
||||
download-dependencies = false
|
||||
|
||||
[diagnostics]
|
||||
log-level = "debug"
|
||||
warn-unimplemented-api = false
|
||||
debug-bridge = true
|
||||
"#;
|
||||
let config = PatchBukkitConfig::parse(toml_str).unwrap();
|
||||
assert_eq!(
|
||||
config.settings.minimum_supported_plugin_api.as_deref(),
|
||||
Some("1.20.4")
|
||||
);
|
||||
assert_eq!(config.jvm.max_heap.as_deref(), Some("4G"));
|
||||
assert_eq!(config.jvm.initial_heap.as_deref(), Some("1G"));
|
||||
assert_eq!(
|
||||
config.jvm.extra_args,
|
||||
vec!["-XX:+UseG1GC", "-Dcustom.prop=true"]
|
||||
);
|
||||
assert_eq!(config.plugins.directory, "plugins");
|
||||
assert_eq!(
|
||||
config.plugins.disabled,
|
||||
vec!["ProblematicPlugin", "OldPlugin"]
|
||||
);
|
||||
assert!(!config.libraries.download_dependencies);
|
||||
assert_eq!(config.diagnostics.log_level, "debug");
|
||||
assert!(!config.diagnostics.warn_unimplemented_api);
|
||||
assert!(config.diagnostics.debug_bridge);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
use std::{fs, path::PathBuf};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use pumpkin::plugin::Context;
|
||||
|
||||
@ -9,7 +12,19 @@ pub struct PatchBukkitDirectories {
|
||||
pub jassets: PathBuf,
|
||||
}
|
||||
|
||||
pub fn setup_directories(server: &Context) -> Result<PatchBukkitDirectories, String> {
|
||||
pub fn get_base_directory(server: &Context) -> Result<PathBuf, String> {
|
||||
let data_folder = std::path::absolute(server.get_data_folder())
|
||||
.map_err(|_| "Failed to get absolute directory from relative")?;
|
||||
let server_root = data_folder
|
||||
.parent()
|
||||
.ok_or("Failed to determine server root from PatchBukkit data folder")?;
|
||||
Ok(server_root.join("patchbukkit"))
|
||||
}
|
||||
|
||||
pub fn setup_directories(
|
||||
server: &Context,
|
||||
plugins_dir_name: &str,
|
||||
) -> Result<PatchBukkitDirectories, String> {
|
||||
let data_folder = std::path::absolute(server.get_data_folder())
|
||||
.map_err(|_| "Failed to get absolute directory from relative")?;
|
||||
let server_root = data_folder
|
||||
@ -17,7 +32,14 @@ pub fn setup_directories(server: &Context) -> Result<PatchBukkitDirectories, Str
|
||||
.ok_or("Failed to determine server root from PatchBukkit data folder")?;
|
||||
let base = server_root.join("patchbukkit");
|
||||
|
||||
let plugins = base.join("patchbukkit-plugins");
|
||||
let plugins = if Path::new(plugins_dir_name).is_absolute() {
|
||||
PathBuf::from(plugins_dir_name)
|
||||
} else if plugins_dir_name == "patchbukkit-plugins" {
|
||||
base.join("patchbukkit-plugins")
|
||||
} else {
|
||||
server_root.join(plugins_dir_name)
|
||||
};
|
||||
|
||||
let plugin_updates = plugins.join("update");
|
||||
let jassets = base.join("jassets");
|
||||
|
||||
@ -25,7 +47,7 @@ pub fn setup_directories(server: &Context) -> Result<PatchBukkitDirectories, Str
|
||||
.map_err(|err| format!("Failed to create jassets folder: {err:?}"))?;
|
||||
|
||||
fs::create_dir_all(&plugins)
|
||||
.map_err(|err| format!("Failed to create patchbukkit-plugins folder: {err:?}"))?;
|
||||
.map_err(|err| format!("Failed to create plugins folder: {err:?}"))?;
|
||||
|
||||
let patchbukkit_jar_dest = jassets.join("patchbukkit.jar");
|
||||
|
||||
|
||||
@ -6605,6 +6605,14 @@ where
|
||||
}
|
||||
|
||||
Box::pin(async move {
|
||||
if let Some(ctx) = crate::java::native_callbacks::CALLBACK_CONTEXT.get() {
|
||||
if ctx.config.diagnostics.debug_bridge {
|
||||
tracing::debug!(
|
||||
"Dispatching async event to JVM worker for plugin {}",
|
||||
self.plugin_name
|
||||
);
|
||||
}
|
||||
}
|
||||
let (tx, rx) = oneshot::channel();
|
||||
if let Err(e) = command_tx
|
||||
.send(JvmCommand::FireEvent {
|
||||
@ -6634,6 +6642,14 @@ where
|
||||
}
|
||||
|
||||
Box::pin(async move {
|
||||
if let Some(ctx) = crate::java::native_callbacks::CALLBACK_CONTEXT.get() {
|
||||
if ctx.config.diagnostics.debug_bridge {
|
||||
tracing::debug!(
|
||||
"Dispatching blocking event to JVM worker for plugin {}",
|
||||
self.plugin_name
|
||||
);
|
||||
}
|
||||
}
|
||||
let (tx, rx) = oneshot::channel();
|
||||
if let Err(e) = command_tx
|
||||
.send(JvmCommand::FireEvent {
|
||||
|
||||
@ -55,11 +55,11 @@ impl JvmWorker {
|
||||
context.clone(),
|
||||
runtime_handle,
|
||||
command_tx.clone(),
|
||||
config,
|
||||
config.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
self.context = Some(context);
|
||||
let result = self.initialize_jvm(&jassets_path);
|
||||
let result = self.initialize_jvm(&jassets_path, &config);
|
||||
let _ = respond_to.send(result);
|
||||
}
|
||||
JvmCommand::LoadPlugin {
|
||||
@ -234,7 +234,11 @@ impl JvmWorker {
|
||||
tracing::info!("JVM worker thread exited");
|
||||
}
|
||||
|
||||
fn initialize_jvm(&mut self, jassets_path: &PathBuf) -> anyhow::Result<()> {
|
||||
fn initialize_jvm(
|
||||
&mut self,
|
||||
jassets_path: &PathBuf,
|
||||
config: &crate::config::patchbukkit::PatchBukkitConfig,
|
||||
) -> anyhow::Result<()> {
|
||||
tracing::info!("Initializing JVM with assets path: {jassets_path:?}");
|
||||
|
||||
let mut jar_paths = Vec::new();
|
||||
@ -284,7 +288,7 @@ impl JvmWorker {
|
||||
.collect::<Vec<_>>()
|
||||
.join(separator);
|
||||
|
||||
let jvm_args = InitArgsBuilder::new()
|
||||
let mut builder = InitArgsBuilder::new()
|
||||
.version(JNIVersion::V21)
|
||||
.option(format!("-Djava.class.path={classpath}"))
|
||||
.option("-XX:+IgnoreUnrecognizedVMOptions")
|
||||
@ -292,7 +296,28 @@ impl JvmWorker {
|
||||
.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")
|
||||
.option("-Dcom.google.protobuf.useUnsafe=false");
|
||||
|
||||
if let Some(ref max_heap) = config.jvm.max_heap {
|
||||
builder = builder.option(format!("-Xmx{max_heap}"));
|
||||
}
|
||||
if let Some(ref initial_heap) = config.jvm.initial_heap {
|
||||
builder = builder.option(format!("-Xms{initial_heap}"));
|
||||
}
|
||||
for arg in &config.jvm.extra_args {
|
||||
builder = builder.option(arg.clone());
|
||||
}
|
||||
|
||||
if config.diagnostics.debug_bridge {
|
||||
tracing::info!(
|
||||
"Configuring JVM: max_heap={:?}, initial_heap={:?}, extra_args={:?}",
|
||||
config.jvm.max_heap,
|
||||
config.jvm.initial_heap,
|
||||
config.jvm.extra_args
|
||||
);
|
||||
}
|
||||
|
||||
let jvm_args = builder
|
||||
.build()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to build JVM init args: {e:?}"))?;
|
||||
|
||||
|
||||
@ -54,7 +54,7 @@ pub fn ffi_native_bridge_register_command_impl(request: RegisterCommandRequest)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let node = init_java_command(primary_name.clone(), command_tx, description);
|
||||
let node = init_java_command(primary_name, command_tx, description);
|
||||
|
||||
let clean_perm = cmd_name.trim_start_matches('/');
|
||||
let permission = format!("patchbukkit:command.{clean_perm}");
|
||||
|
||||
@ -14,6 +14,11 @@ pub fn ffi_native_bridge_get_patch_bukkit_config_impl(
|
||||
.settings
|
||||
.minimum_supported_plugin_api
|
||||
.clone()
|
||||
.unwrap_or("0.0.0".to_string()),
|
||||
.unwrap_or_else(|| "1.13".to_string()),
|
||||
disabled_plugins: context.config.plugins.disabled.clone(),
|
||||
download_dependencies: context.config.libraries.download_dependencies,
|
||||
warn_unimplemented_api: context.config.diagnostics.warn_unimplemented_api,
|
||||
debug_bridge: context.config.diagnostics.debug_bridge,
|
||||
log_level: context.config.diagnostics.log_level.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -154,7 +154,6 @@ pub fn ffi_native_bridge_teleport_entity_impl(request: TeleportEntityRequest) ->
|
||||
with_player(request.uuid.as_ref(), |player| {
|
||||
let position = pumpkin_util::math::vector3::Vector3::new(pos.x, pos.y, pos.z);
|
||||
let world = player.living_entity.entity.world.load_full();
|
||||
let player = player.clone();
|
||||
ctx.runtime.spawn(async move {
|
||||
player.teleport(position, Some(yaw), Some(pitch), world);
|
||||
});
|
||||
@ -367,7 +366,6 @@ pub fn ffi_native_bridge_set_player_list_header_footer_impl(
|
||||
with_player(request.uuid.as_ref(), |player| {
|
||||
let header = request.header;
|
||||
let footer = request.footer;
|
||||
let player = player.clone();
|
||||
ctx.runtime.spawn(async move {
|
||||
let h = pumpkin_util::text::TextComponent::from_legacy_string(&header);
|
||||
let f = pumpkin_util::text::TextComponent::from_legacy_string(&footer);
|
||||
@ -430,7 +428,6 @@ pub fn ffi_native_bridge_set_player_time_impl(request: SetPlayerTimeRequest) ->
|
||||
with_player(request.uuid.as_ref(), |player| {
|
||||
let time = request.time;
|
||||
let relative = request.relative;
|
||||
let player = player.clone();
|
||||
ctx.runtime.spawn(async move {
|
||||
let packet = pumpkin_protocol::java::client::play::CUpdateTime::new(0, time, relative);
|
||||
player.send_client_packet(&packet).await;
|
||||
@ -441,7 +438,6 @@ pub fn ffi_native_bridge_set_player_time_impl(request: SetPlayerTimeRequest) ->
|
||||
pub fn ffi_native_bridge_reset_player_time_impl(request: Uuid) -> Option<()> {
|
||||
let ctx = CALLBACK_CONTEXT.get()?;
|
||||
with_player(Some(&request), |player| {
|
||||
let player = player.clone();
|
||||
ctx.runtime.spawn(async move {
|
||||
let world_time = player
|
||||
.world()
|
||||
@ -461,7 +457,6 @@ pub fn ffi_native_bridge_set_player_weather_impl(request: SetPlayerWeatherReques
|
||||
let ctx = CALLBACK_CONTEXT.get()?;
|
||||
with_player(request.uuid.as_ref(), |player| {
|
||||
let weather = request.weather;
|
||||
let player = player.clone();
|
||||
ctx.runtime.spawn(async move {
|
||||
let event = if weather == 1 {
|
||||
pumpkin_protocol::java::client::play::GameEvent::BeginRaining
|
||||
@ -477,7 +472,6 @@ pub fn ffi_native_bridge_set_player_weather_impl(request: SetPlayerWeatherReques
|
||||
pub fn ffi_native_bridge_reset_player_weather_impl(request: Uuid) -> Option<()> {
|
||||
let ctx = CALLBACK_CONTEXT.get()?;
|
||||
with_player(Some(&request), |player| {
|
||||
let player = player.clone();
|
||||
ctx.runtime.spawn(async move {
|
||||
let is_raining = player.world().is_raining();
|
||||
let event = if is_raining {
|
||||
@ -495,7 +489,6 @@ pub fn ffi_native_bridge_set_compass_target_impl(request: SetCompassTargetReques
|
||||
let ctx = CALLBACK_CONTEXT.get()?;
|
||||
let pos = request.position?;
|
||||
with_player(request.uuid.as_ref(), |player| {
|
||||
let player = player.clone();
|
||||
ctx.runtime.spawn(async move {
|
||||
let block_pos = pumpkin_util::math::position::BlockPos::new(
|
||||
pos.x as i32,
|
||||
@ -609,7 +602,6 @@ pub fn ffi_native_bridge_stop_sound_impl(request: StopSoundRequest) -> Option<()
|
||||
pub fn ffi_native_bridge_send_block_change_impl(request: SendBlockChangeRequest) -> Option<()> {
|
||||
let ctx = CALLBACK_CONTEXT.get()?;
|
||||
with_player(request.uuid.as_ref(), |player| {
|
||||
let player = player.clone();
|
||||
let pos = pumpkin_util::math::position::BlockPos::new(request.x, request.y, request.z);
|
||||
let state_str = request.block_state;
|
||||
let clean_key = state_str
|
||||
@ -635,7 +627,6 @@ pub fn ffi_native_bridge_send_block_change_impl(request: SendBlockChangeRequest)
|
||||
pub fn ffi_native_bridge_send_resource_pack_impl(request: SendResourcePackRequest) -> Option<()> {
|
||||
let ctx = CALLBACK_CONTEXT.get()?;
|
||||
with_player(request.uuid.as_ref(), |player| {
|
||||
let player = player.clone();
|
||||
let url = request.url;
|
||||
let prompt = request.prompt;
|
||||
let required = request.required;
|
||||
@ -678,7 +669,6 @@ pub fn ffi_native_bridge_get_player_connection_info_impl(
|
||||
pub fn ffi_native_bridge_send_game_event_impl(request: SendGameEventRequest) -> Option<()> {
|
||||
let ctx = CALLBACK_CONTEXT.get()?;
|
||||
with_player(request.uuid.as_ref(), |player| {
|
||||
let player = player.clone();
|
||||
let event_type = request.event_type as u8;
|
||||
let value = request.value;
|
||||
ctx.runtime.spawn(async move {
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
use crate::proto::patchbukkit::log::{LogLevel, SendLogRequest};
|
||||
use crate::{
|
||||
java::native_callbacks::CALLBACK_CONTEXT,
|
||||
proto::patchbukkit::log::{LogLevel, SendLogRequest},
|
||||
};
|
||||
|
||||
pub fn ffi_native_bridge_send_log_impl(request: SendLogRequest) -> Option<()> {
|
||||
let logger = &request.logger_name;
|
||||
@ -9,6 +12,39 @@ pub fn ffi_native_bridge_send_log_impl(request: SendLogRequest) -> Option<()> {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
let min_level = CALLBACK_CONTEXT
|
||||
.get()
|
||||
.map(|ctx| ctx.config.diagnostics.log_level.as_str())
|
||||
.unwrap_or("info");
|
||||
|
||||
let debug_bridge = CALLBACK_CONTEXT
|
||||
.get()
|
||||
.map(|ctx| ctx.config.diagnostics.debug_bridge)
|
||||
.unwrap_or(false);
|
||||
|
||||
let allowed = debug_bridge
|
||||
|| match min_level {
|
||||
"error" => matches!(request.level(), LogLevel::Severe),
|
||||
"warn" | "warning" => matches!(request.level(), LogLevel::Severe | LogLevel::Warning),
|
||||
"info" => matches!(
|
||||
request.level(),
|
||||
LogLevel::Severe | LogLevel::Warning | LogLevel::Info
|
||||
),
|
||||
"debug" => matches!(
|
||||
request.level(),
|
||||
LogLevel::Severe | LogLevel::Warning | LogLevel::Info | LogLevel::Config
|
||||
),
|
||||
"trace" | "all" => true,
|
||||
_ => matches!(
|
||||
request.level(),
|
||||
LogLevel::Severe | LogLevel::Warning | LogLevel::Info
|
||||
),
|
||||
};
|
||||
|
||||
if !allowed {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
match request.level() {
|
||||
LogLevel::Severe => tracing::error!(logger = %logger, "{}", msg),
|
||||
LogLevel::Warning => tracing::warn!(logger = %logger, "{}", msg),
|
||||
|
||||
@ -50,9 +50,9 @@ pub use command::*;
|
||||
pub mod server;
|
||||
pub use server::*;
|
||||
|
||||
static CALLBACK_CONTEXT: OnceLock<CallbackContext> = OnceLock::new();
|
||||
pub(crate) static CALLBACK_CONTEXT: OnceLock<CallbackContext> = OnceLock::new();
|
||||
|
||||
struct CallbackContext {
|
||||
pub(crate) struct CallbackContext {
|
||||
pub plugin_context: Arc<Context>,
|
||||
pub runtime: tokio::runtime::Handle,
|
||||
pub command_tx: mpsc::Sender<JvmCommand>,
|
||||
|
||||
@ -312,7 +312,9 @@ pub fn ffi_native_bridge_get_ban_list_impl(
|
||||
if let Ok(list) = ctx.plugin_context.server.data.banned_ip_list.try_read() {
|
||||
for entry in &list.banned_ips {
|
||||
let created = entry.created.unix_timestamp();
|
||||
let expires = entry.expires.map(|e| e.unix_timestamp()).unwrap_or(0);
|
||||
let expires = entry
|
||||
.expires
|
||||
.map_or(0, time::OffsetDateTime::unix_timestamp);
|
||||
entries.push(BanEntryProto {
|
||||
target: entry.ip.to_string(),
|
||||
source: entry.source.clone(),
|
||||
@ -322,11 +324,12 @@ pub fn ffi_native_bridge_get_ban_list_impl(
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Ok(list) = ctx.plugin_context.server.data.banned_player_list.try_read() {
|
||||
} else if let Ok(list) = ctx.plugin_context.server.data.banned_player_list.try_read() {
|
||||
for entry in &list.banned_players {
|
||||
let created = entry.created.unix_timestamp();
|
||||
let expires = entry.expires.map(|e| e.unix_timestamp()).unwrap_or(0);
|
||||
let expires = entry
|
||||
.expires
|
||||
.map_or(0, time::OffsetDateTime::unix_timestamp);
|
||||
entries.push(BanEntryProto {
|
||||
target: entry.name.clone(),
|
||||
source: entry.source.clone(),
|
||||
@ -336,7 +339,6 @@ pub fn ffi_native_bridge_get_ban_list_impl(
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(GetBanListResponse { entries })
|
||||
}
|
||||
|
||||
@ -614,9 +614,8 @@ pub fn ffi_native_bridge_save_world_impl(request: SaveWorldRequest) -> Option<()
|
||||
.cloned()
|
||||
.or_else(|| worlds.first().cloned())?;
|
||||
|
||||
let w = world.clone();
|
||||
ctx.runtime.spawn(async move {
|
||||
let _ = w.save().await;
|
||||
let _ = world.save().await;
|
||||
});
|
||||
|
||||
Some(())
|
||||
|
||||
@ -19,7 +19,7 @@ impl CommandManager {
|
||||
}
|
||||
|
||||
pub fn get_tab_complete(
|
||||
&mut self,
|
||||
&self,
|
||||
env: &mut Env,
|
||||
sender: SimpleCommandSender,
|
||||
full_command: String,
|
||||
@ -35,7 +35,7 @@ impl CommandManager {
|
||||
}
|
||||
|
||||
fn try_tab_complete(
|
||||
&mut self,
|
||||
&self,
|
||||
env: &mut Env,
|
||||
sender: SimpleCommandSender,
|
||||
full_command: String,
|
||||
|
||||
@ -25,7 +25,7 @@ fn cleanup_stale_files(resources_folder: &Path) {
|
||||
|
||||
for entry in walkdir::WalkDir::new(resources_folder)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(std::result::Result::ok)
|
||||
.filter(|e| e.file_type().is_file())
|
||||
{
|
||||
let path = entry.path();
|
||||
@ -41,7 +41,7 @@ fn cleanup_stale_files(resources_folder: &Path) {
|
||||
for entry in walkdir::WalkDir::new(resources_folder)
|
||||
.contents_first(true)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(std::result::Result::ok)
|
||||
.filter(|e| e.file_type().is_dir() && e.path() != resources_folder)
|
||||
{
|
||||
let _ = fs::remove_dir(entry.path());
|
||||
|
||||
@ -12,7 +12,7 @@ pub mod events;
|
||||
pub mod java;
|
||||
pub mod proto;
|
||||
|
||||
use directories::setup_directories;
|
||||
use directories::{get_base_directory, setup_directories};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
use crate::{
|
||||
@ -27,20 +27,24 @@ pub async fn on_load_inner(plugin: &PatchBukkitPlugin, server: Arc<Context>) ->
|
||||
server.init_log();
|
||||
tracing::info!("Starting PatchBukkit");
|
||||
|
||||
// Setup directories
|
||||
let dirs = setup_directories(&server)?;
|
||||
let base = get_base_directory(&server)?;
|
||||
std::fs::create_dir_all(&base)
|
||||
.map_err(|e| format!("Failed to create PatchBukkit base directory: {e}"))?;
|
||||
|
||||
let mut config_path = dirs.base.clone();
|
||||
let mut config_path = base;
|
||||
config_path.push("patchbukkit.config.toml");
|
||||
let config = PatchBukkitConfig::get_or_create(config_path)
|
||||
.map_err(|e| format!("Failed to setup PatchBukkit config: {e}"))?;
|
||||
|
||||
// Setup directories
|
||||
let dirs = setup_directories(&server, &config.plugins.directory)?;
|
||||
|
||||
// Manage embedded resources
|
||||
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();
|
||||
let server_clone = server;
|
||||
|
||||
// Run JVM initialization and Java plugin bootstrap in a background task
|
||||
// on PatchBukkit's dedicated runtime so Pumpkin's main startup is never blocked.
|
||||
|
||||
219
rust/tests/bot_integration_test.rs
Normal file
219
rust/tests/bot_integration_test.rs
Normal file
@ -0,0 +1,219 @@
|
||||
mod common;
|
||||
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use common::bot::BotClient;
|
||||
use patchbukkit::{
|
||||
PatchBukkitPlugin, commands::SimpleCommandSender, directories::setup_directories,
|
||||
java::jvm::commands::JvmCommand,
|
||||
};
|
||||
use pumpkin::{PumpkinServer, data::VanillaData, plugin::Context};
|
||||
use pumpkin_config::{AdvancedConfiguration, BasicConfiguration, TelemetryConfig};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn test_fake_bot_join_and_interaction() {
|
||||
let _ = pumpkin::LOGGER_IMPL.set(None);
|
||||
let _ = tracing_subscriber::fmt().try_init();
|
||||
let temp_dir = tempfile::tempdir().expect("Failed to create tempdir");
|
||||
let prev_dir = std::env::current_dir().expect("Failed to get current dir");
|
||||
std::env::set_current_dir(temp_dir.path()).expect("Failed to set current dir");
|
||||
|
||||
let basic_config = BasicConfiguration {
|
||||
default_level_name: "test_bot_world".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut advanced_config = AdvancedConfiguration::default();
|
||||
advanced_config.networking.java.enabled = true;
|
||||
advanced_config.networking.java.address = "127.0.0.1:0".parse().unwrap();
|
||||
advanced_config.networking.java.online_mode = false;
|
||||
advanced_config.networking.java.encryption = false;
|
||||
advanced_config.networking.bedrock.enabled = false;
|
||||
advanced_config.networking.rcon.enabled = false;
|
||||
advanced_config.commands.use_console = false;
|
||||
|
||||
let vanilla_data = VanillaData::load();
|
||||
let telemetry_config = TelemetryConfig::default();
|
||||
let pumpkin_server = Arc::new(
|
||||
PumpkinServer::new(
|
||||
basic_config,
|
||||
advanced_config,
|
||||
telemetry_config,
|
||||
vanilla_data,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
let server_addr = pumpkin_server
|
||||
.tcp_listener
|
||||
.as_ref()
|
||||
.expect("TCP listener should be bound")
|
||||
.local_addr()
|
||||
.expect("Should have local address");
|
||||
|
||||
println!("Pumpkin TCP server bound to ephemeral port: {server_addr}");
|
||||
|
||||
let metadata = pumpkin::plugin::PluginMetadata {
|
||||
name: "patchbukkit".to_string(),
|
||||
version: "0.1.0".to_string(),
|
||||
authors: vec!["PumpkinMC".to_string()],
|
||||
description: "PatchBukkit".to_string(),
|
||||
dependencies: vec![],
|
||||
permissions: vec![],
|
||||
};
|
||||
|
||||
let handlers = Arc::new(arc_swap::ArcSwap::from_pointee(
|
||||
std::collections::HashMap::new(),
|
||||
));
|
||||
let context = Arc::new(Context::new(
|
||||
metadata,
|
||||
pumpkin_server.server.clone(),
|
||||
handlers,
|
||||
Arc::clone(&pumpkin_server.server.plugin_manager),
|
||||
Arc::clone(&pumpkin::LOGGER_IMPL),
|
||||
));
|
||||
|
||||
// Setup directories
|
||||
let dirs = setup_directories(&context, "patchbukkit-plugins")
|
||||
.expect("Failed to setup PatchBukkit directories");
|
||||
|
||||
// Copy test plugins if present
|
||||
let test_plugins_dir = if prev_dir.join("java").exists() {
|
||||
prev_dir
|
||||
.join("java")
|
||||
.join("patchbukkit")
|
||||
.join("build")
|
||||
.join("test-plugins")
|
||||
} else if let Some(parent) = prev_dir.parent() {
|
||||
parent
|
||||
.join("java")
|
||||
.join("patchbukkit")
|
||||
.join("build")
|
||||
.join("test-plugins")
|
||||
} else {
|
||||
std::path::PathBuf::from("java/patchbukkit/build/test-plugins")
|
||||
};
|
||||
|
||||
let protocollib_source = test_plugins_dir.join("ProtocolLib.jar");
|
||||
if protocollib_source.exists() {
|
||||
let _ = fs::copy(&protocollib_source, dirs.plugins.join("ProtocolLib.jar"));
|
||||
}
|
||||
|
||||
let plugin = PatchBukkitPlugin::new();
|
||||
patchbukkit::on_load_inner(&plugin, context.clone())
|
||||
.await
|
||||
.expect("Failed to on_load PatchBukkit");
|
||||
|
||||
// Wait for JVM initialization
|
||||
for _ in 0..100 {
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
let (tx, rx) = oneshot::channel();
|
||||
if plugin
|
||||
.command_tx
|
||||
.send(JvmCommand::TriggerCommand {
|
||||
full_command: "version".to_string(),
|
||||
command_sender: SimpleCommandSender::Console,
|
||||
respond_to: tx,
|
||||
})
|
||||
.await
|
||||
.is_ok()
|
||||
&& let Ok(res) = rx.await
|
||||
&& res.is_ok()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for ProtocolLib to finish initializing if present
|
||||
if protocollib_source.exists() {
|
||||
for _ in 0..150 {
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
let (tx, rx) = oneshot::channel();
|
||||
if plugin
|
||||
.command_tx
|
||||
.send(JvmCommand::TriggerCommand {
|
||||
full_command: "protocol".to_string(),
|
||||
command_sender: SimpleCommandSender::Console,
|
||||
respond_to: tx,
|
||||
})
|
||||
.await
|
||||
.is_ok()
|
||||
&& let Ok(res) = rx.await
|
||||
&& res.is_ok()
|
||||
{
|
||||
println!("ProtocolLib command responded successfully!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start Pumpkin server network accept loop in background
|
||||
let server_run = pumpkin_server.clone();
|
||||
let server_task = tokio::spawn(async move {
|
||||
server_run.start().await;
|
||||
});
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
// Connect fake bot
|
||||
println!("Connecting fake bot 'TestBot_1' to {server_addr}...");
|
||||
let bot = BotClient::connect(server_addr, "TestBot_1")
|
||||
.await
|
||||
.expect("Failed to connect bot");
|
||||
|
||||
// Await transition into Play state (completes Handshake -> Login -> Config -> Play)
|
||||
println!("Waiting for bot to enter Play state...");
|
||||
bot.wait_for_play(Duration::from_secs(30))
|
||||
.await
|
||||
.expect("Bot should enter Play state successfully");
|
||||
println!("Bot successfully in Play state!");
|
||||
|
||||
// Verify player is registered on server
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
let players = pumpkin_server.server.get_all_players();
|
||||
assert!(
|
||||
!players.is_empty(),
|
||||
"Server should have at least 1 player connected"
|
||||
);
|
||||
let player = players
|
||||
.iter()
|
||||
.find(|p| p.gameprofile.name == "TestBot_1")
|
||||
.expect("TestBot_1 should be registered on the server");
|
||||
assert_eq!(player.gameprofile.name, "TestBot_1");
|
||||
|
||||
// Send arm swing packet from bot
|
||||
bot.swing_arm()
|
||||
.await
|
||||
.expect("Bot should be able to send SSwingArm");
|
||||
|
||||
// Send movement packet from bot
|
||||
bot.move_to(0.0, 64.0, 0.0)
|
||||
.await
|
||||
.expect("Bot should be able to send SPlayerPosition");
|
||||
|
||||
// Send command packet from bot
|
||||
bot.send_command("version")
|
||||
.await
|
||||
.expect("Bot should be able to send SChatCommand");
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
// Disconnect bot
|
||||
bot.disconnect().await;
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
// Gracefully stop server
|
||||
pumpkin::stop_server();
|
||||
let _ = server_task.await;
|
||||
|
||||
// Gracefully unload PatchBukkit
|
||||
let _ = patchbukkit::on_unload_inner(&plugin, context.clone()).await;
|
||||
|
||||
// Restore working directory
|
||||
let _ = std::env::set_current_dir(prev_dir);
|
||||
tokio::task::spawn_blocking(move || drop(plugin))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
268
rust/tests/common/bot.rs
Normal file
268
rust/tests/common/bot.rs
Normal file
@ -0,0 +1,268 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use pumpkin_data::packet::CURRENT_MC_VERSION;
|
||||
use pumpkin_protocol::codec::var_int::VarInt;
|
||||
use pumpkin_protocol::java::client::config::{
|
||||
CConfigDisconnect, CConfigPing, CFinishConfig, CKnownPacks,
|
||||
};
|
||||
use pumpkin_protocol::java::client::login::{CLoginDisconnect, CLoginSuccess, CSetCompression};
|
||||
use pumpkin_protocol::java::client::play::{CKeepAlive, CPlayerPosition};
|
||||
use pumpkin_protocol::java::packet_decoder::TCPNetworkDecoder;
|
||||
use pumpkin_protocol::java::packet_encoder::TCPNetworkEncoder;
|
||||
use pumpkin_protocol::java::server::config::{SAcknowledgeFinishConfig, SConfigPong, SKnownPacks};
|
||||
use pumpkin_protocol::java::server::handshake::SHandShake;
|
||||
use pumpkin_protocol::java::server::login::{SLoginAcknowledged, SLoginStart};
|
||||
use pumpkin_protocol::java::server::play::{
|
||||
SChatCommand, SConfirmTeleport, SKeepAlive, SPlayerLoaded, SPlayerPosition, SSwingArm,
|
||||
};
|
||||
use pumpkin_protocol::ser::NetworkWriteExt;
|
||||
use pumpkin_protocol::{ClientPacket, ConnectionState, MultiVersionJavaPacket, ServerPacket};
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
use tokio::io::{BufReader, BufWriter};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, trace};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct BotClient {
|
||||
pub name: String,
|
||||
pub uuid: Uuid,
|
||||
network_writer: Arc<Mutex<TCPNetworkEncoder<BufWriter<OwnedWriteHalf>>>>,
|
||||
network_reader: Arc<Mutex<TCPNetworkDecoder<BufReader<OwnedReadHalf>>>>,
|
||||
pub in_play: Arc<AtomicBool>,
|
||||
pub closed: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl BotClient {
|
||||
fn write_packet<P: ClientPacket>(packet: &P) -> Result<Vec<u8>, anyhow::Error> {
|
||||
let mut buf = Vec::new();
|
||||
buf.write_var_int(&VarInt(P::to_id(CURRENT_MC_VERSION)))?;
|
||||
packet.write_packet_data(&mut buf, &CURRENT_MC_VERSION)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub async fn send_packet<P: ClientPacket>(&self, packet: &P) -> anyhow::Result<()> {
|
||||
let buf = Self::write_packet(packet)?;
|
||||
let mut encoder = self.network_writer.lock().await;
|
||||
encoder.write_packet(buf.into()).await?;
|
||||
encoder.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn connect(address: SocketAddr, name: &str) -> anyhow::Result<Arc<Self>> {
|
||||
let stream = TcpStream::connect(address).await?;
|
||||
let (read_half, write_half) = stream.into_split();
|
||||
let writer = Arc::new(Mutex::new(TCPNetworkEncoder::new(BufWriter::new(
|
||||
write_half,
|
||||
))));
|
||||
let reader = Arc::new(Mutex::new(TCPNetworkDecoder::new(BufReader::new(
|
||||
read_half,
|
||||
))));
|
||||
|
||||
let bot = Arc::new(Self {
|
||||
name: name.to_string(),
|
||||
uuid: Uuid::new_v4(),
|
||||
network_writer: writer,
|
||||
network_reader: reader,
|
||||
in_play: Arc::new(AtomicBool::new(false)),
|
||||
closed: Arc::new(AtomicBool::new(false)),
|
||||
});
|
||||
|
||||
debug!("[BOT] Connected TCP stream to {address}, sending SHandShake...");
|
||||
// 1. Send Handshake
|
||||
bot.send_packet(&SHandShake {
|
||||
protocol_version: VarInt(CURRENT_MC_VERSION.protocol_version()),
|
||||
server_address: address.ip().to_string().into_boxed_str(),
|
||||
server_port: address.port(),
|
||||
next_state: ConnectionState::Login,
|
||||
})
|
||||
.await?;
|
||||
|
||||
debug!("[BOT] Sent SHandShake, sending SLoginStart (name={name})...");
|
||||
// 2. Send Login Start
|
||||
bot.send_packet(&SLoginStart {
|
||||
name: name.to_string().into_boxed_str(),
|
||||
uuid: bot.uuid,
|
||||
})
|
||||
.await?;
|
||||
|
||||
// 3. Start packet processing loop in background
|
||||
let bot_clone = bot.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut connection_state = ConnectionState::Login;
|
||||
trace!("[BOT] Background reader loop started in state: {connection_state:?}");
|
||||
loop {
|
||||
if bot_clone.closed.load(Ordering::Relaxed) {
|
||||
trace!("[BOT] Reader loop exiting: closed flag set");
|
||||
break;
|
||||
}
|
||||
let packet_opt = {
|
||||
let mut reader_guard = bot_clone.network_reader.lock().await;
|
||||
reader_guard.get_raw_packet().await
|
||||
};
|
||||
|
||||
let raw = match packet_opt {
|
||||
Ok(raw) => raw,
|
||||
Err(e) => {
|
||||
debug!("[BOT] Reader loop error reading packet: {e:?}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
trace!(
|
||||
"[BOT] Received raw packet id=0x{:02X} ({}) len={} in state {:?}",
|
||||
raw.id,
|
||||
raw.id,
|
||||
raw.payload.len(),
|
||||
connection_state
|
||||
);
|
||||
|
||||
match connection_state {
|
||||
ConnectionState::Login => {
|
||||
let mut bytebuf = &raw.payload[..];
|
||||
if raw.id == CSetCompression::to_id(CURRENT_MC_VERSION) {
|
||||
trace!("[BOT] Login -> CSetCompression");
|
||||
if let Ok(pkt) =
|
||||
CSetCompression::read(&mut bytebuf, &CURRENT_MC_VERSION)
|
||||
{
|
||||
debug!("[BOT] Enabling compression threshold {}", pkt.threshold.0);
|
||||
bot_clone
|
||||
.network_reader
|
||||
.lock()
|
||||
.await
|
||||
.set_compression(pkt.threshold.0 as usize);
|
||||
bot_clone
|
||||
.network_writer
|
||||
.lock()
|
||||
.await
|
||||
.set_compression((pkt.threshold.0 as usize, 6));
|
||||
}
|
||||
} else if raw.id == CLoginSuccess::to_id(CURRENT_MC_VERSION) {
|
||||
debug!(
|
||||
"[BOT] Login -> CLoginSuccess! Sending SLoginAcknowledged and SKnownPacks"
|
||||
);
|
||||
let _ = bot_clone.send_packet(&SLoginAcknowledged).await;
|
||||
connection_state = ConnectionState::Config;
|
||||
let _ = bot_clone
|
||||
.send_packet(&SKnownPacks {
|
||||
known_packs: Vec::new(),
|
||||
})
|
||||
.await;
|
||||
} else if raw.id == CLoginDisconnect::to_id(CURRENT_MC_VERSION) {
|
||||
debug!("[BOT] Login -> CLoginDisconnect! Server kicked bot in login");
|
||||
break;
|
||||
} else {
|
||||
trace!("[BOT] Unhandled packet in Login state: id=0x{:02X}", raw.id);
|
||||
}
|
||||
}
|
||||
ConnectionState::Config => {
|
||||
let mut bytebuf = &raw.payload[..];
|
||||
if raw.id == CKnownPacks::to_id(CURRENT_MC_VERSION) {
|
||||
trace!("[BOT] Config -> CKnownPacks, responding SKnownPacks");
|
||||
let _ = bot_clone
|
||||
.send_packet(&SKnownPacks {
|
||||
known_packs: Vec::new(),
|
||||
})
|
||||
.await;
|
||||
} else if raw.id == CConfigPing::to_id(CURRENT_MC_VERSION) {
|
||||
trace!("[BOT] Config -> CConfigPing, responding SConfigPong");
|
||||
if let Ok(pkt) = CConfigPing::read(&mut bytebuf, &CURRENT_MC_VERSION) {
|
||||
let _ = bot_clone.send_packet(&SConfigPong { id: pkt.id }).await;
|
||||
}
|
||||
} else if raw.id == CFinishConfig::to_id(CURRENT_MC_VERSION) {
|
||||
debug!("[BOT] Config -> CFinishConfig! Transitioning to Play");
|
||||
let _ = bot_clone.send_packet(&SAcknowledgeFinishConfig).await;
|
||||
connection_state = ConnectionState::Play;
|
||||
} else if raw.id == CConfigDisconnect::to_id(CURRENT_MC_VERSION) {
|
||||
debug!(
|
||||
"[BOT] Config -> CConfigDisconnect! Server kicked bot in config"
|
||||
);
|
||||
break;
|
||||
} else {
|
||||
trace!(
|
||||
"[BOT] Unhandled packet in Config state: id=0x{:02X}",
|
||||
raw.id
|
||||
);
|
||||
}
|
||||
}
|
||||
ConnectionState::Play => {
|
||||
let mut bytebuf = &raw.payload[..];
|
||||
if raw.id == CKeepAlive::to_id(CURRENT_MC_VERSION) {
|
||||
if let Ok(pkt) = CKeepAlive::read(&mut bytebuf, &CURRENT_MC_VERSION) {
|
||||
let _ = bot_clone
|
||||
.send_packet(&SKeepAlive {
|
||||
keep_alive_id: pkt.keep_alive_id,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
} else if raw.id == CPlayerPosition::to_id(CURRENT_MC_VERSION) {
|
||||
debug!(
|
||||
"[BOT] Play -> CPlayerPosition! Confirming teleport and setting in_play=true"
|
||||
);
|
||||
if let Ok(pkt) =
|
||||
CPlayerPosition::read(&mut bytebuf, &CURRENT_MC_VERSION)
|
||||
{
|
||||
let _ = bot_clone
|
||||
.send_packet(&SConfirmTeleport {
|
||||
teleport_id: pkt.teleport_id,
|
||||
})
|
||||
.await;
|
||||
let _ = bot_clone.send_packet(&SPlayerLoaded).await;
|
||||
bot_clone.in_play.store(true, Ordering::Release);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
trace!("[BOT] Reader loop ended, marking closed=true");
|
||||
bot_clone.closed.store(true, Ordering::Release);
|
||||
});
|
||||
|
||||
Ok(bot)
|
||||
}
|
||||
|
||||
pub async fn wait_for_play(&self, timeout: Duration) -> anyhow::Result<()> {
|
||||
let start = std::time::Instant::now();
|
||||
while start.elapsed() < timeout {
|
||||
if self.in_play.load(Ordering::Acquire) {
|
||||
return Ok(());
|
||||
}
|
||||
if self.closed.load(Ordering::Acquire) {
|
||||
anyhow::bail!("Bot connection closed before entering Play state");
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
anyhow::bail!("Timed out waiting for bot to enter Play state");
|
||||
}
|
||||
|
||||
pub async fn send_command(&self, command: &str) -> anyhow::Result<()> {
|
||||
self.send_packet(&SChatCommand {
|
||||
command: command.trim_start_matches('/'),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn swing_arm(&self) -> anyhow::Result<()> {
|
||||
self.send_packet(&SSwingArm { hand: VarInt(0) }).await
|
||||
}
|
||||
|
||||
pub async fn move_to(&self, x: f64, y: f64, z: f64) -> anyhow::Result<()> {
|
||||
self.send_packet(&SPlayerPosition {
|
||||
position: Vector3::new(x, y, z),
|
||||
collision: 1, // on ground
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn disconnect(&self) {
|
||||
self.closed.store(true, Ordering::Release);
|
||||
let mut writer = self.network_writer.lock().await;
|
||||
let _ = writer.flush().await;
|
||||
}
|
||||
}
|
||||
1
rust/tests/common/mod.rs
Normal file
1
rust/tests/common/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod bot;
|
||||
@ -55,7 +55,8 @@ async fn test_pumpkin_server_with_patchbukkit_and_plugins() {
|
||||
));
|
||||
|
||||
// Setup directories
|
||||
let dirs = setup_directories(&context).expect("Failed to setup PatchBukkit directories");
|
||||
let dirs = setup_directories(&context, "patchbukkit-plugins")
|
||||
.expect("Failed to setup PatchBukkit directories");
|
||||
|
||||
// Resolve test plugins built in java/patchbukkit/build/test-plugins
|
||||
let test_plugins_dir = if prev_dir.join("java").exists() {
|
||||
@ -94,14 +95,40 @@ async fn test_pumpkin_server_with_patchbukkit_and_plugins() {
|
||||
);
|
||||
}
|
||||
|
||||
let grimac_system_source = std::path::PathBuf::from(
|
||||
"/home/alex/Documents/Development/Rust/Pumpkin/plugins/data/patchbukkit/patchbukkit-plugins/grimac-bukkit-2.3.74-61caa53.jar",
|
||||
);
|
||||
if grimac_system_source.exists() {
|
||||
let _ = fs::copy(&grimac_system_source, dirs.plugins.join("GrimAC.jar"));
|
||||
let grimac_source = test_plugins_dir.join("GrimAC.jar");
|
||||
if grimac_source.exists() {
|
||||
let _ = fs::copy(&grimac_source, dirs.plugins.join("GrimAC.jar"));
|
||||
println!("Copied GrimAC.jar to plugins directory: {:?}", dirs.plugins);
|
||||
}
|
||||
|
||||
let test_plugin_source = {
|
||||
let direct = if prev_dir.join("java").exists() {
|
||||
prev_dir.join("java/patchbukkit-test-plugin/build/libs/patchbukkit-test-plugin.jar")
|
||||
} else if let Some(parent) = prev_dir.parent() {
|
||||
parent.join("java/patchbukkit-test-plugin/build/libs/patchbukkit-test-plugin.jar")
|
||||
} else {
|
||||
std::path::PathBuf::from(
|
||||
"java/patchbukkit-test-plugin/build/libs/patchbukkit-test-plugin.jar",
|
||||
)
|
||||
};
|
||||
if direct.exists() {
|
||||
direct
|
||||
} else {
|
||||
test_plugins_dir.join("patchbukkit-test-plugin.jar")
|
||||
}
|
||||
};
|
||||
if test_plugin_source.exists() {
|
||||
fs::copy(
|
||||
&test_plugin_source,
|
||||
dirs.plugins.join("patchbukkit-test-plugin.jar"),
|
||||
)
|
||||
.expect("Failed to copy patchbukkit-test-plugin.jar to plugins dir");
|
||||
println!(
|
||||
"Copied patchbukkit-test-plugin.jar to plugins directory: {:?}",
|
||||
dirs.plugins
|
||||
);
|
||||
}
|
||||
|
||||
let plugin = PatchBukkitPlugin::new();
|
||||
|
||||
// Call on_load_inner
|
||||
@ -150,12 +177,11 @@ async fn test_pumpkin_server_with_patchbukkit_and_plugins() {
|
||||
let pe_res = rx.await.expect("Failed to receive packetevents response");
|
||||
assert!(
|
||||
pe_res.is_ok(),
|
||||
"packetevents command should execute successfully: {:?}",
|
||||
pe_res
|
||||
"packetevents command should execute successfully: {pe_res:?}"
|
||||
);
|
||||
}
|
||||
|
||||
if grimac_system_source.exists() {
|
||||
if grimac_source.exists() {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
plugin
|
||||
.command_tx
|
||||
@ -170,8 +196,7 @@ async fn test_pumpkin_server_with_patchbukkit_and_plugins() {
|
||||
let grim_res = rx.await.expect("Failed to receive grim response");
|
||||
assert!(
|
||||
grim_res.is_ok(),
|
||||
"grim command should execute successfully: {:?}",
|
||||
grim_res
|
||||
"grim command should execute successfully: {grim_res:?}"
|
||||
);
|
||||
|
||||
// Test PlayerJoinEvent with GrimAC and PacketEvents
|
||||
@ -212,8 +237,27 @@ async fn test_pumpkin_server_with_patchbukkit_and_plugins() {
|
||||
.expect("Failed to receive PlayerJoin response");
|
||||
assert!(
|
||||
!join_res.cancelled,
|
||||
"PlayerJoin should not be cancelled: {:?}",
|
||||
join_res
|
||||
"PlayerJoin should not be cancelled: {join_res:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// Run Bukkit API Conformance tests via patchbukkit-test-plugin
|
||||
if test_plugin_source.exists() {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
plugin
|
||||
.command_tx
|
||||
.send(JvmCommand::TriggerCommand {
|
||||
full_command: "pbtest all".to_string(),
|
||||
command_sender: SimpleCommandSender::Console,
|
||||
respond_to: tx,
|
||||
})
|
||||
.await
|
||||
.expect("Failed to send pbtest all TriggerCommand");
|
||||
|
||||
let pbtest_res = rx.await.expect("Failed to receive pbtest all response");
|
||||
assert!(
|
||||
pbtest_res.is_ok(),
|
||||
"pbtest all command should execute successfully: {pbtest_res:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user