From b6b01f8964f1e0ec4871edfafb42cc2bf23c70c8 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 16 Aug 2026 16:24:44 -0600 Subject: [PATCH 1/4] Add dedicated voting proxy routing AI disclosure: This commit message was written with assistance from ChatGPT. --- .../votingplugin/proxy/VotingPluginProxy.java | 82 +++++++++++++++---- .../proxy/VotingPluginProxyConfig.java | 7 ++ .../proxy/bungee/BungeeConfig.java | 11 ++- .../proxy/velocity/VelocityConfig.java | 5 ++ .../src/main/resources/bungeeconfig.yml | 9 +- .../tests/VotingPluginProxyTest.java | 28 +++++++ .../tests/VotingPluginProxyTestImpl.java | 12 +++ 7 files changed, 133 insertions(+), 21 deletions(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java index c9da6486c..293bfe2c4 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java @@ -563,7 +563,7 @@ protected boolean sendProxyBroadcastEnvelopeNow(String server, JsonEnvelope enve public synchronized void checkCachedVotes(String server) { int delay = 1; if (isServerValid(server)) { - if (isSomeoneOnlineServer(server)) { + if (isSomeoneOnlineServerForVoteRouting(server)) { if (getVoteCacheHandler().hasVotes(server) && !getConfig().getBlockedServers().contains(server)) { ArrayList c = getVoteCacheHandler().getVotes(server); ArrayList removed = new ArrayList<>(); @@ -588,10 +588,10 @@ public synchronized void checkCachedVotes(String server) { boolean toSend = true; if (getConfig().getWaitForUserOnline()) { - if (!isPlayerOnline(cache.getPlayerName())) { + if (!isPlayerOnlineForVoteRouting(cache.getPlayerName())) { toSend = false; - } else if (isPlayerOnline(cache.getPlayerName()) - && !getCurrentPlayerServer(cache.getPlayerName()).equals(server)) { + } else if (isPlayerOnlineForVoteRouting(cache.getPlayerName()) + && !getCurrentPlayerServerForVoteRouting(cache.getPlayerName()).equals(server)) { toSend = false; } } @@ -599,8 +599,8 @@ public synchronized void checkCachedVotes(String server) { boolean broadcastHere = cache.needsBroadcastOn(server); if (!cache.isProxyBroadcastHandled() && broadcastHere && getConfig().getProxyBroadcastEnabled()) { - boolean playerOnline = isPlayerOnline(cache.getPlayerName()); - String playerServer = playerOnline ? getCurrentPlayerServer(cache.getPlayerName()) + boolean playerOnline = isPlayerOnlineForVoteRouting(cache.getPlayerName()); + String playerServer = playerOnline ? getCurrentPlayerServerForVoteRouting(cache.getPlayerName()) : null; Set targets = proxyBroadcastDecider.resolveTargets(playerOnline, @@ -636,11 +636,11 @@ && getConfig().getProxyBroadcastEnabled()) { public synchronized void checkOnlineVotes(String player, String uuid, String server) { int delay = 1; - if (isPlayerOnline(player) && getVoteCacheHandler().hasOnlineVotes(uuid)) { + if (isPlayerOnlineForVoteRouting(player) && getVoteCacheHandler().hasOnlineVotes(uuid)) { ArrayList c = getVoteCacheHandler().getOnlineVotes(uuid); if (!c.isEmpty()) { if (server == null) { - server = getCurrentPlayerServer(player); + server = getCurrentPlayerServerForVoteRouting(player); } if (!getConfig().getBlockedServers().contains(server)) { int num = 1; @@ -663,7 +663,7 @@ public synchronized void checkOnlineVotes(String player, String uuid, String ser boolean broadcastHere = cache.needsBroadcastOn(server); if (!cache.isProxyBroadcastHandled() && broadcastHere && getConfig().getProxyBroadcastEnabled()) { - String playerServer = (server != null) ? server : getCurrentPlayerServer(player); + String playerServer = (server != null) ? server : getCurrentPlayerServerForVoteRouting(player); Set targets = proxyBroadcastDecider.resolveTargets(true, playerServer); broadcastHere = proxyBroadcastDecider.shouldBroadcast(server, targets); @@ -997,6 +997,26 @@ public UUID fetchUUID(String playerName) throws IOException, InterruptedExceptio public abstract String getCurrentPlayerServer(String player); + /** + * Resolves a player's server for vote routing. A dedicated voting proxy has no + * local players, so it uses the backend presence tracker instead. + */ + protected String getCurrentPlayerServerForVoteRouting(String player) { + if (isDedicatedVotingProxyEnabled()) { + return backendPlayerPresenceTracker.getPlayer(player).map(presence -> presence.getServer()).orElse(null); + } + return getCurrentPlayerServer(player); + } + + /** + * Dedicated routing is intentionally unavailable on plugin messaging: that + * transport is attached to a player-facing proxy and does not carry backend + * presence snapshots. + */ + protected boolean isDedicatedVotingProxyEnabled() { + return getConfig().getDedicatedVotingProxy() && method != null && method.supportsBackendPresence(); + } + public abstract File getDataFolderPlugin(); public String getMonthTotalsWithDatePath() { @@ -1122,10 +1142,28 @@ protected int[] getProjectedVotePartyState(int acceptedVotes) { public abstract boolean isPlayerOnline(String playerName); + /** + * Checks online state for vote routing, using backend presence only when this + * proxy is explicitly configured as the dedicated voting proxy. + */ + protected boolean isPlayerOnlineForVoteRouting(String playerName) { + return isDedicatedVotingProxyEnabled() ? backendPlayerPresenceTracker.getPlayer(playerName).isPresent() + : isPlayerOnline(playerName); + } + public abstract boolean isServerValid(String server); public abstract boolean isSomeoneOnlineServer(String server); + protected boolean isSomeoneOnlineServerForVoteRouting(String server) { + if (!isDedicatedVotingProxyEnabled()) { + return isSomeoneOnlineServer(server); + } + com.bencodez.votingplugin.proxy.presence.BackendPresenceStatus status = backendPlayerPresenceTracker + .getBackendStatus(server); + return status != null && status.isAvailable() && status.getPlayerCount() > 0; + } + public abstract boolean isVoteCacheIgnoreTime(); public abstract MysqlConfig getVoteCacheMySQLConfig(); @@ -1154,6 +1192,7 @@ public void load(IVoteCache jsonStorage, INonVotedPlayersStorage nonVotedCacheJs if (getMethod() == null) { method = BungeeMethod.PLUGINMESSAGING; } + warnUnsupportedDedicatedVotingProxyMode(); uuidPlayerNameCache = getProxyMySQL().getRowsUUIDNameQuery(); bungeeTimeChecker.setTimeChangeFailSafeBypass(getConfig().getTimeChangeFailSafeBypass()); @@ -2133,7 +2172,7 @@ public void login(String playerName, String uuid, String serverName) { if (getConfig().getOnlineMode()) { addNonVotedPlayer(uuid, playerName); } - if (isPlayerOnline(playerName)) { + if (isPlayerOnlineForVoteRouting(playerName)) { if (getConfig().getGlobalDataEnabled()) { if (getGlobalDataHandler().isTimeChangedHappened()) { getGlobalDataHandler().checkForFinishedTimeChanges(); @@ -2296,12 +2335,20 @@ public void reload() { if (getMethod() == null) { method = BungeeMethod.PLUGINMESSAGING; } + warnUnsupportedDedicatedVotingProxyMode(); setCurrentVotePartyVotesRequired( getConfig().getVotePartyVotesRequired() + getVoteCacheVotePartyIncreaseVotesRequired()); loadMultiProxySupport(); } + private void warnUnsupportedDedicatedVotingProxyMode() { + if (getConfig().getDedicatedVotingProxy() && (method == null || !method.supportsBackendPresence())) { + logSevere("DedicatedVotingProxy requires MYSQL, REDIS, MQTT, or SOCKETS; PLUGINMESSAGING is disabled for " + + "dedicated-proxy routing. Falling back to normal proxy routing."); + } + } + public abstract void runAsync(Runnable run); public abstract void runConsoleCommand(String command); @@ -2468,7 +2515,7 @@ public void sendServerNameMessage() { } public void sendVoteParty(String server) { - if (isSomeoneOnlineServer(server)) { + if (isSomeoneOnlineServerForVoteRouting(server)) { globalMessageProxyHandler.sendMessage(server, 1, VotingPluginWire.votePartyBungee()); } } @@ -2495,7 +2542,7 @@ public void setCurrentVotePartyVotes(int amount) { public void status() { for (String s : getAllAvailableServers()) { - if (!isSomeoneOnlineServer(s)) { + if (!isSomeoneOnlineServerForVoteRouting(s)) { log("No players on server " + s + " to send test status message, please retest with someone online"); } else { log("Sending request for status message on " + s); @@ -2730,8 +2777,8 @@ private synchronized QueuedVoteResult vote(String player, String service, boolea player = getProperName(uuid, player); // Cache online state/server once (IMPORTANT for broadcast logic correctness) - final boolean playerOnline = isPlayerOnline(player); - final String playerServer = playerOnline ? getCurrentPlayerServer(player) : null; + final boolean playerOnline = isPlayerOnlineForVoteRouting(player); + final String playerServer = playerOnline ? getCurrentPlayerServerForVoteRouting(player) : null; long time = queueTime != 0 ? queueTime : LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); @@ -2884,7 +2931,10 @@ private synchronized QueuedVoteResult vote(String player, String service, boolea // =========================== // Send vote(s) to backend(s) // =========================== - if (getConfig().getSendVotesToAllServers()) { + // A dedicated voting proxy has no player-facing proxy state. Its confirmed + // backend presence selects one destination, so never fan a vote out merely + // because a legacy configuration still has SendVotesToAllServers enabled. + if (getConfig().getSendVotesToAllServers() && !isDedicatedVotingProxyEnabled()) { for (String s : getAllAvailableServers()) { boolean forceCache = getConfig().getWaitForUserOnline() @@ -2894,7 +2944,7 @@ private synchronized QueuedVoteResult vote(String player, String service, boolea debug("Forcing vote to cache for server " + s); } - if ((!isSomeoneOnlineServer(s) && method.requiresPlayerOnline()) || forceCache) { + if ((!isSomeoneOnlineServerForVoteRouting(s) && method.requiresPlayerOnline()) || forceCache) { voteStatus = VoteLogStatus.CACHED; boolean broadcastForwarded = standaloneProxyBroadcast && broadcastForwardedServers.containsAll(proxyBroadcastTargets); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxyConfig.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxyConfig.java index d4dc84bdb..27e631d85 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxyConfig.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxyConfig.java @@ -399,6 +399,13 @@ default List getProxyBroadcastOfflineForwardServers() { */ public boolean getSendVotesToAllServers(); + /** + * Gets whether this is the dedicated voting proxy for a multi-proxy network. + * + * @return true when backend-reported presence should drive vote routing + */ + public boolean getDedicatedVotingProxy(); + /** * Gets the configuration for a specific Spigot server. * diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/bungee/BungeeConfig.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/bungee/BungeeConfig.java index 7b35829e1..4a74ae58f 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/bungee/BungeeConfig.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/bungee/BungeeConfig.java @@ -262,9 +262,14 @@ public String getRedisUsername() { } @Override - public boolean getSendVotesToAllServers() { - return getData().getBoolean("SendVotesToAllServers"); - } + public boolean getSendVotesToAllServers() { + return getData().getBoolean("SendVotesToAllServers"); + } + + @Override + public boolean getDedicatedVotingProxy() { + return getData().getBoolean("DedicatedVotingProxy", false); + } @Override public Map getSpigotServerConfiguration(String s) { diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/velocity/VelocityConfig.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/velocity/VelocityConfig.java index ec9806bd2..ca745b995 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/velocity/VelocityConfig.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/velocity/VelocityConfig.java @@ -269,6 +269,11 @@ public boolean getSendVotesToAllServers() { return getBoolean(getNode("SendVotesToAllServers"), true); } + @Override + public boolean getDedicatedVotingProxy() { + return getBoolean(getNode("DedicatedVotingProxy"), false); + } + @Override public Map getSpigotServerConfiguration(String s) { return configToMap(getNode("SpigotServers", s)); diff --git a/VotingPlugin/src/main/resources/bungeeconfig.yml b/VotingPlugin/src/main/resources/bungeeconfig.yml index a46a6f4bb..321d835f6 100644 --- a/VotingPlugin/src/main/resources/bungeeconfig.yml +++ b/VotingPlugin/src/main/resources/bungeeconfig.yml @@ -230,8 +230,13 @@ ProxyBroadcast: Debug: false # Have a reward on each server # If false, will send to online server only -SendVotesToAllServers: true -# List of servers the plugin won't send the vote to +SendVotesToAllServers: true +# Enable only on a single dedicated voting proxy when regional proxies do not +# run VotingPlugin. Requires a non-PLUGINMESSAGING BungeeMethod; online player +# routing then uses backend presence reported through the global message system. +# Unknown players are treated as offline and follow the existing vote cache path. +DedicatedVotingProxy: false +# List of servers the plugin won't send the vote to # Uses names from bungeecoord config, only needed for non SOCKETS setup BlockedServers: - hub diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VotingPluginProxyTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VotingPluginProxyTest.java index afd99791c..86179fb3c 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VotingPluginProxyTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VotingPluginProxyTest.java @@ -163,6 +163,34 @@ void pluginMessagingIgnoresExtendedPresenceLogin() { assertEquals(0, spyProxy.getBackendPlayerPresenceTracker().getOnlinePlayerCount()); } + @Test + void dedicatedVotingProxyRoutesUsingConfirmedBackendPresence() { + Mockito.when(votingPluginProxy.getConfig().getDedicatedVotingProxy()).thenReturn(true); + votingPluginProxy.setMethod(BungeeMethod.MQTT); + long now = System.currentTimeMillis(); + java.util.UUID incarnation = java.util.UUID.randomUUID(); + java.util.UUID playerUuid = java.util.UUID.randomUUID(); + + assertTrue(votingPluginProxy.getBackendPlayerPresenceTracker().backendStarted("Server2", incarnation, + 1000L, 1000L, now)); + assertTrue(votingPluginProxy.getBackendPlayerPresenceTracker().playerOnline("Player", playerUuid.toString(), + "Server2", java.util.UUID.randomUUID(), incarnation, 1000L, 1100L, now)); + + assertTrue(votingPluginProxy.isPlayerOnlineForVoteRoutingForTest("Player")); + assertEquals("Server2", votingPluginProxy.getCurrentPlayerServerForVoteRoutingForTest("Player")); + assertTrue(votingPluginProxy.isSomeoneOnlineServerForVoteRoutingForTest("Server2")); + assertFalse(votingPluginProxy.isPlayerOnlineForVoteRoutingForTest("Unknown")); + } + + @Test + void dedicatedVotingProxyDoesNotUsePluginMessagingPresence() { + Mockito.when(votingPluginProxy.getConfig().getDedicatedVotingProxy()).thenReturn(true); + votingPluginProxy.setMethod(BungeeMethod.PLUGINMESSAGING); + + assertTrue(votingPluginProxy.isPlayerOnlineForVoteRoutingForTest("Player")); + assertEquals("Server1", votingPluginProxy.getCurrentPlayerServerForVoteRoutingForTest("Player")); + } + @Test void handoffBlockedBySnapshotCooldownIsRetried() { votingPluginProxy.setMethod(BungeeMethod.MQTT); diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VotingPluginProxyTestImpl.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VotingPluginProxyTestImpl.java index e63c8ed4e..80c34cef9 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VotingPluginProxyTestImpl.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VotingPluginProxyTestImpl.java @@ -253,6 +253,18 @@ public boolean canForwardStandaloneBroadcastForTest(boolean managesTotals) { return canForwardStandaloneBroadcast(managesTotals); } + public boolean isPlayerOnlineForVoteRoutingForTest(String player) { + return isPlayerOnlineForVoteRouting(player); + } + + public String getCurrentPlayerServerForVoteRoutingForTest(String player) { + return getCurrentPlayerServerForVoteRouting(player); + } + + public boolean isSomeoneOnlineServerForVoteRoutingForTest(String server) { + return isSomeoneOnlineServerForVoteRouting(server); + } + @Override public void setVoteCacheLastUpdated() { // TODO Auto-generated method stub From 07ffb08f2f1583502b00dbdd2562ca6081cd0c62 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 16 Aug 2026 16:28:38 -0600 Subject: [PATCH 2/4] Preserve send-to-all behavior in dedicated mode AI disclosure: This commit message was written with assistance from ChatGPT. --- .../com/bencodez/votingplugin/proxy/VotingPluginProxy.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java index 293bfe2c4..debb6387e 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java @@ -2931,10 +2931,7 @@ private synchronized QueuedVoteResult vote(String player, String service, boolea // =========================== // Send vote(s) to backend(s) // =========================== - // A dedicated voting proxy has no player-facing proxy state. Its confirmed - // backend presence selects one destination, so never fan a vote out merely - // because a legacy configuration still has SendVotesToAllServers enabled. - if (getConfig().getSendVotesToAllServers() && !isDedicatedVotingProxyEnabled()) { + if (getConfig().getSendVotesToAllServers()) { for (String s : getAllAvailableServers()) { boolean forceCache = getConfig().getWaitForUserOnline() From e72772c5af469a8fe3c503debea54c5827c26b8d Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 16 Aug 2026 16:31:32 -0600 Subject: [PATCH 3/4] Drain cached votes after dedicated presence snapshots AI disclosure: This commit message was written with assistance from ChatGPT. --- .../votingplugin/proxy/VotingPluginProxy.java | 26 +++++++++++++++++-- .../tests/VotingPluginProxyTest.java | 19 ++++++++++++++ .../tests/VotingPluginProxyTestImpl.java | 4 +++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java index debb6387e..a805787d9 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java @@ -1519,8 +1519,9 @@ && isPresenceGenerationValid(snapshot.backendIncarnationId, snapshot.backendStar } } else if (backendPlayerPresenceTracker.getPendingSnapshotRequestId(snapshot.server, now) == null) { pendingBackendRecoverySnapshots.remove(presenceServerKey(snapshot.server)); - completePendingPresenceHandoffs(snapshot.requestId, snapshot.server, + Set handoffPlayers = completePendingPresenceHandoffs(snapshot.requestId, snapshot.server, snapshot.backendIncarnationId, snapshot.backendStartedAt, now); + processDedicatedSnapshotLogins(snapshot.server, handoffPlayers); } } }); @@ -1944,9 +1945,10 @@ private boolean isPresenceHandoffValid(PendingPresenceHandoff handoff, long now) && now >= handoff.createdAt && now - handoff.createdAt <= PRESENCE_HANDOFF_TIMEOUT_MILLIS; } - private void completePendingPresenceHandoffs(UUID requestId, String server, UUID backendIncarnationId, + private Set completePendingPresenceHandoffs(UUID requestId, String server, UUID backendIncarnationId, long backendStartedAt, long now) { List completed = new ArrayList<>(); + Set completedPlayers = new LinkedHashSet<>(); synchronized (pendingPresenceHandoffs) { prunePendingPresenceHandoffs(now); pendingPresenceHandoffs.entrySet().removeIf(entry -> { @@ -1963,6 +1965,7 @@ private void completePendingPresenceHandoffs(UUID requestId, String server, UUID }); } for (PendingPresenceHandoff handoff : completed) { + completedPlayers.add(handoff.playerUuid); PlayerPresence presence = backendPlayerPresenceTracker.getPlayer(handoff.playerUuid).orElse(null); if (presence != null && presence.getServer().equalsIgnoreCase(handoff.server) && presence.getConnectionId().equals(handoff.connectionId)) { @@ -1970,6 +1973,25 @@ private void completePendingPresenceHandoffs(UUID requestId, String server, UUID } releaseDestinationClaim(handoff); } + return completedPlayers; + } + + /** + * Drains voter-keyed cached rewards when a complete recovery snapshot first + * confirms a player on a dedicated voting proxy. Cross-backend handoffs are + * already processed by their token-bound completion path and are excluded to + * avoid a second login callback. + */ + protected void processDedicatedSnapshotLogins(String server, Set handoffPlayers) { + if (!isDedicatedVotingProxyEnabled() || server == null || server.isBlank()) { + return; + } + Set excluded = handoffPlayers == null ? Collections.emptySet() : handoffPlayers; + for (PlayerPresence presence : backendPlayerPresenceTracker.getOnlinePlayers()) { + if (presence.getServer().equalsIgnoreCase(server) && !excluded.contains(presence.getUuid())) { + login(presence.getPlayerName(), presence.getUuid().toString(), presence.getServer()); + } + } } private void discardPendingPresenceHandoff(String uuid) { diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VotingPluginProxyTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VotingPluginProxyTest.java index 86179fb3c..3c03997bf 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VotingPluginProxyTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VotingPluginProxyTest.java @@ -191,6 +191,25 @@ void dedicatedVotingProxyDoesNotUsePluginMessagingPresence() { assertEquals("Server1", votingPluginProxy.getCurrentPlayerServerForVoteRoutingForTest("Player")); } + @Test + void dedicatedSnapshotDrainsCachedVotesForConfirmedPlayers() { + Mockito.when(votingPluginProxy.getConfig().getDedicatedVotingProxy()).thenReturn(true); + votingPluginProxy.setMethod(BungeeMethod.MQTT); + long now = System.currentTimeMillis(); + java.util.UUID incarnation = java.util.UUID.randomUUID(); + java.util.UUID playerUuid = java.util.UUID.randomUUID(); + assertTrue(votingPluginProxy.getBackendPlayerPresenceTracker().backendStarted("Server2", incarnation, + 1000L, 1000L, now)); + assertTrue(votingPluginProxy.getBackendPlayerPresenceTracker().playerOnline("Player", playerUuid.toString(), + "Server2", java.util.UUID.randomUUID(), incarnation, 1000L, 1100L, now)); + + VotingPluginProxyTestImpl spyProxy = Mockito.spy(votingPluginProxy); + doNothing().when(spyProxy).login(Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); + spyProxy.processDedicatedSnapshotLoginsForTest("Server2", java.util.Collections.emptySet()); + + verify(spyProxy).login("Player", playerUuid.toString(), "Server2"); + } + @Test void handoffBlockedBySnapshotCooldownIsRetried() { votingPluginProxy.setMethod(BungeeMethod.MQTT); diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VotingPluginProxyTestImpl.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VotingPluginProxyTestImpl.java index 80c34cef9..500c04370 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VotingPluginProxyTestImpl.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VotingPluginProxyTestImpl.java @@ -265,6 +265,10 @@ public boolean isSomeoneOnlineServerForVoteRoutingForTest(String server) { return isSomeoneOnlineServerForVoteRouting(server); } + public void processDedicatedSnapshotLoginsForTest(String server, Set handoffPlayers) { + processDedicatedSnapshotLogins(server, handoffPlayers); + } + @Override public void setVoteCacheLastUpdated() { // TODO Auto-generated method stub From a8a7b8066097f9f8b0ee527c08ab2673dd88620e Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 16 Aug 2026 16:37:38 -0600 Subject: [PATCH 4/4] Drain snapshot players after stale handoffs AI disclosure: This commit message was written with assistance from ChatGPT. --- .../java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java index a805787d9..162d3303a 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java @@ -1965,11 +1965,11 @@ private Set completePendingPresenceHandoffs(UUID requestId, String server, }); } for (PendingPresenceHandoff handoff : completed) { - completedPlayers.add(handoff.playerUuid); PlayerPresence presence = backendPlayerPresenceTracker.getPlayer(handoff.playerUuid).orElse(null); if (presence != null && presence.getServer().equalsIgnoreCase(handoff.server) && presence.getConnectionId().equals(handoff.connectionId)) { login(handoff.playerName, handoff.uuid, handoff.server); + completedPlayers.add(handoff.playerUuid); } releaseDestinationClaim(handoff); }