Skip to content

Commit 6c2ab68

Browse files
committed
Enhance UUID lookup by normalizing offline player names in offline mode
Added command to convert to proper uuids
1 parent 3af5905 commit 6c2ab68

File tree

4 files changed

+294
-16
lines changed

4 files changed

+294
-16
lines changed

VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/CommandLoader.java

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import java.util.HashMap;
66
import java.util.HashSet;
77
import java.util.LinkedList;
8+
import java.util.List;
89
import java.util.Locale;
10+
import java.util.Map;
911
import java.util.Map.Entry;
1012
import java.util.Queue;
1113
import java.util.Set;
@@ -82,6 +84,8 @@
8284
import com.bencodez.votingplugin.user.VotingPluginUser;
8385
import com.bencodez.votingplugin.votesites.VoteSite;
8486

87+
import lombok.var;
88+
8589
// TODO: Auto-generated Javadoc
8690
/**
8791
* The Class CommandLoader.
@@ -1978,6 +1982,249 @@ public void execute(CommandSender sender, String[] args) {
19781982
}
19791983
});
19801984

1985+
// /av MergeOfflineUUIDs [true/false]
1986+
// - false (default) = dry run (prints what would change)
1987+
// - true = perform fixes + merges
1988+
plugin.getAdminVoteCommand().add(new CommandHandler(plugin, new String[] { "MergeOfflineUUIDs", "(Boolean)" },
1989+
"VotingPlugin.Commands.AdminVote.MergeOfflineUUIDs|" + adminPerm,
1990+
"Offline-mode data repair: fixes incorrect UUIDs for names, merges duplicates, and normalizes PlayerName casing. Use /av MergeOfflineUUIDs true to apply.",
1991+
true, true) {
1992+
1993+
@Override
1994+
public void execute(CommandSender sender, String[] args) {
1995+
boolean apply = args.length > 1 && args[1] != null && args[1].equalsIgnoreCase("true");
1996+
1997+
if (plugin.getOptions().isOnlineMode()) {
1998+
sendMessage(sender,
1999+
"&cServer is in online-mode. This command is intended for offline-mode UUID repairs.");
2000+
return;
2001+
}
2002+
2003+
ArrayList<String> allUuids = plugin.getUserManager().getAllUUIDs();
2004+
2005+
int scanned = 0;
2006+
int invalidUuidStrings = 0;
2007+
2008+
int rowsNeedingMove = 0; // uuid doesn't match expected offline uuid for name
2009+
int duplicateGroups = 0; // multiple uuids for the same normalized name
2010+
int rowsRemoved = 0; // rows deleted
2011+
int canonicalEnsured = 0; // canonical rows created/ensured
2012+
int groupsApplied = 0; // groups repaired
2013+
2014+
// normName -> list of uuids (as strings) that claim to be that player
2015+
Map<String, List<String>> byNormName = new HashMap<>();
2016+
2017+
for (String uuid : allUuids) {
2018+
if (uuid == null) {
2019+
continue;
2020+
}
2021+
uuid = uuid.trim();
2022+
if (uuid.isEmpty()) {
2023+
continue;
2024+
}
2025+
scanned++;
2026+
2027+
UUID parsed;
2028+
try {
2029+
parsed = UUID.fromString(uuid);
2030+
} catch (Exception e) {
2031+
invalidUuidStrings++;
2032+
continue;
2033+
}
2034+
2035+
// Load user and read PlayerName
2036+
var user = plugin.getUserManager().getUser(parsed);
2037+
if (user == null) {
2038+
continue;
2039+
}
2040+
2041+
user.userDataFetechMode(com.bencodez.advancedcore.api.user.UserDataFetchMode.NO_CACHE);
2042+
2043+
String name = user.getData().getString("PlayerName",
2044+
com.bencodez.advancedcore.api.user.UserDataFetchMode.NO_CACHE);
2045+
if (name == null) {
2046+
name = "";
2047+
}
2048+
name = name.trim();
2049+
if (name.isEmpty()) {
2050+
continue;
2051+
}
2052+
2053+
String norm = name.toLowerCase(java.util.Locale.ROOT);
2054+
2055+
byNormName.computeIfAbsent(norm, k -> new ArrayList<>()).add(uuid);
2056+
}
2057+
2058+
// Helpers
2059+
java.util.function.Function<String, String> offlineUuidForNormName = (normName) -> java.util.UUID
2060+
.nameUUIDFromBytes(
2061+
("OfflinePlayer:" + normName).getBytes(java.nio.charset.StandardCharsets.UTF_8))
2062+
.toString();
2063+
2064+
java.util.function.Function<String, Integer> getIntSafe = (s) -> {
2065+
try {
2066+
return Integer.parseInt(s);
2067+
} catch (Exception e) {
2068+
return 0;
2069+
}
2070+
};
2071+
2072+
for (Map.Entry<String, List<String>> entry : byNormName.entrySet()) {
2073+
String normName = entry.getKey();
2074+
List<String> uuids = entry.getValue();
2075+
if (uuids == null || uuids.isEmpty()) {
2076+
continue;
2077+
}
2078+
2079+
String canonicalUuid = offlineUuidForNormName.apply(normName);
2080+
2081+
// Count groups with dupes (for reporting)
2082+
if (uuids.size() > 1) {
2083+
duplicateGroups++;
2084+
}
2085+
2086+
// Determine if any uuid in this name-group is NOT the canonical uuid
2087+
boolean needsFix = false;
2088+
for (String u : uuids) {
2089+
if (!u.equalsIgnoreCase(canonicalUuid)) {
2090+
needsFix = true;
2091+
break;
2092+
}
2093+
}
2094+
2095+
if (!needsFix) {
2096+
// Still optionally normalize PlayerName casing to match normName (optional)
2097+
// but we won't touch anything if dry run and no changes needed.
2098+
continue;
2099+
}
2100+
2101+
// This group has either duplicates or "wrong uuid for name"
2102+
rowsNeedingMove += (int) uuids.stream().filter(u -> !u.equalsIgnoreCase(canonicalUuid)).count();
2103+
2104+
if (!apply) {
2105+
sendMessage(sender, "&e[DRY] &7Repair for &f" + normName + "&7 -> canonical &a" + canonicalUuid
2106+
+ "&7, found &f" + uuids.size() + "&7 rows: &f" + uuids);
2107+
continue;
2108+
}
2109+
2110+
try {
2111+
// Ensure canonical user exists/loaded
2112+
var canonicalUser = plugin.getUserManager().getUser(UUID.fromString(canonicalUuid));
2113+
canonicalUser.userDataFetechMode(com.bencodez.advancedcore.api.user.UserDataFetchMode.NO_CACHE);
2114+
canonicalEnsured++;
2115+
2116+
// Merge accumulators
2117+
int allTime = 0, month = 0, week = 0, day = 0, points = 0;
2118+
Map<String, Long> lastVotes = new HashMap<>();
2119+
2120+
// Merge ALL rows in this group into canonical (including canonical if present)
2121+
for (String u : uuids) {
2122+
UUID uid;
2123+
try {
2124+
uid = UUID.fromString(u);
2125+
} catch (Exception e) {
2126+
continue;
2127+
}
2128+
2129+
var fromUser = plugin.getUserManager().getUser(uid);
2130+
if (fromUser == null) {
2131+
continue;
2132+
}
2133+
fromUser.userDataFetechMode(com.bencodez.advancedcore.api.user.UserDataFetchMode.NO_CACHE);
2134+
2135+
allTime += fromUser.getData().getInt("AllTimeTotal",
2136+
com.bencodez.advancedcore.api.user.UserDataFetchMode.NO_CACHE);
2137+
month += fromUser.getData().getInt("MonthTotal",
2138+
com.bencodez.advancedcore.api.user.UserDataFetchMode.NO_CACHE);
2139+
week += fromUser.getData().getInt("WeeklyTotal",
2140+
com.bencodez.advancedcore.api.user.UserDataFetchMode.NO_CACHE);
2141+
day += fromUser.getData().getInt("DailyTotal",
2142+
com.bencodez.advancedcore.api.user.UserDataFetchMode.NO_CACHE);
2143+
points += fromUser.getData().getInt("Points",
2144+
com.bencodez.advancedcore.api.user.UserDataFetchMode.NO_CACHE);
2145+
2146+
String lv = fromUser.getData().getString("LastVotes",
2147+
com.bencodez.advancedcore.api.user.UserDataFetchMode.NO_CACHE);
2148+
if (lv != null && !lv.trim().isEmpty()) {
2149+
for (String line : lv.split(java.util.regex.Pattern.quote("%line%"))) {
2150+
String[] parts = line.split(java.util.regex.Pattern.quote("//"));
2151+
if (parts.length != 2) {
2152+
continue;
2153+
}
2154+
String site = parts[0];
2155+
long t;
2156+
try {
2157+
t = Long.parseLong(parts[1]);
2158+
} catch (Exception ex) {
2159+
continue;
2160+
}
2161+
lastVotes.merge(site, t, Math::max);
2162+
}
2163+
}
2164+
}
2165+
2166+
// Write merged values into canonical
2167+
canonicalUser.getData().setInt("AllTimeTotal", allTime);
2168+
canonicalUser.getData().setInt("MonthTotal", month);
2169+
canonicalUser.getData().setInt("WeeklyTotal", week);
2170+
canonicalUser.getData().setInt("DailyTotal", day);
2171+
canonicalUser.getData().setInt("Points", points);
2172+
2173+
// Rebuild LastVotes string
2174+
if (!lastVotes.isEmpty()) {
2175+
StringBuilder sb = new StringBuilder();
2176+
boolean first = true;
2177+
for (Map.Entry<String, Long> lv : lastVotes.entrySet()) {
2178+
if (!first) {
2179+
sb.append("%line%");
2180+
}
2181+
first = false;
2182+
sb.append(lv.getKey()).append("//").append(lv.getValue());
2183+
}
2184+
canonicalUser.getData().setString("LastVotes", sb.toString());
2185+
} else {
2186+
// optional: clear if none
2187+
// canonicalUser.getData().setString("LastVotes", "");
2188+
}
2189+
2190+
// Normalize stored PlayerName (your choice):
2191+
// - If you want to preserve "nice" casing, you could pick a representative name
2192+
// instead.
2193+
// - For strict consistency, store lowercased (normName) so future comparisons
2194+
// are stable.
2195+
canonicalUser.getData().setString("PlayerName", normName);
2196+
2197+
// Delete ALL non-canonical UUID rows for this normName
2198+
for (String u : uuids) {
2199+
if (u.equalsIgnoreCase(canonicalUuid)) {
2200+
continue;
2201+
}
2202+
try {
2203+
plugin.getUserManager().removeUUID(UUID.fromString(u));
2204+
rowsRemoved++;
2205+
} catch (Exception e) {
2206+
plugin.debug(e);
2207+
}
2208+
}
2209+
2210+
groupsApplied++;
2211+
sendMessage(sender, "&aRepaired &f" + normName + " &a-> &f" + canonicalUuid + " &a(merged "
2212+
+ uuids.size() + " rows, removed " + (uuids.size() - 1) + ")");
2213+
2214+
} catch (Exception e) {
2215+
sendMessage(sender, "&cFailed repairing group for &f" + normName + "&c: " + e.getMessage());
2216+
plugin.debug(e);
2217+
}
2218+
}
2219+
2220+
sendMessage(sender,
2221+
"&7Scanned: &f" + scanned + " &7InvalidUUIDStrings: &f" + invalidUuidStrings
2222+
+ " &7GroupsWithDupes: &f" + duplicateGroups + " &7RowsNeedingMove: &f"
2223+
+ rowsNeedingMove + " &7CanonicalEnsured: &f" + canonicalEnsured + " &7RowsRemoved: &f"
2224+
+ rowsRemoved + " &7GroupsApplied: &f" + groupsApplied + (apply ? "" : " &e(DRY RUN)"));
2225+
}
2226+
});
2227+
19812228
plugin.getAdminVoteCommand()
19822229
.add(new CommandHandler(plugin, new String[] { "MergeDataFrom", "(UserStorage)" },
19832230
"VotingPlugin.Commands.AdminVote.MergeDataFrom|" + adminPerm,

VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,21 @@ public void triggerVote(String player, String service, boolean realVote, boolean
989989
public abstract void log(String message);
990990

991991
public void login(String playerName, String uuid, String serverName) {
992+
// Offline-mode safety: always derive UUID from name using the proxy's UUID
993+
// lookup
994+
if (!getConfig().getOnlineMode()) {
995+
uuid = getUUID(playerName);
996+
}
997+
998+
// Canonicalize UUID string (lowercase dashed) to prevent cache/key splits
999+
try {
1000+
if (uuid != null && !uuid.isEmpty() && !uuid.equalsIgnoreCase("null")) {
1001+
uuid = UUID.fromString(uuid.trim()).toString();
1002+
}
1003+
} catch (Exception ignored) {
1004+
// leave as-is; downstream may handle empty/invalid
1005+
}
1006+
9921007
if (getConfig().getOnlineMode()) { // no need to cache in offline mode
9931008
addNonVotedPlayer(uuid, playerName);
9941009
}
@@ -1374,6 +1389,13 @@ public synchronized void vote(String player, String service, boolean realVote, b
13741389
}
13751390
}
13761391

1392+
// Offline-mode safety: always derive UUID deterministically from the
1393+
// (normalized) player name
1394+
// This prevents duplicate UUIDs caused by name casing differences
1395+
if (!getConfig().getOnlineMode()) {
1396+
uuid = getUUID(player);
1397+
}
1398+
13771399
if (uuid == null || uuid.isEmpty()) {
13781400
uuid = getUUID(player);
13791401
if (uuid.isEmpty() && !getConfig().getBedrockPlayerPrefix().isEmpty()
@@ -1418,6 +1440,15 @@ public synchronized void vote(String player, String service, boolean realVote, b
14181440
uuid = u.toString();
14191441
}
14201442

1443+
// Canonicalize UUID string (lowercase dashed) to prevent cache/key splits
1444+
try {
1445+
if (uuid != null && !uuid.isEmpty() && !uuid.equalsIgnoreCase("null")) {
1446+
uuid = UUID.fromString(uuid.trim()).toString();
1447+
}
1448+
} catch (Exception ignored) {
1449+
// handled below by uuid empty checks
1450+
}
1451+
14211452
player = getProperName(uuid, player);
14221453

14231454
UUID voteId = UUID.randomUUID();
@@ -1585,4 +1616,4 @@ public synchronized void vote(String player, String service, boolean realVote, b
15851616
public abstract void warn(String message);
15861617

15871618
public abstract ScheduledExecutorService getScheduler();
1588-
}
1619+
}

VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/bungee/VotingPluginBungee.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.Map.Entry;
1616
import java.util.Set;
1717
import java.util.UUID;
18+
import java.util.Locale;
1819
import java.util.concurrent.Executors;
1920
import java.util.concurrent.ScheduledExecutorService;
2021
import java.util.concurrent.TimeUnit;
@@ -458,6 +459,11 @@ public String getUUID(String playerName) {
458459
return "";
459460
}
460461

462+
if (!config.getOnlineMode()) {
463+
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName.toLowerCase(Locale.ROOT).trim())
464+
.getBytes(StandardCharsets.UTF_8)).toString();
465+
}
466+
461467
ProxiedPlayer p = getProxy().getPlayer(playerName);
462468
if (p != null && p.isConnected()) {
463469
playerName = p.getName();
@@ -471,11 +477,6 @@ public String getUUID(String playerName) {
471477
}
472478
}
473479

474-
if (!config.getOnlineMode()) {
475-
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName).getBytes(StandardCharsets.UTF_8))
476-
.toString();
477-
}
478-
479480
if (p != null && p.isConnected()) {
480481
return p.getUniqueId().toString();
481482
}
@@ -823,4 +824,4 @@ private void runAsyncNow(Runnable runnable) {
823824
getProxy().getScheduler().runAsync(this, runnable);
824825
}
825826

826-
}
827+
}

0 commit comments

Comments
 (0)