diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDisconnectEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDisconnectEvent.java
index 05e3415a0..f97f32f92 100644
--- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDisconnectEvent.java
+++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDisconnectEvent.java
@@ -50,7 +50,7 @@ public class SessionDisconnectEvent extends ConnectionEvent {
}
/**
- * Sets the disconnect reason, thereby overriding th original reason.
+ * Sets the disconnect message shown to the Bedrock client.
*
* @param disconnectReason the reason for the disconnect
*/
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/connection/GeyserBedrockPingEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/connection/GeyserBedrockPingEvent.java
index 10ccb93d5..64d3cb44f 100644
--- a/api/src/main/java/org/geysermc/geyser/api/event/connection/GeyserBedrockPingEvent.java
+++ b/api/src/main/java/org/geysermc/geyser/api/event/connection/GeyserBedrockPingEvent.java
@@ -33,10 +33,10 @@ import org.geysermc.event.Event;
import java.net.InetSocketAddress;
/**
- * Called whenever Geyser gets pinged
+ * Called whenever Geyser gets pinged by a Bedrock client.
*
- * This event allows you to modify/obtain the MOTD, maximum player count, and current number of players online,
- * Geyser will reply to the client with what was given.
+ * This event allows you to modify/obtain the MOTD, maximum player count, and current number of players online.
+ * Geyser will reply to the client with the information provided in this event.
*/
public interface GeyserBedrockPingEvent extends Event {
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerDefineCommandsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerDefineCommandsEvent.java
index 299c9d6dd..40268d5b2 100644
--- a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerDefineCommandsEvent.java
+++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerDefineCommandsEvent.java
@@ -37,7 +37,7 @@ import java.util.Set;
*
* This event is mapped to the existence of Brigadier on the server.
*/
-public class ServerDefineCommandsEvent extends ConnectionEvent implements Cancellable {
+public final class ServerDefineCommandsEvent extends ConnectionEvent implements Cancellable {
private final Set extends CommandInfo> commands;
private boolean cancelled;
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerTransferEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerTransferEvent.java
index 594e28ef0..f32d84f6a 100644
--- a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerTransferEvent.java
+++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerTransferEvent.java
@@ -37,7 +37,7 @@ import java.util.Map;
* Fired when the Java server sends a transfer request to a different Java server.
* Geyser Extensions can listen to this event and set a target server ip/port for Bedrock players to be transferred to.
*/
-public class ServerTransferEvent extends ConnectionEvent {
+public final class ServerTransferEvent extends ConnectionEvent {
private final String host;
private final int port;
diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
index 065c1f0cc..5171c0633 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
@@ -74,6 +74,7 @@ import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.erosion.UnixSocketClientListener;
import org.geysermc.geyser.event.GeyserEventBus;
+import org.geysermc.geyser.event.type.SessionDisconnectEventImpl;
import org.geysermc.geyser.extension.GeyserExtensionManager;
import org.geysermc.geyser.impl.MinecraftVersionImpl;
import org.geysermc.geyser.level.BedrockDimension;
@@ -86,6 +87,7 @@ import org.geysermc.geyser.registry.provider.ProviderSupplier;
import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.PendingMicrosoftAuthentication;
+import org.geysermc.geyser.session.SessionDisconnectListener;
import org.geysermc.geyser.session.SessionManager;
import org.geysermc.geyser.session.cache.RegistryCache;
import org.geysermc.geyser.skin.FloodgateSkinUploader;
@@ -101,7 +103,6 @@ import org.geysermc.geyser.util.MinecraftAuthLogger;
import org.geysermc.geyser.util.NewsHandler;
import org.geysermc.geyser.util.VersionCheckUtils;
import org.geysermc.geyser.util.WebUtils;
-import org.geysermc.mcprotocollib.network.tcp.TcpSession;
import java.io.File;
import java.io.FileWriter;
@@ -266,6 +267,8 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
// Register our general permissions when possible
eventBus.subscribe(this, GeyserRegisterPermissionsEvent.class, Permissions::register);
+ // Replace disconnect messages whenever necessary
+ eventBus.subscribe(this, SessionDisconnectEventImpl.class, SessionDisconnectListener::onSessionDisconnect);
startInstance();
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaDisconnectTranslator.java b/core/src/main/java/org/geysermc/geyser/event/type/SessionDisconnectEventImpl.java
similarity index 61%
rename from core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaDisconnectTranslator.java
rename to core/src/main/java/org/geysermc/geyser/event/type/SessionDisconnectEventImpl.java
index 0012390cb..b746979df 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaDisconnectTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/event/type/SessionDisconnectEventImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
+ * Copyright (c) 2025 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -23,19 +23,26 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.geyser.translator.protocol.java;
+package org.geysermc.geyser.event.type;
-import org.geysermc.mcprotocollib.protocol.packet.common.clientbound.ClientboundDisconnectPacket;
+import lombok.Getter;
+import net.kyori.adventure.text.Component;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.geyser.api.event.bedrock.SessionDisconnectEvent;
import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.translator.protocol.PacketTranslator;
-import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.translator.text.MessageTranslator;
-@Translator(packet = ClientboundDisconnectPacket.class)
-public class JavaDisconnectTranslator extends PacketTranslator {
+/**
+ * A wrapper around the {@link SessionDisconnectEvent} that allows
+ * Geyser to access the underlying component when replacing disconnect messages.
+ */
+@Getter
+public class SessionDisconnectEventImpl extends SessionDisconnectEvent {
- @Override
- public void translate(GeyserSession session, ClientboundDisconnectPacket packet) {
- session.disconnect(MessageTranslator.convertMessage(packet.getReason(), session.locale()));
+ private final Component reasonComponent;
+
+ public SessionDisconnectEventImpl(@NonNull GeyserSession session, Component reason) {
+ super(session, MessageTranslator.convertToPlainText(reason, session.locale()));
+ this.reasonComponent = reason;
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/network/InvalidPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/InvalidPacketHandler.java
index 1b653891e..974d6fdce 100644
--- a/core/src/main/java/org/geysermc/geyser/network/InvalidPacketHandler.java
+++ b/core/src/main/java/org/geysermc/geyser/network/InvalidPacketHandler.java
@@ -28,6 +28,8 @@ package org.geysermc.geyser.network;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.RequiredArgsConstructor;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.session.GeyserSession;
import java.util.stream.Stream;
@@ -45,16 +47,20 @@ public class InvalidPacketHandler extends ChannelInboundHandlerAdapter {
.findFirst()
.orElse(cause);
+ GeyserLogger logger = GeyserImpl.getInstance().getLogger();
if (!(rootCause instanceof IllegalArgumentException)) {
// Kick users that cause exceptions
- session.getGeyser().getLogger().warning("Exception caught in session of" + session.bedrockUsername() + ": " + rootCause.getMessage());
+ logger.warning("Exception caught in session of" + session.bedrockUsername() + ": " + rootCause.getMessage());
session.disconnect("An internal error occurred!");
return;
}
// Kick users that try to send illegal packets
- session.getGeyser().getLogger().warning(rootCause.getMessage());
+ logger.warning("Illegal packet from " + session.bedrockUsername() + ": " + rootCause.getMessage());
+ if (logger.isDebug()) {
+ cause.printStackTrace();
+ }
session.disconnect("Invalid packet received!");
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/session/DownstreamSession.java b/core/src/main/java/org/geysermc/geyser/session/DownstreamSession.java
index 8845cdbea..c1db89484 100644
--- a/core/src/main/java/org/geysermc/geyser/session/DownstreamSession.java
+++ b/core/src/main/java/org/geysermc/geyser/session/DownstreamSession.java
@@ -27,6 +27,7 @@ package org.geysermc.geyser.session;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
+import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.mcprotocollib.network.packet.Packet;
import org.geysermc.mcprotocollib.network.tcp.TcpSession;
@@ -41,11 +42,11 @@ public class DownstreamSession {
this.session.send(packet);
}
- public void disconnect(String reason) {
+ public void disconnect(Component reason) {
this.session.disconnect(reason);
}
- public void disconnect(String reason, Throwable throwable) {
+ public void disconnect(Component reason, Throwable throwable) {
this.session.disconnect(reason, throwable);
}
diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
index b3a38f32f..111b966f7 100644
--- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
+++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
@@ -41,6 +41,7 @@ import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import net.kyori.adventure.key.Key;
+import net.kyori.adventure.text.Component;
import net.raphimc.minecraftauth.responsehandler.exception.MinecraftRequestException;
import net.raphimc.minecraftauth.step.java.StepMCProfile;
import net.raphimc.minecraftauth.step.java.StepMCToken;
@@ -109,9 +110,6 @@ import org.geysermc.api.util.InputMode;
import org.geysermc.api.util.UiProfile;
import org.geysermc.cumulus.form.Form;
import org.geysermc.cumulus.form.util.FormBuilder;
-import org.geysermc.floodgate.crypto.FloodgateCipher;
-import org.geysermc.floodgate.util.BedrockData;
-import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.bedrock.camera.CameraData;
import org.geysermc.geyser.api.bedrock.camera.CameraShake;
@@ -121,9 +119,7 @@ import org.geysermc.geyser.api.entity.type.GeyserEntity;
import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
import org.geysermc.geyser.api.event.bedrock.SessionDisconnectEvent;
import org.geysermc.geyser.api.event.bedrock.SessionLoginEvent;
-import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.api.network.RemoteServer;
-import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.configuration.EmoteOffhandWorkaroundOption;
import org.geysermc.geyser.configuration.GeyserConfiguration;
@@ -138,6 +134,7 @@ import org.geysermc.geyser.entity.vehicle.ClientVehicle;
import org.geysermc.geyser.erosion.AbstractGeyserboundPacketHandler;
import org.geysermc.geyser.erosion.ErosionCancellationException;
import org.geysermc.geyser.erosion.GeyserboundHandshakePacketHandler;
+import org.geysermc.geyser.event.type.SessionDisconnectEventImpl;
import org.geysermc.geyser.impl.camera.CameraDefinitions;
import org.geysermc.geyser.impl.camera.GeyserCameraData;
import org.geysermc.geyser.inventory.Inventory;
@@ -174,11 +171,8 @@ import org.geysermc.geyser.session.cache.TagCache;
import org.geysermc.geyser.session.cache.TeleportCache;
import org.geysermc.geyser.session.cache.WorldBorder;
import org.geysermc.geyser.session.cache.WorldCache;
-import org.geysermc.geyser.skin.FloodgateSkinUploader;
import org.geysermc.geyser.text.GeyserLocale;
-import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
-import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.ChunkUtils;
import org.geysermc.geyser.util.EntityUtils;
import org.geysermc.geyser.util.InventoryUtils;
@@ -187,19 +181,13 @@ import org.geysermc.geyser.util.MathUtils;
import org.geysermc.geyser.util.MinecraftAuthLogger;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.network.BuiltinFlags;
-import org.geysermc.mcprotocollib.network.Session;
-import org.geysermc.mcprotocollib.network.event.session.ConnectedEvent;
-import org.geysermc.mcprotocollib.network.event.session.DisconnectedEvent;
-import org.geysermc.mcprotocollib.network.event.session.PacketErrorEvent;
-import org.geysermc.mcprotocollib.network.event.session.PacketSendingEvent;
-import org.geysermc.mcprotocollib.network.event.session.SessionAdapter;
import org.geysermc.mcprotocollib.network.packet.Packet;
import org.geysermc.mcprotocollib.network.tcp.TcpClientSession;
import org.geysermc.mcprotocollib.network.tcp.TcpSession;
+import org.geysermc.mcprotocollib.protocol.ClientListener;
import org.geysermc.mcprotocollib.protocol.MinecraftConstants;
import org.geysermc.mcprotocollib.protocol.MinecraftProtocol;
import org.geysermc.mcprotocollib.protocol.data.ProtocolState;
-import org.geysermc.mcprotocollib.protocol.data.UnexpectedEncryptionException;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose;
import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
@@ -212,7 +200,6 @@ import org.geysermc.mcprotocollib.protocol.data.game.setting.SkinPart;
import org.geysermc.mcprotocollib.protocol.data.game.statistic.CustomStatistic;
import org.geysermc.mcprotocollib.protocol.data.game.statistic.Statistic;
import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundClientInformationPacket;
-import org.geysermc.mcprotocollib.protocol.packet.handshake.serverbound.ClientIntentionPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundChatCommandSignedPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundChatPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundClientTickEndPacket;
@@ -221,9 +208,7 @@ import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.Serv
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundUseItemPacket;
import org.geysermc.mcprotocollib.protocol.packet.login.serverbound.ServerboundCustomQueryAnswerPacket;
-import java.net.ConnectException;
import java.net.InetSocketAddress;
-import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
@@ -231,6 +216,7 @@ import java.util.BitSet;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
@@ -359,8 +345,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
// Exposed for GeyserConnect usage
protected boolean sentSpawnPacket;
- private boolean loggedIn;
- private boolean loggingIn;
+ boolean loggedIn;
+ boolean loggingIn;
@Setter
private boolean spawned;
@@ -525,11 +511,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private long blockBreakStartTime;
- /**
- * // TODO
- */
- private long destroyProgress;
-
/**
* Stores whether the player intended to place a bucket.
*/
@@ -658,6 +639,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
private final GeyserEntityData entityData;
+ @Getter(AccessLevel.MODULE)
private MinecraftProtocol protocol;
private int nanosecondsPerTick = 50000000;
@@ -962,7 +944,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
this.cookies = loginEvent.cookies();
this.remoteServer = loginEvent.remoteServer();
- boolean floodgate = this.remoteServer.authType() == AuthType.FLOODGATE;
// Start ticking
tickThread = tickEventLoop.scheduleAtFixedRate(this::tick, nanosecondsPerTick, nanosecondsPerTick, TimeUnit.NANOSECONDS);
@@ -1001,188 +982,30 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
// We'll handle this since we have the registry data on hand
downstream.setFlag(MinecraftConstants.SEND_BLANK_KNOWN_PACKS_RESPONSE, false);
- downstream.addListener(new SessionAdapter() {
- @Override
- public void packetSending(PacketSendingEvent event) {
- //todo move this somewhere else
- if (event.getPacket() instanceof ClientIntentionPacket) {
- String addressSuffix;
- if (floodgate) {
- byte[] encryptedData;
+ // We manually add the default listener to ensure the order of listeners.
+ protocol.setUseDefaultListeners(false);
- try {
- FloodgateSkinUploader skinUploader = geyser.getSkinUploader();
- FloodgateCipher cipher = geyser.getCipher();
+ // MCPL listener comes first to handle protocol state switching before Geyser translates packets
+ downstream.addListener(new ClientListener(ProtocolState.LOGIN, loginEvent.transferring()));
+ // Geyser adapter second to ensure translating packets in the correct states
+ downstream.addListener(new GeyserSessionAdapter(this));
- String bedrockAddress = upstream.getAddress().getAddress().getHostAddress();
- // both BungeeCord and Velocity remove the IPv6 scope (if there is one) for Spigot
- int ipv6ScopeIndex = bedrockAddress.indexOf('%');
- if (ipv6ScopeIndex != -1) {
- bedrockAddress = bedrockAddress.substring(0, ipv6ScopeIndex);
- }
-
- encryptedData = cipher.encryptFromString(BedrockData.of(
- clientData.getGameVersion(),
- authData.name(),
- authData.xuid(),
- clientData.getDeviceOs().ordinal(),
- clientData.getLanguageCode(),
- clientData.getUiProfile().ordinal(),
- clientData.getCurrentInputMode().ordinal(),
- bedrockAddress,
- skinUploader.getId(),
- skinUploader.getVerifyCode()
- ).toString());
- } catch (Exception e) {
- geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e);
- disconnect(GeyserLocale.getPlayerLocaleString("geyser.auth.floodgate.encrypt_fail", getClientData().getLanguageCode()));
- return;
- }
-
- addressSuffix = '\0' + new String(encryptedData, StandardCharsets.UTF_8);
- } else {
- addressSuffix = "";
- }
-
- ClientIntentionPacket intentionPacket = event.getPacket();
-
- String address;
- if (geyser.getConfig().getRemote().isForwardHost()) {
- address = clientData.getServerAddress().split(":")[0];
- } else {
- address = intentionPacket.getHostname();
- }
-
- event.setPacket(intentionPacket.withHostname(address + addressSuffix));
- }
- }
-
- @Override
- public void connected(ConnectedEvent event) {
- loggingIn = false;
- loggedIn = true;
-
- if (downstream instanceof LocalSession) {
- // Connected directly to the server
- geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.connect_internal",
- authData.name(), protocol.getProfile().getName()));
- } else {
- // Connected to an IP address
- geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.connect",
- authData.name(), protocol.getProfile().getName(), remoteServer.address()));
- }
-
- UUID uuid = protocol.getProfile().getId();
- if (uuid == null) {
- // Set what our UUID *probably* is going to be
- if (remoteServer.authType() == AuthType.FLOODGATE) {
- uuid = new UUID(0, Long.parseLong(authData.xuid()));
- } else {
- uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + protocol.getProfile().getName()).getBytes(StandardCharsets.UTF_8));
- }
- }
- playerEntity.setUuid(uuid);
- playerEntity.setUsername(protocol.getProfile().getName());
-
- String locale = clientData.getLanguageCode();
-
- // Let the user know there locale may take some time to download
- // as it has to be extracted from a JAR
- if (locale.equalsIgnoreCase("en_us") && !MinecraftLocale.LOCALE_MAPPINGS.containsKey("en_us")) {
- // This should probably be left hardcoded as it will only show for en_us clients
- sendMessage("Loading your locale (en_us); if this isn't already downloaded, this may take some time");
- }
-
- // Download and load the language for the player
- MinecraftLocale.downloadAndLoadLocale(locale);
- }
-
- @Override
- public void disconnected(DisconnectedEvent event) {
- loggingIn = false;
-
- String disconnectMessage;
- Throwable cause = event.getCause();
- if (cause instanceof UnexpectedEncryptionException) {
- if (remoteServer.authType() != AuthType.FLOODGATE) {
- // Server expects online mode
- disconnectMessage = GeyserLocale.getPlayerLocaleString("geyser.network.remote.authentication_type_mismatch", locale());
- // Explain that they may be looking for Floodgate.
- geyser.getLogger().warning(GeyserLocale.getLocaleStringLog(
- geyser.getPlatformType() == PlatformType.STANDALONE ?
- "geyser.network.remote.floodgate_explanation_standalone"
- : "geyser.network.remote.floodgate_explanation_plugin",
- Constants.FLOODGATE_DOWNLOAD_LOCATION
- ));
- } else {
- // Likely that Floodgate is not configured correctly.
- disconnectMessage = GeyserLocale.getPlayerLocaleString("geyser.network.remote.floodgate_login_error", locale());
- if (geyser.getPlatformType() == PlatformType.STANDALONE) {
- geyser.getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.network.remote.floodgate_login_error_standalone"));
- }
- }
- } else if (cause instanceof ConnectException) {
- // Server is offline, probably
- disconnectMessage = GeyserLocale.getPlayerLocaleString("geyser.network.remote.server_offline", locale());
- } else {
- disconnectMessage = MessageTranslator.convertMessage(event.getReason());
- }
-
- if (downstream instanceof LocalSession) {
- geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.disconnect_internal", authData.name(), disconnectMessage));
- } else {
- geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.disconnect", authData.name(), remoteServer.address(), disconnectMessage));
- }
- if (cause != null) {
- if (cause.getMessage() != null) {
- GeyserImpl.getInstance().getLogger().error(cause.getMessage());
- } else {
- GeyserImpl.getInstance().getLogger().error("An exception occurred: ", cause);
- }
- if (geyser.getConfig().isDebugMode()) {
- cause.printStackTrace();
- }
- }
- if ((!GeyserSession.this.closed && GeyserSession.this.loggedIn) || cause != null) {
- // GeyserSession is disconnected via session.disconnect() called indirectly be the server
- // This needs to be "initiated" here when there is an exception, but also when the Netty connection
- // is closed without a disconnect packet - in this case, closed will still be false, but loggedIn
- // will also be true as GeyserSession#disconnect will not have been called.
- GeyserSession.this.disconnect(disconnectMessage);
- }
-
- loggedIn = false;
- }
-
- @Override
- public void packetReceived(Session session, Packet packet) {
- Registries.JAVA_PACKET_TRANSLATORS.translate(packet.getClass(), packet, GeyserSession.this, true);
- }
-
- @Override
- public void packetError(PacketErrorEvent event) {
- geyser.getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.network.downstream_error",
- (event.getPacketClass() != null ? "(" + event.getPacketClass().getSimpleName() + ")" : "") +
- event.getCause().getMessage())
- );
- if (geyser.getConfig().isDebugMode())
- event.getCause().printStackTrace();
- event.setSuppress(true);
- }
- });
+ downstream.connect(false, loginEvent.transferring());
if (!daylightCycle) {
setDaylightCycle(true);
}
-
- downstream.connect(false, loginEvent.transferring());
}
public void disconnect(String reason) {
+ disconnect(Component.text(reason));
+ }
+
+ public void disconnect(Component reason) {
if (!closed) {
loggedIn = false;
- SessionDisconnectEvent disconnectEvent = new SessionDisconnectEvent(this, reason);
+ SessionDisconnectEvent disconnectEvent = new SessionDisconnectEventImpl(this, reason);
if (authData != null && clientData != null) { // can occur if player disconnects before Bedrock auth finishes
// Fire SessionDisconnectEvent
geyser.getEventBus().fire(disconnectEvent);
@@ -1819,7 +1642,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
}
if (protocol.getOutboundState() != intendedState) {
- geyser.getLogger().debug("Tried to send " + packet.getClass().getSimpleName() + " packet while not in " + intendedState.name() + " outbound state");
+ geyser.getLogger().warning("Tried to send " + packet.getClass().getSimpleName() + " packet while not in " + intendedState.name() + " outbound state. Current state: " + protocol.getOutboundState().name());
return;
}
@@ -2016,7 +1839,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
* Send a packet to the server to indicate client render distance, locale, skin parts, and hand preference.
*/
public void sendJavaClientSettings() {
- ServerboundClientInformationPacket clientSettingsPacket = new ServerboundClientInformationPacket(locale(),
+ // Locale is lowercase on Java - (https://github.com/GeyserMC/Geyser/issues/5235)
+ ServerboundClientInformationPacket clientSettingsPacket = new ServerboundClientInformationPacket(locale().toLowerCase(Locale.ROOT),
getRenderDistance(), ChatVisibility.FULL, true, SKIN_PARTS,
HandPreference.RIGHT_HAND, false, true, ParticleStatus.ALL); // TODO particle status
sendDownstreamPacket(clientSettingsPacket);
diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSessionAdapter.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSessionAdapter.java
new file mode 100644
index 000000000..9e17e9cd3
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSessionAdapter.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (c) 2025 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.session;
+
+import org.geysermc.floodgate.crypto.FloodgateCipher;
+import org.geysermc.floodgate.util.BedrockData;
+import org.geysermc.geyser.Constants;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.network.AuthType;
+import org.geysermc.geyser.api.util.PlatformType;
+import org.geysermc.geyser.network.netty.LocalSession;
+import org.geysermc.geyser.registry.Registries;
+import org.geysermc.geyser.session.auth.BedrockClientData;
+import org.geysermc.geyser.skin.FloodgateSkinUploader;
+import org.geysermc.geyser.text.GeyserLocale;
+import org.geysermc.geyser.text.MinecraftLocale;
+import org.geysermc.geyser.translator.text.MessageTranslator;
+import org.geysermc.mcprotocollib.network.Session;
+import org.geysermc.mcprotocollib.network.event.session.ConnectedEvent;
+import org.geysermc.mcprotocollib.network.event.session.DisconnectedEvent;
+import org.geysermc.mcprotocollib.network.event.session.PacketErrorEvent;
+import org.geysermc.mcprotocollib.network.event.session.PacketSendingEvent;
+import org.geysermc.mcprotocollib.network.event.session.SessionAdapter;
+import org.geysermc.mcprotocollib.network.packet.Packet;
+import org.geysermc.mcprotocollib.protocol.data.UnexpectedEncryptionException;
+import org.geysermc.mcprotocollib.protocol.packet.handshake.serverbound.ClientIntentionPacket;
+
+import java.net.ConnectException;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+
+public class GeyserSessionAdapter extends SessionAdapter {
+
+ private final GeyserImpl geyser;
+ private final GeyserSession geyserSession;
+ private final boolean floodgate;
+ private final String locale;
+
+ public GeyserSessionAdapter(GeyserSession session) {
+ this.geyserSession = session;
+ this.floodgate = session.remoteServer().authType() == AuthType.FLOODGATE;
+ this.geyser = GeyserImpl.getInstance();
+ this.locale = session.locale();
+ }
+
+ @Override
+ public void packetSending(PacketSendingEvent event) {
+ if (event.getPacket() instanceof ClientIntentionPacket) {
+ BedrockClientData clientData = geyserSession.getClientData();
+
+ String addressSuffix;
+ if (floodgate) {
+ byte[] encryptedData;
+
+ try {
+ FloodgateSkinUploader skinUploader = geyser.getSkinUploader();
+ FloodgateCipher cipher = geyser.getCipher();
+
+ String bedrockAddress = geyserSession.getUpstream().getAddress().getAddress().getHostAddress();
+ // both BungeeCord and Velocity remove the IPv6 scope (if there is one) for Spigot
+ int ipv6ScopeIndex = bedrockAddress.indexOf('%');
+ if (ipv6ScopeIndex != -1) {
+ bedrockAddress = bedrockAddress.substring(0, ipv6ScopeIndex);
+ }
+
+ encryptedData = cipher.encryptFromString(BedrockData.of(
+ clientData.getGameVersion(),
+ geyserSession.bedrockUsername(),
+ geyserSession.xuid(),
+ clientData.getDeviceOs().ordinal(),
+ clientData.getLanguageCode(),
+ clientData.getUiProfile().ordinal(),
+ clientData.getCurrentInputMode().ordinal(),
+ bedrockAddress,
+ skinUploader.getId(),
+ skinUploader.getVerifyCode()
+ ).toString());
+ } catch (Exception e) {
+ geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e);
+ geyserSession.disconnect(GeyserLocale.getPlayerLocaleString("geyser.auth.floodgate.encrypt_fail", locale));
+ return;
+ }
+
+ addressSuffix = '\0' + new String(encryptedData, StandardCharsets.UTF_8);
+ } else {
+ addressSuffix = "";
+ }
+
+ ClientIntentionPacket intentionPacket = event.getPacket();
+
+ String address;
+ if (geyser.getConfig().getRemote().isForwardHost()) {
+ address = clientData.getServerAddress().split(":")[0];
+ } else {
+ address = intentionPacket.getHostname();
+ }
+
+ event.setPacket(intentionPacket.withHostname(address + addressSuffix));
+ }
+ }
+
+ @Override
+ public void connected(ConnectedEvent event) {
+ geyserSession.loggingIn = false;
+ geyserSession.loggedIn = true;
+
+ if (geyserSession.getDownstream().getSession() instanceof LocalSession) {
+ // Connected directly to the server
+ geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.connect_internal",
+ geyserSession.bedrockUsername(), geyserSession.getProtocol().getProfile().getName()));
+ } else {
+ // Connected to an IP address
+ geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.connect",
+ geyserSession.bedrockUsername(), geyserSession.getProtocol().getProfile().getName(), geyserSession.remoteServer().address()));
+ }
+
+ UUID uuid = geyserSession.getProtocol().getProfile().getId();
+ if (uuid == null) {
+ // Set what our UUID *probably* is going to be
+ if (geyserSession.remoteServer().authType() == AuthType.FLOODGATE) {
+ uuid = new UUID(0, Long.parseLong(geyserSession.xuid()));
+ } else {
+ uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + geyserSession.getProtocol().getProfile().getName()).getBytes(StandardCharsets.UTF_8));
+ }
+ }
+ geyserSession.getPlayerEntity().setUuid(uuid);
+ geyserSession.getPlayerEntity().setUsername(geyserSession.getProtocol().getProfile().getName());
+
+ String locale = geyserSession.getClientData().getLanguageCode();
+
+ // Let the user know there locale may take some time to download
+ // as it has to be extracted from a JAR
+ if (locale.equalsIgnoreCase("en_us") && !MinecraftLocale.LOCALE_MAPPINGS.containsKey("en_us")) {
+ // This should probably be left hardcoded as it will only show for en_us clients
+ geyserSession.sendMessage("Loading your locale (en_us); if this isn't already downloaded, this may take some time");
+ }
+
+ // Download and load the language for the player
+ MinecraftLocale.downloadAndLoadLocale(locale);
+ }
+
+ @Override
+ public void disconnected(DisconnectedEvent event) {
+ geyserSession.loggingIn = false;
+
+ String disconnectMessage, customDisconnectMessage = null;
+ Throwable cause = event.getCause();
+ if (cause instanceof UnexpectedEncryptionException) {
+ if (geyserSession.remoteServer().authType() != AuthType.FLOODGATE) {
+ // Server expects online mode
+ customDisconnectMessage = GeyserLocale.getPlayerLocaleString("geyser.network.remote.authentication_type_mismatch", locale);
+ // Explain that they may be looking for Floodgate.
+ geyser.getLogger().warning(GeyserLocale.getLocaleStringLog(
+ geyser.getPlatformType() == PlatformType.STANDALONE ?
+ "geyser.network.remote.floodgate_explanation_standalone"
+ : "geyser.network.remote.floodgate_explanation_plugin",
+ Constants.FLOODGATE_DOWNLOAD_LOCATION
+ ));
+ } else {
+ // Likely that Floodgate is not configured correctly.
+ customDisconnectMessage = GeyserLocale.getPlayerLocaleString("geyser.network.remote.floodgate_login_error", locale);
+ if (geyser.getPlatformType() == PlatformType.STANDALONE) {
+ geyser.getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.network.remote.floodgate_login_error_standalone"));
+ }
+ }
+ } else if (cause instanceof ConnectException) {
+ // Server is offline, probably
+ customDisconnectMessage = GeyserLocale.getPlayerLocaleString("geyser.network.remote.server_offline", locale);
+ }
+
+ // Use our helpful disconnect message whenever possible
+ disconnectMessage = customDisconnectMessage != null ? customDisconnectMessage : MessageTranslator.convertMessage(event.getReason());;
+
+ if (geyserSession.getDownstream().getSession() instanceof LocalSession) {
+ geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.disconnect_internal", geyserSession.bedrockUsername(), disconnectMessage));
+ } else {
+ geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.disconnect", geyserSession.bedrockUsername(), geyserSession.remoteServer().address(), disconnectMessage));
+ }
+ if (cause != null) {
+ if (cause.getMessage() != null) {
+ GeyserImpl.getInstance().getLogger().error(cause.getMessage());
+ } else {
+ GeyserImpl.getInstance().getLogger().error("An exception occurred: ", cause);
+ }
+ if (geyser.getConfig().isDebugMode()) {
+ cause.printStackTrace();
+ }
+ }
+ if ((!geyserSession.isClosed() && geyserSession.loggedIn) || cause != null) {
+ // GeyserSession is disconnected via session.disconnect() called indirectly be the server
+ // This needs to be "initiated" here when there is an exception, but also when the Netty connection
+ // is closed without a disconnect packet - in this case, closed will still be false, but loggedIn
+ // will also be true as GeyserSession#disconnect will not have been called.
+ if (customDisconnectMessage != null) {
+ geyserSession.disconnect(customDisconnectMessage);
+ } else {
+ geyserSession.disconnect(event.getReason());
+ }
+ }
+
+ geyserSession.loggedIn = false;
+ }
+
+ @Override
+ public void packetReceived(Session session, Packet packet) {
+ Registries.JAVA_PACKET_TRANSLATORS.translate(packet.getClass(), packet, geyserSession, true);
+ }
+
+ @Override
+ public void packetError(PacketErrorEvent event) {
+ geyser.getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.network.downstream_error",
+ (event.getPacketClass() != null ? "(" + event.getPacketClass().getSimpleName() + ")" : "") +
+ event.getCause().getMessage())
+ );
+ if (geyser.getConfig().isDebugMode())
+ event.getCause().printStackTrace();
+ event.setSuppress(true);
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginDisconnectTranslator.java b/core/src/main/java/org/geysermc/geyser/session/SessionDisconnectListener.java
similarity index 58%
rename from core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginDisconnectTranslator.java
rename to core/src/main/java/org/geysermc/geyser/session/SessionDisconnectListener.java
index 0dd843dfa..da1ed75f4 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginDisconnectTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/session/SessionDisconnectListener.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
+ * Copyright (c) 2025 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -23,59 +23,59 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.geyser.translator.protocol.java;
+package org.geysermc.geyser.session;
-import org.geysermc.mcprotocollib.protocol.packet.login.clientbound.ClientboundLoginDisconnectPacket;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent;
import org.geysermc.geyser.api.util.PlatformType;
+import org.geysermc.geyser.event.type.SessionDisconnectEventImpl;
import org.geysermc.geyser.network.GameProtocol;
-import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale;
-import org.geysermc.geyser.translator.protocol.PacketTranslator;
-import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.translator.text.MessageTranslator;
import java.util.List;
-@Translator(packet = ClientboundLoginDisconnectPacket.class)
-public class JavaLoginDisconnectTranslator extends PacketTranslator {
+/**
+ * Geyser's internal listener to modify disconnection messages
+ * for user-friendly messages.
+ * By listening to the event instead of firing the event with the changed message,
+ * third-party-users are able to see the original disconnection message.
+ */
+public final class SessionDisconnectListener {
- @Override
- public void translate(GeyserSession session, ClientboundLoginDisconnectPacket packet) {
- Component disconnectReason = packet.getReason();
+ private SessionDisconnectListener() {
+ // no-op
+ }
+
+ public static void onSessionDisconnect(SessionDisconnectEventImpl event) {
+ Component disconnectReason = event.getReasonComponent();
+ GeyserSession session = (GeyserSession) event.connection();
String serverDisconnectMessage = MessageTranslator.convertMessage(disconnectReason, session.locale());
- String disconnectMessage;
if (testForOutdatedServer(disconnectReason)) {
String locale = session.locale();
PlatformType platform = session.getGeyser().getPlatformType();
String outdatedType = (platform == PlatformType.BUNGEECORD || platform == PlatformType.VELOCITY || platform == PlatformType.VIAPROXY) ?
- "geyser.network.remote.outdated.proxy" : "geyser.network.remote.outdated.server";
- disconnectMessage = GeyserLocale.getPlayerLocaleString(outdatedType, locale, GameProtocol.getJavaVersions().get(0)) + '\n'
- + GeyserLocale.getPlayerLocaleString("geyser.network.remote.original_disconnect_message", locale, serverDisconnectMessage);
+ "geyser.network.remote.outdated.proxy" : "geyser.network.remote.outdated.server";
+ event.disconnectReason(GeyserLocale.getPlayerLocaleString(outdatedType, locale, GameProtocol.getJavaVersions().get(0)) + '\n'
+ + GeyserLocale.getPlayerLocaleString("geyser.network.remote.original_disconnect_message", locale, serverDisconnectMessage));
} else if (testForMissingProfilePublicKey(disconnectReason)) {
- disconnectMessage = "Please set `enforce-secure-profile` to `false` in server.properties for Bedrock players to be able to connect." + '\n'
- + GeyserLocale.getPlayerLocaleString("geyser.network.remote.original_disconnect_message", session.locale(), serverDisconnectMessage);
- } else {
- disconnectMessage = serverDisconnectMessage;
+ event.disconnectReason("Please set `enforce-secure-profile` to `false` in server.properties for Bedrock players to be able to connect." + '\n'
+ + GeyserLocale.getPlayerLocaleString("geyser.network.remote.original_disconnect_message", session.locale(), serverDisconnectMessage));
}
-
- // The client doesn't manually get disconnected so we have to do it ourselves
- session.disconnect(disconnectMessage);
}
- private boolean testForOutdatedServer(Component disconnectReason) {
+ private static boolean testForOutdatedServer(Component disconnectReason) {
if (disconnectReason instanceof TranslatableComponent component) {
String key = component.key();
return "multiplayer.disconnect.incompatible".equals(key) ||
- // Seen with Velocity 1.18 rejecting a 1.19 client
- "multiplayer.disconnect.outdated_client".equals(key) ||
- // Legacy string (starting from at least 1.15.2)
- "multiplayer.disconnect.outdated_server".equals(key)
- // Reproduced on 1.15.2 server with ViaVersion 4.0.0-21w20a with 1.18.2 Java client
- || key.startsWith("Outdated server!");
+ // Seen with Velocity 1.18 rejecting a 1.19 client
+ "multiplayer.disconnect.outdated_client".equals(key) ||
+ // Legacy string (starting from at least 1.15.2)
+ "multiplayer.disconnect.outdated_server".equals(key)
+ // Reproduced on 1.15.2 server with ViaVersion 4.0.0-21w20a with 1.18.2 Java client
+ || key.startsWith("Outdated server!");
} else {
if (disconnectReason instanceof TextComponent component) {
if (component.content().startsWith("Outdated server!")) {
@@ -95,7 +95,8 @@ public class JavaLoginDisconnectTranslator extends PacketTranslator