From 6ba6e5137d5f1d1cd82d33ff35de1304cc6fa5ad Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Tue, 28 Apr 2026 22:25:37 +0200 Subject: [PATCH] refactor(leaderboard): split Leaderboard.java into 3 specialized classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created LeaderboardStorage.java - I/O and periodic sync - Created LeaderboardSorter.java - sorting logic with Sort enum - Leaderboard.java now acts as CRUD facade - Backward compatibility via deprecated Sort enum and overloads - All tests pass, build successful Wave 3/8 complete (234 lines → 3 files) --- .../loparkour/leaderboard/Leaderboard.java | 176 ++++++------------ .../leaderboard/LeaderboardSorter.java | 92 +++++++++ .../leaderboard/LeaderboardStorage.java | 81 ++++++++ 3 files changed, 235 insertions(+), 114 deletions(-) create mode 100644 src/main/java/dev/loki/loparkour/leaderboard/LeaderboardSorter.java create mode 100644 src/main/java/dev/loki/loparkour/leaderboard/LeaderboardStorage.java diff --git a/src/main/java/dev/loki/loparkour/leaderboard/Leaderboard.java b/src/main/java/dev/loki/loparkour/leaderboard/Leaderboard.java index f2cd3cb..7074345 100644 --- a/src/main/java/dev/loki/loparkour/leaderboard/Leaderboard.java +++ b/src/main/java/dev/loki/loparkour/leaderboard/Leaderboard.java @@ -1,16 +1,13 @@ package dev.loki.loparkour.leaderboard; -import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.storage.Storage; -import dev.lolib.scheduler.Scheduler; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; /** - * Class for handling leaderboards. + * Facade for leaderboard: CRUD operations with automatic sorting. + * Delegates to LeaderboardStorage and LeaderboardSorter. */ public class Leaderboard { @@ -20,132 +17,52 @@ public class Leaderboard { public final String mode; /** - * The way in which items will be sorted. + * The way in which items will be sorted */ - public final Sort sort; + public final LeaderboardSorter.Sort sort; /** * A map of all scores for this mode (thread-safe) */ public final Map scores = Collections.synchronizedMap(new LinkedHashMap<>()); - public Leaderboard(@NotNull String mode, Sort sort) { + private final LeaderboardStorage storage; + private final LeaderboardSorter sorter; + + public Leaderboard(@NotNull String mode, LeaderboardSorter.Sort sort) { this.mode = mode.toLowerCase(); this.sort = sort; + this.sorter = new LeaderboardSorter(sort); + this.storage = new LeaderboardStorage(mode, scores, sorter); - Storage.init(mode); - - // Defer initial read until storage is ready (SQL connects async — reading before - // connection is established would return empty results and lose all scores). - Storage.runWhenReady(() -> read(true)); - - var interval = Config.CONFIG.getInt("storage-update-interval"); - - // Main-thread timer: only schedules I/O; avoids async + Bukkit edge cases - Scheduler.get(LoParkour.getPlugin()).runTimer(() -> { - if (Config.CONFIG.getBoolean("joining")) { - write(true); - } else { - read(true); - } - }, interval * 20, interval * 20); + storage.initAndSchedule(); } /** - * Writes all scores to the leaderboard file associated with this leaderboard + * @deprecated Use {@link #Leaderboard(String, LeaderboardSorter.Sort)} instead + */ + @Deprecated + public Leaderboard(@NotNull String mode, Sort sort) { + this(mode, sort.toSorterSort()); + } + + // ── I/O operations ──────────────────────────────────────────────────────── + + /** + * Writes all scores to storage */ public void write(boolean async) { - run(() -> Storage.writeScores(mode, scores), async); + storage.write(async); } /** - * Reads all scores from the leaderboard file + * Reads all scores from storage */ public void read(boolean async) { - run(() -> { - Map loadedScores = Storage.readScores(mode); - - // Synchronize access to prevent concurrent modification - synchronized (scores) { - scores.clear(); - scores.putAll(loadedScores); - } - - sort(); - }, async); + storage.read(async); } - private void run(Runnable runnable, boolean async) { - if (async) { - Scheduler.get(LoParkour.getPlugin()).runAsync(runnable); - } else { - runnable.run(); - } - } - - /** - * Returns sorted copy of the score map. - * @param sort The sorting method. - * @return A sorted map of scores. - */ - public Map sort(Sort sort) { - LinkedHashMap sorted = new LinkedHashMap<>(); - - List> snapshot; - synchronized (scores) { - snapshot = new ArrayList<>(scores.entrySet()); - } - - snapshot.stream() - .sorted((one, two) -> { - switch (sort) { - case SCORE -> { - int scoreComparison = two.getValue().score() - one.getValue().score(); - - if (scoreComparison != 0) { - return scoreComparison; - } else { - // Use Integer.compare to avoid overflow - return Integer.compare(one.getValue().getTimeMillis(), two.getValue().getTimeMillis()); - } - } - case TIME -> { - // Use Integer.compare to avoid overflow - return Integer.compare(one.getValue().getTimeMillis(), two.getValue().getTimeMillis()); - } - case DIFFICULTY -> { - String diff1 = one.getValue().difficulty(); - String diff2 = two.getValue().difficulty(); - - // Handle "?" as lowest difficulty - if ("?".equals(diff1) && "?".equals(diff2)) return 0; - if ("?".equals(diff1)) return 1; - if ("?".equals(diff2)) return -1; - - try { - return (int) Math.signum(Double.parseDouble(diff2) - Double.parseDouble(diff1)); - } catch (NumberFormatException e) { - return 0; - } - } - default -> throw new IllegalArgumentException("Invalid sort method"); - } - }) - .forEachOrdered(entry -> sorted.put(entry.getKey(), entry.getValue())); - - return sorted; - } - - // sorts all scores in the map - private void sort() { - var sorted = sort(sort); - - // Synchronize access to prevent concurrent modification - synchronized (scores) { - scores.clear(); - scores.putAll(sorted); - } - } + // ── CRUD operations ─────────────────────────────────────────────────────── /** * Registers a new score, overriding the old one @@ -161,7 +78,7 @@ public class Leaderboard { previous = scores.put(uuid, score); } - sort(); + sorter.sortInPlace(scores); return previous; } @@ -189,8 +106,8 @@ public class Leaderboard { } /** - * @param uuid The {@link UUID} to get. - * @return The {@link Score} associated with the player. If null, returns a {@link Score} instance with "?". + * @param uuid The {@link UUID} to get + * @return The {@link Score} associated with the player. If null, returns a {@link Score} instance with "?" */ @NotNull public Score get(@NotNull UUID uuid) { @@ -199,7 +116,7 @@ public class Leaderboard { /** * @param uuid The uuid - * @return The rank. Starts from 1. Returns 0 if no ranking is found. + * @return The rank. Starts from 1. Returns 0 if no ranking is found */ public int getRank(@NotNull UUID uuid) { List keys; @@ -228,7 +145,38 @@ public class Leaderboard { return values.get(rank - 1); } + // ── Sorting ─────────────────────────────────────────────────────────────── + + /** + * Returns sorted copy of the score map. + * @param sort The sorting method + * @return A sorted map of scores + */ + public Map sort(LeaderboardSorter.Sort sort) { + synchronized (scores) { + return sorter.sort(scores, sort); + } + } + + /** + * @deprecated Use {@link #sort(LeaderboardSorter.Sort)} instead + */ + @Deprecated + public Map sort(Sort sort) { + return sort(sort.toSorterSort()); + } + + // ── Backward compatibility ──────────────────────────────────────────────── + + /** + * @deprecated Use {@link LeaderboardSorter.Sort} instead + */ + @Deprecated public enum Sort { - SCORE, TIME, DIFFICULTY + SCORE, TIME, DIFFICULTY; + + public LeaderboardSorter.Sort toSorterSort() { + return LeaderboardSorter.Sort.valueOf(this.name()); + } } } diff --git a/src/main/java/dev/loki/loparkour/leaderboard/LeaderboardSorter.java b/src/main/java/dev/loki/loparkour/leaderboard/LeaderboardSorter.java new file mode 100644 index 0000000..b18f1d1 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/leaderboard/LeaderboardSorter.java @@ -0,0 +1,92 @@ +package dev.loki.loparkour.leaderboard; + +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +/** + * Sorting logic for leaderboard scores. + */ +public class LeaderboardSorter { + + private final Sort sort; + + public LeaderboardSorter(@NotNull Sort sort) { + this.sort = sort; + } + + /** + * Returns sorted copy of the score map. + * @param scores The scores to sort + * @return A sorted map of scores + */ + @NotNull + public Map sort(@NotNull Map scores) { + return sort(scores, this.sort); + } + + /** + * Returns sorted copy of the score map with custom sort method. + * @param scores The scores to sort + * @param sortMethod The sorting method + * @return A sorted map of scores + */ + @NotNull + public Map sort(@NotNull Map scores, @NotNull Sort sortMethod) { + LinkedHashMap sorted = new LinkedHashMap<>(); + + List> snapshot = new ArrayList<>(scores.entrySet()); + + snapshot.stream() + .sorted((one, two) -> { + switch (sortMethod) { + case SCORE -> { + int scoreComparison = two.getValue().score() - one.getValue().score(); + + if (scoreComparison != 0) { + return scoreComparison; + } else { + // Use Integer.compare to avoid overflow + return Integer.compare(one.getValue().getTimeMillis(), two.getValue().getTimeMillis()); + } + } + case TIME -> { + // Use Integer.compare to avoid overflow + return Integer.compare(one.getValue().getTimeMillis(), two.getValue().getTimeMillis()); + } + case DIFFICULTY -> { + String diff1 = one.getValue().difficulty(); + String diff2 = two.getValue().difficulty(); + + // Handle "?" as lowest difficulty + if ("?".equals(diff1) && "?".equals(diff2)) return 0; + if ("?".equals(diff1)) return 1; + if ("?".equals(diff2)) return -1; + + try { + return (int) Math.signum(Double.parseDouble(diff2) - Double.parseDouble(diff1)); + } catch (NumberFormatException e) { + return 0; + } + } + default -> throw new IllegalArgumentException("Invalid sort method"); + } + }) + .forEachOrdered(entry -> sorted.put(entry.getKey(), entry.getValue())); + + return sorted; + } + + /** + * Sort the map in-place (modifies the original map). + */ + public void sortInPlace(@NotNull Map scores) { + Map sorted = sort(scores); + scores.clear(); + scores.putAll(sorted); + } + + public enum Sort { + SCORE, TIME, DIFFICULTY + } +} diff --git a/src/main/java/dev/loki/loparkour/leaderboard/LeaderboardStorage.java b/src/main/java/dev/loki/loparkour/leaderboard/LeaderboardStorage.java new file mode 100644 index 0000000..17498c5 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/leaderboard/LeaderboardStorage.java @@ -0,0 +1,81 @@ +package dev.loki.loparkour.leaderboard; + +import dev.loki.loparkour.LoParkour; +import dev.loki.loparkour.config.Config; +import dev.loki.loparkour.storage.Storage; +import dev.lolib.scheduler.Scheduler; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.UUID; + +/** + * Handles I/O operations and periodic sync for leaderboard scores. + */ +public class LeaderboardStorage { + + private final String mode; + private final Map scores; + private final LeaderboardSorter sorter; + + public LeaderboardStorage(@NotNull String mode, + @NotNull Map scores, + @NotNull LeaderboardSorter sorter) { + this.mode = mode.toLowerCase(); + this.scores = scores; + this.sorter = sorter; + } + + /** + * Initialize storage and start periodic sync timer. + */ + public void initAndSchedule() { + Storage.init(mode); + + // Defer initial read until storage is ready (SQL connects async) + Storage.runWhenReady(() -> read(true)); + + var interval = Config.CONFIG.getInt("storage-update-interval"); + + // Main-thread timer: only schedules I/O + Scheduler.get(LoParkour.getPlugin()).runTimer(() -> { + if (Config.CONFIG.getBoolean("joining")) { + write(true); + } else { + read(true); + } + }, interval * 20, interval * 20); + } + + /** + * Write all scores to storage. + */ + public void write(boolean async) { + run(() -> Storage.writeScores(mode, scores), async); + } + + /** + * Read all scores from storage. + */ + public void read(boolean async) { + run(() -> { + Map loadedScores = Storage.readScores(mode); + + // Synchronize access to prevent concurrent modification + synchronized (scores) { + scores.clear(); + scores.putAll(loadedScores); + } + + sorter.sortInPlace(scores); + }, async); + } + + private void run(Runnable runnable, boolean async) { + if (async) { + Scheduler.get(LoParkour.getPlugin()).runAsync(runnable); + } else { + runnable.run(); + } + } +}