Skip to content
193 changes: 160 additions & 33 deletions src/main/java/fr/openmc/core/features/economy/EconomyManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@
import fr.openmc.core.bootstrap.features.annotations.Credit;
import fr.openmc.core.bootstrap.features.types.DatabaseFeature;
import fr.openmc.core.bootstrap.features.types.HasCommands;
import fr.openmc.core.bootstrap.integration.OMCLogger;
import fr.openmc.core.OMCPlugin;
import fr.openmc.core.features.economy.commands.Baltop;
import fr.openmc.core.features.economy.commands.History;
import fr.openmc.core.features.economy.commands.Money;
import fr.openmc.core.features.economy.commands.Pay;
import fr.openmc.core.features.economy.models.EconomyPlayer;
import fr.openmc.core.hooks.itemsadder.ItemsAdderHook;
import lombok.Getter;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitTask;

import javax.annotation.Nullable;
import java.math.BigDecimal;
Expand All @@ -26,10 +29,14 @@

@Credit(developers = {"Axeno", "Piquel Chips", "PuppyTransGirl", "Gyro"})
public class EconomyManager extends Feature implements DatabaseFeature, HasCommands {
@Getter
private static Map<UUID, EconomyPlayer> balances;

private static Dao<EconomyPlayer, String> playersDao;
private static final Set<UUID> dirtyBalances = new HashSet<>();
private static final Object balancesLock = new Object();
private static final Object saveLock = new Object();
private static final long AUTO_SAVE_INTERVAL_TICKS = 20L * 60L * 5L;
private static BukkitTask autoSaveTask;

private static final DecimalFormat decimalFormat = new DecimalFormat("#.##");
private static final NavigableMap<Long, String> suffixes = new TreeMap<>(Map.of(
Expand All @@ -43,6 +50,8 @@ public class EconomyManager extends Feature implements DatabaseFeature, HasComma
@Override
public void init() {
balances = loadAllBalances();
dirtyBalances.clear();
startAutoSaveTask();
}

@Override
Expand All @@ -61,18 +70,39 @@ public void initDB(ConnectionSource connectionSource) throws SQLException {
playersDao = DaoManager.createDao(connectionSource, EconomyPlayer.class);
}

@Override
protected void save() {
stopAutoSaveTask();
saveAllBalances(true);
}

public static double getBalance(UUID playerUUID) {
EconomyPlayer bank = getPlayerBank(playerUUID);
return bank.getBalance();
synchronized (balancesLock) {
EconomyPlayer bank = balances.get(playerUUID);
return bank == null ? 0 : bank.getBalance();
}
}

public static Map<UUID, EconomyPlayer> getBalances() {
synchronized (balancesLock) {
Map<UUID, EconomyPlayer> snapshot = new HashMap<>();

balances.forEach((playerUUID, player) -> snapshot.put(playerUUID, copyPlayer(player)));

return Collections.unmodifiableMap(snapshot);
}
}

public static void addBalance(UUID playerUUID, double amount) {
addBalance(playerUUID, amount, null);
}

public static void addBalance(UUID playerUUID, double amount, @Nullable String reason) {
EconomyPlayer bank = getPlayerBank(playerUUID);
bank.deposit(amount);
synchronized (balancesLock) {
EconomyPlayer bank = getOrCreatePlayerBank(playerUUID);
bank.deposit(amount);
markPlayerBankDirty(bank);
}

if (reason != null) {
TransactionsManager.registerTransaction(new Transaction(
Expand All @@ -83,32 +113,33 @@ public static void addBalance(UUID playerUUID, double amount, @Nullable String r
));
}

savePlayerBank(bank);
}

public static boolean withdrawBalance(UUID playerUUID, double amount) {
return withdrawBalance(playerUUID, amount, null);
}

public static boolean withdrawBalance(UUID playerUUID, double amount, @Nullable String reason) {
EconomyPlayer bank = getPlayerBank(playerUUID);
synchronized (balancesLock) {
EconomyPlayer bank = getOrCreatePlayerBank(playerUUID);

if (bank.withdraw(amount)) {
if (reason != null) {
TransactionsManager.registerTransaction(new Transaction(
"CONSOLE",
playerUUID.toString(),
amount,
reason
));
if (!bank.withdraw(amount)) {
return false;
}

savePlayerBank(bank);
markPlayerBankDirty(bank);
}

return true;
if (reason != null) {
TransactionsManager.registerTransaction(new Transaction(
"CONSOLE",
playerUUID.toString(),
amount,
reason
));
}

return false;
return true;
}

/**
Expand Down Expand Up @@ -152,10 +183,11 @@ public static boolean transferBalance(UUID fromPlayer, UUID toPlayer, double amo
}

public static void setBalance(UUID playerUUID, double amount) {
EconomyPlayer bank = getPlayerBank(playerUUID);
bank.withdraw(bank.getBalance());
bank.deposit(amount);
savePlayerBank(bank);
synchronized (balancesLock) {
EconomyPlayer bank = getOrCreatePlayerBank(playerUUID);
bank.setBalance(amount);
markPlayerBankDirty(bank);
}
}

public static String getMiniBalance(UUID playerUUID) {
Expand All @@ -164,20 +196,93 @@ public static String getMiniBalance(UUID playerUUID) {
return getFormattedSimplifiedNumber(balance);
}

public static void savePlayerBank(EconomyPlayer player) {
try {
balances.put(player.getPlayerUUID(), player);
playersDao.createOrUpdate(player);
} catch (SQLException e) {
throw new RuntimeException(e);
public static void markPlayerBankDirty(EconomyPlayer player) {
synchronized (balancesLock) {
balances.put(player.getPlayerUUID(), copyPlayer(player));
dirtyBalances.add(player.getPlayerUUID());
}
}

/**
* @deprecated Use {@link #markPlayerBankDirty(EconomyPlayer)}. This method
* only updates the in-memory cache and marks the balance dirty; persistence is
* deferred to {@link #saveAllBalances()} or the shutdown save.
*/
@Deprecated(since = "2.5.0", forRemoval = false)
public static void savePlayerBank(EconomyPlayer player) {
markPlayerBankDirty(player);
}

/**
* Returns a snapshot of a player's economy data.
* <p>
* Mutating the returned {@link EconomyPlayer} does not update the cache or mark
* the balance dirty. Use {@link #setBalance(UUID, double)},
* {@link #addBalance(UUID, double)} or {@link #withdrawBalance(UUID, double)}
* to change a player's balance.
*/
public static EconomyPlayer getPlayerBank(UUID playerUUID) {
EconomyPlayer bank = balances.get(playerUUID);
if (bank != null)
return bank;
return new EconomyPlayer(playerUUID);
synchronized (balancesLock) {
EconomyPlayer bank = balances.get(playerUUID);
return bank == null ? new EconomyPlayer(playerUUID) : copyPlayer(bank);
}
}

private static EconomyPlayer getOrCreatePlayerBank(UUID playerUUID) {
return balances.computeIfAbsent(playerUUID, EconomyPlayer::new);
}

public static void saveAllBalances() {
saveAllBalances(false);
}

private static void saveAllBalances(boolean finalSave) {
synchronized (saveLock) {
do {
List<EconomyPlayer> playersToSave;

synchronized (balancesLock) {
if (dirtyBalances.isEmpty()) {
return;
}

playersToSave = dirtyBalances.stream()
.map(balances::get)
.filter(Objects::nonNull)
.map(EconomyManager::copyPlayer)
.toList();
dirtyBalances.clear();
}

try {
playersDao.callBatchTasks(() -> {
for (EconomyPlayer player : playersToSave) {
playersDao.createOrUpdate(player);
}

return null;
});
} catch (Exception e) {
synchronized (balancesLock) {
for (EconomyPlayer player : playersToSave) {
dirtyBalances.add(player.getPlayerUUID());
}
}

if (finalSave) {
OMCLogger.error("CRITICAL: Failed to save economy balances during shutdown. Unsaved balances may be lost if the server stops.", e);
} else {
OMCLogger.error("Failed to save economy balances. Dirty balances will be retried on the next save.", e);
}

return;
}
} while (finalSave);
}
}

private static EconomyPlayer copyPlayer(EconomyPlayer player) {
return new EconomyPlayer(player.getPlayerUUID(), player.getBalance());
}

public static Map<UUID, EconomyPlayer> loadAllBalances() {
Expand All @@ -194,6 +299,28 @@ public static Map<UUID, EconomyPlayer> loadAllBalances() {
return balances;
}

private static void startAutoSaveTask() {
if (OMCPlugin.isUnitTestVersion() || autoSaveTask != null) {
return;
}

autoSaveTask = Bukkit.getScheduler().runTaskTimerAsynchronously(
OMCPlugin.getInstance(),
() -> EconomyManager.saveAllBalances(),
AUTO_SAVE_INTERVAL_TICKS,
AUTO_SAVE_INTERVAL_TICKS
);
}

private static void stopAutoSaveTask() {
if (autoSaveTask == null) {
return;
}

autoSaveTask.cancel();
autoSaveTask = null;
}

public static String getFormattedBalance(UUID playerUUID) {
String balance = String.valueOf(getBalance(playerUUID));
Currency currency = Currency.getInstance(Locale.FRANCE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public EconomyPlayer(UUID playerUUID) {
this.balance = 0;
}

public EconomyPlayer(UUID playerUUID, double balance) {
this.playerUUID = playerUUID;
this.balance = balance;
}

public void deposit(double amount) {
balance += amount;
}
Expand All @@ -35,4 +40,8 @@ public boolean withdraw(double amount) {
}
return false;
}

public void setBalance(double balance) {
this.balance = balance;
}
}
Loading
Loading