refactor(leaderboard): split Leaderboard.java into 3 specialized classes
- 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)
This commit is contained in:
parent
0a93c9628f
commit
6ba6e5137d
3 changed files with 235 additions and 114 deletions
|
|
@ -1,16 +1,13 @@
|
||||||
package dev.loki.loparkour.leaderboard;
|
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.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for handling leaderboards.
|
* Facade for leaderboard: CRUD operations with automatic sorting.
|
||||||
|
* Delegates to LeaderboardStorage and LeaderboardSorter.
|
||||||
*/
|
*/
|
||||||
public class Leaderboard {
|
public class Leaderboard {
|
||||||
|
|
||||||
|
|
@ -20,132 +17,52 @@ public class Leaderboard {
|
||||||
public final String mode;
|
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)
|
* A map of all scores for this mode (thread-safe)
|
||||||
*/
|
*/
|
||||||
public final Map<UUID, Score> scores = Collections.synchronizedMap(new LinkedHashMap<>());
|
public final Map<UUID, Score> 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.mode = mode.toLowerCase();
|
||||||
this.sort = sort;
|
this.sort = sort;
|
||||||
|
this.sorter = new LeaderboardSorter(sort);
|
||||||
|
this.storage = new LeaderboardStorage(mode, scores, sorter);
|
||||||
|
|
||||||
Storage.init(mode);
|
storage.initAndSchedule();
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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) {
|
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) {
|
public void read(boolean async) {
|
||||||
run(() -> {
|
storage.read(async);
|
||||||
Map<UUID, Score> loadedScores = Storage.readScores(mode);
|
|
||||||
|
|
||||||
// Synchronize access to prevent concurrent modification
|
|
||||||
synchronized (scores) {
|
|
||||||
scores.clear();
|
|
||||||
scores.putAll(loadedScores);
|
|
||||||
}
|
|
||||||
|
|
||||||
sort();
|
|
||||||
}, async);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void run(Runnable runnable, boolean async) {
|
// ── CRUD operations ───────────────────────────────────────────────────────
|
||||||
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<UUID, Score> sort(Sort sort) {
|
|
||||||
LinkedHashMap<UUID, Score> sorted = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
List<Map.Entry<UUID, Score>> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a new score, overriding the old one
|
* Registers a new score, overriding the old one
|
||||||
|
|
@ -161,7 +78,7 @@ public class Leaderboard {
|
||||||
previous = scores.put(uuid, score);
|
previous = scores.put(uuid, score);
|
||||||
}
|
}
|
||||||
|
|
||||||
sort();
|
sorter.sortInPlace(scores);
|
||||||
|
|
||||||
return previous;
|
return previous;
|
||||||
}
|
}
|
||||||
|
|
@ -189,8 +106,8 @@ public class Leaderboard {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param uuid The {@link UUID} to get.
|
* @param uuid The {@link UUID} to get
|
||||||
* @return The {@link Score} associated with the player. If null, returns a {@link Score} instance with "?".
|
* @return The {@link Score} associated with the player. If null, returns a {@link Score} instance with "?"
|
||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
public Score get(@NotNull UUID uuid) {
|
public Score get(@NotNull UUID uuid) {
|
||||||
|
|
@ -199,7 +116,7 @@ public class Leaderboard {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param uuid The uuid
|
* @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) {
|
public int getRank(@NotNull UUID uuid) {
|
||||||
List<UUID> keys;
|
List<UUID> keys;
|
||||||
|
|
@ -228,7 +145,38 @@ public class Leaderboard {
|
||||||
return values.get(rank - 1);
|
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<UUID, Score> sort(LeaderboardSorter.Sort sort) {
|
||||||
|
synchronized (scores) {
|
||||||
|
return sorter.sort(scores, sort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link #sort(LeaderboardSorter.Sort)} instead
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public Map<UUID, Score> sort(Sort sort) {
|
||||||
|
return sort(sort.toSorterSort());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Backward compatibility ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link LeaderboardSorter.Sort} instead
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public enum Sort {
|
public enum Sort {
|
||||||
SCORE, TIME, DIFFICULTY
|
SCORE, TIME, DIFFICULTY;
|
||||||
|
|
||||||
|
public LeaderboardSorter.Sort toSorterSort() {
|
||||||
|
return LeaderboardSorter.Sort.valueOf(this.name());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<UUID, Score> sort(@NotNull Map<UUID, Score> 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<UUID, Score> sort(@NotNull Map<UUID, Score> scores, @NotNull Sort sortMethod) {
|
||||||
|
LinkedHashMap<UUID, Score> sorted = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
List<Map.Entry<UUID, Score>> 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<UUID, Score> scores) {
|
||||||
|
Map<UUID, Score> sorted = sort(scores);
|
||||||
|
scores.clear();
|
||||||
|
scores.putAll(sorted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Sort {
|
||||||
|
SCORE, TIME, DIFFICULTY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<UUID, Score> scores;
|
||||||
|
private final LeaderboardSorter sorter;
|
||||||
|
|
||||||
|
public LeaderboardStorage(@NotNull String mode,
|
||||||
|
@NotNull Map<UUID, Score> 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<UUID, Score> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue