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
|
||||||
/rust/target/
|
/rust/target/
|
||||||
/target/
|
/target/
|
||||||
|
**/target/
|
||||||
|
**/*.rs.bk
|
||||||
|
*.pdb
|
||||||
|
|
||||||
# Rust resources
|
# Rust resources
|
||||||
rust/resources/jassets/*
|
rust/resources/jassets/*
|
||||||
|
!rust/resources/jassets/.gitkeep
|
||||||
|
|
||||||
# Gradle cache
|
# Gradle cache
|
||||||
.gradle/
|
.gradle/
|
||||||
java/.gradle/
|
java/.gradle/
|
||||||
|
**/.gradle/
|
||||||
|
.gradletasknamecache
|
||||||
|
gradle-app.setting
|
||||||
|
|
||||||
# Build outputs
|
# Build outputs
|
||||||
build/
|
build/
|
||||||
@ -19,22 +26,72 @@ java/protoc-gen-ffi/bin/
|
|||||||
java/patchbukkit-test-plugin/build/
|
java/patchbukkit-test-plugin/build/
|
||||||
java/patchbukkit-test-plugin/bin/
|
java/patchbukkit-test-plugin/bin/
|
||||||
!rust/build/
|
!rust/build/
|
||||||
|
!rust/build/**
|
||||||
|
|
||||||
# Examples
|
# Java compiled classes and JAR packages
|
||||||
examples/*/bin
|
*.class
|
||||||
examples/*/run
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.ear
|
||||||
|
|
||||||
# Keep Gradle wrapper
|
# Keep Gradle wrapper
|
||||||
!gradle/wrapper/gradle-wrapper.jar
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
!java/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
|
# Never ignore source build dirs if any exist
|
||||||
!**/src/main/**/build/
|
!**/src/main/**/build/
|
||||||
!**/src/test/**/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
|
# IntelliJ IDEA
|
||||||
.idea/
|
.idea/
|
||||||
java/.idea/
|
java/.idea/
|
||||||
|
**/.idea/
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
*.ipr
|
||||||
|
|
||||||
# Eclipse
|
# Eclipse
|
||||||
.classpath
|
.classpath
|
||||||
@ -54,11 +111,20 @@ java/.idea/
|
|||||||
|
|
||||||
# VS Code
|
# VS Code
|
||||||
.vscode/
|
.vscode/
|
||||||
|
**/.vscode/
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# OS and Environment
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
# Runtime configs
|
.LSOverride
|
||||||
config/
|
._*
|
||||||
java/patchbukkit/config/
|
Thumbs.db
|
||||||
logs/
|
desktop.ini
|
||||||
java/patchbukkit/logs/
|
*~
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.direnv/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!*.example
|
||||||
|
|||||||
@ -27,3 +27,5 @@ tasks.withType<JavaCompile> {
|
|||||||
"-nowarn"
|
"-nowarn"
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -23,10 +23,10 @@ public final class EntityTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConformanceTest(name = "Server.getEntity(UUID) stub", category = TestCategory.ENTITY,
|
@ConformanceTest(name = "Server.getEntity(UUID) returns null for random UUID", category = TestCategory.ENTITY)
|
||||||
expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
|
||||||
public void testGetEntity() {
|
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)
|
@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()");
|
assertNotNull(stream, "Registry.SOUNDS.stream()");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConformanceTest(name = "Server.getRegistry(Sound.class) returns registry", category = TestCategory.REGISTRY,
|
@ConformanceTest(name = "Server.getRegistry(Sound.class) returns registry", category = TestCategory.REGISTRY)
|
||||||
expectation = TestExpectation.EXPECT_UNSUPPORTED)
|
|
||||||
public void testServerGetRegistry() {
|
public void testServerGetRegistry() {
|
||||||
// Server.getRegistry() returns null in PatchBukkit — might throw or return null
|
|
||||||
Registry<Sound> reg = Bukkit.getServer().getRegistry(Sound.class);
|
Registry<Sound> reg = Bukkit.getServer().getRegistry(Sound.class);
|
||||||
if (reg == null) {
|
assertNotNull(reg, "Server.getRegistry(Sound.class)");
|
||||||
throw new UnsupportedOperationException("Server.getRegistry() returned null (not implemented)");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,4 +102,106 @@ public final class ServerTests {
|
|||||||
assertTrue(cmd == null, "Server.getPluginCommand(unknown) should be null");
|
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 {
|
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)
|
@ConformanceTest(name = "PaperLib detection classes and PaperLib.isPaper() are available", category = TestCategory.STUBS)
|
||||||
public void testPaperLibDetection() throws ClassNotFoundException {
|
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;
|
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
|
||||||
|
|
||||||
public final class LibraryResolver {
|
public final class LibraryResolver {
|
||||||
|
private static final java.util.logging.Logger LOGGER =
|
||||||
|
java.util.logging.Logger.getLogger("LibraryResolver");
|
||||||
|
|
||||||
private LibraryResolver() {}
|
private LibraryResolver() {}
|
||||||
|
|
||||||
public static List<File> resolveLibraries(
|
public static List<File> resolveLibraries(
|
||||||
@ -32,6 +35,14 @@ public final class LibraryResolver {
|
|||||||
return List.of();
|
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<>();
|
Set<String> unique = new LinkedHashSet<>();
|
||||||
for (String line : coordinates.split("\n")) {
|
for (String line : coordinates.split("\n")) {
|
||||||
String trimmed = line.trim();
|
String trimmed = line.trim();
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import java.util.Date;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@ -727,21 +728,70 @@ public class PatchBukkitServer implements Server {
|
|||||||
rootConfig.removeAppender("ServerGuiConsole");
|
rootConfig.removeAppender("ServerGuiConsole");
|
||||||
rootConfig.removeAppender("Async");
|
rootConfig.removeAppender("Async");
|
||||||
|
|
||||||
rootConfig.addAppender(appender, org.apache.logging.log4j.Level.ALL, null);
|
org.apache.logging.log4j.Level targetLevel = getEffectiveLog4jLevel();
|
||||||
rootConfig.setLevel(org.apache.logging.log4j.Level.ALL);
|
rootConfig.addAppender(appender, targetLevel, null);
|
||||||
|
rootConfig.setLevel(targetLevel);
|
||||||
context.updateLoggers();
|
context.updateLoggers();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
ORIGINAL_ERR.println("[PatchBukkit] Failed to initialize Log4j native bridge appender: " + t.getMessage());
|
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() {
|
private static void configureJulLogger() {
|
||||||
Logger root = Logger.getLogger("");
|
Logger root = Logger.getLogger("");
|
||||||
root.setUseParentHandlers(false);
|
root.setUseParentHandlers(false);
|
||||||
for (java.util.logging.Handler h : root.getHandlers()) {
|
for (java.util.logging.Handler h : root.getHandlers()) {
|
||||||
root.removeHandler(h);
|
root.removeHandler(h);
|
||||||
}
|
}
|
||||||
root.setLevel(Level.ALL);
|
root.setLevel(getEffectiveJulLevel());
|
||||||
try {
|
try {
|
||||||
root.addHandler(new org.bukkit.craftbukkit.util.ForwardLogHandler());
|
root.addHandler(new org.bukkit.craftbukkit.util.ForwardLogHandler());
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
|
|||||||
@ -71,8 +71,13 @@ public class PatchBukkitUnsafeValues implements UnsafeValues {
|
|||||||
public boolean isSupportedApiVersion(String apiVersion) {
|
public boolean isSupportedApiVersion(String apiVersion) {
|
||||||
if (apiVersion == null) return false;
|
if (apiVersion == null) return false;
|
||||||
final ApiVersion toCheck = ApiVersion.getOrCreateVersion(apiVersion);
|
final ApiVersion toCheck = ApiVersion.getOrCreateVersion(apiVersion);
|
||||||
var minimumApi = NativeBridgeFfi.getPatchBukkitConfig(EmptyRequest.newBuilder().build()).getMinimumSupportedPluginApi();
|
String minimumApi = null;
|
||||||
final ApiVersion minimumVersion = ApiVersion.getOrCreateVersion(minimumApi);
|
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);
|
return !toCheck.isNewerThan(ApiVersion.CURRENT) && !toCheck.isOlderThan(minimumVersion);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -297,13 +297,17 @@ public class PatchBukkitEntity implements Entity {
|
|||||||
public @NotNull Location getLocation() {
|
public @NotNull Location getLocation() {
|
||||||
try {
|
try {
|
||||||
var location = NativeBridgeFfi.getLocation(BridgeUtils.convertUuid(this.uuid));
|
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 world = PatchBukkitWorld.getOrCreate(BridgeUtils.convertUuid(location.getWorld().getUuid()));
|
||||||
var position = location.getPosition();
|
var position = location.getPosition();
|
||||||
return new Location(world, position.getX(), position.getY(), position.getZ(), location.getYaw(), location.getPitch());
|
return new Location(world, position.getX(), position.getY(), position.getZ(), location.getYaw(), location.getPitch());
|
||||||
}
|
}
|
||||||
} catch (Throwable ignored) {}
|
} 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
|
@Override
|
||||||
@ -387,9 +391,17 @@ public class PatchBukkitEntity implements Entity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull World getWorld() {
|
public @NotNull World getWorld() {
|
||||||
|
try {
|
||||||
var location = NativeBridgeFfi.getLocation(BridgeUtils.convertUuid(this.uuid));
|
var location = NativeBridgeFfi.getLocation(BridgeUtils.convertUuid(this.uuid));
|
||||||
|
if (location != null && location.hasWorld() && location.getWorld().hasUuid()) {
|
||||||
return PatchBukkitWorld.getOrCreate(BridgeUtils.convertUuid(location.getWorld().getUuid()));
|
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
|
@Override
|
||||||
public void setRotation(float yaw, float pitch) {
|
public void setRotation(float yaw, float pitch) {
|
||||||
|
|||||||
@ -38,12 +38,32 @@ public class PatchBukkitBootstrap {
|
|||||||
return true;
|
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<>();
|
Map<String, PluginHolder> holders = new LinkedHashMap<>();
|
||||||
|
|
||||||
for (File jarFile : jarFiles) {
|
for (File jarFile : jarFiles) {
|
||||||
try {
|
try {
|
||||||
PluginHolder holder = parsePluginHolder(jarFile);
|
PluginHolder holder = parsePluginHolder(jarFile);
|
||||||
if (holder != null) {
|
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);
|
holders.put(holder.name.toLowerCase(Locale.ENGLISH), holder);
|
||||||
}
|
}
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
|
|||||||
@ -17,7 +17,10 @@ public class CloudReflectionTest {
|
|||||||
public void testGrimAcCloudReflection() throws Exception {
|
public void testGrimAcCloudReflection() throws Exception {
|
||||||
PatchBukkitServer.initServer();
|
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;
|
if (!grimFile.exists()) return;
|
||||||
|
|
||||||
PatchBukkitPluginClassLoader loader = new PatchBukkitPluginClassLoader(
|
PatchBukkitPluginClassLoader loader = new PatchBukkitPluginClassLoader(
|
||||||
|
|||||||
@ -75,7 +75,7 @@ public class ProtocolPluginsLoadTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLoadGrimAC() throws Exception {
|
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()) {
|
if (!grimFile.exists()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,4 +9,9 @@ option java_package = "patchbukkit.config";
|
|||||||
|
|
||||||
message GetPatchBukkitConfigResponse {
|
message GetPatchBukkitConfigResponse {
|
||||||
string minimum_supported_plugin_api = 1;
|
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};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)]
|
||||||
pub struct PatchBukkitConfig {
|
pub struct PatchBukkitConfig {
|
||||||
|
#[serde(default)]
|
||||||
pub settings: SettingsConfig,
|
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")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct SettingsConfig {
|
pub struct SettingsConfig {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub minimum_supported_plugin_api: Option<String>,
|
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 {
|
impl PatchBukkitConfig {
|
||||||
pub const DEFAULT: PatchBukkitConfig = PatchBukkitConfig {
|
pub const DEFAULT: PatchBukkitConfig = PatchBukkitConfig {
|
||||||
settings: SettingsConfig {
|
settings: SettingsConfig {
|
||||||
minimum_supported_plugin_api: None,
|
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> {
|
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> {
|
pub fn get_or_create<P: AsRef<Path>>(config_path: P) -> anyhow::Result<Self> {
|
||||||
let config_path = config_path.as_ref();
|
let config_path = config_path.as_ref();
|
||||||
if !config_path.exists() {
|
if !config_path.exists() {
|
||||||
let default = PatchBukkitConfig::DEFAULT;
|
let default = PatchBukkitConfig::default();
|
||||||
std::fs::write(config_path, &toml::to_string_pretty(&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);
|
return Ok(default);
|
||||||
}
|
}
|
||||||
let config = PatchBukkitConfig::parse(fs::read_to_string(config_path)?)?;
|
let config = PatchBukkitConfig::parse(fs::read_to_string(config_path)?)?;
|
||||||
Ok(config)
|
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;
|
use pumpkin::plugin::Context;
|
||||||
|
|
||||||
@ -9,7 +12,19 @@ pub struct PatchBukkitDirectories {
|
|||||||
pub jassets: PathBuf,
|
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())
|
let data_folder = std::path::absolute(server.get_data_folder())
|
||||||
.map_err(|_| "Failed to get absolute directory from relative")?;
|
.map_err(|_| "Failed to get absolute directory from relative")?;
|
||||||
let server_root = data_folder
|
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")?;
|
.ok_or("Failed to determine server root from PatchBukkit data folder")?;
|
||||||
let base = server_root.join("patchbukkit");
|
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 plugin_updates = plugins.join("update");
|
||||||
let jassets = base.join("jassets");
|
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:?}"))?;
|
.map_err(|err| format!("Failed to create jassets folder: {err:?}"))?;
|
||||||
|
|
||||||
fs::create_dir_all(&plugins)
|
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");
|
let patchbukkit_jar_dest = jassets.join("patchbukkit.jar");
|
||||||
|
|
||||||
|
|||||||
@ -6605,6 +6605,14 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
Box::pin(async move {
|
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();
|
let (tx, rx) = oneshot::channel();
|
||||||
if let Err(e) = command_tx
|
if let Err(e) = command_tx
|
||||||
.send(JvmCommand::FireEvent {
|
.send(JvmCommand::FireEvent {
|
||||||
@ -6634,6 +6642,14 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
Box::pin(async move {
|
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();
|
let (tx, rx) = oneshot::channel();
|
||||||
if let Err(e) = command_tx
|
if let Err(e) = command_tx
|
||||||
.send(JvmCommand::FireEvent {
|
.send(JvmCommand::FireEvent {
|
||||||
|
|||||||
@ -55,11 +55,11 @@ impl JvmWorker {
|
|||||||
context.clone(),
|
context.clone(),
|
||||||
runtime_handle,
|
runtime_handle,
|
||||||
command_tx.clone(),
|
command_tx.clone(),
|
||||||
config,
|
config.clone(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
self.context = Some(context);
|
self.context = Some(context);
|
||||||
let result = self.initialize_jvm(&jassets_path);
|
let result = self.initialize_jvm(&jassets_path, &config);
|
||||||
let _ = respond_to.send(result);
|
let _ = respond_to.send(result);
|
||||||
}
|
}
|
||||||
JvmCommand::LoadPlugin {
|
JvmCommand::LoadPlugin {
|
||||||
@ -234,7 +234,11 @@ impl JvmWorker {
|
|||||||
tracing::info!("JVM worker thread exited");
|
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:?}");
|
tracing::info!("Initializing JVM with assets path: {jassets_path:?}");
|
||||||
|
|
||||||
let mut jar_paths = Vec::new();
|
let mut jar_paths = Vec::new();
|
||||||
@ -284,7 +288,7 @@ impl JvmWorker {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(separator);
|
.join(separator);
|
||||||
|
|
||||||
let jvm_args = InitArgsBuilder::new()
|
let mut builder = InitArgsBuilder::new()
|
||||||
.version(JNIVersion::V21)
|
.version(JNIVersion::V21)
|
||||||
.option(format!("-Djava.class.path={classpath}"))
|
.option(format!("-Djava.class.path={classpath}"))
|
||||||
.option("-XX:+IgnoreUnrecognizedVMOptions")
|
.option("-XX:+IgnoreUnrecognizedVMOptions")
|
||||||
@ -292,7 +296,28 @@ impl JvmWorker {
|
|||||||
.option("--enable-final-field-mutation=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=ALL-UNNAMED")
|
||||||
.option("--add-opens=java.base/java.lang.reflect=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()
|
.build()
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to build JVM init args: {e:?}"))?;
|
.map_err(|e| anyhow::anyhow!("Failed to build JVM init args: {e:?}"))?;
|
||||||
|
|
||||||
|
|||||||
@ -54,7 +54,7 @@ pub fn ffi_native_bridge_register_command_impl(request: RegisterCommandRequest)
|
|||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.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 clean_perm = cmd_name.trim_start_matches('/');
|
||||||
let permission = format!("patchbukkit:command.{clean_perm}");
|
let permission = format!("patchbukkit:command.{clean_perm}");
|
||||||
|
|||||||
@ -14,6 +14,11 @@ pub fn ffi_native_bridge_get_patch_bukkit_config_impl(
|
|||||||
.settings
|
.settings
|
||||||
.minimum_supported_plugin_api
|
.minimum_supported_plugin_api
|
||||||
.clone()
|
.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| {
|
with_player(request.uuid.as_ref(), |player| {
|
||||||
let position = pumpkin_util::math::vector3::Vector3::new(pos.x, pos.y, pos.z);
|
let position = pumpkin_util::math::vector3::Vector3::new(pos.x, pos.y, pos.z);
|
||||||
let world = player.living_entity.entity.world.load_full();
|
let world = player.living_entity.entity.world.load_full();
|
||||||
let player = player.clone();
|
|
||||||
ctx.runtime.spawn(async move {
|
ctx.runtime.spawn(async move {
|
||||||
player.teleport(position, Some(yaw), Some(pitch), world);
|
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| {
|
with_player(request.uuid.as_ref(), |player| {
|
||||||
let header = request.header;
|
let header = request.header;
|
||||||
let footer = request.footer;
|
let footer = request.footer;
|
||||||
let player = player.clone();
|
|
||||||
ctx.runtime.spawn(async move {
|
ctx.runtime.spawn(async move {
|
||||||
let h = pumpkin_util::text::TextComponent::from_legacy_string(&header);
|
let h = pumpkin_util::text::TextComponent::from_legacy_string(&header);
|
||||||
let f = pumpkin_util::text::TextComponent::from_legacy_string(&footer);
|
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| {
|
with_player(request.uuid.as_ref(), |player| {
|
||||||
let time = request.time;
|
let time = request.time;
|
||||||
let relative = request.relative;
|
let relative = request.relative;
|
||||||
let player = player.clone();
|
|
||||||
ctx.runtime.spawn(async move {
|
ctx.runtime.spawn(async move {
|
||||||
let packet = pumpkin_protocol::java::client::play::CUpdateTime::new(0, time, relative);
|
let packet = pumpkin_protocol::java::client::play::CUpdateTime::new(0, time, relative);
|
||||||
player.send_client_packet(&packet).await;
|
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<()> {
|
pub fn ffi_native_bridge_reset_player_time_impl(request: Uuid) -> Option<()> {
|
||||||
let ctx = CALLBACK_CONTEXT.get()?;
|
let ctx = CALLBACK_CONTEXT.get()?;
|
||||||
with_player(Some(&request), |player| {
|
with_player(Some(&request), |player| {
|
||||||
let player = player.clone();
|
|
||||||
ctx.runtime.spawn(async move {
|
ctx.runtime.spawn(async move {
|
||||||
let world_time = player
|
let world_time = player
|
||||||
.world()
|
.world()
|
||||||
@ -461,7 +457,6 @@ pub fn ffi_native_bridge_set_player_weather_impl(request: SetPlayerWeatherReques
|
|||||||
let ctx = CALLBACK_CONTEXT.get()?;
|
let ctx = CALLBACK_CONTEXT.get()?;
|
||||||
with_player(request.uuid.as_ref(), |player| {
|
with_player(request.uuid.as_ref(), |player| {
|
||||||
let weather = request.weather;
|
let weather = request.weather;
|
||||||
let player = player.clone();
|
|
||||||
ctx.runtime.spawn(async move {
|
ctx.runtime.spawn(async move {
|
||||||
let event = if weather == 1 {
|
let event = if weather == 1 {
|
||||||
pumpkin_protocol::java::client::play::GameEvent::BeginRaining
|
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<()> {
|
pub fn ffi_native_bridge_reset_player_weather_impl(request: Uuid) -> Option<()> {
|
||||||
let ctx = CALLBACK_CONTEXT.get()?;
|
let ctx = CALLBACK_CONTEXT.get()?;
|
||||||
with_player(Some(&request), |player| {
|
with_player(Some(&request), |player| {
|
||||||
let player = player.clone();
|
|
||||||
ctx.runtime.spawn(async move {
|
ctx.runtime.spawn(async move {
|
||||||
let is_raining = player.world().is_raining();
|
let is_raining = player.world().is_raining();
|
||||||
let event = if 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 ctx = CALLBACK_CONTEXT.get()?;
|
||||||
let pos = request.position?;
|
let pos = request.position?;
|
||||||
with_player(request.uuid.as_ref(), |player| {
|
with_player(request.uuid.as_ref(), |player| {
|
||||||
let player = player.clone();
|
|
||||||
ctx.runtime.spawn(async move {
|
ctx.runtime.spawn(async move {
|
||||||
let block_pos = pumpkin_util::math::position::BlockPos::new(
|
let block_pos = pumpkin_util::math::position::BlockPos::new(
|
||||||
pos.x as i32,
|
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<()> {
|
pub fn ffi_native_bridge_send_block_change_impl(request: SendBlockChangeRequest) -> Option<()> {
|
||||||
let ctx = CALLBACK_CONTEXT.get()?;
|
let ctx = CALLBACK_CONTEXT.get()?;
|
||||||
with_player(request.uuid.as_ref(), |player| {
|
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 pos = pumpkin_util::math::position::BlockPos::new(request.x, request.y, request.z);
|
||||||
let state_str = request.block_state;
|
let state_str = request.block_state;
|
||||||
let clean_key = state_str
|
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<()> {
|
pub fn ffi_native_bridge_send_resource_pack_impl(request: SendResourcePackRequest) -> Option<()> {
|
||||||
let ctx = CALLBACK_CONTEXT.get()?;
|
let ctx = CALLBACK_CONTEXT.get()?;
|
||||||
with_player(request.uuid.as_ref(), |player| {
|
with_player(request.uuid.as_ref(), |player| {
|
||||||
let player = player.clone();
|
|
||||||
let url = request.url;
|
let url = request.url;
|
||||||
let prompt = request.prompt;
|
let prompt = request.prompt;
|
||||||
let required = request.required;
|
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<()> {
|
pub fn ffi_native_bridge_send_game_event_impl(request: SendGameEventRequest) -> Option<()> {
|
||||||
let ctx = CALLBACK_CONTEXT.get()?;
|
let ctx = CALLBACK_CONTEXT.get()?;
|
||||||
with_player(request.uuid.as_ref(), |player| {
|
with_player(request.uuid.as_ref(), |player| {
|
||||||
let player = player.clone();
|
|
||||||
let event_type = request.event_type as u8;
|
let event_type = request.event_type as u8;
|
||||||
let value = request.value;
|
let value = request.value;
|
||||||
ctx.runtime.spawn(async move {
|
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<()> {
|
pub fn ffi_native_bridge_send_log_impl(request: SendLogRequest) -> Option<()> {
|
||||||
let logger = &request.logger_name;
|
let logger = &request.logger_name;
|
||||||
@ -9,6 +12,39 @@ pub fn ffi_native_bridge_send_log_impl(request: SendLogRequest) -> Option<()> {
|
|||||||
return Some(());
|
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() {
|
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),
|
||||||
|
|||||||
@ -50,9 +50,9 @@ pub use command::*;
|
|||||||
pub mod server;
|
pub mod server;
|
||||||
pub use 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 plugin_context: Arc<Context>,
|
||||||
pub runtime: tokio::runtime::Handle,
|
pub runtime: tokio::runtime::Handle,
|
||||||
pub command_tx: mpsc::Sender<JvmCommand>,
|
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() {
|
if let Ok(list) = ctx.plugin_context.server.data.banned_ip_list.try_read() {
|
||||||
for entry in &list.banned_ips {
|
for entry in &list.banned_ips {
|
||||||
let created = entry.created.unix_timestamp();
|
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 {
|
entries.push(BanEntryProto {
|
||||||
target: entry.ip.to_string(),
|
target: entry.ip.to_string(),
|
||||||
source: entry.source.clone(),
|
source: entry.source.clone(),
|
||||||
@ -322,11 +324,12 @@ pub fn ffi_native_bridge_get_ban_list_impl(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if let Ok(list) = ctx.plugin_context.server.data.banned_player_list.try_read() {
|
||||||
if let Ok(list) = ctx.plugin_context.server.data.banned_player_list.try_read() {
|
|
||||||
for entry in &list.banned_players {
|
for entry in &list.banned_players {
|
||||||
let created = entry.created.unix_timestamp();
|
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 {
|
entries.push(BanEntryProto {
|
||||||
target: entry.name.clone(),
|
target: entry.name.clone(),
|
||||||
source: entry.source.clone(),
|
source: entry.source.clone(),
|
||||||
@ -336,7 +339,6 @@ pub fn ffi_native_bridge_get_ban_list_impl(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Some(GetBanListResponse { entries })
|
Some(GetBanListResponse { entries })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -614,9 +614,8 @@ pub fn ffi_native_bridge_save_world_impl(request: SaveWorldRequest) -> Option<()
|
|||||||
.cloned()
|
.cloned()
|
||||||
.or_else(|| worlds.first().cloned())?;
|
.or_else(|| worlds.first().cloned())?;
|
||||||
|
|
||||||
let w = world.clone();
|
|
||||||
ctx.runtime.spawn(async move {
|
ctx.runtime.spawn(async move {
|
||||||
let _ = w.save().await;
|
let _ = world.save().await;
|
||||||
});
|
});
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
|
|||||||
@ -19,7 +19,7 @@ impl CommandManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_tab_complete(
|
pub fn get_tab_complete(
|
||||||
&mut self,
|
&self,
|
||||||
env: &mut Env,
|
env: &mut Env,
|
||||||
sender: SimpleCommandSender,
|
sender: SimpleCommandSender,
|
||||||
full_command: String,
|
full_command: String,
|
||||||
@ -35,7 +35,7 @@ impl CommandManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn try_tab_complete(
|
fn try_tab_complete(
|
||||||
&mut self,
|
&self,
|
||||||
env: &mut Env,
|
env: &mut Env,
|
||||||
sender: SimpleCommandSender,
|
sender: SimpleCommandSender,
|
||||||
full_command: String,
|
full_command: String,
|
||||||
|
|||||||
@ -25,7 +25,7 @@ fn cleanup_stale_files(resources_folder: &Path) {
|
|||||||
|
|
||||||
for entry in walkdir::WalkDir::new(resources_folder)
|
for entry in walkdir::WalkDir::new(resources_folder)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(std::result::Result::ok)
|
||||||
.filter(|e| e.file_type().is_file())
|
.filter(|e| e.file_type().is_file())
|
||||||
{
|
{
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
@ -41,7 +41,7 @@ fn cleanup_stale_files(resources_folder: &Path) {
|
|||||||
for entry in walkdir::WalkDir::new(resources_folder)
|
for entry in walkdir::WalkDir::new(resources_folder)
|
||||||
.contents_first(true)
|
.contents_first(true)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(std::result::Result::ok)
|
||||||
.filter(|e| e.file_type().is_dir() && e.path() != resources_folder)
|
.filter(|e| e.file_type().is_dir() && e.path() != resources_folder)
|
||||||
{
|
{
|
||||||
let _ = fs::remove_dir(entry.path());
|
let _ = fs::remove_dir(entry.path());
|
||||||
|
|||||||
@ -12,7 +12,7 @@ pub mod events;
|
|||||||
pub mod java;
|
pub mod java;
|
||||||
pub mod proto;
|
pub mod proto;
|
||||||
|
|
||||||
use directories::setup_directories;
|
use directories::{get_base_directory, setup_directories};
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -27,20 +27,24 @@ pub async fn on_load_inner(plugin: &PatchBukkitPlugin, server: Arc<Context>) ->
|
|||||||
server.init_log();
|
server.init_log();
|
||||||
tracing::info!("Starting PatchBukkit");
|
tracing::info!("Starting PatchBukkit");
|
||||||
|
|
||||||
// Setup directories
|
let base = get_base_directory(&server)?;
|
||||||
let dirs = setup_directories(&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");
|
config_path.push("patchbukkit.config.toml");
|
||||||
let config = PatchBukkitConfig::get_or_create(config_path)
|
let config = PatchBukkitConfig::get_or_create(config_path)
|
||||||
.map_err(|e| format!("Failed to setup PatchBukkit config: {e}"))?;
|
.map_err(|e| format!("Failed to setup PatchBukkit config: {e}"))?;
|
||||||
|
|
||||||
|
// Setup directories
|
||||||
|
let dirs = setup_directories(&server, &config.plugins.directory)?;
|
||||||
|
|
||||||
// Manage embedded resources
|
// Manage embedded resources
|
||||||
setup_resources(&dirs.jassets).map_err(|e| format!("Failed to setup resources: {e}"))?;
|
setup_resources(&dirs.jassets).map_err(|e| format!("Failed to setup resources: {e}"))?;
|
||||||
|
|
||||||
let runtime_handle = plugin.runtime.handle().clone();
|
let runtime_handle = plugin.runtime.handle().clone();
|
||||||
let command_tx = plugin.command_tx.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
|
// Run JVM initialization and Java plugin bootstrap in a background task
|
||||||
// on PatchBukkit's dedicated runtime so Pumpkin's main startup is never blocked.
|
// 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
|
// 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
|
// Resolve test plugins built in java/patchbukkit/build/test-plugins
|
||||||
let test_plugins_dir = if prev_dir.join("java").exists() {
|
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(
|
let grimac_source = test_plugins_dir.join("GrimAC.jar");
|
||||||
"/home/alex/Documents/Development/Rust/Pumpkin/plugins/data/patchbukkit/patchbukkit-plugins/grimac-bukkit-2.3.74-61caa53.jar",
|
if grimac_source.exists() {
|
||||||
);
|
let _ = fs::copy(&grimac_source, dirs.plugins.join("GrimAC.jar"));
|
||||||
if grimac_system_source.exists() {
|
|
||||||
let _ = fs::copy(&grimac_system_source, dirs.plugins.join("GrimAC.jar"));
|
|
||||||
println!("Copied GrimAC.jar to plugins directory: {:?}", dirs.plugins);
|
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();
|
let plugin = PatchBukkitPlugin::new();
|
||||||
|
|
||||||
// Call on_load_inner
|
// 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");
|
let pe_res = rx.await.expect("Failed to receive packetevents response");
|
||||||
assert!(
|
assert!(
|
||||||
pe_res.is_ok(),
|
pe_res.is_ok(),
|
||||||
"packetevents command should execute successfully: {:?}",
|
"packetevents command should execute successfully: {pe_res:?}"
|
||||||
pe_res
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if grimac_system_source.exists() {
|
if grimac_source.exists() {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
plugin
|
plugin
|
||||||
.command_tx
|
.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");
|
let grim_res = rx.await.expect("Failed to receive grim response");
|
||||||
assert!(
|
assert!(
|
||||||
grim_res.is_ok(),
|
grim_res.is_ok(),
|
||||||
"grim command should execute successfully: {:?}",
|
"grim command should execute successfully: {grim_res:?}"
|
||||||
grim_res
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Test PlayerJoinEvent with GrimAC and PacketEvents
|
// 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");
|
.expect("Failed to receive PlayerJoin response");
|
||||||
assert!(
|
assert!(
|
||||||
!join_res.cancelled,
|
!join_res.cancelled,
|
||||||
"PlayerJoin should not be cancelled: {:?}",
|
"PlayerJoin should not be cancelled: {join_res:?}"
|
||||||
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