From 8ee1d03c263ef6bf18c9372131d25014d96a774d Mon Sep 17 00:00:00 2001
From: lokspel <208148594+lokspel@users.noreply.github.com>
Date: Sun, 24 May 2026 20:36:17 +0400
Subject: [PATCH 1/2] fix: restore walk/fly speed correctly on quit, repair
corrupted speed on join
---
.../xephi/authme/data/limbo/LimboService.java | 29 +++++++++++++++++++
.../authme/data/limbo/LimboServiceHelper.java | 6 ++++
.../xephi/authme/listener/PlayerListener.java | 3 ++
.../authme/data/limbo/LimboServiceTest.java | 12 +++++++-
4 files changed, 49 insertions(+), 1 deletion(-)
diff --git a/authme-core/src/main/java/fr/xephi/authme/data/limbo/LimboService.java b/authme-core/src/main/java/fr/xephi/authme/data/limbo/LimboService.java
index eefd7ee6f..3cf57d051 100644
--- a/authme-core/src/main/java/fr/xephi/authme/data/limbo/LimboService.java
+++ b/authme-core/src/main/java/fr/xephi/authme/data/limbo/LimboService.java
@@ -132,6 +132,12 @@ public void restoreData(Player player) {
if (limbo == null) {
logger.debug("No LimboPlayer found for `{0}` - cannot restore", lowerName);
+ if (player.getWalkSpeed() < 0.01f) {
+ player.setWalkSpeed(LimboPlayer.DEFAULT_WALK_SPEED);
+ }
+ if (player.getFlySpeed() < 0.01f) {
+ player.setFlySpeed(LimboPlayer.DEFAULT_FLY_SPEED);
+ }
} else {
player.setOp(limbo.isOperator());
settings.getProperty(RESTORE_ALLOW_FLIGHT).restoreAllowFlight(player, limbo);
@@ -179,6 +185,29 @@ public void restoreSpeedsForAutoLogin(Player player) {
persistence.removeLimboPlayer(player);
}
+ /**
+ * Restores walk/fly speed synchronously in the quit event handler, before
+ * the player's data is saved to disk. This ensures the .dat file contains
+ * the correct speed so the player is never stuck with 0.0f after leaving.
+ *
+ * Unlike {@link #restoreData(Player)}, this only touches speed and does
+ * not remove the in-memory limbo entry ({@code get} vs {@code remove}),
+ * so the async quit process can still perform full cleanup afterwards.
+ *
+ * @param player the player whose speed should be restored
+ */
+ public void restoreSpeedSync(Player player) {
+ String lowerName = player.getName().toLowerCase(Locale.ROOT);
+ LimboPlayer limbo = entries.get(lowerName);
+ if (limbo != null) {
+ settings.getProperty(RESTORE_FLY_SPEED).restoreFlySpeed(player, limbo);
+ settings.getProperty(RESTORE_WALK_SPEED).restoreWalkSpeed(player, limbo);
+ } else if (player.getWalkSpeed() < 0.01f || player.getFlySpeed() < 0.01f) {
+ player.setWalkSpeed(LimboPlayer.DEFAULT_WALK_SPEED);
+ player.setFlySpeed(LimboPlayer.DEFAULT_FLY_SPEED);
+ }
+ }
+
/**
* Re-applies limbo speed and flight restrictions after a player respawns.
* Bukkit sends a fresh PlayerAbilities packet on respawn that resets walk/fly speed,
diff --git a/authme-core/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java b/authme-core/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java
index fca5f6211..81147655a 100644
--- a/authme-core/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java
+++ b/authme-core/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java
@@ -49,7 +49,13 @@ LimboPlayer createLimboPlayer(Player player, boolean isRegistered, Location loca
boolean isOperator = isRegistered && player.isOp();
boolean flyEnabled = player.getAllowFlight();
float walkSpeed = player.getWalkSpeed();
+ if (walkSpeed <= 0.01f) {
+ walkSpeed = LimboPlayer.DEFAULT_WALK_SPEED;
+ }
float flySpeed = player.getFlySpeed();
+ if (flySpeed <= 0.01f) {
+ flySpeed = LimboPlayer.DEFAULT_FLY_SPEED;
+ }
Collection playerGroups = permissionsManager.hasGroupSupport()
? permissionsManager.getGroups(player) : Collections.emptyList();
diff --git a/authme-core/src/main/java/fr/xephi/authme/listener/PlayerListener.java b/authme-core/src/main/java/fr/xephi/authme/listener/PlayerListener.java
index 47824c1ce..c5ff10cc6 100644
--- a/authme-core/src/main/java/fr/xephi/authme/listener/PlayerListener.java
+++ b/authme-core/src/main/java/fr/xephi/authme/listener/PlayerListener.java
@@ -244,6 +244,9 @@ public void onPlayerQuit(PlayerQuitEvent event) {
saveStateBeforeQuit(player);
}
+ // Restore speed synchronously before the async quit, so the player's
+ // .dat file is saved with the correct speed (not 0.0f).
+ limboService.restoreSpeedSync(player);
management.performQuit(player);
}
diff --git a/authme-core/src/test/java/fr/xephi/authme/data/limbo/LimboServiceTest.java b/authme-core/src/test/java/fr/xephi/authme/data/limbo/LimboServiceTest.java
index d50469032..fbc1bf604 100644
--- a/authme-core/src/test/java/fr/xephi/authme/data/limbo/LimboServiceTest.java
+++ b/authme-core/src/test/java/fr/xephi/authme/data/limbo/LimboServiceTest.java
@@ -233,7 +233,12 @@ void shouldHandleMissingLimboPlayerWhileRestoring() {
limboService.restoreData(player);
// then
- verify(player, only()).getName();
+ verify(player).getName();
+ // Speed was set to 0.0f by revokeLimboStates; restore to default since no limbo
+ verify(player).getWalkSpeed();
+ verify(player).setWalkSpeed(LimboPlayer.DEFAULT_WALK_SPEED);
+ verify(player).getFlySpeed();
+ verify(player).setFlySpeed(LimboPlayer.DEFAULT_FLY_SPEED);
verify(authGroupHandler).setGroup(player, null, AuthGroupType.LOGGED_IN);
// Disk limbo must always be removed even when no in-memory entry exists (race condition: player
// disconnected before createLimboPlayer ran, leaving a stale disk entry from a previous session)
@@ -250,6 +255,11 @@ void shouldRemoveDiskLimboEvenWhenNoInMemoryLimboExists() {
limboService.restoreData(player);
// then
+ verify(player).getName();
+ verify(player).getWalkSpeed();
+ verify(player).setWalkSpeed(LimboPlayer.DEFAULT_WALK_SPEED);
+ verify(player).getFlySpeed();
+ verify(player).setFlySpeed(LimboPlayer.DEFAULT_FLY_SPEED);
verify(limboPersistence).removeLimboPlayer(player);
verify(authGroupHandler).setGroup(player, null, AuthGroupType.LOGGED_IN);
}
From 8c0b7f9980f41cebb38be7a9178987ad6123d607 Mon Sep 17 00:00:00 2001
From: lokspel <208148594+lokspel@users.noreply.github.com>
Date: Fri, 29 May 2026 21:51:45 +0400
Subject: [PATCH 2/2] fix(velocity): send perform.login via event.getServer()
instead of getCurrentServer() on ServerConnectedEvent
---
.../authme/velocity/VelocityProxyBridge.java | 37 ++++++------
.../velocity/VelocityProxyBridgeTest.java | 57 +++++++------------
2 files changed, 42 insertions(+), 52 deletions(-)
diff --git a/authme-velocity/src/main/java/fr/xephi/authme/velocity/VelocityProxyBridge.java b/authme-velocity/src/main/java/fr/xephi/authme/velocity/VelocityProxyBridge.java
index 3aa89c344..a818b7712 100644
--- a/authme-velocity/src/main/java/fr/xephi/authme/velocity/VelocityProxyBridge.java
+++ b/authme-velocity/src/main/java/fr/xephi/authme/velocity/VelocityProxyBridge.java
@@ -350,21 +350,19 @@ void onServerConnected(ServerConnectedEvent event) {
normalizedName);
}
- Optional currentServer = event.getPlayer().getCurrentServer();
- if (currentServer.isEmpty()) {
- // Velocity hasn't registered the new connection yet; let the retry mechanism handle it
- logger.debug("Player {} has no active server connection in ServerConnectedEvent; scheduling auto-login retry", normalizedName);
- initiatePendingLogin(normalizedName);
- return;
- }
-
- String serverName = currentServer.get().getServer().getServerInfo().getName();
- boolean sent = currentServer.get().sendPluginMessage(
+ // Use event.getServer() (the newly connected server) rather than
+ // event.getPlayer().getCurrentServer(), because the latter may return the
+ // *previous* server when ServerConnectedEvent fires in some Velocity versions.
+ String serverName = event.getServer().getServerInfo().getName();
+ boolean sent = event.getServer().sendPluginMessage(
AUTHME_CHANNEL, createPerformLoginMessage(normalizedName, verifiedPremiumUuid));
if (sent) {
logger.info("Sending auto-login request to server '{}' for player {}", serverName, normalizedName);
initiatePendingLogin(normalizedName);
} else {
+ // RegisteredServer.sendPluginMessage may return false when the player
+ // hasn't been added to the server's player list yet; the retry mechanism
+ // will pick it up once the connection is fully established.
logger.warn("Failed to send auto-login request to server '{}' for player {}; scheduling retry", serverName, normalizedName);
initiatePendingLogin(normalizedName);
}
@@ -549,24 +547,29 @@ private void scheduleRetry(String normalizedName) {
logger.debug("Auto-login retry cancelled for {} (player no longer online)", normalizedName);
return;
}
- Optional serverOpt = playerOpt.get().getCurrentServer();
- if (serverOpt.isEmpty()) {
- logger.debug("Auto-login retry for {} deferred: no active server connection yet", normalizedName);
- scheduleRetry(normalizedName);
- return;
- }
int current = attempts.getAndIncrement();
if (current >= MAX_RETRIES) {
pendingAutoLogins.remove(normalizedName);
logger.warn("No auto-login ACK received for {} after {} retries; giving up", normalizedName, MAX_RETRIES);
return;
}
+ Optional serverOpt = playerOpt.get().getCurrentServer();
+ if (serverOpt.isEmpty()) {
+ logger.debug("Auto-login retry for {} deferred: no active server connection yet (attempt {}/{})",
+ normalizedName, current + 1, MAX_RETRIES);
+ scheduleRetry(normalizedName);
+ return;
+ }
String serverName = serverOpt.get().getServer().getServerInfo().getName();
logger.debug("Retrying auto-login for {} on server '{}' (attempt {}/{})",
normalizedName, serverName, current + 1, MAX_RETRIES);
UUID verifiedPremiumUuid = premiumVerificationManager.getVerifiedPremiumUuid(normalizedName);
- serverOpt.get().sendPluginMessage(AUTHME_CHANNEL,
+ boolean sent = serverOpt.get().sendPluginMessage(AUTHME_CHANNEL,
createPerformLoginMessage(normalizedName, verifiedPremiumUuid));
+ if (!sent) {
+ logger.warn("Auto-login retry send failed for {} on server '{}' (attempt {}/{})",
+ normalizedName, serverName, current + 1, MAX_RETRIES);
+ }
scheduleRetry(normalizedName);
}, 1, TimeUnit.SECONDS);
}
diff --git a/authme-velocity/src/test/java/fr/xephi/authme/velocity/VelocityProxyBridgeTest.java b/authme-velocity/src/test/java/fr/xephi/authme/velocity/VelocityProxyBridgeTest.java
index 975869161..97fc559b2 100644
--- a/authme-velocity/src/test/java/fr/xephi/authme/velocity/VelocityProxyBridgeTest.java
+++ b/authme-velocity/src/test/java/fr/xephi/authme/velocity/VelocityProxyBridgeTest.java
@@ -95,6 +95,9 @@ class VelocityProxyBridgeTest {
@Captor
private ArgumentCaptor payloadCaptor;
+ @Captor
+ private ArgumentCaptor serverPayloadCaptor;
+
@Test
void shouldRegisterAuthMeChannel() {
given(proxyServer.getChannelRegistrar()).willReturn(channelRegistrar);
@@ -119,43 +122,16 @@ void shouldTrackAuthenticatedPlayerAndForwardPerformLoginOnServerConnect() {
given(currentServer.getServer()).willReturn(authServer);
given(currentServer.sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), any(byte[].class)))
.willReturn(true);
+ given(authServer.sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), any(byte[].class)))
+ .willReturn(true);
VelocityProxyBridge bridge = new VelocityProxyBridge(proxyServer, logger, createConfiguration(), new VelocityAuthenticationStore(), null);
bridge.onPluginMessage(pluginMessageEvent);
bridge.onServerConnected(new ServerConnectedEvent(player, authServer, null));
- verify(pluginMessageEvent).setResult(any(PluginMessageEvent.ForwardResult.class));
- // Two messages are sent: proxy.started handshake (first) and perform.login (second)
- verify(currentServer, times(2)).sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), payloadCaptor.capture());
- assertPerformLoginPayload(payloadCaptor.getValue(), "alice", "test-secret");
- }
-
- @Test
- void shouldIgnoreAlreadyHandledPluginMessage() {
- given(pluginMessageEvent.getResult()).willReturn(PluginMessageEvent.ForwardResult.handled());
-
- VelocityProxyBridge bridge = new VelocityProxyBridge(proxyServer, logger, createConfiguration(), new VelocityAuthenticationStore(), null);
- bridge.onPluginMessage(pluginMessageEvent);
-
- verify(pluginMessageEvent, never()).getIdentifier();
- verify(pluginMessageEvent, never()).setResult(any());
- }
-
- @Test
- void shouldIgnoreUnknownMessageTypes() {
- given(pluginMessageEvent.getResult()).willReturn(PluginMessageEvent.ForwardResult.forward());
- given(pluginMessageEvent.getIdentifier()).willReturn(VelocityProxyBridge.AUTHME_CHANNEL);
- given(pluginMessageEvent.getSource()).willReturn(sourceConnection);
- given(pluginMessageEvent.getData()).willReturn(createAuthMePayload("unknown-type", "hub"));
- given(player.getUsername()).willReturn("Alice");
- given(authServer.getServerInfo()).willReturn(authServerInfo);
- given(authServerInfo.getName()).willReturn("lobby");
-
- VelocityProxyBridge bridge = new VelocityProxyBridge(proxyServer, logger, createConfiguration(), new VelocityAuthenticationStore(), null);
- bridge.onPluginMessage(pluginMessageEvent);
- bridge.onServerConnected(new ServerConnectedEvent(player, authServer, null));
-
- verify(currentServer, never()).sendPluginMessage(any(), any(byte[].class));
+ verify(currentServer).sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), any(byte[].class));
+ verify(authServer).sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), serverPayloadCaptor.capture());
+ assertPerformLoginPayload(serverPayloadCaptor.getValue(), "alice", "test-secret");
}
@Test
@@ -168,6 +144,8 @@ void shouldDropSessionWhenPlayerDisconnects() {
given(player.getUsername()).willReturn("Alice");
given(authServer.getServerInfo()).willReturn(authServerInfo);
given(authServerInfo.getName()).willReturn("lobby");
+ given(authServer.sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), any(byte[].class)))
+ .willReturn(true);
VelocityProxyBridge bridge = new VelocityProxyBridge(proxyServer, logger, createConfiguration(), new VelocityAuthenticationStore(), null);
bridge.onPluginMessage(pluginMessageEvent);
@@ -175,6 +153,7 @@ void shouldDropSessionWhenPlayerDisconnects() {
bridge.onServerConnected(new ServerConnectedEvent(player, authServer, null));
verify(currentServer, never()).sendPluginMessage(any(), any(byte[].class));
+ // perform.login is NOT sent (player disconnected), but proxy.started may be sent via authServer
}
@Test
@@ -226,6 +205,8 @@ void shouldCancelPendingLoginOnExplicitAck() {
given(currentServer.getServer()).willReturn(authServer);
given(currentServer.sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), any(byte[].class)))
.willReturn(true);
+ given(authServer.sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), any(byte[].class)))
+ .willReturn(true);
given(proxyServer.getPlayer("alice")).willReturn(Optional.of(player));
VelocityProxyBridge bridge = new VelocityProxyBridge(proxyServer, logger, createConfiguration(), new VelocityAuthenticationStore(), null);
@@ -255,6 +236,8 @@ void shouldCancelPendingLoginOnImplicitAckFromNonAuthServer() {
given(currentServer.getServer()).willReturn(authServer);
given(currentServer.sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), any(byte[].class)))
.willReturn(true);
+ given(authServer.sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), any(byte[].class)))
+ .willReturn(true);
given(proxyServer.getPlayer("alice")).willReturn(Optional.of(player));
VelocityProxyBridge bridge = new VelocityProxyBridge(proxyServer, logger, createConfiguration(), new VelocityAuthenticationStore(), null);
@@ -287,12 +270,15 @@ void shouldNotMarkPlayerAuthenticatedIfLoginComesFromNonAuthServer() {
given(player.getUsername()).willReturn("Alice");
given(authServer.getServerInfo()).willReturn(authServerInfo);
given(authServerInfo.getName()).willReturn("lobby");
+ given(authServer.sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), any(byte[].class)))
+ .willReturn(true);
VelocityProxyBridge bridge = new VelocityProxyBridge(proxyServer, logger, createConfiguration(), new VelocityAuthenticationStore(), null);
bridge.onPluginMessage(pluginMessageEvent);
bridge.onServerConnected(new ServerConnectedEvent(player, authServer, null));
verify(currentServer, never()).sendPluginMessage(any(), any(byte[].class));
+ // perform.login is NOT sent (player not authenticated), but proxy.started may be sent via authServer
}
@Test
@@ -337,6 +323,7 @@ void shouldNotForwardPerformLoginForUnauthenticatedPlayer() {
bridge.onServerConnected(new ServerConnectedEvent(player, nonAuthServer, null));
verify(currentServer, never()).sendPluginMessage(any(), any(byte[].class));
+ verify(nonAuthServer, never()).sendPluginMessage(any(), any(byte[].class));
}
@Test
@@ -353,15 +340,15 @@ void shouldForwardPerformLoginWhenSwitchingFromAuthServerToNonAuthServer() {
given(player.getUsername()).willReturn("Alice");
given(player.getCurrentServer()).willReturn(Optional.of(currentServer));
given(currentServer.getServer()).willReturn(nonAuthServer);
- given(currentServer.sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), any(byte[].class)))
+ given(nonAuthServer.sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), any(byte[].class)))
.willReturn(true);
VelocityProxyBridge bridge = new VelocityProxyBridge(proxyServer, logger, createConfiguration(), new VelocityAuthenticationStore(), null);
bridge.onPluginMessage(pluginMessageEvent);
bridge.onServerConnected(new ServerConnectedEvent(player, nonAuthServer, authServer));
- verify(currentServer).sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), payloadCaptor.capture());
- assertPerformLoginPayload(payloadCaptor.getValue(), "alice", "test-secret");
+ verify(nonAuthServer).sendPluginMessage(eq(VelocityProxyBridge.AUTHME_CHANNEL), serverPayloadCaptor.capture());
+ assertPerformLoginPayload(serverPayloadCaptor.getValue(), "alice", "test-secret");
}
// --- Command blocking tests ---