diff --git a/api/src/main/java/org/geysermc/floodgate/api/FloodgateApi.java b/api/src/main/java/org/geysermc/floodgate/api/FloodgateApi.java
index a51252a3..708df16f 100644
--- a/api/src/main/java/org/geysermc/floodgate/api/FloodgateApi.java
+++ b/api/src/main/java/org/geysermc/floodgate/api/FloodgateApi.java
@@ -25,10 +25,10 @@
package org.geysermc.floodgate.api;
-import java.util.Collection;
+import java.net.InetSocketAddress;import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
-import org.geysermc.cumulus.form.Form;
+import org.checkerframework.checker.nullness.qual.Nullable;import org.geysermc.cumulus.form.Form;
import org.geysermc.cumulus.form.util.FormBuilder;
import org.geysermc.floodgate.api.event.FloodgateEventBus;
import org.geysermc.floodgate.api.link.PlayerLink;
@@ -59,6 +59,26 @@ static FloodgateApi getInstance() {
*/
int getPlayerCount();
+ /**
+ * Get a pending FloodgatePlayer by their connection address.
+ * This is useful in PreLoginEvent where UUID is not yet available.
+ *
+ * @param address the InetSocketAddress of the connection
+ * @return FloodgatePlayer if this is a pending Bedrock connection, null otherwise
+ */
+ @Nullable
+ FloodgatePlayer getPendingPlayer(InetSocketAddress address);
+
+ /**
+ * Get a pending FloodgatePlayer by their raw username (without prefix).
+ * This is useful in PreLoginEvent where the username hasn't been modified yet.
+ *
+ * @param rawUsername the raw username without Floodgate prefix
+ * @return FloodgatePlayer if this is a pending Bedrock connection, null otherwise
+ */
+ @Nullable
+ FloodgatePlayer getPendingPlayerByUsername(String rawUsername);
+
/**
* Method to determine if the given online player is a bedrock player
*
diff --git a/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java b/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java
index e5100da5..cdbd2ca6 100644
--- a/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java
+++ b/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java
@@ -32,6 +32,8 @@
import io.netty.channel.Channel;
import io.netty.util.AttributeKey;
import java.lang.reflect.Field;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
import java.util.UUID;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.event.LoginEvent;
@@ -47,6 +49,7 @@
import org.geysermc.floodgate.api.ProxyFloodgateApi;
import org.geysermc.floodgate.api.logger.FloodgateLogger;
import org.geysermc.floodgate.api.player.FloodgatePlayer;
+import org.geysermc.floodgate.player.PendingPlayerManager;
import org.geysermc.floodgate.skin.SkinApplier;
import org.geysermc.floodgate.skin.SkinDataImpl;
import org.geysermc.floodgate.util.LanguageManager;
@@ -82,6 +85,7 @@ public final class BungeeListener implements Listener {
private AttributeKey kickMessageAttribute;
@Inject private MojangUtils mojangUtils;
+ @Inject private PendingPlayerManager pendingPlayerManager;
@EventHandler(priority = EventPriority.LOWEST)
public void onPreLogin(PreLoginEvent event) {
@@ -130,6 +134,13 @@ public void onLogin(LoginEvent event) {
@EventHandler(priority = EventPriority.LOWEST)
public void onPostLogin(PostLoginEvent event) {
+ SocketAddress socketAddress = event.getPlayer().getSocketAddress();
+
+ if (socketAddress instanceof InetSocketAddress) {
+ InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress;
+ pendingPlayerManager.remove(inetSocketAddress);
+ }
+
FloodgatePlayer player = api.getPlayer(event.getPlayer().getUniqueId());
if (player == null) {
return;
@@ -160,5 +171,12 @@ public void onPostLogin(PostLoginEvent event) {
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerDisconnect(PlayerDisconnectEvent event) {
api.playerRemoved(event.getPlayer().getUniqueId());
+
+ SocketAddress socketAddress = event.getPlayer().getSocketAddress();
+
+ if (socketAddress instanceof InetSocketAddress) {
+ InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress;
+ pendingPlayerManager.remove(inetSocketAddress);
+ }
}
}
diff --git a/core/src/main/java/org/geysermc/floodgate/api/SimpleFloodgateApi.java b/core/src/main/java/org/geysermc/floodgate/api/SimpleFloodgateApi.java
index baf05da8..b8291646 100644
--- a/core/src/main/java/org/geysermc/floodgate/api/SimpleFloodgateApi.java
+++ b/core/src/main/java/org/geysermc/floodgate/api/SimpleFloodgateApi.java
@@ -31,18 +31,21 @@
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.inject.Inject;
+import java.net.InetSocketAddress;
import java.util.Collection;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.cumulus.form.Form;
import org.geysermc.cumulus.form.util.FormBuilder;
import org.geysermc.floodgate.api.logger.FloodgateLogger;
import org.geysermc.floodgate.api.player.FloodgatePlayer;
import org.geysermc.floodgate.api.unsafe.Unsafe;
import org.geysermc.floodgate.config.FloodgateConfig;
+import org.geysermc.floodgate.player.PendingPlayerManager;
import org.geysermc.floodgate.pluginmessage.PluginMessageManager;
import org.geysermc.floodgate.pluginmessage.channel.FormChannel;
import org.geysermc.floodgate.pluginmessage.channel.TransferChannel;
@@ -61,6 +64,7 @@ public class SimpleFloodgateApi implements FloodgateApi {
@Inject private FloodgateConfig config;
@Inject private HttpClient httpClient;
@Inject private FloodgateLogger logger;
+ @Inject private PendingPlayerManager pendingPlayerManager;
@Override
public String getPlayerPrefix() {
@@ -225,4 +229,16 @@ private FloodgatePlayer getPendingRemovePlayer(UUID correctUuid) {
}
return null;
}
+
+ @Override
+ @Nullable
+ public FloodgatePlayer getPendingPlayer(InetSocketAddress address) {
+ return pendingPlayerManager.get(address);
+ }
+
+ @Override
+ @Nullable
+ public FloodgatePlayer getPendingPlayerByUsername(String rawUsername) {
+ return pendingPlayerManager.getByUsername(rawUsername);
+ }
}
diff --git a/core/src/main/java/org/geysermc/floodgate/module/CommonModule.java b/core/src/main/java/org/geysermc/floodgate/module/CommonModule.java
index cddb93f7..2485bf19 100644
--- a/core/src/main/java/org/geysermc/floodgate/module/CommonModule.java
+++ b/core/src/main/java/org/geysermc/floodgate/module/CommonModule.java
@@ -67,6 +67,7 @@
import org.geysermc.floodgate.link.PlayerLinkHolder;
import org.geysermc.floodgate.packet.PacketHandlersImpl;
import org.geysermc.floodgate.player.FloodgateHandshakeHandler;
+import org.geysermc.floodgate.player.PendingPlayerManager;
import org.geysermc.floodgate.pluginmessage.PluginMessageManager;
import org.geysermc.floodgate.skin.SkinUploadManager;
import org.geysermc.floodgate.util.Constants;
@@ -175,10 +176,11 @@ public FloodgateHandshakeHandler handshakeHandler(
SkinUploadManager skinUploadManager,
@Named("playerAttribute") AttributeKey playerAttribute,
FloodgateLogger logger,
- LanguageManager languageManager) {
+ LanguageManager languageManager,
+ PendingPlayerManager pendingPlayerManager) {
return new FloodgateHandshakeHandler(handshakeHandlers, api, cipher, config,
- skinUploadManager, playerAttribute, logger, languageManager);
+ skinUploadManager, playerAttribute, logger, languageManager, pendingPlayerManager);
}
@Provides
diff --git a/core/src/main/java/org/geysermc/floodgate/player/FloodgateHandshakeHandler.java b/core/src/main/java/org/geysermc/floodgate/player/FloodgateHandshakeHandler.java
index b84ff1ba..770c6ac9 100644
--- a/core/src/main/java/org/geysermc/floodgate/player/FloodgateHandshakeHandler.java
+++ b/core/src/main/java/org/geysermc/floodgate/player/FloodgateHandshakeHandler.java
@@ -67,6 +67,7 @@ public final class FloodgateHandshakeHandler {
private final AttributeKey playerAttribute;
private final FloodgateLogger logger;
private final LanguageManager languageManager;
+ private final PendingPlayerManager pendingPlayerManager;
public FloodgateHandshakeHandler(
HandshakeHandlersImpl handshakeHandlers,
@@ -76,7 +77,8 @@ public FloodgateHandshakeHandler(
SkinUploadManager skinUploadManager,
AttributeKey playerAttribute,
FloodgateLogger logger,
- LanguageManager languageManager) {
+ LanguageManager languageManager,
+ PendingPlayerManager pendingPlayerManager) {
this.handshakeHandlers = handshakeHandlers;
this.api = api;
@@ -86,6 +88,7 @@ public FloodgateHandshakeHandler(
this.playerAttribute = playerAttribute;
this.logger = logger;
this.languageManager = languageManager;
+ this.pendingPlayerManager = pendingPlayerManager;
}
/**
@@ -241,6 +244,8 @@ private HandshakeResult handlePart2(
InetSocketAddress socketAddress = new InetSocketAddress(handshakeData.getIp(), port);
player.addProperty(PropertyKey.SOCKET_ADDRESS, socketAddress);
+ pendingPlayerManager.add(socketAddress, player);
+
return new HandshakeResult(ResultType.SUCCESS, handshakeData, bedrockData, player);
} catch (Exception exception) {
exception.printStackTrace();
diff --git a/core/src/main/java/org/geysermc/floodgate/player/PendingPlayerManager.java b/core/src/main/java/org/geysermc/floodgate/player/PendingPlayerManager.java
new file mode 100644
index 00000000..2239a5dc
--- /dev/null
+++ b/core/src/main/java/org/geysermc/floodgate/player/PendingPlayerManager.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2019-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/Floodgate
+ */
+
+package org.geysermc.floodgate.player;
+
+import jakarta.inject.Singleton;
+import java.net.InetSocketAddress;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.geysermc.floodgate.api.player.FloodgatePlayer;
+
+@Singleton
+public class PendingPlayerManager {
+
+ private final Map pendingByAddress = new ConcurrentHashMap<>();
+ private final Map pendingByUsername = new ConcurrentHashMap<>();
+
+ /**
+ * Register a pending player after successful handshake decryption.
+ * Called from platform-specific handshake handlers.
+ *
+ * @param address the connection address
+ * @param player the FloodgatePlayer instance
+ */
+ public void add(InetSocketAddress address, FloodgatePlayer player) {
+ pendingByAddress.put(address, player);
+ pendingByUsername.put(player.getUsername().toLowerCase(), player);
+ }
+
+ /**
+ * Remove a pending player after login completion or disconnect.
+ *
+ * @param address the connection address
+ */
+ public void remove(InetSocketAddress address) {
+ FloodgatePlayer player = pendingByAddress.remove(address);
+ if (player != null) {
+ pendingByUsername.remove(player.getUsername().toLowerCase());
+ }
+ }
+
+ /**
+ * Remove a pending player by username.
+ *
+ * @param rawUsername the raw username without prefix
+ */
+ public void removeByUsername(String rawUsername) {
+ if (rawUsername == null) return;
+ FloodgatePlayer player = pendingByUsername.remove(rawUsername.toLowerCase());
+ if (player != null) {
+ pendingByAddress.entrySet().removeIf(entry ->
+ entry.getValue().getUsername().equalsIgnoreCase(rawUsername));
+ }
+ }
+
+ /**
+ * Get a pending player by connection address.
+ *
+ * @param address the connection address
+ * @return the FloodgatePlayer or null if not found
+ */
+ public FloodgatePlayer get(InetSocketAddress address) {
+ return pendingByAddress.get(address);
+ }
+
+ /**
+ * Get a pending player by raw username (without prefix).
+ *
+ * @param rawUsername the raw username
+ * @return the FloodgatePlayer or null if not found
+ */
+ public FloodgatePlayer getByUsername(String rawUsername) {
+ if (rawUsername == null) return null;
+ return pendingByUsername.get(rawUsername.toLowerCase());
+ }
+}
diff --git a/spigot/src/main/java/org/geysermc/floodgate/listener/SpigotListener.java b/spigot/src/main/java/org/geysermc/floodgate/listener/SpigotListener.java
index 5ef02e92..bd15014b 100644
--- a/spigot/src/main/java/org/geysermc/floodgate/listener/SpigotListener.java
+++ b/spigot/src/main/java/org/geysermc/floodgate/listener/SpigotListener.java
@@ -26,15 +26,18 @@
package org.geysermc.floodgate.listener;
import com.google.inject.Inject;
+import java.net.InetSocketAddress;
import java.util.UUID;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.geysermc.floodgate.api.SimpleFloodgateApi;
import org.geysermc.floodgate.api.logger.FloodgateLogger;
import org.geysermc.floodgate.api.player.FloodgatePlayer;
+import org.geysermc.floodgate.player.PendingPlayerManager;
import org.geysermc.floodgate.skin.SkinApplier;
import org.geysermc.floodgate.util.LanguageManager;
import org.geysermc.floodgate.util.MojangUtils;
@@ -43,6 +46,7 @@ public final class SpigotListener implements Listener {
@Inject private SimpleFloodgateApi api;
@Inject private LanguageManager languageManager;
@Inject private FloodgateLogger logger;
+ @Inject private PendingPlayerManager pendingPlayerManager;
@Inject private MojangUtils mojangUtils;
@Inject private SkinApplier skinApplier;
@@ -76,8 +80,15 @@ public void onPlayerJoin(PlayerJoinEvent event) {
}
}
+ @EventHandler
+ public void onPlayerLogin(PlayerLoginEvent event) {
+ InetSocketAddress address = event.getPlayer().getAddress();
+ pendingPlayerManager.remove(address);
+ }
+
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent event) {
api.playerRemoved(event.getPlayer().getUniqueId());
+ pendingPlayerManager.remove(event.getPlayer().getAddress());
}
}
diff --git a/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java b/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java
index 6c67a31f..2493550d 100644
--- a/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java
+++ b/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java
@@ -57,6 +57,7 @@
import org.geysermc.floodgate.api.logger.FloodgateLogger;
import org.geysermc.floodgate.api.player.FloodgatePlayer;
import org.geysermc.floodgate.config.ProxyFloodgateConfig;
+import org.geysermc.floodgate.player.PendingPlayerManager;
import org.geysermc.floodgate.skin.SkinDataImpl;
import org.geysermc.floodgate.util.Constants;
import org.geysermc.floodgate.util.LanguageManager;
@@ -117,6 +118,9 @@ public final class VelocityListener {
@Inject
private MojangUtils mojangUtils;
+ @Inject
+ private PendingPlayerManager pendingPlayerManager;
+
@Subscribe(order = PostOrder.EARLY)
public void onPreLogin(PreLoginEvent event) {
FloodgatePlayer player = null;
@@ -197,10 +201,13 @@ public void onLogin(LoginEvent event) {
languageManager.loadLocale(player.getLanguageCode());
}
}
+
+ pendingPlayerManager.remove(event.getPlayer().getRemoteAddress());
}
@Subscribe(order = PostOrder.LAST)
public void onDisconnect(DisconnectEvent event) {
api.playerRemoved(event.getPlayer().getUniqueId());
+ pendingPlayerManager.remove(event.getPlayer().getRemoteAddress());
}
}