diff --git a/api/src/main/java/net/elytrium/limboapi/api/LimboSessionHandler.java b/api/src/main/java/net/elytrium/limboapi/api/LimboSessionHandler.java index ce0212a4..5f0fc799 100644 --- a/api/src/main/java/net/elytrium/limboapi/api/LimboSessionHandler.java +++ b/api/src/main/java/net/elytrium/limboapi/api/LimboSessionHandler.java @@ -8,6 +8,7 @@ package net.elytrium.limboapi.api; import net.elytrium.limboapi.api.player.LimboPlayer; +import net.kyori.adventure.key.Key; public interface LimboSessionHandler { @@ -50,6 +51,19 @@ default void onGeneric(Object packet) { } + /** + * Called when the client sends a cookie response (reply to {@code Player#requestCookie}) while + * the player is still inside the Limbo. The response is also buffered and replayed as a + * {@code CookieReceiveEvent} when the player is handed back to Velocity, so this hook is meant + * for handlers that need the cookie value during the Limbo session itself. + * + * @param key the cookie key + * @param data the cookie payload + */ + default void onCookieResponse(Key key, byte[] data) { + + } + default void onDisconnect() { } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 221c4f98..efb903f9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/plugin/build.gradle b/plugin/build.gradle index e6801031..b46387a0 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -276,6 +276,43 @@ File getServerJar(String version) { return jarFile } +// Some Minecraft versions require a newer JDK to run their data generator than the JDK running Gradle +// (e.g. 26.1 needs Java 25). Pick a suitable installed JDK by reading sibling JDKs' "release" files. +@SuppressWarnings('GrMethodMayBeStatic') +File selectJavaHome(int requiredMajor) { + File current = new File(System.getProperty("java.home")) + if (requiredMajor <= Runtime.version().feature()) { + return current + } + + boolean win = System.getProperty("os.name").toLowerCase().contains("win") + File jdksDir = current.getParentFile() + File best = null + int bestMajor = Integer.MAX_VALUE + jdksDir.listFiles()?.each { File dir -> + File release = new File(dir, "release") + File javaExe = new File(dir, win ? "bin/java.exe" : "bin/java") + if (!release.exists() || !javaExe.exists()) { + return + } + String verLine = release.readLines().find { it.startsWith("JAVA_VERSION=") } + if (verLine == null) { + return + } + String ver = verLine.substring(verLine.indexOf('=') + 1).replace('"', '').trim() + int major = ver.startsWith("1.") ? Integer.parseInt(ver.split("\\.")[1]) : Integer.parseInt(ver.split("[._-]")[0]) + if (major >= requiredMajor && major < bestMajor) { + best = dir + bestMajor = major + } + } + if (best == null) { + throw new RuntimeException("No installed JDK satisfies required Java ${requiredMajor} (searched ${jdksDir}). Install JDK ${requiredMajor}+.") + } + this.println("> Using JDK ${bestMajor} at ${best} for data generation (version requires Java ${requiredMajor})") + return best +} + File generateData(MinecraftVersion version) { File cache = getGeneratedCache(version) if (cache != null) { @@ -292,6 +329,10 @@ File generateData(MinecraftVersion version) { // Ignored. } + Object versionManifest = new JsonSlurper().parse(new File(parent, "manifest.json")) + int requiredJava = (versionManifest.javaVersion?.majorVersion ?: Runtime.version().feature()) as int + File javaHome = this.selectJavaHome(requiredJava) + String command if (version >= MinecraftVersion.MINECRAFT_1_18) { command = "\"%s\" -DbundlerMainClass=net.minecraft.data.Main -jar \"${jarFile.getAbsolutePath()}\" --reports --server" @@ -301,13 +342,15 @@ File generateData(MinecraftVersion version) { List commandLine; if (System.getProperty("os.name").toLowerCase().contains("win")) { - File java = new File(System.getProperty("java.home"), "bin/java.exe") + File java = new File(javaHome, "bin/java.exe") commandLine = ["cmd", "/c", String.format(command, java)] } else { - File java = new File(System.getProperty("java.home"), "bin/java") + File java = new File(javaHome, "bin/java") commandLine = ["bash", "-c", String.format(command, java)] } - commandLine.execute([], parent).waitFor() + Process process = commandLine.execute([], parent) + process.consumeProcessOutput() // drain stdout/stderr so the generator can't deadlock on a full pipe buffer + process.waitFor() // Remove/compact files, reduces disk usage from ~2.9gb to ~92mb (or ~9.5mb on a compressed filesystem) jarFile.delete() diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java index c312718f..64408aff 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java @@ -39,6 +39,7 @@ import com.velocitypowered.api.event.connection.LoginEvent; import com.velocitypowered.api.event.connection.PostLoginEvent; import com.velocitypowered.api.event.permission.PermissionsSetupEvent; +import com.velocitypowered.api.event.player.CookieReceiveEvent; import com.velocitypowered.api.event.player.GameProfileRequestEvent; import com.velocitypowered.api.event.player.PlayerClientBrandEvent; import com.velocitypowered.api.network.ProtocolVersion; @@ -48,6 +49,7 @@ import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.client.AuthSessionHandler; import com.velocitypowered.proxy.connection.client.ClientConfigSessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; @@ -55,6 +57,7 @@ import com.velocitypowered.proxy.network.Connections; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; +import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -77,6 +80,7 @@ import net.elytrium.limboapi.injection.login.confirmation.LoginConfirmHandler; import net.elytrium.limboapi.server.LimboSessionHandlerImpl; import net.elytrium.limboapi.utils.LambdaUtil; +import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import org.slf4j.Logger; @@ -272,6 +276,26 @@ private void connectToServer(Logger logger, ConnectedPlayer player, MinecraftCon throw new ReflectionException(e); } } + + for (ServerboundCookieResponsePacket cookie : sessionHandler.getCookies()) { + // Replay cookie responses buffered while in the Limbo, now that the player is being + // handed back to Velocity. Mirrors ClientPlaySessionHandler: fire CookieReceiveEvent + // and forward the (possibly rewritten) response to the backend when one is connected. + this.server.getEventManager() + .fire(new CookieReceiveEvent(this.player, cookie.getKey(), cookie.getPayload())) + .thenAcceptAsync(event -> { + if (event.getResult().isAllowed()) { + VelocityServerConnection serverConnection = this.player.getConnectedServer(); + if (serverConnection != null) { + Key resultedKey = event.getResult().getKey() == null + ? event.getOriginalKey() : event.getResult().getKey(); + byte[] resultedData = event.getResult().getData() == null + ? event.getOriginalData() : event.getResult().getData(); + serverConnection.ensureConnected().write(new ServerboundCookieResponsePacket(resultedKey, resultedData)); + } + } + }, this.player.getConnection().eventLoop()); + } } this.plugin.setActiveSessionHandler(connection, StateRegistry.CONFIG, configHandler); diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java index 61e76037..d1f0e166 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java @@ -31,6 +31,7 @@ import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; +import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerChatPacket; import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerCommandPacket; import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChatPacket; @@ -47,6 +48,8 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadLocalRandom; @@ -88,6 +91,7 @@ public class LimboSessionHandlerImpl implements MinecraftSessionHandler { private LimboPlayer limboPlayer; private ClientSettingsPacket settings; private String brand; + private final List cookies = new ArrayList<>(); private ScheduledFuture keepAliveTask; private ScheduledFuture chatSessionTimeoutTask; private ScheduledFuture respawnTask; @@ -118,6 +122,7 @@ public LimboSessionHandlerImpl(LimboAPI plugin, LimboImpl limbo, ConnectedPlayer if (originalHandler instanceof LimboSessionHandlerImpl sessionHandler) { this.settings = sessionHandler.getSettings(); this.brand = sessionHandler.getBrand(); + this.cookies.addAll(sessionHandler.getCookies()); } } @@ -324,6 +329,18 @@ public boolean handle(SessionPlayerCommandPacket packet) { return this.handleChat("/" + packet.getCommand()); } + @Override + public boolean handle(ServerboundCookieResponsePacket packet) { + // The player is usually not attached to a backend while inside a Limbo, so the cookie + // response can't be delivered here. Buffer it (just like ClientSettings/brand) and let + // LoginTasksQueue replay it once the player is handed back to Velocity's session handling. + this.cookies.add(packet); + // Also surface it live to the Limbo handler so plugins can use the cookie value during the + // session (the buffered CookieReceiveEvent only fires later, on hand-back to Velocity). + this.callback.onCookieResponse(packet.getKey(), packet.getPayload()); + return true; + } + private boolean handleChat(String message) { int messageLength = message.length(); if (messageLength > Settings.IMP.MAIN.MAX_CHAT_MESSAGE_LENGTH) { @@ -501,6 +518,10 @@ public String getBrand() { return this.brand; } + public List getCookies() { + return this.cookies; + } + static { try { TEARDOWN_METHOD = MethodHandles.privateLookupIn(ConnectedPlayer.class, MethodHandles.lookup())