Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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() {

}
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
49 changes: 46 additions & 3 deletions plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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"
Expand All @@ -301,13 +342,15 @@ File generateData(MinecraftVersion version) {

List<String> 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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -48,13 +49,15 @@
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;
import com.velocitypowered.proxy.connection.client.InitialConnectSessionHandler;
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;
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -88,6 +91,7 @@ public class LimboSessionHandlerImpl implements MinecraftSessionHandler {
private LimboPlayer limboPlayer;
private ClientSettingsPacket settings;
private String brand;
private final List<ServerboundCookieResponsePacket> cookies = new ArrayList<>();
private ScheduledFuture<?> keepAliveTask;
private ScheduledFuture<?> chatSessionTimeoutTask;
private ScheduledFuture<?> respawnTask;
Expand Down Expand Up @@ -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());
}
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -501,6 +518,10 @@ public String getBrand() {
return this.brand;
}

public List<ServerboundCookieResponsePacket> getCookies() {
return this.cookies;
}

static {
try {
TEARDOWN_METHOD = MethodHandles.privateLookupIn(ConnectedPlayer.class, MethodHandles.lookup())
Expand Down