Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<OfflineBungeeVote> c = getVoteCacheHandler().getVotes(server);
ArrayList<OfflineBungeeVote> removed = new ArrayList<>();
Expand All @@ -588,19 +588,19 @@ 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;
}
}
if (toSend) {
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<String> targets = proxyBroadcastDecider.resolveTargets(playerOnline,
Expand Down Expand Up @@ -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<OfflineBungeeVote> c = getVoteCacheHandler().getOnlineVotes(uuid);
if (!c.isEmpty()) {
if (server == null) {
server = getCurrentPlayerServer(player);
server = getCurrentPlayerServerForVoteRouting(player);
}
if (!getConfig().getBlockedServers().contains(server)) {
int num = 1;
Expand All @@ -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<String> targets = proxyBroadcastDecider.resolveTargets(true, playerServer);
broadcastHere = proxyBroadcastDecider.shouldBroadcast(server, targets);
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -1480,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<UUID> handoffPlayers = completePendingPresenceHandoffs(snapshot.requestId, snapshot.server,
snapshot.backendIncarnationId, snapshot.backendStartedAt, now);
processDedicatedSnapshotLogins(snapshot.server, handoffPlayers);
}
}
});
Expand Down Expand Up @@ -1905,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<UUID> completePendingPresenceHandoffs(UUID requestId, String server, UUID backendIncarnationId,
long backendStartedAt, long now) {
List<PendingPresenceHandoff> completed = new ArrayList<>();
Set<UUID> completedPlayers = new LinkedHashSet<>();
synchronized (pendingPresenceHandoffs) {
prunePendingPresenceHandoffs(now);
pendingPresenceHandoffs.entrySet().removeIf(entry -> {
Expand All @@ -1928,9 +1969,29 @@ private void completePendingPresenceHandoffs(UUID requestId, String server, UUID
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);
}
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<UUID> handoffPlayers) {
if (!isDedicatedVotingProxyEnabled() || server == null || server.isBlank()) {
return;
}
Set<UUID> 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) {
Expand Down Expand Up @@ -2133,7 +2194,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();
Expand Down Expand Up @@ -2296,12 +2357,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);
Expand Down Expand Up @@ -2468,7 +2537,7 @@ public void sendServerNameMessage() {
}

public void sendVoteParty(String server) {
if (isSomeoneOnlineServer(server)) {
if (isSomeoneOnlineServerForVoteRouting(server)) {
globalMessageProxyHandler.sendMessage(server, 1, VotingPluginWire.votePartyBungee());
}
}
Expand All @@ -2495,7 +2564,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);
Expand Down Expand Up @@ -2730,8 +2799,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();

Expand Down Expand Up @@ -2894,7 +2963,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,13 @@ default List<String> 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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object> getSpigotServerConfiguration(String s) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,11 @@ public boolean getSendVotesToAllServers() {
return getBoolean(getNode("SendVotesToAllServers"), true);
}

@Override
public boolean getDedicatedVotingProxy() {
return getBoolean(getNode("DedicatedVotingProxy"), false);
}

@Override
public Map<String, Object> getSpigotServerConfiguration(String s) {
return configToMap(getNode("SpigotServers", s));
Expand Down
9 changes: 7 additions & 2 deletions VotingPlugin/src/main/resources/bungeeconfig.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,53 @@ 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 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,22 @@ 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);
}

public void processDedicatedSnapshotLoginsForTest(String server, Set<UUID> handoffPlayers) {
processDedicatedSnapshotLogins(server, handoffPlayers);
}

@Override
public void setVoteCacheLastUpdated() {
// TODO Auto-generated method stub
Expand Down
Loading