diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 0f2db7a2..40f43640 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -2,6 +2,8 @@ object Versions { const val PAPER_API = "1.20.4-R0.1-SNAPSHOT" + const val TRIUMPH_GUI = "3.1.13" + const val OKAERI_CONFIGS = "5.0.13" const val LITE_COMMANDS = "3.10.9" @@ -10,13 +12,12 @@ object Versions { const val JETBRAINS_ANNOTATIONS = "26.1.0" - const val ADVENTURE_PLATFORM_BUKKIT = "4.4.1" - const val ADVENTURE_API = "4.24.0" - const val VAULT_API = "1.7.1" const val PLACEHOLDER_API = "2.12.2" + const val LITE_SKULL_API = "2.0.0" + const val MARIA_DB = "3.5.7" const val POSTGRESQL = "42.7.10" const val H2 = "2.4.240" diff --git a/eternaleconomy-core/build.gradle.kts b/eternaleconomy-core/build.gradle.kts index 4fe3b748..57842c91 100644 --- a/eternaleconomy-core/build.gradle.kts +++ b/eternaleconomy-core/build.gradle.kts @@ -60,6 +60,10 @@ dependencies { compileOnly("me.clip:placeholderapi:${Versions.PLACEHOLDER_API}") + // TriumphGUI for GUI + implementation("dev.triumphteam:triumph-gui:${Versions.TRIUMPH_GUI}") + paperLibrary("dev.rollczi:liteskullapi:${Versions.LITE_SKULL_API}") + testImplementation(platform("org.junit:junit-bom:6.0.3")) testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") 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 c69e4f9c..09ea860d 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/EconomyBukkitPlugin.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/EconomyBukkitPlugin.java @@ -35,7 +35,7 @@ import com.eternalcode.economy.database.DatabaseManager; import com.eternalcode.economy.format.DecimalFormatter; import com.eternalcode.economy.format.DecimalFormatterImpl; -import com.eternalcode.economy.leaderboard.LeaderboardCommand; +import com.eternalcode.economy.leaderboard.LeaderboardConfigurer; import com.eternalcode.economy.multification.NoticeBroadcastHandler; import com.eternalcode.economy.multification.NoticeHandler; import com.eternalcode.economy.multification.NoticeService; @@ -48,9 +48,13 @@ import com.eternalcode.multification.notice.NoticeBroadcast; import com.google.common.base.Stopwatch; import dev.rollczi.litecommands.LiteCommands; +import dev.rollczi.litecommands.LiteCommandsBuilder; import dev.rollczi.litecommands.bukkit.LiteBukkitFactory; +import dev.rollczi.litecommands.bukkit.LiteBukkitSettings; import dev.rollczi.litecommands.jakarta.LiteJakartaExtension; import dev.rollczi.litecommands.message.LiteMessages; +import dev.rollczi.liteskullapi.LiteSkullFactory; +import dev.rollczi.liteskullapi.SkullAPI; import jakarta.validation.constraints.Min; import java.io.File; import java.math.BigDecimal; @@ -68,7 +72,7 @@ public class EconomyBukkitPlugin extends JavaPlugin { private static final String PLUGIN_STARTED = "EternalEconomy has been enabled in %dms."; private DatabaseManager databaseManager; - + private SkullAPI skullAPI; private LiteCommands liteCommands; @Override @@ -97,6 +101,11 @@ public void onEnable() { NoticeService noticeService = new NoticeService(messageConfig, miniMessage); + this.skullAPI = LiteSkullFactory.builder() + .cacheExpireAfterWrite(Duration.ofMinutes(45L)) + .bukkitScheduler(this) + .build(); + Scheduler scheduler = EconomySchedulerAdapter.getAdaptiveScheduler(this); this.databaseManager = new DatabaseManager(this.getLogger(), dataFolder, pluginConfig.database); @@ -132,13 +141,13 @@ public void onEnable() { Economy.class, vaultEconomyProvider, this, ServicePriority.Highest); - this.liteCommands = LiteBukkitFactory.builder("eternaleconomy", this, server) + LiteCommandsBuilder liteCommandsBuilder = LiteBukkitFactory + .builder("eternaleconomy", this, server) .extension( new LiteJakartaExtension<>(), settings -> settings .violationMessage( Min.class, BigDecimal.class, - new InvalidBigDecimalMessage<>( - noticeService))) + new InvalidBigDecimalMessage<>(noticeService))) .annotations(extension -> extension.validator( Account.class, @@ -178,19 +187,26 @@ public void onEnable() { new MoneyTransferCommand( accountPaymentService, decimalFormatter, noticeService, pluginConfig), - new EconomyReloadCommand(configService, noticeService), - new LeaderboardCommand( - noticeService, decimalFormatter, - accountManager.getLeaderboardService(), - pluginConfig)) + new EconomyReloadCommand(configService, noticeService)) .context(Account.class, new AccountContext(accountManager, messageConfig)) .argument(Account.class, new AccountArgument(accountManager, noticeService, server)) .result(Notice.class, new NoticeHandler(noticeService)) - .result(NoticeBroadcast.class, new NoticeBroadcastHandler()) + .result(NoticeBroadcast.class, new NoticeBroadcastHandler()); - .build(); + new LeaderboardConfigurer().configure( + this, + accountManager.getLeaderboardService(), + scheduler, + configService, + pluginConfig, + noticeService, + decimalFormatter, + this.skullAPI, + liteCommandsBuilder); + + this.liteCommands = liteCommandsBuilder.build(); server.getPluginManager().registerEvents(new AccountController(accountManager), this); @@ -206,8 +222,8 @@ public void onEnable() { accountManager, decimalFormatter, server, - this, - this.getLogger()); + this.getLogger(), + scheduler); bridgeManager.init(); Duration elapsed = started.elapsed(); @@ -216,6 +232,10 @@ public void onEnable() { @Override public void onDisable() { + if (this.skullAPI != null) { + this.skullAPI.shutdown(); + } + if (this.liteCommands != null) { this.liteCommands.unregister(); } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/MiniMessageHolder.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/MiniMessageHolder.java new file mode 100644 index 00000000..09e417cb --- /dev/null +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/MiniMessageHolder.java @@ -0,0 +1,15 @@ +package com.eternalcode.economy; + +import com.eternalcode.commons.adventure.AdventureLegacyColorPostProcessor; +import com.eternalcode.commons.adventure.AdventureLegacyColorPreProcessor; +import com.eternalcode.commons.adventure.AdventureUrlPostProcessor; +import net.kyori.adventure.text.minimessage.MiniMessage; + +public interface MiniMessageHolder { + + MiniMessage MINI_MESSAGE = MiniMessage.builder() + .postProcessor(new AdventureUrlPostProcessor()) + .postProcessor(new AdventureLegacyColorPostProcessor()) + .preProcessor(new AdventureLegacyColorPreProcessor()) + .build(); +} diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/Account.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/Account.java index b4d0543f..d2d092d5 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/Account.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/Account.java @@ -17,7 +17,9 @@ public Account withBalance(UnaryOperator operator) { @Override public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; + if (o == null || getClass() != o.getClass()) { + return false; + } Account account = (Account) o; return Objects.equals(uuid, account.uuid) && Objects.equals(name, account.name); } @@ -26,5 +28,4 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(uuid, name); } - -} \ No newline at end of file +} 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 166c1ee5..2ce8a259 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,8 +2,8 @@ import com.eternalcode.economy.account.database.AccountRepository; import com.eternalcode.economy.config.implementation.PluginConfig; -import com.eternalcode.economy.leaderboard.LeaderboardServiceImpl; import com.eternalcode.economy.leaderboard.LeaderboardService; +import com.eternalcode.economy.leaderboard.LeaderboardServiceImpl; import java.math.BigDecimal; import java.util.Collection; import java.util.Collections; @@ -18,7 +18,7 @@ public class AccountManager { private final Map accountByUniqueId = new ConcurrentHashMap<>(); private final NavigableMap accountByName = new ConcurrentSkipListMap<>( - String.CASE_INSENSITIVE_ORDER); + String.CASE_INSENSITIVE_ORDER); private final AccountRepository accountRepository; private final LeaderboardServiceImpl leaderboardService; @@ -34,16 +34,16 @@ public static AccountManager create(AccountRepository accountRepository, PluginC AccountManager accountManager = new AccountManager(accountRepository, config, logger); accountRepository.getAllAccounts() - .thenAccept(accounts -> { - for (Account account : accounts) { - accountManager.save(account); - } - }) - .exceptionally(throwable -> { - logger.severe("Failed to load accounts: " + throwable.getMessage()); - throwable.printStackTrace(); - return null; - }); + .thenAccept(accounts -> { + for (Account account : accounts) { + accountManager.save(account); + } + }) + .exceptionally(throwable -> { + logger.severe("Failed to load accounts: " + throwable.getMessage()); + throwable.printStackTrace(); + return null; + }); return accountManager; } @@ -97,7 +97,7 @@ public void save(Account account) { public Collection getAccountStartingWith(String prefix) { return Collections.unmodifiableCollection( - this.accountByName.subMap(prefix, true, prefix + Character.MAX_VALUE, true).values()); + this.accountByName.subMap(prefix, true, prefix + Character.MAX_VALUE, true).values()); } public Collection getAccounts() { @@ -107,5 +107,4 @@ public Collection getAccounts() { public LeaderboardService getLeaderboardService() { return this.leaderboardService; } - } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/database/AccountRepositoryImpl.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/database/AccountRepositoryImpl.java index 267197c5..18f3ec79 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/database/AccountRepositoryImpl.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/database/AccountRepositoryImpl.java @@ -5,6 +5,7 @@ import com.eternalcode.economy.database.AbstractRepositoryOrmLite; import com.eternalcode.economy.database.DatabaseException; import com.eternalcode.economy.database.DatabaseManager; +import com.j256.ormlite.dao.Dao; import com.j256.ormlite.stmt.QueryBuilder; import com.j256.ormlite.stmt.Where; import com.j256.ormlite.table.TableUtils; @@ -16,77 +17,90 @@ public class AccountRepositoryImpl extends AbstractRepositoryOrmLite implements AccountRepository { - public AccountRepositoryImpl( - DatabaseManager databaseManager, - Scheduler scheduler) { + public AccountRepositoryImpl(DatabaseManager databaseManager, Scheduler scheduler) { super(databaseManager, scheduler); try { - TableUtils.createTableIfNotExists(databaseManager.connectionSource(), AccountWrapper.class); - } catch (SQLException exception) { - throw new DatabaseException("Failed to create table for AccountWrapper.", exception); + TableUtils.createTableIfNotExists(databaseManager.connectionSource(), AccountTable.class); + this.createIndexes(); + } + catch (SQLException exception) { + throw new DatabaseException("Failed to create table for AccountTable.", exception); } } @Override public CompletableFuture save(Account account) { - return this.save(AccountWrapper.class, AccountWrapper.fromAccount(account)).thenApply(status -> null); + return this.save(AccountTable.class, AccountTable.fromAccount(account)).thenApply(status -> null); } @Override public CompletableFuture delete(Account account) { - return this.delete(AccountWrapper.class, AccountWrapper.fromAccount(account)).thenApply(status -> null); + return this.delete(AccountTable.class, AccountTable.fromAccount(account)).thenApply(status -> null); } @Override public CompletableFuture> getAllAccounts() { - return this.selectAll(AccountWrapper.class) - .thenApply(accountWrappers -> accountWrappers.stream() - .map(accountWrapper -> accountWrapper.toAccount()) - .toList()); + return this.selectAll(AccountTable.class) + .thenApply(accountWrappers -> accountWrappers.stream().map(AccountTable::toAccount).toList()); } @Override public CompletableFuture> getTopAccounts(int limit, int offset) { - return this.>action( - AccountWrapper.class, dao -> { - QueryBuilder queryBuilder = dao.queryBuilder(); - queryBuilder.orderBy("balance", false); - queryBuilder.orderBy("uuid", true); - - if (limit > 0) { - queryBuilder.limit((long) limit); - } - - if (offset > 0) { - queryBuilder.offset((long) offset); - } - - return queryBuilder.query().stream() - .map(AccountWrapper::toAccount) - .toList(); - }); + return this.>action( + AccountTable.class, dao -> { + QueryBuilder queryBuilder = dao.queryBuilder(); + queryBuilder.orderBy(AccountTable.BALANCE, false); + queryBuilder.orderBy(AccountTable.UUID, true); + + if (limit > 0) { + queryBuilder.limit((long) limit); + } + + if (offset > 0) { + queryBuilder.offset((long) offset); + } + + return queryBuilder.query().stream().map(AccountTable::toAccount).toList(); + }); } @Override public CompletableFuture getPosition(Account target) { - return this.action( - AccountWrapper.class, dao -> { - QueryBuilder qb = dao.queryBuilder(); - Where where = qb.where(); - - where.gt("balance", target.balance()); - where.or(); - where.eq("balance", target.balance()); - where.and(); - where.lt("uuid", target.uuid()); - - return (int) qb.countOf() + 1; - }); + return this.action( + AccountTable.class, dao -> { + QueryBuilder qb = dao.queryBuilder(); + Where where = qb.where(); + + where.gt(AccountTable.BALANCE, target.balance()); + where.or(); + where.eq(AccountTable.BALANCE, target.balance()); + where.and(); + where.lt(AccountTable.UUID, target.uuid()); + + return (int) qb.countOf() + 1; + }); } @Override public CompletableFuture countAccounts() { - return this.action(AccountWrapper.class, dao -> dao.countOf()); + return this.action(AccountTable.class, Dao::countOf); + } + + /* + * ORMLite annotations (@DatabaseField(index = true)) do not support creating + * composite indexes with specific sort directions (ASC/DESC). + * For the leaderboard, we need an index on (balance DESC, uuid ASC) to + * efficiently query the top accounts without sorting the entire table. + * This optimization significantly improves performance for large datasets. + * 20.01.2026 - vlucky + */ + private void createIndexes() { + this.action( + AccountTable.class, dao -> { + dao.executeRaw( + "CREATE INDEX IF NOT EXISTS idx_leaderboard_composite ON eternaleconomy_accounts(balance DESC, uuid ASC)"); + return null; + }); } } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/database/AccountRepositoryInMemory.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/database/AccountRepositoryInMemory.java index bf1e42ad..35dfd12b 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/database/AccountRepositoryInMemory.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/database/AccountRepositoryInMemory.java @@ -33,11 +33,11 @@ public CompletableFuture> getAllAccounts() { @Override public CompletableFuture> getTopAccounts(int limit, int offset) { List sorted = accounts.values().stream() - .sorted(Comparator.comparing(Account::balance).reversed() - .thenComparing(Account::uuid)) - .skip(offset) - .limit(limit) - .toList(); + .sorted(Comparator.comparing(Account::balance).reversed() + .thenComparing(Account::uuid)) + .skip(offset) + .limit(limit) + .toList(); return CompletableFuture.completedFuture(sorted); } @@ -45,10 +45,10 @@ public CompletableFuture> getTopAccounts(int limit, int offset) { @Override public CompletableFuture getPosition(Account target) { long position = accounts.values().stream() - .sorted(Comparator.comparing(Account::balance).reversed() - .thenComparing(Account::uuid)) - .takeWhile(account -> !account.uuid().equals(target.uuid())) - .count() + 1; + .sorted(Comparator.comparing(Account::balance).reversed() + .thenComparing(Account::uuid)) + .takeWhile(account -> !account.uuid().equals(target.uuid())) + .count() + 1; return CompletableFuture.completedFuture((int) position); } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/database/AccountWrapper.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/database/AccountTable.java similarity index 57% rename from eternaleconomy-core/src/main/java/com/eternalcode/economy/account/database/AccountWrapper.java rename to eternaleconomy-core/src/main/java/com/eternalcode/economy/account/database/AccountTable.java index 62d45457..807b93d7 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/database/AccountWrapper.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/account/database/AccountTable.java @@ -8,28 +8,32 @@ import java.util.UUID; @DatabaseTable(tableName = "eternaleconomy_accounts") -class AccountWrapper { +class AccountTable { - @DatabaseField(id = true) + static final String UUID = "uuid"; + static final String NAME = "name"; + static final String BALANCE = "balance"; + + @DatabaseField(id = true, unique = true, columnName = UUID) private UUID uuid; - @DatabaseField(index = true, unique = true) + @DatabaseField(index = true, unique = true, columnName = NAME) private String name; - @DatabaseField(dataType = DataType.BIG_DECIMAL_NUMERIC, index = true) + @DatabaseField(dataType = DataType.BIG_DECIMAL_NUMERIC, columnName = BALANCE) private BigDecimal balance; - public AccountWrapper() { + public AccountTable() { } - public AccountWrapper(UUID uuid, String name, BigDecimal balance) { + public AccountTable(UUID uuid, String name, BigDecimal balance) { this.uuid = uuid; this.name = name; this.balance = balance; } - public static AccountWrapper fromAccount(Account account) { - return new AccountWrapper(account.uuid(), account.name(), account.balance()); + public static AccountTable fromAccount(Account account) { + return new AccountTable(account.uuid(), account.name(), account.balance()); } public Account toAccount() { diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/bridge/BridgeManager.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/bridge/BridgeManager.java index 9e1d5f54..1331db5a 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/bridge/BridgeManager.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/bridge/BridgeManager.java @@ -1,14 +1,12 @@ package com.eternalcode.economy.bridge; +import com.eternalcode.commons.scheduler.Scheduler; import com.eternalcode.economy.account.AccountManager; import com.eternalcode.economy.bridge.placeholderapi.PlaceholderEconomyExpansion; import com.eternalcode.economy.format.DecimalFormatter; import io.papermc.paper.plugin.configuration.PluginMeta; import java.util.logging.Logger; -import org.bukkit.Bukkit; import org.bukkit.Server; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginManager; public class BridgeManager { @@ -19,42 +17,44 @@ public class BridgeManager { private final DecimalFormatter decimalFormatter; private final Server server; - private final Plugin plugin; private final Logger logger; + private final Scheduler scheduler; public BridgeManager( PluginMeta pluginMeta, AccountManager accountManager, DecimalFormatter decimalFormatter, Server server, - Plugin plugin, - Logger logger + Logger logger, + Scheduler scheduler ) { this.pluginMeta = pluginMeta; this.accountManager = accountManager; this.decimalFormatter = decimalFormatter; this.server = server; - this.plugin = plugin; this.logger = logger; + this.scheduler = scheduler; } public void init() { - // Using "load: STARTUP" in plugin.yml causes the plugin to load before PlaceholderAPI. - // Therefore, we need to delay the bridge initialization until the server is fully started. - // The scheduler runs the code after the "Done" message, ensuring the server is fully operational. - Bukkit.getScheduler().runTask(this.plugin, () -> { - this.setupBridge("PlaceholderAPI", () -> { - PlaceholderEconomyExpansion placeholderEconomyExpansion = new PlaceholderEconomyExpansion( - this.pluginMeta, - this.accountManager, - this.decimalFormatter - ); + // Using "load: STARTUP" in plugin.yml causes the plugin to load before + // PlaceholderAPI. + // Therefore, we need to delay the bridge initialization until the server is + // fully started. + // The scheduler runs the code after the "Done" message, ensuring the server is + // fully operational. + this.scheduler.run( + () -> { + this.setupBridge( + "PlaceholderAPI", () -> { + PlaceholderEconomyExpansion placeholderEconomyExpansion = new PlaceholderEconomyExpansion( + this.pluginMeta, + this.accountManager, + this.decimalFormatter); - placeholderEconomyExpansion.register(); - - System.out.println("PlaceholderAPI bridge initialized!"); + placeholderEconomyExpansion.register(); + }); }); - }); // other bridges (do not put bridges in the scheduler if not needed) } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/bridge/placeholderapi/PlaceholderEconomyExpansion.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/bridge/placeholderapi/PlaceholderEconomyExpansion.java index 72dfd8af..d05092b5 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/bridge/placeholderapi/PlaceholderEconomyExpansion.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/bridge/placeholderapi/PlaceholderEconomyExpansion.java @@ -18,9 +18,9 @@ public class PlaceholderEconomyExpansion extends PlaceholderExpansion implements private final DecimalFormatter decimalFormatter; public PlaceholderEconomyExpansion( - PluginMeta pluginMeta, - AccountManager accountManager, - DecimalFormatter decimalFormatter) { + PluginMeta pluginMeta, + AccountManager accountManager, + DecimalFormatter decimalFormatter) { this.pluginMeta = pluginMeta; this.accountManager = accountManager; this.decimalFormatter = decimalFormatter; diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/cooldown/CommandCooldownConfig.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/cooldown/CommandCooldownConfig.java index bfedef9a..7198c34f 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/cooldown/CommandCooldownConfig.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/cooldown/CommandCooldownConfig.java @@ -3,7 +3,6 @@ import com.eternalcode.multification.notice.Notice; import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; - import java.time.Duration; public class CommandCooldownConfig extends OkaeriConfig { diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/cooldown/CommandCooldownEditor.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/cooldown/CommandCooldownEditor.java index 884d47e6..48221c2e 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/cooldown/CommandCooldownEditor.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/cooldown/CommandCooldownEditor.java @@ -5,9 +5,8 @@ import dev.rollczi.litecommands.cooldown.CooldownContext; import dev.rollczi.litecommands.editor.Editor; import dev.rollczi.litecommands.meta.Meta; -import org.bukkit.command.CommandSender; - import java.util.Map; +import org.bukkit.command.CommandSender; public class CommandCooldownEditor implements Editor { @@ -37,5 +36,4 @@ public CommandBuilder edit(CommandBuilder commandB return commandBuilder; } - } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/cooldown/CommandCooldownMessage.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/cooldown/CommandCooldownMessage.java index 6a55f010..1e3e5d8f 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/cooldown/CommandCooldownMessage.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/cooldown/CommandCooldownMessage.java @@ -7,9 +7,8 @@ import dev.rollczi.litecommands.message.InvokedMessage; import dev.rollczi.litecommands.message.LiteMessages; import dev.rollczi.litecommands.time.DurationParser; -import org.bukkit.command.CommandSender; - import java.time.Duration; +import org.bukkit.command.CommandSender; public class CommandCooldownMessage implements InvokedMessage { @@ -29,12 +28,12 @@ public Object get(Invocation invocation, CooldownState cooldownSt return LiteMessages.COMMAND_COOLDOWN.getDefaultMessage(cooldownState); } - String formatted = DurationParser.TIME_UNITS.format(Duration.ofSeconds(cooldownState.getRemainingDuration().getSeconds())); + String formatted = + DurationParser.TIME_UNITS.format(Duration.ofSeconds(cooldownState.getRemainingDuration().getSeconds())); return noticeService.create() .notice(notice -> cooldown.message) .placeholder("{TIME}", formatted) .viewer(invocation.sender()); } - } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/impl/WithdrawCommand.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/impl/WithdrawCommand.java index aa4cd886..00ca1f3c 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/impl/WithdrawCommand.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/impl/WithdrawCommand.java @@ -11,7 +11,6 @@ import dev.rollczi.litecommands.annotations.execute.Execute; import dev.rollczi.litecommands.annotations.permission.Permission; import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.Positive; import java.math.BigDecimal; import java.time.Duration; import java.util.UUID; diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/impl/admin/AdminSetCommand.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/impl/admin/AdminSetCommand.java index 1aac9574..f43e650d 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/impl/admin/AdminSetCommand.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/impl/admin/AdminSetCommand.java @@ -58,5 +58,4 @@ private void setAccountBalance(CommandSender sender, Account receiver, BigDecima .player(receiver.uuid()) .send(); } - } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/message/InvalidBigDecimalMessage.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/message/InvalidBigDecimalMessage.java index bf19d6f0..f101305d 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/message/InvalidBigDecimalMessage.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/command/message/InvalidBigDecimalMessage.java @@ -8,7 +8,8 @@ import java.math.BigDecimal; import org.bukkit.command.CommandSender; -public class InvalidBigDecimalMessage implements InvokedMessage> { +public class InvalidBigDecimalMessage + implements InvokedMessage> { private final NoticeService noticeService; diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/ConfigService.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/ConfigService.java index 3efd7f7e..fc9e2191 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/ConfigService.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/ConfigService.java @@ -30,6 +30,7 @@ public T create(Class config, File file) { NoticeResolverRegistry noticeRegistry = NoticeResolverDefaults.createRegistry() .registerResolver(new SoundBukkitResolver()); + configFile .withConfigurer(yamlConfigurer, new SerdesCommons(), new SerdesBukkit()) .withConfigurer(yamlConfigurer, new MultificationSerdesPack(noticeRegistry)) diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/implementation/CommandsConfig.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/implementation/CommandsConfig.java index a78c9db0..1d0aec8c 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/implementation/CommandsConfig.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/implementation/CommandsConfig.java @@ -3,7 +3,6 @@ import com.eternalcode.economy.command.cooldown.CommandCooldownConfig; import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; - import java.util.Map; public class CommandsConfig extends OkaeriConfig { @@ -15,5 +14,4 @@ public class CommandsConfig extends OkaeriConfig { public Map cooldowns = Map.of( "pay", new CommandCooldownConfig() ); - } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/implementation/PluginConfig.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/implementation/PluginConfig.java index 3ff6b3f6..d6b4071a 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/implementation/PluginConfig.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/implementation/PluginConfig.java @@ -6,12 +6,11 @@ import com.eternalcode.economy.format.DecimalUnit; import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; -import org.bukkit.Material; - import java.math.BigDecimal; import java.util.List; +import org.bukkit.Material; -@SuppressWarnings({ "FieldMayBeFinal", "FieldCanBeLocal" }) +@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) public class PluginConfig extends OkaeriConfig { @Comment("Units settings") @@ -32,18 +31,24 @@ public class PluginConfig extends OkaeriConfig { @Comment("Should leaderboard command show player's position in the leaderboard") public boolean showLeaderboardPosition = true; + @Comment("Should leaderboard command show GUI instead of chat message") + public boolean showLeaderboardGui = true; + + @Comment("Interval for refreshing the leaderboard cache") + public java.time.Duration leaderboardRefreshInterval = java.time.Duration.ofSeconds(30); + @Comment("Currency item settings") public WithdrawItem withdraw = new WithdrawItem(); public static class Units extends OkaeriConfig { public List format = List.of( - new DecimalUnit(1_000L, 'k'), - new DecimalUnit(1_000_000L, 'm'), - new DecimalUnit(1_000_000_000L, 'b'), - new DecimalUnit(1_000_000_000_000L, 't'), - new DecimalUnit(1_000_000_000_000_000L, 'p'), - new DecimalUnit(1_000_000_000_000_000_000L, 'e')); + new DecimalUnit(1_000L, 'k'), + new DecimalUnit(1_000_000L, 'm'), + new DecimalUnit(1_000_000_000L, 'b'), + new DecimalUnit(1_000_000_000_000L, 't'), + new DecimalUnit(1_000_000_000_000_000L, 'p'), + new DecimalUnit(1_000_000_000_000_000_000L, 'e')); } public static class WithdrawItem extends OkaeriConfig { @@ -53,42 +58,42 @@ public static class WithdrawItem extends OkaeriConfig { @Comment("Default item used when multi-item system is disabled or as fallback") public ConfigItem item = ConfigItem.builder() - .withName("Check worth {VALUE}$") - .withLore(List.of("Right click to redeem")) - .withMaterial(Material.PAPER) - .withTexture(0) - .withGlow(true) - .build(); + .withName("Check worth {VALUE}$") + .withLore(List.of("Right click to redeem")) + .withMaterial(Material.PAPER) + .withTexture(0) + .withGlow(true) + .build(); @Comment({ - "Enable multi-item system: different items for different banknote values", - "When disabled, always uses the default 'item' above" + "Enable multi-item system: different items for different banknote values", + "When disabled, always uses the default 'item' above" }) public boolean multiItemEnabled = false; @Comment({ - "Item configurations for specific value thresholds", - "System selects the entry with highest minValue <= banknote value", - "Example: minValue 1.0 = coins, minValue 100.0 = banknotes" + "Item configurations for specific value thresholds", + "System selects the entry with highest minValue <= banknote value", + "Example: minValue 1.0 = coins, minValue 100.0 = banknotes" }) public List multiItemEntries = List.of( - new WithdrawItemEntry( - BigDecimal.ONE, - ConfigItem.builder() - .withName("Coin worth {VALUE}$") - .withLore(List.of("Right click to redeem")) - .withMaterial(Material.GOLD_NUGGET) - .withTexture(1) - .withGlow(false) - .build()), - new WithdrawItemEntry( - BigDecimal.valueOf(100), - ConfigItem.builder() - .withName("Banknote worth {VALUE}$") - .withLore(List.of("Right click to redeem")) - .withMaterial(Material.PAPER) - .withTexture(100) - .withGlow(true) - .build())); + new WithdrawItemEntry( + BigDecimal.ONE, + ConfigItem.builder() + .withName("Coin worth {VALUE}$") + .withLore(List.of("Right click to redeem")) + .withMaterial(Material.GOLD_NUGGET) + .withTexture(1) + .withGlow(false) + .build()), + new WithdrawItemEntry( + BigDecimal.valueOf(100), + ConfigItem.builder() + .withName("Banknote worth {VALUE}$") + .withLore(List.of("Right click to redeem")) + .withMaterial(Material.PAPER) + .withTexture(100) + .withGlow(true) + .build())); } } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/implementation/messages/MessageAdminSubSection.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/implementation/messages/MessageAdminSubSection.java index 7805414d..4ce52cca 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/implementation/messages/MessageAdminSubSection.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/implementation/messages/MessageAdminSubSection.java @@ -8,13 +8,19 @@ public class MessageAdminSubSection extends OkaeriConfig { public Notice insufficientFunds = - Notice.chat("ECONOMY Player {PLAYER} has insufficient funds, they are missing {MISSING_BALANCE}."); + Notice.chat( + "ECONOMY Player {PLAYER} has insufficient funds, they are missing {MISSING_BALANCE}."); - public Notice added = Notice.chat("ECONOMY Added {AMOUNT} to {PLAYER}."); - public Notice removed = Notice.chat("ECONOMY Removed {AMOUNT} from {PLAYER}."); - public Notice set = Notice.chat("ECONOMY Set {PLAYER}'s balance to {AMOUNT}."); - public Notice reset = Notice.chat("ECONOMY Reset {PLAYER}'s balance."); - public Notice balance = Notice.chat("ECONOMY {PLAYER}'s balance is {BALANCE}."); + public Notice added = Notice.chat( + "ECONOMY Added {AMOUNT} to {PLAYER}."); + public Notice removed = Notice.chat( + "ECONOMY Removed {AMOUNT} from {PLAYER}."); + public Notice set = Notice.chat( + "ECONOMY Set {PLAYER}'s balance to {AMOUNT}."); + public Notice reset = Notice.chat( + "ECONOMY Reset {PLAYER}'s balance."); + public Notice balance = Notice.chat( + "ECONOMY {PLAYER}'s balance is {BALANCE}."); } 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 9d3f293a..347cc7ec 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 @@ -6,23 +6,34 @@ public class MessagesPlayerSubSection extends OkaeriConfig { - public Notice added = Notice.chat("ECONOMY Added {AMOUNT} to your account."); - public Notice removed = Notice.chat("ECONOMY Removed {AMOUNT} from your account."); - public Notice set = Notice.chat("ECONOMY Set your balance to {AMOUNT}."); - public Notice reset = Notice.chat("ECONOMY Resetted your balance."); - public Notice balance = Notice.chat("ECONOMY Your balance is {BALANCE}."); + public Notice added = Notice.chat( + "ECONOMY Added {AMOUNT} to your account."); + public Notice removed = Notice.chat( + "ECONOMY Removed {AMOUNT} from your account."); + public Notice set = Notice.chat( + "ECONOMY Set your balance to {AMOUNT}."); + public Notice reset = Notice.chat( + "ECONOMY Resetted your balance."); + public Notice balance = Notice.chat( + "ECONOMY Your balance is {BALANCE}."); public Notice balanceOther = - Notice.chat("ECONOMY {PLAYER}'s balance is {BALANCE}."); - public Notice insufficientBalance = Notice.chat("ECONOMY Insufficient funds, you are missing {MISSING_BALANCE}."); - public Notice transferSuccess = Notice.chat("ECONOMY Successfully transferred {AMOUNT} to {PLAYER}."); - public Notice transferReceived = Notice.chat("ECONOMY Received {AMOUNT} from {PLAYER}."); - public Notice transferLimit = Notice.chat("ECONOMY Transaction limit is {LIMIT}."); + Notice.chat( + "ECONOMY {PLAYER}'s balance is {BALANCE}."); + public Notice insufficientBalance = Notice.chat( + "ECONOMY Insufficient funds, you are missing {MISSING_BALANCE}."); + public Notice transferSuccess = Notice.chat( + "ECONOMY Successfully transferred {AMOUNT} to {PLAYER}."); + public Notice transferReceived = Notice.chat( + "ECONOMY Received {AMOUNT} from {PLAYER}."); + public Notice transferLimit = Notice.chat( + "ECONOMY Transaction limit is {LIMIT}."); @Comment({ "Use {PAGE} placeholder to show the current page number", "Use {TOTAL_PAGES} placeholder to show the total number of pages" }) - public Notice leaderboardHeader = Notice.chat(" Balance leaderboard (Page {PAGE}/{TOTAL_PAGES}): "); + public Notice leaderboardHeader = + Notice.chat(" Balance leaderboard (Page {PAGE}/{TOTAL_PAGES}): "); @Comment({ "Leaderboard entry notice, only displayed if showLeaderboardPosition is set to true in the config.yml", @@ -31,7 +42,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", @@ -46,9 +58,11 @@ public class MessagesPlayerSubSection extends OkaeriConfig { "Use {PAGE} placeholder to show the current page number" }) public Notice leaderboardFooter = Notice.builder() - .chat(" Click Go to page {NEXT_PAGE}'>/baltop {NEXT_PAGE} to go to the next page.") + .chat( + " Click Go to page {NEXT_PAGE}'>/baltop {NEXT_PAGE} to go to the next page.") .build(); @Comment("Leaderboard is empty notice") - public Notice leaderboardEmpty = Notice.chat("ECONOMY Leaderboard is empty :("); + public Notice leaderboardEmpty = Notice.chat( + "ECONOMY Leaderboard is empty :("); } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/item/ConfigItem.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/item/ConfigItem.java index 6ea1dff7..bf82ea47 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/item/ConfigItem.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/item/ConfigItem.java @@ -1,17 +1,16 @@ package com.eternalcode.economy.config.item; import eu.okaeri.configs.OkaeriConfig; -import java.util.Collections; import java.util.List; import org.bukkit.Material; public class ConfigItem extends OkaeriConfig { - private String name; - private List lore; - private Material material; - private Integer texture; - private boolean glow; + private final String name; + private final List lore; + private final Material material; + private final Integer texture; + private final boolean glow; public ConfigItem(String name, List lore, Material material, Integer texture, boolean glow) { this.name = name; diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/item/WithdrawItemEntry.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/item/WithdrawItemEntry.java index af85e9bc..50a73997 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/item/WithdrawItemEntry.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/config/item/WithdrawItemEntry.java @@ -7,8 +7,8 @@ public class WithdrawItemEntry extends OkaeriConfig { - private BigDecimal minValue; - private ConfigItem item; + private final BigDecimal minValue; + private final ConfigItem item; public WithdrawItemEntry(BigDecimal minValue, ConfigItem item) { this.minValue = minValue; diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/database/DatabaseConnectionDriverConstant.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/database/DatabaseConnectionDriverConstant.java index 19cf0740..e4656d31 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/database/DatabaseConnectionDriverConstant.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/database/DatabaseConnectionDriverConstant.java @@ -2,27 +2,22 @@ final class DatabaseConnectionDriverConstant { - private DatabaseConnectionDriverConstant() {} - // mysql static final String MYSQL_DRIVER = "com.mysql.cj.jdbc.Driver"; static final String MYSQL_JDBC_URL = "jdbc:mysql://%s:%s/%s?sslMode=%s"; - // mariadb — accepts "true"/"false" as valid sslMode values (aliases for "verify-full" and "disable") static final String MARIADB_DRIVER = "org.mariadb.jdbc.Driver"; static final String MARIADB_JDBC_URL = "jdbc:mariadb://%s:%s/%s?sslMode=%s"; - // h2 static final String H2_DRIVER = "org.h2.Driver"; static final String H2_JDBC_URL = "jdbc:h2:./%s/database"; - // sqlite static final String SQLITE_DRIVER = "org.sqlite.JDBC"; static final String SQLITE_JDBC_URL = "jdbc:sqlite:%s/database.db"; - // postgresql static final String POSTGRESQL_DRIVER = "org.postgresql.Driver"; static final String POSTGRESQL_JDBC_URL = "jdbc:postgresql://%s:%s/%s?sslmode=%s"; + private DatabaseConnectionDriverConstant() {} static String sslParamForMySQL(boolean enabled) { return enabled ? "REQUIRED" : "DISABLED"; diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/database/DatabaseDriverType.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/database/DatabaseDriverType.java index 8a795286..444841b6 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/database/DatabaseDriverType.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/database/DatabaseDriverType.java @@ -1,6 +1,15 @@ package com.eternalcode.economy.database; -import static com.eternalcode.economy.database.DatabaseConnectionDriverConstant.*; +import static com.eternalcode.economy.database.DatabaseConnectionDriverConstant.H2_DRIVER; +import static com.eternalcode.economy.database.DatabaseConnectionDriverConstant.H2_JDBC_URL; +import static com.eternalcode.economy.database.DatabaseConnectionDriverConstant.MARIADB_DRIVER; +import static com.eternalcode.economy.database.DatabaseConnectionDriverConstant.MARIADB_JDBC_URL; +import static com.eternalcode.economy.database.DatabaseConnectionDriverConstant.MYSQL_DRIVER; +import static com.eternalcode.economy.database.DatabaseConnectionDriverConstant.MYSQL_JDBC_URL; +import static com.eternalcode.economy.database.DatabaseConnectionDriverConstant.POSTGRESQL_DRIVER; +import static com.eternalcode.economy.database.DatabaseConnectionDriverConstant.POSTGRESQL_JDBC_URL; +import static com.eternalcode.economy.database.DatabaseConnectionDriverConstant.SQLITE_DRIVER; +import static com.eternalcode.economy.database.DatabaseConnectionDriverConstant.SQLITE_JDBC_URL; public enum DatabaseDriverType { diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/delay/Delay.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/delay/Delay.java index 6fa20dc0..47b51dcd 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/delay/Delay.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/delay/Delay.java @@ -2,7 +2,6 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; - import java.time.Duration; import java.time.Instant; import java.util.function.Supplier; @@ -19,8 +18,12 @@ private Delay(Supplier defaultDelay) { this.defaultDelay = defaultDelay; this.cache = Caffeine.newBuilder() - .expireAfter(new InstantExpiry()) - .build(); + .expireAfter(new InstantExpiry()) + .build(); + } + + public static Delay withDefault(Supplier defaultDelay) { + return new Delay<>(defaultDelay); } public void markDelay(T key, Duration delay) { @@ -51,8 +54,4 @@ public Duration getRemaining(T key) { private Instant getExpireAt(T key) { return this.cache.asMap().getOrDefault(key, Instant.MIN); } - - public static Delay withDefault(Supplier defaultDelay) { - return new Delay<>(defaultDelay); - } } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/delay/InstantExpiry.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/delay/InstantExpiry.java index 7618fd02..0e6acde2 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/delay/InstantExpiry.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/delay/InstantExpiry.java @@ -6,6 +6,20 @@ class InstantExpiry implements Expiry { + private static long timeToExpire(Instant expireTime) { + Duration toExpire = Duration.between(Instant.now(), expireTime); + if (toExpire.isNegative()) { + return 0; + } + + long nanos = toExpire.toNanos(); + if (nanos == 0) { + return 1; + } + + return nanos; + } + @Override public long expireAfterCreate(T key, Instant expireTime, long currentTime) { return timeToExpire(expireTime); @@ -20,18 +34,4 @@ public long expireAfterUpdate(T key, Instant newExpireTime, long currentTime, lo public long expireAfterRead(T key, Instant value, long currentTime, long currentDuration) { return currentDuration; } - - private static long timeToExpire(Instant expireTime) { - Duration toExpire = Duration.between(Instant.now(), expireTime); - if (toExpire.isNegative()) { - return 0; - } - - long nanos = toExpire.toNanos(); - if (nanos == 0) { - return 1; - } - - return nanos; - } } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/format/DecimalFormatterImpl.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/format/DecimalFormatterImpl.java index fa77835d..57f87ae8 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/format/DecimalFormatterImpl.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/format/DecimalFormatterImpl.java @@ -46,7 +46,7 @@ private static String getTruncatedAmount(double amount) { fractionalPart *= 100; fractionalPart = (fractionalPart < 99 && fractionalPart % 1 >= 0.5) ? ceil(fractionalPart) - : floor(fractionalPart); + : floor(fractionalPart); return (long) amount + TRUNCATED_AMOUNT_DELIMITER + (long) fractionalPart; } @@ -67,9 +67,9 @@ public String getFormattedDecimal(double amount) { DecimalUnit decimalUnit = units.get(nearestScaleDivider); return getFormattedAmountWithSuffix( - amount, - decimalUnit.getFactor(), - decimalUnit.getSuffix()); + amount, + decimalUnit.getFactor(), + decimalUnit.getSuffix()); } @Override 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 80cebefb..31e94ebe 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 @@ -1,9 +1,11 @@ package com.eternalcode.economy.leaderboard; +import com.eternalcode.commons.concurrent.FutureHandler; import com.eternalcode.economy.EconomyPermissionConstant; import com.eternalcode.economy.account.Account; import com.eternalcode.economy.config.implementation.PluginConfig; import com.eternalcode.economy.format.DecimalFormatter; +import com.eternalcode.economy.leaderboard.menu.LeaderboardMenu; import com.eternalcode.economy.multification.NoticeService; import dev.rollczi.litecommands.annotations.argument.Arg; import dev.rollczi.litecommands.annotations.command.Command; @@ -12,10 +14,13 @@ import dev.rollczi.litecommands.annotations.execute.ExecuteDefault; import dev.rollczi.litecommands.annotations.permission.Permission; import jakarta.validation.constraints.Min; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + import java.util.List; @SuppressWarnings("unused") -@Command(name = "balancetop", aliases = {"baltop"}) +@Command(name = "balancetop", aliases = {"baltop", "btgui", "topgui"}) @Permission(EconomyPermissionConstant.PLAYER_BALANCE_TOP_PERMISSION) public class LeaderboardCommand { @@ -23,17 +28,20 @@ public class LeaderboardCommand { private final DecimalFormatter decimalFormatter; private final LeaderboardService leaderboardService; private final PluginConfig pluginConfig; + private final LeaderboardMenu leaderboardMenu; public LeaderboardCommand( NoticeService noticeService, DecimalFormatter decimalFormatter, LeaderboardService leaderboardService, - PluginConfig pluginConfig + PluginConfig pluginConfig, + LeaderboardMenu leaderboardMenu ) { this.noticeService = noticeService; this.decimalFormatter = decimalFormatter; this.leaderboardService = leaderboardService; this.pluginConfig = pluginConfig; + this.leaderboardMenu = leaderboardMenu; } @ExecuteDefault @@ -43,12 +51,21 @@ void executeDefault(@Context Account account) { @Execute void execute(@Context Account account, @Min(1) @Arg("page") int page) { - this.leaderboardService.getLeaderboardPage(page - 1, this.pluginConfig.leaderboardPageSize) - .thenAccept(leaderboardPage -> showPage(account, leaderboardPage)); + if (this.pluginConfig.showLeaderboardGui) { + Player bukkitPlayer = Bukkit.getPlayer(account.uuid()); + + if (bukkitPlayer != null) { + this.leaderboardMenu.open(bukkitPlayer, page); + return; + } + } + + this.leaderboardService.getLeaderboardPage(page, this.pluginConfig.leaderboardPageSize) + .thenAccept(leaderboardPage -> this.showPage(account, leaderboardPage)); } private void showPage(Account account, LeaderboardPage page) { - int currentPage = page.currentPage() + 1; + int currentPage = page.currentPage(); List entries = page.entries(); if (entries.isEmpty()) { @@ -85,13 +102,13 @@ private void showPage(Account account, LeaderboardPage page) { .placeholder("{POSITION}", String.valueOf(entry.position())) .player(account.uuid()) .send(); - }); + }).exceptionally(FutureHandler::handleException); } if (page.nextPage() != -1) { this.noticeService.create() .notice(messages -> messages.player.leaderboardFooter) - .placeholder("{NEXT_PAGE}", String.valueOf(page.nextPage() + 1)) + .placeholder("{NEXT_PAGE}", String.valueOf(page.nextPage())) .placeholder("{TOTAL_PAGES}", String.valueOf(page.maxPages())) .placeholder("{PAGE}", String.valueOf(currentPage)) .player(account.uuid()) diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardConfigurer.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardConfigurer.java new file mode 100644 index 00000000..5abcdc96 --- /dev/null +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardConfigurer.java @@ -0,0 +1,63 @@ +package com.eternalcode.economy.leaderboard; + +import com.eternalcode.commons.bukkit.scheduler.BukkitSchedulerImpl; +import com.eternalcode.commons.scheduler.Scheduler; +import com.eternalcode.economy.config.ConfigService; +import com.eternalcode.economy.config.implementation.PluginConfig; +import com.eternalcode.economy.format.DecimalFormatter; +import com.eternalcode.economy.leaderboard.menu.LeaderboardConfig; +import com.eternalcode.economy.leaderboard.menu.LeaderboardMenu; +import com.eternalcode.economy.multification.NoticeService; +import dev.rollczi.litecommands.LiteCommandsBuilder; +import dev.rollczi.litecommands.bukkit.LiteBukkitSettings; +import dev.rollczi.liteskullapi.SkullAPI; +import java.io.File; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.Plugin; + +public class LeaderboardConfigurer { + + public void configure( + Plugin plugin, + LeaderboardService leaderboardService, + Scheduler scheduler, + ConfigService configService, + PluginConfig pluginConfig, + NoticeService noticeService, + DecimalFormatter decimalFormatter, + SkullAPI skullAPI, + LiteCommandsBuilder liteCommandsBuilder + ) { + + LeaderboardRefreshTask refreshTask = new LeaderboardRefreshTask( + leaderboardService, scheduler, + pluginConfig + ); + refreshTask.start(); + + LeaderboardConfig leaderboardConfig = configService.create( + LeaderboardConfig.class, + new File(plugin.getDataFolder(), "baltop-gui.yml") + ); + + Scheduler bukkitScheduler = new BukkitSchedulerImpl(plugin); + + LeaderboardMenu leaderboardMenu = new LeaderboardMenu( + leaderboardConfig, + bukkitScheduler, + leaderboardService, + decimalFormatter, + skullAPI + ); + + liteCommandsBuilder.commands( + new LeaderboardCommand( + noticeService, + decimalFormatter, + leaderboardService, + pluginConfig, + leaderboardMenu + ) + ); + } +} diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardRefreshTask.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardRefreshTask.java new file mode 100644 index 00000000..aaa1f17c --- /dev/null +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardRefreshTask.java @@ -0,0 +1,30 @@ +package com.eternalcode.economy.leaderboard; + +import com.eternalcode.commons.concurrent.FutureHandler; +import com.eternalcode.commons.scheduler.Scheduler; +import com.eternalcode.economy.config.implementation.PluginConfig; +import java.time.Duration; + +public final class LeaderboardRefreshTask { + + private final LeaderboardService leaderboardService; + private final Scheduler scheduler; + private final PluginConfig pluginConfig; + + public LeaderboardRefreshTask( + LeaderboardService leaderboardService, + Scheduler scheduler, + PluginConfig pluginConfig) { + this.leaderboardService = leaderboardService; + this.scheduler = scheduler; + this.pluginConfig = pluginConfig; + } + + public void start() { + this.scheduler.timerAsync( + () -> this.leaderboardService.refreshSnapshot() + .exceptionally(FutureHandler::handleException), + Duration.ZERO, + this.pluginConfig.leaderboardRefreshInterval); + } +} 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 fdc71227..1488ae42 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 @@ -9,4 +9,5 @@ public interface LeaderboardService { CompletableFuture getLeaderboardPage(int page, int pageSize); + CompletableFuture refreshSnapshot(); } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardServiceImpl.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardServiceImpl.java index 196df3db..a7cde65d 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardServiceImpl.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardServiceImpl.java @@ -2,22 +2,18 @@ import com.eternalcode.economy.account.Account; import com.eternalcode.economy.account.database.AccountRepository; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; -import java.time.Duration; + import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; public class LeaderboardServiceImpl implements LeaderboardService { - private static final String CACHE_KEY = "top_leaderboard"; - private static final int TOP_CACHE_SIZE = 100; + private static final int SNAPSHOT_SIZE = 10_000; private final AccountRepository repository; - private final Cache> topCache = Caffeine.newBuilder() - .expireAfterWrite(Duration.ofSeconds(30)) - .build(); + private final AtomicReference snapshotRef = new AtomicReference<>(); public LeaderboardServiceImpl(AccountRepository repository) { this.repository = repository; @@ -25,82 +21,84 @@ public LeaderboardServiceImpl(AccountRepository repository) { @Override public CompletableFuture getLeaderboardPage(int page, int pageSize) { - int startIndex = page * pageSize; + int startIndex = (page - 1) * pageSize; - if (startIndex < TOP_CACHE_SIZE) { - return getPageFromCache(page, pageSize); + if (startIndex < SNAPSHOT_SIZE) { + return this.getPageFromSnapshot(page, pageSize, startIndex); } - return getPageFromDatabase(page, pageSize); + return this.getPageFromDatabaseKeyset(page, pageSize, startIndex); } @Override public CompletableFuture getLeaderboardPosition(Account target) { - return getOrRefreshTopCache().thenCompose(topAccounts -> { - int position = topAccounts.indexOf(target); + LeaderboardSnapshot snapshot = this.snapshotRef.get(); - if (position != -1) { - return CompletableFuture.completedFuture(new LeaderboardEntry(target, position + 1)); + if (snapshot != null) { + Integer position = snapshot.getPosition(target.uuid()); + if (position != null) { + return CompletableFuture.completedFuture(new LeaderboardEntry(target, position)); } + } - return calculatePositionFromAll(target); - }); + return this.repository.getPosition(target).thenApply(position -> new LeaderboardEntry(target, position)); } public void invalidateCache() { - this.topCache.invalidate(CACHE_KEY); + this.snapshotRef.set(null); } - private CompletableFuture getPageFromCache(int page, int pageSize) { - return getOrRefreshTopCache().thenCombine(this.repository.countAccounts(), (topAccounts, totalEntries) -> { - int startIndex = page * pageSize; - int endIndex = Math.min(startIndex + pageSize, topAccounts.size()); - - List entries = new ArrayList<>(); - for (int i = startIndex; i < endIndex; i++) { - entries.add(new LeaderboardEntry(topAccounts.get(i), i + 1)); - } - - int maxPages = Math.max(1, (int) Math.ceil((double) totalEntries / pageSize)); - int nextPage = page + 1 < maxPages ? page + 1 : -1; - - return new LeaderboardPage(entries, page, maxPages, nextPage); - }); + public CompletableFuture refreshSnapshot() { + return this.repository.getTopAccounts(SNAPSHOT_SIZE, 0).thenCombine( + this.repository.countAccounts(), (accounts, totalCount) -> { + LeaderboardSnapshot newSnapshot = new LeaderboardSnapshot(accounts, totalCount); + this.snapshotRef.set(newSnapshot); + return null; + }); } - private CompletableFuture getPageFromDatabase(int page, int pageSize) { - return repository.getTopAccounts(pageSize, page * pageSize) - .thenCompose(accounts -> repository.countAccounts().thenApply(totalEntries -> { - List entries = new ArrayList<>(); - int startPosition = page * pageSize; + private CompletableFuture getOrRefreshSnapshot() { + LeaderboardSnapshot current = this.snapshotRef.get(); + if (current != null) { + return CompletableFuture.completedFuture(current); + } - for (int i = 0; i < accounts.size(); i++) { - entries.add(new LeaderboardEntry(accounts.get(i), startPosition + i + 1)); - } + return this.repository.getTopAccounts(SNAPSHOT_SIZE, 0).thenCombine( + this.repository.countAccounts(), (accounts, totalCount) -> { + LeaderboardSnapshot newSnapshot = new LeaderboardSnapshot(accounts, totalCount); + this.snapshotRef.set(newSnapshot); + return newSnapshot; + }); + } - int maxPages = Math.max(1, (int) Math.ceil((double) totalEntries / pageSize)); - int nextPage = page + 1 < maxPages ? page + 1 : -1; + private CompletableFuture getPageFromSnapshot(int page, int pageSize, int startIndex) { + return this.getOrRefreshSnapshot().thenApply(snapshot -> { + int endIndex = Math.min(startIndex + pageSize, snapshot.size()); + List accounts = snapshot.accounts().subList(startIndex, endIndex); - return new LeaderboardPage(entries, page, maxPages, nextPage); - })); + return this.createPage(accounts, page, pageSize, startIndex, snapshot.totalCount()); + }); } - private CompletableFuture> getOrRefreshTopCache() { - List cached = topCache.getIfPresent(CACHE_KEY); + private CompletableFuture getPageFromDatabaseKeyset(int page, int pageSize, int startIndex) { + return this.getOrRefreshSnapshot().thenCompose(snapshot -> { + int offset = startIndex - SNAPSHOT_SIZE; + return this.repository.getTopAccounts(pageSize, SNAPSHOT_SIZE + offset) + .thenApply(accounts -> this.createPage(accounts, page, pageSize, startIndex, snapshot.totalCount())); + }); + } - if (cached != null) { - return CompletableFuture.completedFuture(cached); + private LeaderboardPage createPage( + List accounts, int page, int pageSize, int startIndex, + long totalCount) { + List entries = new ArrayList<>(accounts.size()); + for (int i = 0; i < accounts.size(); i++) { + entries.add(new LeaderboardEntry(accounts.get(i), startIndex + i + 1)); } - return repository.getTopAccounts(TOP_CACHE_SIZE, 0) - .thenApply(topAccounts -> { - topCache.put(CACHE_KEY, topAccounts); - return topAccounts; - }); - } + int maxPages = Math.max(1, (int) Math.ceil((double) totalCount / pageSize)); + int nextPage = page < maxPages ? page + 1 : -1; - private CompletableFuture calculatePositionFromAll(Account target) { - return repository.getPosition(target) - .thenApply(position -> new LeaderboardEntry(target, position)); + return new LeaderboardPage(entries, page, maxPages, nextPage); } } diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardSnapshot.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardSnapshot.java new file mode 100644 index 00000000..3f06db7c --- /dev/null +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/LeaderboardSnapshot.java @@ -0,0 +1,30 @@ +package com.eternalcode.economy.leaderboard; + +import com.eternalcode.economy.account.Account; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +record LeaderboardSnapshot(List accounts, long totalCount, Map positionIndex) { + + LeaderboardSnapshot(List accounts, long totalCount) { + this(List.copyOf(accounts), totalCount, buildPositionIndex(accounts)); + } + + private static Map buildPositionIndex(List accounts) { + Map index = new ConcurrentHashMap<>(accounts.size()); + for (int i = 0; i < accounts.size(); i++) { + index.put(accounts.get(i).uuid(), i + 1); + } + return index; + } + + Integer getPosition(UUID uuid) { + return this.positionIndex.get(uuid); + } + + int size() { + return this.accounts.size(); + } +} diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/menu/LeaderboardConfig.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/menu/LeaderboardConfig.java new file mode 100644 index 00000000..033b9168 --- /dev/null +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/menu/LeaderboardConfig.java @@ -0,0 +1,150 @@ +package com.eternalcode.economy.leaderboard.menu; + +import eu.okaeri.configs.OkaeriConfig; +import eu.okaeri.configs.annotation.Comment; +import java.util.List; +import org.bukkit.Material; + +public class LeaderboardConfig extends OkaeriConfig { + + @Comment("GUI title (supports MiniMessage)") + public String title = + "ʙᴀʟᴛᴏᴘ - Page {CURRENT_PAGE}/{TOTAL_PAGES}"; + + @Comment("Number of GUI rows (1-6)") + public int rows = 6; + + @Comment("Player items configuration") + public PlayerItems playerItems = new PlayerItems(); + + @Comment("Pagination items") + public PaginationItems paginationItems = new PaginationItems(); + + @Comment("Fill settings") + public FillSettings fillSettings = new FillSettings(); + + public static class PlayerItems extends OkaeriConfig { + + @Comment({ + "Player item template", + "Placeholders: {POSITION}, {PLAYER}, {BALANCE}, {FORMATTED_BALANCE}" + }) + public LeaderboardItemRepresenter template = LeaderboardItemRepresenter.of( + Material.PLAYER_HEAD, + "#{POSITION} - {PLAYER}", + List.of( + "", + " Balance: {FORMATTED_BALANCE}", + "" + ), + false, + "{PLAYER_HEAD}", + 0 + ); + + @Comment("Slots for player items (0-53)") + public List slots = List.of( + 10, 11, 12, 13, 14, 15, 16, + 19, 20, 21, 22, 23, 24, 25, + 28, 29, 30, 31, 32, 33, 34, + 37, 38, 39, 40, 41, 42, 43); + } + + public static class LeaderboardItem extends OkaeriConfig { + public Material material; + public boolean glow; + public String texture; + + public LeaderboardItem() { + } + + public LeaderboardItem(Material material, boolean glow, String texture) { + this.material = material; + this.glow = glow; + this.texture = texture; + } + } + + public static class PaginationItems extends OkaeriConfig { + + @Comment("Previous page button") + public LeaderboardItemRepresenter previousPage = LeaderboardItemRepresenter.of( + Material.ARROW, + "← Previous page", + List.of( + "", + " Current page: {CURRENT_PAGE}", + "", + " Click to go back"), + false, + "none", + 48); + + @Comment("Previous page button (disabled)") + public LeaderboardItemRepresenter previousPageDisabled = LeaderboardItemRepresenter.of( + Material.GRAY_DYE, + "← Previous page", + List.of( + "", + " You are on the first page"), + false, + "none", + 48); + + @Comment("Next page button") + public LeaderboardItemRepresenter nextPage = LeaderboardItemRepresenter.of( + Material.ARROW, + "Next page →", + List.of( + "", + " Current page: {CURRENT_PAGE}", + "", + " Click to go forward"), + false, + "none", + 50); + + @Comment("Next page button (disabled)") + public LeaderboardItemRepresenter nextPageDisabled = LeaderboardItemRepresenter.of( + Material.GRAY_DYE, + "Next page →", + List.of( + "", + " No more pages"), + false, + "none", + 50); + + @Comment("Page info item") + public LeaderboardItemRepresenter pageInfo = LeaderboardItemRepresenter.of( + Material.PAPER, + "Page {CURRENT_PAGE}/{TOTAL_PAGES}", + List.of( + "", + " Total players: {TOTAL_PLAYERS}"), + false, + "none", + 49); + + @Comment("Close button") + public LeaderboardItemRepresenter closeButton = LeaderboardItemRepresenter.of( + Material.BARRIER, + "Close", + List.of( + "", + " Click to close"), + false, + "none", + 45); + } + + public static class FillSettings extends OkaeriConfig { + @Comment("Enable fill items") + public boolean enableFillItems = true; + + @Comment("Materials for fill items") + public List fillItems = List.of( + Material.BLACK_STAINED_GLASS_PANE, + Material.GRAY_STAINED_GLASS_PANE); + } +} diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/menu/LeaderboardItemRepresenter.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/menu/LeaderboardItemRepresenter.java new file mode 100644 index 00000000..271b0f41 --- /dev/null +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/menu/LeaderboardItemRepresenter.java @@ -0,0 +1,119 @@ +package com.eternalcode.economy.leaderboard.menu; + +import com.eternalcode.economy.MiniMessageHolder; +import com.eternalcode.multification.shared.Formatter; +import dev.rollczi.liteskullapi.SkullAPI; +import dev.triumphteam.gui.builder.item.ItemBuilder; +import dev.triumphteam.gui.components.GuiAction; +import dev.triumphteam.gui.guis.GuiItem; +import eu.okaeri.configs.annotation.Exclude; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Material; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; + +public class LeaderboardItemRepresenter implements Serializable, MiniMessageHolder { + + @Exclude + private static final Component RESET_ITEM = Component.text() + .decoration(TextDecoration.ITALIC, false) + .build(); + + public Material material; + public String name; + public List lore; + public boolean glow; + public String texture; + public int slot; + + private LeaderboardItemRepresenter( + Material material, + String name, + List lore, + boolean glow, + String texture, + int slot + ) { + this.material = material; + this.name = name; + this.lore = lore; + this.glow = glow; + this.texture = texture; + this.slot = slot; + } + + public LeaderboardItemRepresenter() { + } + + public static LeaderboardItemRepresenter of( + Material material, + String name, + List lore, + boolean glow, + String texture, + int slot + ) { + return new LeaderboardItemRepresenter(material, name, lore, glow, texture, slot); + } + + public GuiItem asGuiItem(SkullAPI skullAPI, Formatter formatter, GuiAction action) { + return createGuiItem(skullAPI, formatter, action, null); + } + + public GuiItem asGuiItem(SkullAPI skullAPI, GuiAction action) { + return this.asGuiItem(skullAPI, new Formatter(), action); + } + + public GuiItem asGuiItemPreloaded( + Formatter formatter, + GuiAction action, + ItemStack preloadedSkull + ) { + return createGuiItem(null, formatter, action, preloadedSkull); + } + + private GuiItem createGuiItem( + SkullAPI skullAPI, + Formatter formatter, + GuiAction action, + ItemStack preloadedSkull + ) { + int size = this.lore.size(); + List loreComponents = new ArrayList<>(size); + + for (int i = 0; i < size; i++) { + String formatted = formatter.format(this.lore.get(i)); + loreComponents.add(RESET_ITEM.append(MINI_MESSAGE.deserialize(formatted))); + } + + Component nameComponent = RESET_ITEM.append(MINI_MESSAGE.deserialize(formatter.format(this.name))); + + ItemBuilder builder; + + if (preloadedSkull != null) { + builder = ItemBuilder.from(preloadedSkull); + } + else if (this.texture != null && !this.texture.equals("none") && skullAPI != null) { + builder = ItemBuilder.from(Material.PLAYER_HEAD); + } + else { + builder = ItemBuilder.from(this.material); + } + + builder.name(nameComponent) + .lore(loreComponents) + .glow(this.glow); + + GuiItem guiItem = builder.asGuiItem(action); + + if (preloadedSkull == null && this.texture != null && !this.texture.equals("none") && skullAPI != null) { + skullAPI.getSkull(this.texture).thenAccept(guiItem::setItemStack); + } + + return guiItem; + } +} diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/menu/LeaderboardMenu.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/menu/LeaderboardMenu.java new file mode 100644 index 00000000..cb0f8b5c --- /dev/null +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/leaderboard/menu/LeaderboardMenu.java @@ -0,0 +1,193 @@ +package com.eternalcode.economy.leaderboard.menu; + +import com.eternalcode.commons.concurrent.FutureHandler; +import com.eternalcode.commons.scheduler.Scheduler; +import com.eternalcode.economy.MiniMessageHolder; +import com.eternalcode.economy.format.DecimalFormatter; +import com.eternalcode.economy.leaderboard.LeaderboardEntry; +import com.eternalcode.economy.leaderboard.LeaderboardPage; +import com.eternalcode.economy.leaderboard.LeaderboardService; +import com.eternalcode.multification.shared.Formatter; +import dev.rollczi.liteskullapi.SkullAPI; +import dev.triumphteam.gui.builder.item.ItemBuilder; +import dev.triumphteam.gui.guis.Gui; +import dev.triumphteam.gui.guis.GuiItem; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class LeaderboardMenu implements MiniMessageHolder { + + private static final Component RESET = Component.text().decoration(TextDecoration.ITALIC, false).build(); + + private final LeaderboardConfig config; + private final Scheduler scheduler; + private final LeaderboardService leaderboardService; + private final DecimalFormatter decimalFormatter; + private final SkullAPI skullAPI; + + public LeaderboardMenu( + LeaderboardConfig config, + Scheduler scheduler, + LeaderboardService leaderboardService, + DecimalFormatter decimalFormatter, + SkullAPI skullAPI + ) { + this.config = config; + this.scheduler = scheduler; + this.leaderboardService = leaderboardService; + this.decimalFormatter = decimalFormatter; + this.skullAPI = skullAPI; + } + + public void open(Player player, int page) { + int pageSize = config.playerItems.slots.size(); + + this.scheduler.runAsync(() -> + this.leaderboardService.getLeaderboardPage(page, pageSize) + .thenCompose(result -> preloadSkulls(result) + .thenAccept(skullCache -> scheduler.run(() -> { + Gui gui = createGui(player, result, skullCache); + gui.open(player); + })) + ) + .exceptionally(FutureHandler::handleException) + ); + } + + private CompletableFuture> preloadSkulls(LeaderboardPage page) { + List entries = page.entries(); + List> futures = new ArrayList<>(entries.size()); + + for (LeaderboardEntry entry : entries) { + LeaderboardItemRepresenter item = this.config.playerItems.template; + + String textureToUse = item.texture; + + if (textureToUse != null && textureToUse.equals("{PLAYER_HEAD}")) { + futures.add(this.skullAPI.getSkull(entry.account().uuid())); + } + else if (textureToUse != null && !textureToUse.equals("none")) { + futures.add(this.skullAPI.getSkull(textureToUse)); + } + else { + futures.add(CompletableFuture.completedFuture(null)); + } + } + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> { + List skulls = new ArrayList<>(futures.size()); + for (CompletableFuture future : futures) { + skulls.add(future.join()); + } + return skulls; + }); + } + + private Gui createGui(Player player, LeaderboardPage page, List skullCache) { + int current = page.currentPage(); + int max = page.maxPages(); + int pageSize = this.config.playerItems.slots.size(); + int players = page.maxPages() <= 1 + ? page.entries().size() + : (page.maxPages() - 1) * pageSize + page.entries().size(); + + Formatter formatter = new Formatter() + .register("{CURRENT_PAGE}", current) + .register("{TOTAL_PAGES}", max) + .register("{TOTAL_PLAYERS}", players); + + Component title = RESET.append( + MINI_MESSAGE.deserialize(formatter.format(this.config.title)) + ); + + Gui gui = Gui.gui() + .title(title) + .rows(this.config.rows) + .disableAllInteractions() + .create(); + + if (this.config.fillSettings.enableFillItems) { + gui.getFiller().fill( + this.config.fillSettings.fillItems.stream() + .map(ItemBuilder::from) + .map(ItemBuilder::asGuiItem) + .toList() + ); + } + + renderEntries(gui, page, skullCache); + renderPagination(gui, player, current, max, players); + + return gui; + } + + private void renderEntries(Gui gui, LeaderboardPage page, List skullCache) { + List slots = this.config.playerItems.slots; + List entries = page.entries(); + + for (int i = 0; i < Math.min(slots.size(), entries.size()); i++) { + LeaderboardEntry entry = entries.get(i); + int position = entry.position(); + int slot = slots.get(i); + ItemStack preloadedSkull = skullCache.get(i); + + String formattedBalance = this.decimalFormatter.format(entry.account().balance()); + LeaderboardItemRepresenter item = this.config.playerItems.template; + + Formatter formatter = new Formatter() + .register("{POSITION}", position) + .register("{PLAYER}", entry.account().name()) + .register("{BALANCE}", entry.account().balance()::toPlainString) + .register("{FORMATTED_BALANCE}", formattedBalance); + + GuiItem guiItem = item.asGuiItemPreloaded(formatter, event -> {}, preloadedSkull); + gui.setItem(slot, guiItem); + } + } + + private void renderPagination(Gui gui, Player player, int current, int max, int players) { + Formatter formatter = new Formatter() + .register("{CURRENT_PAGE}", current) + .register("{TOTAL_PAGES}", max) + .register("{TOTAL_PLAYERS}", players); + + LeaderboardItemRepresenter item1 = + current > 1 ? this.config.paginationItems.previousPage : this.config.paginationItems.previousPageDisabled; + GuiItem guiItem1 = item1.asGuiItem( + this.skullAPI, formatter, e2 -> { + if (!(current > 1)) { + return; + } + + ((Runnable) () -> open(player, current - 1)).run(); + }); + + gui.setItem(item1.slot, guiItem1); + + LeaderboardItemRepresenter item = + current < max ? this.config.paginationItems.nextPage : this.config.paginationItems.nextPageDisabled; + GuiItem guiItem = item.asGuiItem( + this.skullAPI, formatter, e1 -> { + if (!(current < max)) { + return; + } + + ((Runnable) () -> open(player, current + 1)).run(); + }); + + gui.setItem(item.slot, guiItem); + + gui.setItem( + this.config.paginationItems.pageInfo.slot, + this.config.paginationItems.pageInfo.asGuiItem(this.skullAPI, formatter, e -> {})); + + LeaderboardItemRepresenter close = this.config.paginationItems.closeButton; + gui.setItem(close.slot, close.asGuiItem(this.skullAPI, e -> gui.close(player))); + } +} diff --git a/eternaleconomy-core/src/main/java/com/eternalcode/economy/withdraw/WithdrawItemServiceImpl.java b/eternaleconomy-core/src/main/java/com/eternalcode/economy/withdraw/WithdrawItemServiceImpl.java index b8f28607..d3ff0839 100644 --- a/eternaleconomy-core/src/main/java/com/eternalcode/economy/withdraw/WithdrawItemServiceImpl.java +++ b/eternaleconomy-core/src/main/java/com/eternalcode/economy/withdraw/WithdrawItemServiceImpl.java @@ -173,7 +173,8 @@ public BigDecimal getValue(ItemStack itemStack) { try { return new BigDecimal(value); - } catch (NumberFormatException ignored) { + } + catch (NumberFormatException ignored) { return BigDecimal.ZERO; } }