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 ---