Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions eternaleconomy-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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<CommandSender> liteCommands;

@Override
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -132,13 +141,13 @@ public void onEnable() {
Economy.class, vaultEconomyProvider, this,
ServicePriority.Highest);

this.liteCommands = LiteBukkitFactory.builder("eternaleconomy", this, server)
LiteCommandsBuilder<CommandSender, LiteBukkitSettings, ?> 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,
Expand Down Expand Up @@ -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);

Expand All @@ -206,8 +222,8 @@ public void onEnable() {
accountManager,
decimalFormatter,
server,
this,
this.getLogger());
this.getLogger(),
scheduler);
bridgeManager.init();

Duration elapsed = started.elapsed();
Expand All @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ public Account withBalance(UnaryOperator<BigDecimal> 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);
}
Expand All @@ -26,5 +28,4 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(uuid, name);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,7 +18,7 @@ public class AccountManager {

private final Map<UUID, Account> accountByUniqueId = new ConcurrentHashMap<>();
private final NavigableMap<String, Account> accountByName = new ConcurrentSkipListMap<>(
String.CASE_INSENSITIVE_ORDER);
String.CASE_INSENSITIVE_ORDER);

private final AccountRepository accountRepository;
private final LeaderboardServiceImpl leaderboardService;
Expand All @@ -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;
}
Expand Down Expand Up @@ -97,7 +97,7 @@ public void save(Account account) {

public Collection<Account> 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<Account> getAccounts() {
Expand All @@ -107,5 +107,4 @@ public Collection<Account> getAccounts() {
public LeaderboardService getLeaderboardService() {
return this.leaderboardService;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Void> 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<Void> 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<Collection<Account>> 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<List<Account>> getTopAccounts(int limit, int offset) {
return this.<AccountWrapper, UUID, List<Account>>action(
AccountWrapper.class, dao -> {
QueryBuilder<AccountWrapper, UUID> 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.<AccountTable, UUID, List<Account>>action(
AccountTable.class, dao -> {
QueryBuilder<AccountTable, UUID> 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<Integer> getPosition(Account target) {
return this.<AccountWrapper, UUID, Integer>action(
AccountWrapper.class, dao -> {
QueryBuilder<AccountWrapper, UUID> qb = dao.queryBuilder();
Where<AccountWrapper, UUID> 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.<AccountTable, UUID, Integer>action(
AccountTable.class, dao -> {
QueryBuilder<AccountTable, UUID> qb = dao.queryBuilder();
Where<AccountTable, UUID> 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<Long> countAccounts() {
return this.<AccountWrapper, UUID, Long>action(AccountWrapper.class, dao -> dao.countOf());
return this.<AccountTable, UUID, Long>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;
});
}
}
Loading