From 58e2f17e08998c4945153c2bb25b7259f3d4e217 Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 12 Jan 2025 20:44:33 +0100 Subject: [PATCH] Fix: Brand message being sent too early (#5265) * Fix: Brand message not being sent, send lowercase locale, ensure the MCPL default listener comes first * Refactor disconnect handling * apparently default listeners aren't always first... huh * fix issue with bundle cache attempting to check a null inventory --- .../event/bedrock/SessionDisconnectEvent.java | 2 +- .../connection/GeyserBedrockPingEvent.java | 6 +- .../event/java/ServerDefineCommandsEvent.java | 2 +- .../api/event/java/ServerTransferEvent.java | 2 +- .../java/org/geysermc/geyser/GeyserImpl.java | 5 +- .../type/SessionDisconnectEventImpl.java} | 27 +- .../geyser/network/InvalidPacketHandler.java | 10 +- .../geyser/session/DownstreamSession.java | 5 +- .../geyser/session/GeyserSession.java | 220 ++-------------- .../geyser/session/GeyserSessionAdapter.java | 242 ++++++++++++++++++ .../SessionDisconnectListener.java} | 61 ++--- .../java/JavaLoginFinishedTranslator.java | 3 +- .../player/JavaTransferPacketTranslator.java | 4 +- .../geysermc/geyser/util/InventoryUtils.java | 2 +- 14 files changed, 338 insertions(+), 253 deletions(-) rename core/src/main/java/org/geysermc/geyser/{translator/protocol/java/JavaDisconnectTranslator.java => event/type/SessionDisconnectEventImpl.java} (61%) create mode 100644 core/src/main/java/org/geysermc/geyser/session/GeyserSessionAdapter.java rename core/src/main/java/org/geysermc/geyser/{translator/protocol/java/JavaLoginDisconnectTranslator.java => session/SessionDisconnectListener.java} (58%) 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 05e3415a070..f97f32f92d7 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 SessionDisconnectEvent(@NonNull GeyserConnection connection, @NonNull Str } /** - * 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 10ccb93d52c..64d3cb44f84 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 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 299c9d6dd6a..40268d5b2c3 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 @@ *
* 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 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 594e28ef056..f32d84f6a51 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 @@ * 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 065c1f0cc63..5171c0633a7 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.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.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.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 void initialize() { // 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 0012390cbe9..b746979df9d 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 { + + private final Component reasonComponent; - @Override - public void translate(GeyserSession session, ClientboundDisconnectPacket packet) { - session.disconnect(MessageTranslator.convertMessage(packet.getReason(), session.locale())); + 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 1b653891ee7..974d6fdce76 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 @@ 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 void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws E .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 8845cdbeae0..c1db894845b 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 @@ 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 void sendPacket(@NonNull Packet packet) { 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 b3a38f32f18..111b966f793 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.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.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.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.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.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.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.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.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.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 @@ private void connectDownstream() { 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 @@ private void connectDownstream() { // 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; - - try { - FloodgateSkinUploader skinUploader = geyser.getSkinUploader(); - FloodgateCipher cipher = geyser.getCipher(); - - 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; + // We manually add the default listener to ensure the order of listeners. + protocol.setUseDefaultListeners(false); - 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); - } + // 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)); - @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 void sendDownstreamPacket(Packet packet, ProtocolState intendedState) { } 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 @@ private int getRenderDistance() { * 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 00000000000..9e17e9cd33f --- /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 0dd843dfa80..da1ed75f488 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 { + + private SessionDisconnectListener() { + // no-op + } - @Override - public void translate(GeyserSession session, ClientboundLoginDisconnectPacket packet) { - Component disconnectReason = packet.getReason(); + 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 @@ private boolean testForOutdatedServer(Component disconnectReason) { return false; } - private boolean testForMissingProfilePublicKey(Component disconnectReason) { + private static boolean testForMissingProfilePublicKey(Component disconnectReason) { return disconnectReason instanceof TranslatableComponent component && "multiplayer.disconnect.missing_public_key".equals(component.key()); } + } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginFinishedTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginFinishedTranslator.java index cebf71efd48..36c1ef19768 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginFinishedTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginFinishedTranslator.java @@ -34,6 +34,7 @@ import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.util.PluginMessageUtils; import org.geysermc.mcprotocollib.auth.GameProfile; +import org.geysermc.mcprotocollib.protocol.data.ProtocolState; import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket; import org.geysermc.mcprotocollib.protocol.packet.login.clientbound.ClientboundLoginFinishedPacket; @@ -73,7 +74,7 @@ public void translate(GeyserSession session, ClientboundLoginFinishedPacket pack session.getClientData().setOriginalString(null); // configuration phase stuff that the vanilla client replies with after receiving the GameProfilePacket - session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(Key.key("brand"), PluginMessageUtils.getGeyserBrandData())); + session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(Key.key("brand"), PluginMessageUtils.getGeyserBrandData()), ProtocolState.CONFIGURATION); session.sendJavaClientSettings(); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaTransferPacketTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaTransferPacketTranslator.java index ad793f934e4..6f9085b2bf5 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaTransferPacketTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaTransferPacketTranslator.java @@ -25,10 +25,10 @@ package org.geysermc.geyser.translator.protocol.java.entity.player; +import net.kyori.adventure.text.Component; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.java.ServerTransferEvent; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.mcprotocollib.protocol.packet.common.clientbound.ClientboundTransferPacket; @@ -49,7 +49,7 @@ public void translate(GeyserSession session, ClientboundTransferPacket packet) { if (event.bedrockHost() != null && !event.bedrockHost().isBlank() && event.bedrockPort() != -1) { session.transfer(event.bedrockHost(), event.bedrockPort()); } else { - session.disconnect(MinecraftLocale.getLocaleString("disconnect.transfer", session.locale())); + session.disconnect(Component.translatable("disconnect.transfer")); } } } diff --git a/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java b/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java index ba11a20c762..b71caa1dc35 100644 --- a/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java @@ -138,9 +138,9 @@ public static void closeInventory(GeyserSession session, int javaId, boolean con ) { session.setClosingInventory(true); } + session.getBundleCache().onInventoryClose(inventory); } session.setInventoryTranslator(InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR); - session.getBundleCache().onInventoryClose(inventory); session.setOpenInventory(null); }