diff --git a/eternaleconomy-core/src/jmh/java/com/eternalcode/economy/account/AccountBenchmark.java b/eternaleconomy-core/src/jmh/java/com/eternalcode/economy/account/AccountBenchmark.java index 680134c..19497f9 100644 --- a/eternaleconomy-core/src/jmh/java/com/eternalcode/economy/account/AccountBenchmark.java +++ b/eternaleconomy-core/src/jmh/java/com/eternalcode/economy/account/AccountBenchmark.java @@ -1,68 +1,68 @@ -package com.eternalcode.economy.account; - -import com.eternalcode.economy.account.database.AccountRepositoryInMemory; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.logging.Logger; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Warmup; - -import java.util.UUID; - -@State(Scope.Thread) -public class AccountBenchmark { - - private static final Logger LOGGER = Logger.getLogger(AccountBenchmark.class.getName()); - private static final char[] ALPHABET = "abcdefghijklmnopqrstuvwxyz".toCharArray(); - - private final List searches = new ArrayList<>(); - private AccountManager accountManager; - - @Setup - public void setUp() { - accountManager = new AccountManager(new AccountRepositoryInMemory()); - - // zapełnienie TreeMapy różnymi nazwami zapewnia, że będzie ona miała optymalne wyniki - // tree mapa rozdziela elementy na podstawie ich klucza, więc im bardziej zróżnicowane klucze, tym "lepsze' wyniki - for (char first : ALPHABET) { - for (char second : ALPHABET) { - for (char third : ALPHABET) { - String name = String.valueOf(first) + second + third; - - accountManager.create(UUID.randomUUID(), name); - } - } - } - - // pre-generowanie losowych wyszukiwań, które będą wykonywane w benchmarku (nie wpływamy na czas wykonania samego benchmarku) - for (char first : ALPHABET) { - searches.add(String.valueOf(first)); - for (char second : ALPHABET) { - searches.add(String.valueOf(first) + second); - for (char third : ALPHABET) { - searches.add(String.valueOf(first) + second + third); - } - } - } - - Collections.shuffle(searches); // mieszamy, aby zapewnić losowy dostęp - - LOGGER.info("Acounts size: " + accountManager.getAccounts().size() + ", Searches size: " + searches.size()); - } - - // mimo że nie jest to bezpieczne dla wielu wątków, to w przypadku JMH można to zignorować i tak potrzebujemy losowości - private int index = 0; - - @Benchmark - @Warmup(iterations = 5, time = 1) - @Measurement(iterations = 10, time = 1) - public void benchmarkGetAccountStartingWith() { - accountManager.getAccountStartingWith(searches.get(index++ % searches.size())); - } - -} +// package com.eternalcode.economy.account; +// +// import com.eternalcode.economy.account.database.AccountRepositoryInMemory; +// import java.util.ArrayList; +// import java.util.Collections; +// import java.util.List; +// import java.util.logging.Logger; +// import org.openjdk.jmh.annotations.Benchmark; +// import org.openjdk.jmh.annotations.Setup; +// import org.openjdk.jmh.annotations.State; +// import org.openjdk.jmh.annotations.Scope; +// import org.openjdk.jmh.annotations.Measurement; +// import org.openjdk.jmh.annotations.Warmup; +// +// import java.util.UUID; +// +// @State(Scope.Thread) +// public class AccountBenchmark { +// +// private static final Logger LOGGER = Logger.getLogger(AccountBenchmark.class.getName()); +// private static final char[] ALPHABET = "abcdefghijklmnopqrstuvwxyz".toCharArray(); +// +// private final List searches = new ArrayList<>(); +// private AccountManager accountManager; +// +// @Setup +// public void setUp() { +// accountManager = new AccountManager(new AccountRepositoryInMemory()); +// +// // zapełnienie TreeMapy różnymi nazwami zapewnia, że będzie ona miała optymalne wyniki +// // tree mapa rozdziela elementy na podstawie ich klucza, więc im bardziej zróżnicowane klucze, tym "lepsze' wyniki +// for (char first : ALPHABET) { +// for (char second : ALPHABET) { +// for (char third : ALPHABET) { +// String name = String.valueOf(first) + second + third; +// +// accountManager.create(UUID.randomUUID(), name); +// } +// } +// } +// +// // pre-generowanie losowych wyszukiwań, które będą wykonywane w benchmarku (nie wpływamy na czas wykonania samego benchmarku) +// for (char first : ALPHABET) { +// searches.add(String.valueOf(first)); +// for (char second : ALPHABET) { +// searches.add(String.valueOf(first) + second); +// for (char third : ALPHABET) { +// searches.add(String.valueOf(first) + second + third); +// } +// } +// } +// +// Collections.shuffle(searches); // mieszamy, aby zapewnić losowy dostęp +// +// LOGGER.info("Acounts size: " + accountManager.getAccounts().size() + ", Searches size: " + searches.size()); +// } +// +// // mimo że nie jest to bezpieczne dla wielu wątków, to w przypadku JMH można to zignorować i tak potrzebujemy losowości +// private int index = 0; +// +// @Benchmark +// @Warmup(iterations = 5, time = 1) +// @Measurement(iterations = 10, time = 1) +// public void benchmarkGetAccountStartingWith() { +// accountManager.getAccountStartingWith(searches.get(index++ % searches.size())); +// } +// +// } diff --git a/eternaleconomy-core/src/jmh/java/com/eternalcode/economy/leaderboard/LeaderboardServiceBenchmark.java b/eternaleconomy-core/src/jmh/java/com/eternalcode/economy/leaderboard/LeaderboardServiceBenchmark.java index 08e77b4..90af8ed 100644 --- a/eternaleconomy-core/src/jmh/java/com/eternalcode/economy/leaderboard/LeaderboardServiceBenchmark.java +++ b/eternaleconomy-core/src/jmh/java/com/eternalcode/economy/leaderboard/LeaderboardServiceBenchmark.java @@ -1,15 +1,19 @@ package com.eternalcode.economy.leaderboard; import com.eternalcode.economy.account.Account; +import com.eternalcode.economy.account.AccountManager; import com.eternalcode.economy.account.database.AccountRepositoryInMemory; import com.eternalcode.economy.config.implementation.PluginConfig; import java.math.BigDecimal; import java.util.Collection; +import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; @@ -20,36 +24,59 @@ import org.openjdk.jmh.infra.Blackhole; @State(Scope.Benchmark) +@Fork(value = 2) +@Warmup(iterations = 5, time = 3) +@Measurement(iterations = 10, time = 3) public class LeaderboardServiceBenchmark { private LeaderboardService leaderboardService; @Setup public void setUp() { - AccountRepositoryInMemory accountRepository = new AccountRepositoryInMemory(); + AccountManager accountManager = new AccountManager(new AccountRepositoryInMemory()); PluginConfig pluginConfig = new PluginConfig(); pluginConfig.leaderboardEntriesPerPage = 100; - this.leaderboardService = new LeaderboardService(accountRepository); + this.leaderboardService = new LeaderboardService(accountManager); for (int i = 0; i < 10_000; i++) { UUID uuid = UUID.randomUUID(); String name = "Player" + i; - BigDecimal balance = BigDecimal.valueOf(10_000 - i); + BigDecimal balance = BigDecimal.valueOf(ThreadLocalRandom.current().nextInt(0, 10_001)); Account account = new Account(uuid, name, balance); - accountRepository.save(account); + accountManager.create(account); } } @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) - @Warmup(iterations = 5, time = 1) - @Measurement(iterations = 10, time = 1) public void benchmarkGetLeaderboard(Blackhole blackhole) { - CompletableFuture> future = this.leaderboardService.getLeaderboard(); - Collection leaderboard = future.join(); + CompletableFuture> future = this.leaderboardService.getLeaderboard(); + List leaderboard = future.join(); blackhole.consume(leaderboard); } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void benchmarkGetLeaderboardPage(Blackhole blackhole) { + CompletableFuture future = this.leaderboardService.getLeaderboardPage(1, 100); + LeaderboardPage leaderboardPage = future.join(); + blackhole.consume(leaderboardPage); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void benchmarkGetLeaderboardPosition(Blackhole blackhole) { + CompletableFuture> future = this.leaderboardService.getLeaderboard(); + List leaderboard = future.join(); + LeaderboardEntry targetAccount = leaderboard.iterator().next(); + + CompletableFuture positionFuture = this.leaderboardService.getLeaderboardPosition(targetAccount.account()); + LeaderboardEntry position = positionFuture.join(); + blackhole.consume(position); + } } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/EconomyBukkitPlugin.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/EconomyBukkitPlugin.java index 48e5dfe..b902d10 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/EconomyBukkitPlugin.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/EconomyBukkitPlugin.java @@ -99,7 +99,7 @@ public void onEnable() { AccountRepository accountRepository = new AccountRepositoryImpl(this.databaseManager, scheduler); AccountManager accountManager = AccountManager.create(accountRepository); - LeaderboardService leaderboardService = new LeaderboardService(accountRepository); + LeaderboardService leaderboardService = new LeaderboardService(accountManager); DecimalFormatter decimalFormatter = new DecimalFormatterImpl(pluginConfig); AccountPaymentService accountPaymentService = new AccountPaymentService(accountManager, pluginConfig); diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/AccountManager.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/AccountManager.java index 17a04b9..b99f19d 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/AccountManager.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/AccountManager.java @@ -2,18 +2,23 @@ import com.eternalcode.economy.account.database.AccountRepository; import java.math.BigDecimal; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.NavigableMap; +import java.util.Set; import java.util.TreeMap; import java.util.UUID; -import java.util.Collection; -import java.util.Collections; public class AccountManager { private final Map accountByUniqueId = new HashMap<>(); private final Map accountByName = new HashMap<>(); private final TreeMap accountIndex = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + private final NavigableMap> accountsByBalance = new TreeMap<>(Comparator.reverseOrder()); private final AccountRepository accountRepository; @@ -28,6 +33,8 @@ public static AccountManager create(AccountRepository accountRepository) { for (Account account : accounts) { accountManager.accountByUniqueId.put(account.uuid(), account); accountManager.accountByName.put(account.name(), account); + accountManager.accountIndex.put(account.name(), account); + accountManager.accountsByBalance.computeIfAbsent(account.balance(), k -> new HashSet<>()).add(account); } }); @@ -67,13 +74,39 @@ public Account create(UUID uuid, String name) { this.accountByUniqueId.put(uuid, account); this.accountByName.put(name, account); this.accountIndex.put(name, account); + this.accountsByBalance.computeIfAbsent(account.balance(), k -> new HashSet<>()).add(account); return account; } - void save(Account account) { + public Account create(Account account) { + if (this.accountByUniqueId.containsKey(account.uuid())) { + throw new IllegalArgumentException("Account already exists: " + account.uuid()); + } + this.accountByUniqueId.put(account.uuid(), account); this.accountByName.put(account.name(), account); + this.accountIndex.put(account.name(), account); + this.accountsByBalance.computeIfAbsent(account.balance(), k -> new HashSet<>()).add(account); + + return account; + } + + public void save(Account account) { + Account previous = this.accountByUniqueId.put(account.uuid(), account); + if (previous != null) { + Set accountsWithPreviousBalance = this.accountsByBalance.get(previous.balance()); + if (accountsWithPreviousBalance != null) { + accountsWithPreviousBalance.remove(previous); + if (accountsWithPreviousBalance.isEmpty()) { + this.accountsByBalance.remove(previous.balance()); + } + } + } + + this.accountByName.put(account.name(), account); + this.accountIndex.put(account.name(), account); + this.accountsByBalance.computeIfAbsent(account.balance(), k -> new HashSet<>()).add(account); this.accountRepository.save(account); } @@ -86,4 +119,8 @@ public Collection getAccountStartingWith(String prefix) { public Collection getAccounts() { return Collections.unmodifiableCollection(this.accountByUniqueId.values()); } + + public NavigableMap> getAccountsByBalance() { + return Collections.unmodifiableNavigableMap(this.accountsByBalance); + } } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/implementation/messages/MessagesPlayerSubSection.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/implementation/messages/MessagesPlayerSubSection.java index 9b7fd42..6276c92 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/implementation/messages/MessagesPlayerSubSection.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/implementation/messages/MessagesPlayerSubSection.java @@ -45,7 +45,8 @@ public class MessagesPlayerSubSection extends OkaeriConfig { "Use {BALANCE} placeholder to show the player's formatted balance", "Use {BALANCE_RAW} placeholder to show the player's raw balance" }) - public Notice leaderboardEntry = Notice.chat("#{POSITION} {PLAYER} - {BALANCE}"); + public Notice leaderboardEntry = Notice.chat(" #{POSITION} {PLAYER} -" + + " {BALANCE}"); @Comment({ "Leaderboard position notice, only displayed if showLeaderboardPosition is set to true in the config.yml", @@ -59,7 +60,7 @@ public class MessagesPlayerSubSection extends OkaeriConfig { "Use {TOTAL_PAGES} placeholder to show the total number of pages", "Use {PAGE} placeholder to show the current page number" }) - public Notice leaderboardFooter = Notice.chat(" Click Go to page {PAGE}'>/baltop {PAGE} to go to the next page."); + public Notice leaderboardFooter = Notice.chat(" Click Go to page {NEXT_PAGE}'>/baltop {NEXT_PAGE} to go to the next page."); @Comment("Leaderboard is empty notice") public Notice leaderboardEmpty = Notice.chat("ECONOMY " diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardCommand.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardCommand.java index 23e4dc5..384bdfd 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardCommand.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardCommand.java @@ -11,9 +11,7 @@ import dev.rollczi.litecommands.annotations.execute.Execute; import dev.rollczi.litecommands.annotations.execute.ExecuteDefault; import dev.rollczi.litecommands.annotations.permission.Permission; -import java.util.ArrayList; import java.util.List; -import java.util.UUID; @SuppressWarnings("unused") @Command(name = "balancetop", aliases = {"baltop"}) @@ -44,46 +42,34 @@ void executeDefault(@Context Account account) { @Execute void execute(@Context Account account, @Arg("page") int page) { - UUID uuid = account.uuid(); - int entriesPerPage = this.pluginConfig.leaderboardEntriesPerPage; - this.leaderboardService.getLeaderboard().thenAccept(leaderboard -> { - List leaderboardList = new ArrayList<>(leaderboard); + this.leaderboardService.getLeaderboardPage(page, entriesPerPage).thenAccept(leadboardPage -> { + List entries = leadboardPage.entries(); - if (leaderboardList.isEmpty()) { + if (entries.isEmpty()) { this.noticeService.create() .notice(messageConfig -> messageConfig.player.leaderboardEmpty) - .player(uuid) + .player(account.uuid()) .send(); return; } - int totalPages = (int) Math.ceil((double) leaderboardList.size() / entriesPerPage); - int finalPage = Math.max(1, Math.min(page, totalPages)); - this.noticeService.create() .notice(messageConfig -> messageConfig.player.leaderboardHeader) - .placeholder("{PAGE}", String.valueOf(finalPage)) - .placeholder("{TOTAL_PAGES}", String.valueOf(totalPages)) - .player(uuid) + .placeholder("{PAGE}", String.valueOf(leadboardPage.currentPage())) + .placeholder("{TOTAL_PAGES}", String.valueOf(leadboardPage.maxPages())) + .player(account.uuid()) .send(); - int startIndex = (finalPage - 1) * entriesPerPage; - int endIndex = Math.min(startIndex + entriesPerPage, leaderboardList.size()); - List pageEntries = leaderboardList.subList(startIndex, endIndex); - - for (int i = 0; i < pageEntries.size(); i++) { - Account topAccount = pageEntries.get(i); - int position = startIndex + i + 1; - + for (LeaderboardEntry entry : entries) { this.noticeService.create() .notice(messageConfig -> messageConfig.player.leaderboardEntry) - .placeholder("{POSITION}", String.valueOf(position)) - .placeholder("{PLAYER}", topAccount.name()) - .placeholder("{BALANCE}", this.decimalFormatter.format(topAccount.balance())) - .placeholder("{BALANCE_RAW}", String.valueOf(topAccount.balance())) - .player(uuid) + .placeholder("{POSITION}", String.valueOf(entry.position())) + .placeholder("{PLAYER}", entry.account().name()) + .placeholder("{BALANCE}", this.decimalFormatter.format(entry.account().balance())) + .placeholder("{BALANCE_RAW}", String.valueOf(entry.account().balance())) + .player(account.uuid()) .send(); } @@ -92,19 +78,18 @@ void execute(@Context Account account, @Arg("page") int page) { this.noticeService.create() .notice(messageConfig -> messageConfig.player.leaderboardPosition) .placeholder("{POSITION}", String.valueOf(leaderboardPosition.position())) - .player(uuid) + .player(account.uuid()) .send() ); } - if (finalPage < totalPages) { - int nextPage = finalPage + 1; + if (leadboardPage.nextPage() != -1) { this.noticeService.create() .notice(messageConfig -> messageConfig.player.leaderboardFooter) - .placeholder("{NEXT_PAGE}", String.valueOf(nextPage)) - .placeholder("{TOTAL_PAGES}", String.valueOf(totalPages)) - .placeholder("{PAGE}", String.valueOf(finalPage)) - .player(uuid) + .placeholder("{NEXT_PAGE}", String.valueOf(leadboardPage.nextPage())) + .placeholder("{TOTAL_PAGES}", String.valueOf(leadboardPage.maxPages())) + .placeholder("{PAGE}", String.valueOf(leadboardPage.currentPage())) + .player(account.uuid()) .send(); } }); diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardPosition.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardEntry.java similarity index 59% rename from eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardPosition.java rename to eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardEntry.java index e17d494..23d9e0b 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardPosition.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardEntry.java @@ -2,5 +2,5 @@ import com.eternalcode.economy.account.Account; -public record LeaderboardPosition(Account account, int position) { +public record LeaderboardEntry(Account account, int position) { } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardPage.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardPage.java new file mode 100644 index 0000000..0e2e5e6 --- /dev/null +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardPage.java @@ -0,0 +1,5 @@ +package com.eternalcode.economy.leaderboard; + +import java.util.List; + +public record LeaderboardPage(List entries, int currentPage, int maxPages, int nextPage) {} diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardService.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardService.java index 14a2903..bc1d920 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardService.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardService.java @@ -1,81 +1,57 @@ package com.eternalcode.economy.leaderboard; import com.eternalcode.economy.account.Account; -import com.eternalcode.economy.account.database.AccountRepository; -import java.math.BigDecimal; -import java.util.Collection; -import java.util.Comparator; +import com.eternalcode.economy.account.AccountManager; +import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.TreeMap; +import java.util.Set; +import java.util.NavigableMap; import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; public class LeaderboardService { - private final AccountRepository accountRepository; - private final Object lock = new Object(); - private final TreeMap> leaderboardIndex = new TreeMap<>(Comparator.reverseOrder()); + private final AccountManager accountManager; - public LeaderboardService(AccountRepository accountRepository) { - this.accountRepository = accountRepository; + public LeaderboardService(AccountManager accountManager) { + this.accountManager = accountManager; } - public CompletableFuture> getLeaderboard() { - return this.accountRepository.getAllAccounts().thenApply(accounts -> { - synchronized (lock) { - leaderboardIndex.clear(); - leaderboardIndex.putAll(accounts.stream() - .collect(Collectors.groupingBy( - Account::balance, - () -> new TreeMap<>(Comparator.reverseOrder()), - Collectors.toList() - ))); - - return leaderboardIndex.values().stream() - .flatMap(List::stream) - .collect(Collectors.toList()); + public CompletableFuture> getLeaderboard() { + List entries = new ArrayList<>(); + int position = 1; + + NavigableMap> balanceTiers = accountManager.getAccountsByBalance(); + for (Set accounts : balanceTiers.values()) { + for (Account account : accounts) { + entries.add(new LeaderboardEntry(account, position)); } - }); + position += accounts.size(); + } + return CompletableFuture.completedFuture(entries); } - public CompletableFuture getLeaderboardPosition(Account targetAccount) { - return this.accountRepository.getAllAccounts().thenApply(accounts -> { - Map> grouped = accounts.stream() - .sorted(Comparator.comparing(Account::balance).reversed()) - .collect(Collectors.groupingBy( - Account::balance, - () -> new TreeMap<>(Comparator.reverseOrder()), - Collectors.toList() - )); - - BigDecimal targetBalance = targetAccount.balance(); - int cumulative = 0; - - for (Map.Entry> entry : grouped.entrySet()) { - BigDecimal currentBalance = entry.getKey(); - List group = entry.getValue(); - - int comparison = currentBalance.compareTo(targetBalance); - - if (comparison > 0) { - cumulative += group.size(); - continue; - } - - if (comparison == 0) { - for (Account account : group) { - if (account.equals(targetAccount)) { - return new LeaderboardPosition(targetAccount, cumulative + 1); - } - } - return new LeaderboardPosition(targetAccount, -1); - } + public CompletableFuture getLeaderboardPage(int page, int entriesPerPage) { + return getLeaderboard().thenApply(entries -> { + int totalEntries = entries.size(); + int maxPages = (int) Math.ceil((double) totalEntries / entriesPerPage); + int currentPage = Math.max(1, Math.min(page, maxPages)); + int startIndex = (currentPage - 1) * entriesPerPage; + int endIndex = Math.min(startIndex + entriesPerPage, totalEntries); + List pageEntries = entries.subList(startIndex, endIndex); + int nextPage = currentPage < maxPages ? currentPage + 1 : -1; + return new LeaderboardPage(pageEntries, currentPage, maxPages, nextPage); + }); + } - break; + public CompletableFuture getLeaderboardPosition(Account target) { + int position = 1; + NavigableMap> balanceTiers = accountManager.getAccountsByBalance(); + for (Set accounts : balanceTiers.values()) { + if (accounts.contains(target)) { + return CompletableFuture.completedFuture(new LeaderboardEntry(target, position)); } - - return new LeaderboardPosition(targetAccount, -1); - }); + position += accounts.size(); + } + return CompletableFuture.completedFuture(new LeaderboardEntry(target, -1)); } } diff --git a/eternaleconomy-core/src/test/java/com/eternalcode/economy/leaderboard/LeaderboardServiceTest.java b/eternaleconomy-core/src/test/java/com/eternalcode/economy/leaderboard/LeaderboardServiceTest.java deleted file mode 100644 index 8d06645..0000000 --- a/eternaleconomy-core/src/test/java/com/eternalcode/economy/leaderboard/LeaderboardServiceTest.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.eternalcode.economy.leaderboard; - -import com.eternalcode.economy.account.Account; -import com.eternalcode.economy.account.database.AccountRepositoryInMemory; -import com.eternalcode.economy.config.implementation.PluginConfig; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; -import java.util.Collection; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -import static org.junit.jupiter.api.Assertions.*; - -class LeaderboardServiceTest { - - private LeaderboardService leaderboardService; - private AccountRepositoryInMemory accountRepository; - - @BeforeEach - void setUp() { - this.accountRepository = new AccountRepositoryInMemory(); - PluginConfig pluginConfig = new PluginConfig(); - pluginConfig.leaderboardEntriesPerPage = 3; - this.leaderboardService = new LeaderboardService(this.accountRepository); - } - - @Test - void testGetLeaderboard() { - Account account1 = new Account(UUID.randomUUID(), "Player1", BigDecimal.valueOf(100)); - Account account2 = new Account(UUID.randomUUID(), "Player2", BigDecimal.valueOf(200)); - Account account3 = new Account(UUID.randomUUID(), "Player3", BigDecimal.valueOf(300)); - Account account4 = new Account(UUID.randomUUID(), "Player4", BigDecimal.valueOf(50)); - - this.accountRepository.save(account1); - this.accountRepository.save(account2); - this.accountRepository.save(account3); - this.accountRepository.save(account4); - - Collection leaderboard = this.leaderboardService.getLeaderboard().join(); - - assertEquals(3, leaderboard.size(), "Leaderboard should contain top 3 accounts"); - List leaderboardList = List.copyOf(leaderboard); - assertEquals(account3, leaderboardList.get(0), "Top account should be the one with the highest balance"); - assertEquals(account2, leaderboardList.get(1), "Second account should be the one with the second-highest balance"); - assertEquals(account1, leaderboardList.get(2), "Third account should be the one with the third-highest balance"); - } - - @Test - void testGetLeaderboardPosition() { - Account account1 = new Account(UUID.randomUUID(), "Player1", BigDecimal.valueOf(100)); - Account account2 = new Account(UUID.randomUUID(), "Player2", BigDecimal.valueOf(200)); - Account account3 = new Account(UUID.randomUUID(), "Player3", BigDecimal.valueOf(300)); - - this.accountRepository.save(account1); - this.accountRepository.save(account2); - this.accountRepository.save(account3); - - CompletableFuture position1Future = this.leaderboardService.getLeaderboardPosition(account1); - CompletableFuture position2Future = this.leaderboardService.getLeaderboardPosition(account2); - CompletableFuture position3Future = this.leaderboardService.getLeaderboardPosition(account3); - - LeaderboardPosition position1 = position1Future.join(); - LeaderboardPosition position2 = position2Future.join(); - LeaderboardPosition position3 = position3Future.join(); - - assertEquals(3, position1.position(), "Player1 should be in position 3"); - assertEquals(2, position2.position(), "Player2 should be in position 2"); - assertEquals(1, position3.position(), "Player3 should be in position 1"); - } - - @Test - void testLeaderboardWithSameBalances() { - Account account1 = new Account(UUID.randomUUID(), "Player1", BigDecimal.valueOf(100)); - Account account2 = new Account(UUID.randomUUID(), "Player2", BigDecimal.valueOf(100)); - Account account3 = new Account(UUID.randomUUID(), "Player3", BigDecimal.valueOf(100)); - - this.accountRepository.save(account1); - this.accountRepository.save(account2); - this.accountRepository.save(account3); - - CompletableFuture position1Future = this.leaderboardService.getLeaderboardPosition(account1); - CompletableFuture position2Future = this.leaderboardService.getLeaderboardPosition(account2); - CompletableFuture position3Future = this.leaderboardService.getLeaderboardPosition(account3); - - LeaderboardPosition position1 = position1Future.join(); - LeaderboardPosition position2 = position2Future.join(); - LeaderboardPosition position3 = position3Future.join(); - - assertEquals(1, position1.position(), "Player1 should be in position 1"); - assertEquals(1, position2.position(), "Player2 should be in position 1"); - assertEquals(1, position3.position(), "Player3 should be in position 1"); - } - - @Test - void testLeaderboardWithNoAccounts() { - Collection leaderboard = this.leaderboardService.getLeaderboard().join(); - assertTrue(leaderboard.isEmpty(), "Leaderboard should be empty when no accounts exist"); - } -}