|
5 | 5 | import java.util.HashMap; |
6 | 6 | import java.util.HashSet; |
7 | 7 | import java.util.LinkedList; |
| 8 | +import java.util.List; |
8 | 9 | import java.util.Locale; |
| 10 | +import java.util.Map; |
9 | 11 | import java.util.Map.Entry; |
10 | 12 | import java.util.Queue; |
11 | 13 | import java.util.Set; |
|
82 | 84 | import com.bencodez.votingplugin.user.VotingPluginUser; |
83 | 85 | import com.bencodez.votingplugin.votesites.VoteSite; |
84 | 86 |
|
| 87 | +import lombok.var; |
| 88 | + |
85 | 89 | // TODO: Auto-generated Javadoc |
86 | 90 | /** |
87 | 91 | * The Class CommandLoader. |
@@ -1978,6 +1982,249 @@ public void execute(CommandSender sender, String[] args) { |
1978 | 1982 | } |
1979 | 1983 | }); |
1980 | 1984 |
|
| 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 | + |
1981 | 2228 | plugin.getAdminVoteCommand() |
1982 | 2229 | .add(new CommandHandler(plugin, new String[] { "MergeDataFrom", "(UserStorage)" }, |
1983 | 2230 | "VotingPlugin.Commands.AdminVote.MergeDataFrom|" + adminPerm, |
|
0 commit comments