Skip to content

Commit 43d911e

Browse files
committed
Add async PAPI leaderboard cache and placeholders
Fixes #5171 Fixes #4800 Fixes #4291
1 parent 9717fd8 commit 43d911e

23 files changed

Lines changed: 2845 additions & 114 deletions

Changelog.txt

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,25 @@ Version 2.2.055
44
Fixed Maces Cripple playing an extra anvil break sound that could not be disabled in sounds.yml (See notes)
55
Fixed Super Breaker and Tree Feller mishandling durability on items with a custom max_damage component (See notes)
66
Fixed Tree Feller ignoring durability damage changes made by other plugins via PlayerItemDamageEvent
7+
Fixed a thread-safety issue that could cause incorrect FlatFile leaderboard results
8+
Fixed renamed players' leftover database rows showing up in /mctop and being counted by /mcrank on SQL databases (See notes)
9+
Fixed FlatFile leaderboards not retrying a failed rebuild until the next refresh interval
710
Improved performance when players gain skill XP (See notes)
811
Improved performance when taking smelted items out of furnaces (See notes)
12+
Added PlaceholderAPI leaderboard rank-position placeholders for all skills except child skills (Salvage, Smelting) and for overall power level (See notes)
13+
Improved SQL leaderboard query performance on large databases (See notes)
14+
Added automatic per-skill leaderboard indexes for SQL databases (See notes)
15+
Changed SQL leaderboard ordering for players with identical levels (See notes)
16+
Changed the leaderboard placeholder cache to rebuild from a single user file scan on FlatFile and to pause refreshes while no leaderboard placeholders are in use (See notes)
17+
Added 'General.PlaceholderAPI.Leaderboards.Max_Tracked_Rank' to config.yml
18+
Added 'General.Leaderboards.Refresh_Interval_Seconds.SQL' to config.yml
19+
Added 'General.Leaderboards.Refresh_Interval_Seconds.FlatFile' to config.yml
920
(API) McMMOScoreboardObjectiveEvent is only fired on non-Folia servers (See notes)
21+
(API) Added DatabaseManager#readLeaderboardSnapshot for reading every leaderboard scope in one call
1022
(Codebase) Added scoreboard-library 2.8.0 as a shaded dependency for the packet-based scoreboard implementation
1123
(Codebase) Added ViaVersion to plugin.yml softdepend
1224
(Codebase) Added unit tests covering the power level cap and skill level cap checks
25+
(Codebase) Added async leaderboard snapshot caching for rank-position PlaceholderAPI lookups
1326

1427
NOTES:
1528
Scoreboards on Folia (and Folia forks like Canvas) now use a packet-based implementation, since the Bukkit scoreboard API is not safe to use there. Paper and Spigot servers keep the existing Bukkit scoreboard implementation and are unaffected.
@@ -21,6 +34,72 @@ Version 2.2.055
2134
Cripple now plays a single sound which can be adjusted or disabled with the CRIPPLE entry in sounds.yml.
2235
Items with a custom maximum durability (set through the max_damage item component by data packs or item plugins) previously could make super abilities stop working or appear to restore durability; ability durability loss now always uses the item's own maximum.
2336

37+
-- PlaceholderAPI leaderboard rank-position placeholders --
38+
This update adds placeholders that let you request specific leaderboard positions directly.
39+
Format examples:
40+
%mcmmo_mctop_<skill>:<position>%
41+
%mcmmo_mctop_name_<skill>:<position>%
42+
43+
Mining examples:
44+
%mcmmo_mctop_mining:1% -> skill level of the #1 ranked Mining player
45+
%mcmmo_mctop_name_mining:1% -> name of the #1 ranked Mining player
46+
%mcmmo_mctop_mining:34% -> skill level of the #34 ranked Mining player
47+
%mcmmo_mctop_name_mining:34% -> name of the #34 ranked Mining player
48+
49+
Overall / power level examples:
50+
%mcmmo_mctop_overall:1%
51+
%mcmmo_mctop_name_overall:1%
52+
%mcmmo_mctop_all:10%
53+
%mcmmo_mctop_name_all:10%
54+
%mcmmo_mctop_powerlevel:25%
55+
%mcmmo_mctop_name_powerlevel:25%
56+
57+
Alias behavior:
58+
'overall', 'all', and 'powerlevel' all map to the same overall leaderboard data.
59+
60+
Config notes:
61+
'General.PlaceholderAPI.Leaderboards.Max_Tracked_Rank' controls the highest supported placeholder position.
62+
Default is 100. Values are clamped between 10 and 1000, since higher positions cost memory and database reads on every cache refresh.
63+
Requests above this value return an empty result.
64+
Invalid positions (blank, non-numeric, 0, negative) also return an empty result.
65+
Positions that no player currently holds also return an empty result.
66+
67+
'General.Leaderboards.Refresh_Interval_Seconds.FlatFile' controls how often FlatFile leaderboards are rebuilt.
68+
This affects /mctop, /mcrank, and the leaderboard placeholders.
69+
Default is 600 seconds (10 minutes), matching previous behavior.
70+
'General.Leaderboards.Refresh_Interval_Seconds.SQL' controls how often the placeholder cache refreshes on SQL databases.
71+
/mctop and /mcrank on SQL are unaffected and always query live data.
72+
Default is 60 seconds.
73+
Both intervals have a 60 second minimum; values below 60 are treated as 60.
74+
Placeholder requests are served from cache between refreshes.
75+
76+
Each cache refresh reads every leaderboard in one pass; on FlatFile this is a single scan
77+
of the user file no matter how many skills are tracked.
78+
If no leaderboard placeholder has been used since the previous refresh, the next refresh
79+
is skipped and the cache sits idle. The first request afterwards is served from the cached
80+
data and triggers an immediate background refresh. Servers that never use these
81+
placeholders do no recurring leaderboard cache work.
82+
83+
-- SQL leaderboard performance --
84+
Leaderboard queries on SQL databases are now much faster on servers with large player tables.
85+
mcMMO will automatically add per-skill indexes to the skills table on startup if they are missing.
86+
On very large databases the first startup after updating may take longer while these indexes are built.
87+
This runs once, is safe to re-run, and mcMMO will continue to start normally even if an index cannot be added.
88+
Fresh SQL installs include these indexes from the start, and skill columns added by future
89+
mcMMO updates get their index automatically.
90+
If an index named idx_<skill> already exists but covers a different column, mcMMO assumes
91+
it was created deliberately, leaves it alone, and does not retry on later startups.
92+
93+
When a player renames, the database row for their old name is kept internally under a
94+
placeholder name. On SQL databases these rows could previously appear in /mctop and the
95+
leaderboard placeholders, and were counted when computing /mcrank positions; they are now
96+
excluded from all of them.
97+
98+
As part of this change, players with identical levels on SQL databases are now ordered
99+
newest-registered-first in /mctop, /mcrank, and the leaderboard placeholders (previously
100+
ties were ordered alphabetically by name). /mctop and /mcrank agree on tie positions.
101+
FlatFile databases still order ties alphabetically.
102+
24103
Version 2.2.054
25104
Added compatibility for new blocks and items from Chaos Cubed (Minecraft 26.2) to mcMMO
26105
Fixed party/admin chat allowing players to use color codes without the 'mcmmo.chat.colors' permission

src/main/java/com/gmail/nossr50/config/GeneralConfig.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@
2323
import org.jetbrains.annotations.Nullable;
2424

2525
public class GeneralConfig extends BukkitConfig {
26+
// Floor for how often leaderboards may rebuild; shared with FlatFileDatabaseManager's
27+
// rebuild throttle so the config clamp and the runtime throttle can never disagree.
28+
public static final int MIN_LEADERBOARD_REFRESH_INTERVAL_SECONDS = 60;
29+
// Each tracked rank is cached for every non-child skill plus overall, and every cache
30+
// refresh re-reads that many rows per leaderboard, so the ceiling bounds both memory
31+
// use and periodic database load.
32+
private static final int MIN_PAPI_LEADERBOARD_TRACKED_RANK = 10;
33+
private static final int MAX_PAPI_LEADERBOARD_TRACKED_RANK = 1000;
34+
2635
private @Nullable Material repairAnvilMaterial;
2736
private @Nullable Material salvageAnvilMaterial;
2837

@@ -249,6 +258,35 @@ public boolean getRegionDataMigrationBackupsEnabled() {
249258
return config.getBoolean("General.RegionDataMigrationBackups", true);
250259
}
251260

261+
/**
262+
* @return Highest leaderboard position kept in the PlaceholderAPI cache, clamped between
263+
* {@value #MIN_PAPI_LEADERBOARD_TRACKED_RANK} and {@value #MAX_PAPI_LEADERBOARD_TRACKED_RANK}.
264+
*/
265+
public int getPapiLeaderboardMaxTrackedRank() {
266+
final int configured =
267+
config.getInt("General.PlaceholderAPI.Leaderboards.Max_Tracked_Rank", 100);
268+
return Math.min(MAX_PAPI_LEADERBOARD_TRACKED_RANK,
269+
Math.max(MIN_PAPI_LEADERBOARD_TRACKED_RANK, configured));
270+
}
271+
272+
/**
273+
* @return SQL leaderboard cache refresh interval in seconds, never below
274+
* {@value #MIN_LEADERBOARD_REFRESH_INTERVAL_SECONDS}.
275+
*/
276+
public int getLeaderboardRefreshIntervalSecondsSQL() {
277+
return Math.max(MIN_LEADERBOARD_REFRESH_INTERVAL_SECONDS,
278+
config.getInt("General.Leaderboards.Refresh_Interval_Seconds.SQL", 60));
279+
}
280+
281+
/**
282+
* @return FlatFile leaderboard cache refresh interval in seconds, never below
283+
* {@value #MIN_LEADERBOARD_REFRESH_INTERVAL_SECONDS}.
284+
*/
285+
public int getLeaderboardRefreshIntervalSecondsFlatFile() {
286+
return Math.max(MIN_LEADERBOARD_REFRESH_INTERVAL_SECONDS,
287+
config.getInt("General.Leaderboards.Refresh_Interval_Seconds.FlatFile", 600));
288+
}
289+
252290
public boolean getMobHealthbarEnabled() {
253291
return config.getBoolean("Mob_Healthbar.Enabled", true);
254292
}

src/main/java/com/gmail/nossr50/database/DatabaseManager.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
import com.gmail.nossr50.api.exceptions.InvalidSkillException;
44
import com.gmail.nossr50.datatypes.database.DatabaseType;
5+
import com.gmail.nossr50.datatypes.database.LeaderboardSnapshot;
56
import com.gmail.nossr50.datatypes.database.PlayerStat;
67
import com.gmail.nossr50.datatypes.player.PlayerProfile;
78
import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
9+
import com.gmail.nossr50.util.skills.SkillTools;
10+
import java.util.EnumMap;
811
import java.util.List;
912
import java.util.Map;
1013
import java.util.UUID;
@@ -62,6 +65,40 @@ public interface DatabaseManager {
6265
@NotNull List<PlayerStat> readLeaderboard(@Nullable PrimarySkillType skill, int pageNumber,
6366
int statsPerPage) throws InvalidSkillException;
6467

68+
/**
69+
* Retrieve the top rows of every leaderboard scope (each non-child skill plus the power level
70+
* leaderboard) directly from the backend in one bulk call, for callers that build caches from
71+
* the result.
72+
* <p>
73+
* Unlike {@link #readLeaderboard(PrimarySkillType, int, int)}, implementations must propagate
74+
* backend read failures instead of returning a partial or empty result, so callers can tell a
75+
* failed read apart from genuinely empty leaderboards. Implementations should also bypass any
76+
* backend-level result caching so callers always observe current data. The default
77+
* implementation reads each scope through
78+
* {@link #readLeaderboard(PrimarySkillType, int, int)} and therefore inherits that method's
79+
* failure handling; custom database managers should override it to honor this contract.
80+
*
81+
* @param perScopeLimit The maximum number of rows to include per leaderboard scope
82+
* @return the top rows of every leaderboard scope
83+
* @throws RuntimeException when the backend read fails
84+
*/
85+
default @NotNull LeaderboardSnapshot readLeaderboardSnapshot(int perScopeLimit) {
86+
final Map<PrimarySkillType, List<PlayerStat>> skillLeaderboards =
87+
new EnumMap<>(PrimarySkillType.class);
88+
89+
try {
90+
for (PrimarySkillType skill : SkillTools.NON_CHILD_SKILLS) {
91+
skillLeaderboards.put(skill, readLeaderboard(skill, 1, perScopeLimit));
92+
}
93+
94+
return new LeaderboardSnapshot(skillLeaderboards,
95+
readLeaderboard(null, 1, perScopeLimit));
96+
} catch (InvalidSkillException e) {
97+
// Scopes are fixed to non-child skills plus overall, so this cannot happen.
98+
throw new IllegalStateException(e);
99+
}
100+
}
101+
65102
/**
66103
* Retrieve rank info into a HashMap from PrimarySkillType to the rank.
67104
* <p>

src/main/java/com/gmail/nossr50/database/DatabaseManagerFactory.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ public static DatabaseManager getDatabaseManager(@NotNull String userFilePath,
3030

3131
return mcMMO.p.getGeneralConfig().getUseMySQL()
3232
? new SQLDatabaseManager(logger, MYSQL_DRIVER)
33-
: new FlatFileDatabaseManager(userFilePath, logger, purgeTime, startingLevel);
33+
: new FlatFileDatabaseManager(userFilePath, logger, purgeTime, startingLevel,
34+
flatFileLeaderboardRefreshIntervalMillis());
35+
}
36+
37+
private static long flatFileLeaderboardRefreshIntervalMillis() {
38+
return 1000L * mcMMO.p.getGeneralConfig().getLeaderboardRefreshIntervalSecondsFlatFile();
3439
}
3540

3641
/**
@@ -65,7 +70,8 @@ public static Class<? extends DatabaseManager> getCustomDatabaseManagerClass() {
6570
switch (type) {
6671
case FLATFILE:
6772
LogUtils.debug(mcMMO.p.getLogger(), "Using FlatFile Database");
68-
return new FlatFileDatabaseManager(userFilePath, logger, purgeTime, startingLevel);
73+
return new FlatFileDatabaseManager(userFilePath, logger, purgeTime, startingLevel,
74+
flatFileLeaderboardRefreshIntervalMillis());
6975

7076
case SQL:
7177
LogUtils.debug(mcMMO.p.getLogger(), "Using SQL Database");

0 commit comments

Comments
 (0)