diff --git a/README.md b/README.md index ffa50cd..4556ac0 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ dependencies { } ``` ## API usage - +YOU PROBABLY DON'T NEED TO USE REDISECONOMY API: USE VAULT API https://github.com/MilkBowl/VaultAPI ```java // Access Point RedisEconomyAPI api = RedisEconomyAPI.getAPI(); @@ -53,7 +53,7 @@ api.getCurrencyBySymbol("€");//Gets the currency by symbol //Currency is a Vault Economy https://github.com/MilkBowl/VaultAPI/blob/master/src/main/java/net/milkbowl/vault/economy/Economy.java, //same methods and everything currency.getBalance(offlinePlayer); -currency.withdrawPlayer(offlinePlayer, 100); +currency.withdrawPlayer(offlinePlayer, 100, "Reason of withdrawal"); //Modify a player balance (default currency) api.getDefaultCurrency().setPlayerBalance(player.getUniqueId(), 1000); diff --git a/jitpack.yml b/jitpack.yml index f0cebce..f29a667 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,5 +1,5 @@ jdk: - - openjdk16 + - openjdk17 before_install: - - sdk install java 16.0.1-open - - sdk use java 16.0.1-open \ No newline at end of file + - sdk install java 17.0.1-open + - sdk use java 17.0.1-open \ No newline at end of file diff --git a/pom.xml b/pom.xml index b5eb37d..6f073d0 100644 --- a/pom.xml +++ b/pom.xml @@ -6,13 +6,13 @@ dev.unnm3d RedisEconomy - 4.3.8-SNAPSHOT + 4.3.21 jar RedisEconomy - 16 + 17 UTF-8 @@ -23,8 +23,8 @@ maven-compiler-plugin 3.8.1 - 16 - 16 + 17 + 17 @@ -44,6 +44,10 @@ net.kyori dev.unnm3d.shaded.kyori + + de.exlll.configlib + dev.unnm3d.shaded.configlib + com.github.Anon8281.universalScheduler dev.unnm3d.shaded.universalScheduler @@ -114,8 +118,9 @@ https://repo.extendedclip.com/content/repositories/placeholderapi/ - devmart-other - https://nexuslite.gcnt.net/repos/other/ + maven_central + Maven Central + https://repo.maven.apache.org/maven2/ @@ -123,7 +128,7 @@ org.spigotmc spigot-api - 1.16.5-R0.1-SNAPSHOT + 1.17.1-R0.1-SNAPSHOT provided @@ -135,35 +140,35 @@ org.projectlombok lombok - 1.18.26 + 1.18.34 provided net.kyori adventure-text-minimessage - 4.14.0 + 4.17.0 net.kyori adventure-platform-bukkit - 4.3.0 + 4.3.3 me.clip placeholderapi - 2.11.3 + 2.11.5 provided io.lettuce lettuce-core - 6.2.4.RELEASE + 6.4.0.RELEASE provided - com.github.Emibergo02 - ConfigLib - master-SNAPSHOT + de.exlll + configlib-yaml + 4.5.0 com.github.Anon8281 diff --git a/src/main/java/dev/unnm3d/rediseconomy/RedisEconomyPlugin.java b/src/main/java/dev/unnm3d/rediseconomy/RedisEconomyPlugin.java index 740ce43..04a4a4b 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/RedisEconomyPlugin.java +++ b/src/main/java/dev/unnm3d/rediseconomy/RedisEconomyPlugin.java @@ -30,12 +30,13 @@ import org.bukkit.plugin.java.JavaPlugin; import java.time.Duration; +import java.util.UUID; import java.util.concurrent.TimeUnit; public final class RedisEconomyPlugin extends JavaPlugin { + @Getter private static RedisEconomyPlugin instance; - //private EzRedisMessenger ezRedisMessenger; @Getter private ConfigManager configManager; @Getter @@ -43,6 +44,10 @@ public final class RedisEconomyPlugin extends JavaPlugin { private RedisManager redisManager; @Getter private TaskScheduler scheduler; + @Getter + private Plugin vaultPlugin; + @Getter + private static UUID instanceUUID; public Settings settings() { @@ -53,13 +58,11 @@ public Langs langs() { return configManager.getLangs(); } - public static RedisEconomyPlugin getInstance() { - return instance; - } - @Override public void onLoad() { instance = this; + //Generate a unique instance id to not send redis updates to itself + instanceUUID = UUID.randomUUID(); this.configManager = new ConfigManager(this); @@ -67,27 +70,32 @@ public void onLoad() { this.getLogger().severe("Disabling: redis server unreachable!"); this.getLogger().severe("Please setup a redis server before running this plugin!"); this.getServer().getPluginManager().disablePlugin(this); - return; } else { this.getLogger().info("Redis server connected!"); } - - if (!setupVault()) { //creates currenciesManager and exchange - this.getLogger().severe("Disabled due to no Vault dependency found!"); - this.getServer().getPluginManager().disablePlugin(this); - } else { - this.getLogger().info("Hooked into Vault!"); - } } @Override public void onEnable() { + if (redisManager == null) return; + this.scheduler = UniversalScheduler.getScheduler(this); - this.configManager.postStartupLoad(); + this.configManager.loadLangs(); + this.vaultPlugin = getServer().getPluginManager().getPlugin("Vault"); + if (this.vaultPlugin == null) { //creates currenciesManager and exchange + this.getLogger().severe("Disabled due to no Vault dependency found!"); + this.getServer().getPluginManager().disablePlugin(this); + return; + } + + this.currenciesManager = new CurrenciesManager(redisManager, this, configManager); + this.getLogger().info("Hooked into Vault!"); + if (settings().migrationEnabled) { scheduler.runTaskLater(() -> - currenciesManager.getCompleteMigration().complete(null), - 100L);//load: STARTUP doesn't consider dependencies on load so i have to wait a bit (bukkit bug?) + currenciesManager.migrateFromOfflinePlayers(getServer().getOfflinePlayers()), 100L); + } else { + currenciesManager.loadDefaultCurrency(this.vaultPlugin); } getServer().getPluginManager().registerEvents(currenciesManager, this); @@ -124,11 +132,10 @@ public void onEnable() { @Override public void onDisable() { - redisManager.close(); + if (redisManager != null) + redisManager.close(); if (currenciesManager != null) this.getServer().getServicesManager().unregister(Economy.class, currenciesManager.getDefaultCurrency()); - this.getServer().getMessenger().unregisterOutgoingPluginChannel(this); - this.getServer().getMessenger().unregisterIncomingPluginChannel(this); getLogger().info("RedisEconomy disabled successfully!"); } @@ -143,16 +150,16 @@ private boolean setupRedis() { if (configManager.getSettings().redis.user().equals("changecredentials")) getLogger().warning("You are using default redis credentials. Please change them in the config.yml file!"); //Authentication params - redisURIBuilder = configManager.getSettings().redis.password().equals("") ? + redisURIBuilder = configManager.getSettings().redis.password().isEmpty() ? redisURIBuilder : - configManager.getSettings().redis.user().equals("") ? + configManager.getSettings().redis.user().isEmpty() ? redisURIBuilder.withPassword(configManager.getSettings().redis.password().toCharArray()) : redisURIBuilder.withAuthentication(configManager.getSettings().redis.user(), configManager.getSettings().redis.password()); getLogger().info("Connecting to redis server " + redisURIBuilder.build().toString() + "..."); - this.redisManager = new RedisManager(RedisClient.create(redisURIBuilder.build())); + this.redisManager = new RedisManager(RedisClient.create(redisURIBuilder.build()), configManager.getSettings().redis.getPoolSize()); redisManager.isConnected().get(1, java.util.concurrent.TimeUnit.SECONDS); - if (!configManager.getSettings().clusterId.equals("")) + if (!configManager.getSettings().clusterId.isEmpty()) RedisKeys.setClusterId(configManager.getSettings().clusterId); return true; } catch (Exception e) { @@ -164,14 +171,6 @@ private boolean setupRedis() { } } - private boolean setupVault() { - Plugin vault = getServer().getPluginManager().getPlugin("Vault"); - if (vault == null) - return false; - this.currenciesManager = new CurrenciesManager(redisManager, this, configManager); - currenciesManager.loadDefaultCurrency(vault); - return true; - } private void loadCommand(String cmdName, CommandExecutor executor, TabCompleter tabCompleter) { PluginCommand cmd = getServer().getPluginCommand(cmdName); diff --git a/src/main/java/dev/unnm3d/rediseconomy/command/MainCommand.java b/src/main/java/dev/unnm3d/rediseconomy/command/MainCommand.java index 2e143ba..e315d62 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/command/MainCommand.java +++ b/src/main/java/dev/unnm3d/rediseconomy/command/MainCommand.java @@ -27,17 +27,29 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command } else if (args.length == 1) { if (!args[0].equalsIgnoreCase("reload")) return true; if (!sender.hasPermission("rediseconomy.admin")) return true; - String serverId = plugin.getConfigManager().getSettings().serverId; //Keep serverId plugin.getConfigManager().loadSettingsConfig();//Reload configs - plugin.getConfigManager().loadLangs(); - plugin.getConfigManager().getSettings().serverId = serverId; //Restore serverId + plugin.getConfigManager().loadLangs(); //Reload langs plugin.getConfigManager().saveConfigs(); //Save configs this.adventureWebuiEditorAPI = new AdventureWebuiEditorAPI(plugin.getConfigManager().getSettings().webEditorUrl); - sender.sendMessage("§aReloaded successfully " + plugin.getConfigManager().getSettings().serverId + "!"); + sender.sendMessage("§aReloaded successfully!"); return true; } String langField = args[1]; + if (args[0].equalsIgnoreCase("expandpool")) { + if (!sender.hasPermission("rediseconomy.admin.expandpool")) { + plugin.getConfigManager().getLangs().send(sender, plugin.getConfigManager().getLangs().noPermission); + return true; + } + try { + plugin.getCurrenciesManager().getRedisManager().expandPool(Integer.parseInt(args[1])); + plugin.getConfigManager().getLangs().send(sender, "§aPool expanded successfully!"); + } catch (Exception e) { + plugin.getConfigManager().getLangs().send(sender, "§cError expanding pool: " + e.getMessage()); + } + return true; + } + if (!sender.hasPermission("rediseconomy.admin.editmessage")) { plugin.getConfigManager().getLangs().send(sender, plugin.getConfigManager().getLangs().noPermission); return true; @@ -83,9 +95,15 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command @Override public @NotNull List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) { if (args.length == 1) { - return List.of("reload", "editmessage"); - } else if (args.length == 2 && sender.hasPermission("rediseconomy.admin.editmessage")) { - return Arrays.stream(plugin.getConfigManager().getLangs().getClass().getFields()).filter(field -> field.getType().equals(String.class)).map(Field::getName).toList(); + return List.of("reload", "editmessage", "expandpool"); + } else if (args.length == 2 && args[0].equalsIgnoreCase("expandpool")) { + return List.of("1", "2", "3", "4", "5"); + } else if (args.length == 2 && sender.hasPermission("rediseconomy.admin.editmessage") && args[0].equalsIgnoreCase("editmessage")) { + return Arrays.stream(plugin.getConfigManager().getLangs().getClass().getFields()) + .filter(field -> field.getType().equals(String.class)) + .map(Field::getName) + .filter(name -> name.startsWith(args[1])) + .toList(); } return List.of(); } diff --git a/src/main/java/dev/unnm3d/rediseconomy/command/PayCommand.java b/src/main/java/dev/unnm3d/rediseconomy/command/PayCommand.java index 5bbbe48..f88a91a 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/command/PayCommand.java +++ b/src/main/java/dev/unnm3d/rediseconomy/command/PayCommand.java @@ -45,8 +45,10 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command if (args.length == 2) { payCurrency(p, currenciesManager.getDefaultCurrency(), args); } else { - if (!sender.hasPermission("rediseconomy.pay." + args[2])) + if (!sender.hasPermission("rediseconomy.pay." + args[2])) { plugin.langs().send(sender, plugin.langs().noPermission); + return true; + } Currency currency = currenciesManager.getCurrencyByName(args[2]); if (currency == null) { plugin.langs().send(sender, plugin.langs().invalidCurrency); @@ -66,7 +68,7 @@ private void payCurrency(Player sender, Currency currency, String[] args) { } long init = System.currentTimeMillis(); final String target = args[0]; - final double amount = plugin.langs().formatAmountString(args[1]); + final double amount = currenciesManager.formatAmountString(sender.getName(), currency, args[1]); if (amount <= 0) { plugin.langs().send(sender, plugin.langs().invalidAmount); return; @@ -78,6 +80,13 @@ private void payCurrency(Player sender, Currency currency, String[] args) { if (target.equalsIgnoreCase(sender.getName())) { plugin.langs().send(sender, plugin.langs().paySelf); return; + } else if (target.equals("*")) { + if (sender.hasPermission("rediseconomy.payall")) { + payCurrencyAll(sender, currency, amount); + } else { + plugin.langs().send(sender, plugin.langs().noPermission); + } + return; } if (!currency.hasAccount(target)) { plugin.langs().send(sender, plugin.langs().playerNotFound); @@ -121,7 +130,48 @@ private void payCurrency(Player sender, Currency currency, String[] args) { } return currenciesManager.getExchange().savePaymentTransaction(sender.getUniqueId(), targetUUID, amount, currency, reason); }); + } + /** + * Pay to all online players + * + * @param sender Player who sends the payment + * @param currency Currency to pay + * @param amount Amount to pay + */ + private void payCurrencyAll(Player sender, Currency currency, double amount) { + for (Player onlinePlayer : plugin.getServer().getOnlinePlayers()) { + if (onlinePlayer.getName().equals(sender.getName())) continue; + + if (currenciesManager.isAccountLocked(onlinePlayer.getUniqueId(), sender.getUniqueId())) { + plugin.langs().send(sender, plugin.langs().blockedPayment.replace("%player%", onlinePlayer.getName())); + continue; + } + + final EconomyResponse response = currency.payPlayer(sender.getName(), onlinePlayer.getName(), amount); + if (!response.transactionSuccess()) { + if (response.errorMessage.equals("Insufficient funds")) { + plugin.langs().send(sender, plugin.langs().insufficientFunds); + } else { + plugin.langs().send(sender, plugin.langs().payFail); + } + return; + } + //Send msg to sender + plugin.langs().send(sender, + plugin.langs().paySuccess + .replace("%amount%", currency.format(amount)) + .replace("%player%", onlinePlayer.getName()) + .replace("%tax_percentage%", (currency.getTransactionTax() * 100) + "%") + .replace("%tax_applied%", currency.format(currency.getTransactionTax() * amount)) + ); + //Send msg to target + currenciesManager.getRedisManager().getConnectionAsync(commands -> { + commands.publish(MSG_CHANNEL.toString(), sender.getName() + ";;" + onlinePlayer.getName() + ";;" + currency.format(amount)); + //Register transaction + return currenciesManager.getExchange().savePaymentTransaction(sender.getUniqueId(), onlinePlayer.getUniqueId(), amount, currency, "Payment to all online players"); + }); + } } @Override @@ -129,12 +179,14 @@ private void payCurrency(Player sender, Currency currency, String[] args) { if (args.length == 1) { if (args[0].length() < plugin.settings().tab_complete_chars) return List.of(); - return currenciesManager.getNameUniqueIds().keySet().stream().filter(name -> name.toUpperCase().startsWith(args[0].toUpperCase())).toList(); - } else if (args.length == 2) - return List.of("69"); - else if (args.length == 3) + return currenciesManager.getNameUniqueIds().keySet().stream() + .filter(name -> name.toUpperCase().startsWith(args[0].toUpperCase())) + .toList(); + } else if (args.length == 2) { + return List.of("1"); + } else if (args.length == 3) { return currenciesManager.getCurrencies().stream().map(Currency::getCurrencyName).filter(name -> name.startsWith(args[2]) && sender.hasPermission("rediseconomy.pay." + args[2])).toList(); - + } return List.of(); } diff --git a/src/main/java/dev/unnm3d/rediseconomy/command/PurgeUserCommand.java b/src/main/java/dev/unnm3d/rediseconomy/command/PurgeUserCommand.java index 71304f7..7f984c6 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/command/PurgeUserCommand.java +++ b/src/main/java/dev/unnm3d/rediseconomy/command/PurgeUserCommand.java @@ -44,13 +44,11 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command nameUUIDs = currenciesManager.removeNamePattern(target, !onlyNameUUID); successMsg = plugin.langs().purgeUserSuccess.replace("%player%", target); } - if (nameUUIDs.size() == 0) { + if (nameUUIDs.isEmpty()) { plugin.langs().send(sender, plugin.langs().playerNotFound); return true; } plugin.langs().send(sender, successMsg); - - return true; } diff --git a/src/main/java/dev/unnm3d/rediseconomy/command/balance/BalanceCommand.java b/src/main/java/dev/unnm3d/rediseconomy/command/balance/BalanceCommand.java index 796438b..dc0207f 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/command/balance/BalanceCommand.java +++ b/src/main/java/dev/unnm3d/rediseconomy/command/balance/BalanceCommand.java @@ -36,7 +36,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command selfBalancePlayer(sender, defaultCurrency); return true; } - String target = economy.getCaseSensitiveName(args[0]); + final String target = economy.getCaseSensitiveName(args[0]); if (args.length == 1) { balancePlayer(sender, defaultCurrency, target); } else if (args.length == 2) { @@ -62,7 +62,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command plugin.langs().send(sender, plugin.langs().invalidCurrency); return true; } - double amount = plugin.langs().formatAmountString(args[3]); + double amount = plugin.getCurrenciesManager().formatAmountString(target, currency, args[3]); if (amount < 0) { plugin.langs().send(sender, plugin.langs().invalidAmount); return true; @@ -84,6 +84,9 @@ else if (reasonOrCommand.startsWith("/")) { } else if (args[2].equalsIgnoreCase("set")) { setPlayer(sender, currency, amount, target); + + } else if (args[2].equalsIgnoreCase("set-max")) { + setPlayerMaxBalance(sender, currency, amount, target); } } if (plugin.settings().debug) @@ -105,6 +108,8 @@ else if (reasonOrCommand.startsWith("/")) { protected abstract void setPlayer(CommandSender sender, Currency currency, double amount, String target); + protected abstract void setPlayerMaxBalance(CommandSender sender, Currency currency, double amount, String target); + @Override public @NotNull List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (args.length == 1) { @@ -118,7 +123,7 @@ else if (reasonOrCommand.startsWith("/")) { } else if (args.length == 2) return economy.getCurrencies().stream().map(Currency::getCurrencyName).filter(name -> name.startsWith(args[1]) && sender.hasPermission("rediseconomy.balance." + args[1])).toList(); else if (args.length == 3) - return List.of("give", "take", "set"); + return List.of("give", "take", "set", "set-max"); else if (args.length == 4) return List.of("69"); return List.of(); diff --git a/src/main/java/dev/unnm3d/rediseconomy/command/balance/BalanceSubCommands.java b/src/main/java/dev/unnm3d/rediseconomy/command/balance/BalanceSubCommands.java index c09f54d..b7e43c0 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/command/balance/BalanceSubCommands.java +++ b/src/main/java/dev/unnm3d/rediseconomy/command/balance/BalanceSubCommands.java @@ -3,10 +3,13 @@ import dev.unnm3d.rediseconomy.RedisEconomyPlugin; import dev.unnm3d.rediseconomy.currency.CurrenciesManager; import dev.unnm3d.rediseconomy.currency.Currency; +import dev.unnm3d.rediseconomy.utils.DecimalUtils; import net.milkbowl.vault.economy.EconomyResponse; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import java.util.UUID; + public class BalanceSubCommands extends BalanceCommand { public BalanceSubCommands(CurrenciesManager economy, RedisEconomyPlugin plugin) { @@ -19,7 +22,12 @@ protected void balancePlayer(CommandSender sender, Currency currency, String tar plugin.langs().send(sender, plugin.langs().playerNotFound); return; } - plugin.langs().send(sender, plugin.langs().balanceOther.replace("%balance%", String.valueOf(currency.format(currency.getBalance(target)))).replace("%player%", target)); + double currentBalance = currency.getBalance(target); + plugin.langs().send(sender, plugin.langs().balanceOther + .replace("%balance%", String.valueOf(currency.format(currentBalance))) + .replace("%balance_short%", DecimalUtils.shortAmount(currentBalance, currency.getDecimalFormat()) + + (currentBalance == 1 ? currency.getCurrencySingular() : currency.getCurrencyPlural())) + .replace("%player%", target)); } @Override @@ -28,7 +36,11 @@ protected void selfBalancePlayer(CommandSender sender, Currency currency) { plugin.langs().send(sender, plugin.langs().noConsole); return; } - plugin.langs().send(sender, plugin.langs().balance.replace("%balance%", String.valueOf(currency.format(currency.getBalance(p))))); + + plugin.langs().send(sender, plugin.langs().balance.replace("%balance_short%", + DecimalUtils.shortAmount(currency.getBalance(p), currency.getDecimalFormat()) + + (currency.getBalance(p) == 1 ? currency.getCurrencySingular() : currency.getCurrencyPlural())) + .replace("%balance%", String.valueOf(currency.format(currency.getBalance(p))))); } @Override @@ -72,4 +84,16 @@ protected void setPlayer(CommandSender sender, Currency currency, double amount, plugin.langs().send(sender, plugin.langs().balanceSet.replace("%balance%", currency.format(er.balance)).replace("%player%", target)); else sender.sendMessage(er.errorMessage); } + + @Override + protected void setPlayerMaxBalance(CommandSender sender, Currency currency, double amount, String target) { + final UUID targetUUID = plugin.getCurrenciesManager().getUUIDFromUsernameCache(target); + if (targetUUID == null) { + plugin.langs().send(sender, plugin.langs().playerNotFound); + return; + } + currency.setPlayerMaxBalance(targetUUID, amount); + plugin.langs().send(sender, plugin.langs().maxBalanceSet.replace("%player%", target).replace("%amount%", currency.format(amount))); + } + } diff --git a/src/main/java/dev/unnm3d/rediseconomy/command/balance/BalanceTopCommand.java b/src/main/java/dev/unnm3d/rediseconomy/command/balance/BalanceTopCommand.java index 11330fb..f35b6d2 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/command/balance/BalanceTopCommand.java +++ b/src/main/java/dev/unnm3d/rediseconomy/command/balance/BalanceTopCommand.java @@ -3,6 +3,7 @@ import dev.unnm3d.rediseconomy.RedisEconomyPlugin; import dev.unnm3d.rediseconomy.currency.CurrenciesManager; import dev.unnm3d.rediseconomy.currency.Currency; +import dev.unnm3d.rediseconomy.utils.DecimalUtils; import io.lettuce.core.ScoredValue; import lombok.AllArgsConstructor; import org.bukkit.command.Command; @@ -65,6 +66,9 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command plugin.langs().send(sender, plugin.langs().balanceTopFormat .replace("%pos%", String.valueOf((pageData.pageNumber - 1) * 10 + i)) .replace("%player%", username == null ? tuple.getValue() + "-Unknown" : username) + .replace("%balance_short%", + DecimalUtils.shortAmount(tuple.getScore(), baltopCurrency.getDecimalFormat()) + + (tuple.getScore() == 1 ? baltopCurrency.getCurrencySingular() : baltopCurrency.getCurrencyPlural())) .replace("%balance%", baltopCurrency.format(tuple.getScore()))); i++; } diff --git a/src/main/java/dev/unnm3d/rediseconomy/command/transaction/BrowseTransactionsCommand.java b/src/main/java/dev/unnm3d/rediseconomy/command/transaction/BrowseTransactionsCommand.java index f4ed64d..6dd22f5 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/command/transaction/BrowseTransactionsCommand.java +++ b/src/main/java/dev/unnm3d/rediseconomy/command/transaction/BrowseTransactionsCommand.java @@ -63,8 +63,6 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command sendTransaction(sender, i, transactions.get(i), afterDateString + " " + beforeDateString); - if (plugin.settings().debug) - sender.sendMessage("Time: " + (System.currentTimeMillis() - init)); } plugin.langs().send(sender, plugin.langs().transactionsEnd diff --git a/src/main/java/dev/unnm3d/rediseconomy/config/ConfigManager.java b/src/main/java/dev/unnm3d/rediseconomy/config/ConfigManager.java index c10ab23..ba966dc 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/config/ConfigManager.java +++ b/src/main/java/dev/unnm3d/rediseconomy/config/ConfigManager.java @@ -1,19 +1,12 @@ package dev.unnm3d.rediseconomy.config; -import com.google.common.io.ByteArrayDataInput; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; import de.exlll.configlib.YamlConfigurationProperties; import de.exlll.configlib.YamlConfigurations; import dev.unnm3d.rediseconomy.RedisEconomyPlugin; import lombok.Getter; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.HandlerList; -import org.bukkit.event.Listener; import java.io.File; -import java.util.concurrent.CompletableFuture; +import java.nio.charset.StandardCharsets; public class ConfigManager { private final RedisEconomyPlugin plugin; @@ -22,95 +15,51 @@ public class ConfigManager { @Getter private Langs langs; + private static final YamlConfigurationProperties PROPERTIES = YamlConfigurationProperties.newBuilder() + .header( + """ + ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ RedisEconomy Config ┃ + ┃ Developed by Unnm3d ┃ + ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + """ + ) + .footer("Authors: Unnm3d") + .charset(StandardCharsets.UTF_8) + .build(); + public ConfigManager(RedisEconomyPlugin plugin) { this.plugin = plugin; loadSettingsConfig(); } - public void postStartupLoad() { - loadLangs(); - getServerId().thenAccept(s -> { - settings.serverId = s; - saveConfigs(); - }); - } - public void loadSettingsConfig() { - YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder() - .header( - """ - ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ RedisEconomy Config ┃ - ┃ Developed by Unnm3d ┃ - ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - """ - ) - .footer("Authors: Unnm3d") - .build(); File settingsFile = new File(plugin.getDataFolder(), "config.yml"); settings = YamlConfigurations.update( settingsFile.toPath(), Settings.class, - properties + PROPERTIES ); + if (settings.redis.tryAgainCount() < 2 || settings.redis.poolSize() < 2) + plugin.getLogger().severe("Please regenerate the redis configuration section. New settings have been added."); } public void saveConfigs() { - YamlConfigurations.save(new File(plugin.getDataFolder(), "config.yml").toPath(), Settings.class, settings); - YamlConfigurations.save(new File(plugin.getDataFolder(), settings.lang + ".yml").toPath(), Langs.class, langs); + YamlConfigurations.save(new File(plugin.getDataFolder(), "config.yml").toPath(), Settings.class, settings, PROPERTIES); + YamlConfigurations.save(new File(plugin.getDataFolder(), settings.lang + ".yml").toPath(), Langs.class, langs, PROPERTIES); } public void loadLangs() { File settingsFile = new File(plugin.getDataFolder(), settings.lang + ".yml"); if (!settingsFile.exists()) { - plugin.saveResource("it-IT.yml", false);//save default lang + plugin.saveResource("it-IT.yml", false); + plugin.saveResource("de-DE.yml", false); } langs = YamlConfigurations.update( settingsFile.toPath(), - Langs.class + Langs.class, + PROPERTIES ); } - @SuppressWarnings("UnstableApiUsage") - public CompletableFuture getServerId() { - CompletableFuture future = new CompletableFuture<>(); - plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, "BungeeCord"); - plugin.getServer().getMessenger().registerIncomingPluginChannel(plugin, "BungeeCord", (channel, player, message) -> { - if (future.isDone()) return; - ByteArrayDataInput in = ByteStreams.newDataInput(message); - String subchannel = in.readUTF(); - if (subchannel.equals("GetServer")) { - future.complete(in.readUTF());//Receive server name - } - }); - Listener listener = new Listener() { - @EventHandler - public void onJoin(org.bukkit.event.player.PlayerJoinEvent event) { - if (future.isDone()) { - return; - } - plugin.getScheduler().runTaskLaterAsynchronously(() -> sendServerIdRequest(event.getPlayer()), 20L); - } - }; - if (plugin.getServer().getOnlinePlayers().size() > 0) { - sendServerIdRequest(plugin.getServer().getOnlinePlayers().iterator().next()); - } else { - plugin.getServer().getPluginManager().registerEvents(listener, plugin); - } - return future.thenApply(s -> { - //Remove listener and channel listeners - HandlerList.unregisterAll(listener); - plugin.getServer().getMessenger().unregisterIncomingPluginChannel(plugin, "BungeeCord"); - plugin.getServer().getMessenger().unregisterOutgoingPluginChannel(plugin, "BungeeCord"); - return s; - }); - } - - @SuppressWarnings("UnstableApiUsage") - private void sendServerIdRequest(Player p) { - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - out.writeUTF("GetServer"); - p.sendPluginMessage(plugin, "BungeeCord", out.toByteArray());//Request server name - } - } diff --git a/src/main/java/dev/unnm3d/rediseconomy/config/Langs.java b/src/main/java/dev/unnm3d/rediseconomy/config/Langs.java index c314284..14698f8 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/config/Langs.java +++ b/src/main/java/dev/unnm3d/rediseconomy/config/Langs.java @@ -9,6 +9,8 @@ import org.jetbrains.annotations.Nullable; import java.lang.reflect.Field; +import java.util.NavigableMap; +import java.util.TreeMap; @Configuration public final class Langs { @@ -22,15 +24,16 @@ public final class Langs { public String payCooldown = "Your previous payment is still in progress! Wait please"; public String invalidCurrency = "Invalid currency!"; public String insufficientFunds = "You do not have enough money!"; - public String balance = "You have %balance%!"; + public String balance = "You have %balance_short%!"; public String balanceSet = "You set %player% account to %balance% !"; - public String balanceOther = "%player% has %balance% !"; + public String maxBalanceSet = "You set %player% max balance to %amount% !"; + public String balanceOther = "%player% has %balance_short% !"; public String balanceTop = "Top richest players:
%prevpage% %page% %nextpage%"; public String blockedAccounts = "Blocked accounts:
%list%"; public String blockedAccountSuccess = "Account %player% has been blocked!"; public String unblockedAccountSuccess = "Account %player% has been unblocked!"; public String blockedPayment = "Your payments to %player% have been blocked!"; - public String balanceTopFormat = "%pos% - %player% %balance%"; + public String balanceTopFormat = "%pos% - %player% %balance_short%"; public String paySelf = "You cannot pay yourself!"; @Comment("Use %tax_percentage% for tax percentage and %tax_applied% for tax applied to the transaction.") public String paySuccess = "You paid %player% %amount%"; @@ -75,27 +78,19 @@ public record UnitSymbols( ) { } - public void send(CommandSender sender, String text) { - audiences.sender(sender).sendMessage(MiniMessage.miniMessage().deserialize(text)); + public NavigableMap getSuffixes() { + return new TreeMap<>() {{ + put(unitSymbols.thousand(), 1_000L); + put(unitSymbols.million(), 1_000_000L); + put(unitSymbols.billion(), 1_000_000_000L); + put(unitSymbols.trillion(), 1_000_000_000_000L); + put(unitSymbols.quadrillion(), 1_000_000_000_000_000L); + }}; + } - public double formatAmountString(String amount) { - try { - if (amount.endsWith(unitSymbols.quadrillion())) { - return Double.parseDouble(amount.substring(0, amount.length() - unitSymbols.quadrillion().length())) * 1_000_000_000_000_000D; - } else if (amount.endsWith(unitSymbols.trillion())) { - return Double.parseDouble(amount.substring(0, amount.length() - unitSymbols.trillion().length())) * 1_000_000_000_000D; - } else if (amount.endsWith(unitSymbols.billion())) { - return Double.parseDouble(amount.substring(0, amount.length() - unitSymbols.billion().length())) * 1_000_000_000D; - } else if (amount.endsWith(unitSymbols.million())) { - return Double.parseDouble(amount.substring(0, amount.length() - unitSymbols.million().length())) * 1_000_000D; - } else if (amount.endsWith(unitSymbols.thousand())) { - return Double.parseDouble(amount.substring(0, amount.length() - unitSymbols.thousand().length())) * 1_000D; - } - return Double.parseDouble(amount); - } catch (NumberFormatException e) { - return -1; - } + public void send(CommandSender sender, String text) { + audiences.sender(sender).sendMessage(MiniMessage.miniMessage().deserialize(text)); } public @Nullable Field getStringField(String name) throws NoSuchFieldException { diff --git a/src/main/java/dev/unnm3d/rediseconomy/config/Settings.java b/src/main/java/dev/unnm3d/rediseconomy/config/Settings.java index 2856051..402b74a 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/config/Settings.java +++ b/src/main/java/dev/unnm3d/rediseconomy/config/Settings.java @@ -4,30 +4,32 @@ import de.exlll.configlib.Configuration; import java.util.List; -import java.util.UUID; @SuppressWarnings("unused") @Configuration public class Settings { - @Comment({"This is automatically generated on server startup", - "Change it only if you have disabled plugin messages on the proxy"}) - public String serverId = String.valueOf(UUID.randomUUID()); @Comment("Language file") public String lang = "en-US"; @Comment("Webeditor URL") public String webEditorUrl = "https://webui.advntr.dev/"; @Comment("Activate this before reporting an issue") public boolean debug = false; + @Comment("A specific debug for cache update") + public boolean debugUpdateCache = false; @Comment("If true, the plugin registers who's calling it's methods inside transactions") public boolean registerCalls = false; + @Comment("List of regex to be excluded from the registerCalls") + public List callBlacklistRegex = List.of("^org\\.bukkit.*", "^dev\\.unnm3d\\.rediseconomy.*","^com\\.mojang.*"); @Comment({"if true, migrates the bukkit offline uuids accounts to the default RedisEconomy currency", "During the migration, the plugin will be disabled. Restart all RedisEconomy instances after the migration."}) public boolean migrationEnabled = false; + @Comment("Allow paying with percentage (ex: /pay player 10% sends 'player' 10% of the sender balance)") + public boolean allowPercentagePayments = true; @Comment({"Leave password or user empty if you don't have a password or user", "Don't use the default credentials in production!! Generate new credentials on RedisLabs -> https://github.com/Emibergo02/RedisEconomy/wiki/Install-redis", "Default credentials lead to a non-persistent redis server, only for testing!!", }) - public RedisSettings redis = new RedisSettings("localhost", 6379, "", "", 0, 2000, "RedisEconomy"); + public RedisSettings redis = new RedisSettings("localhost", 6379, "", "", 0, 300, "RedisEconomy", 5, 3); @Comment({"All RedisEconomy instances with the same cluster id will share the same data"}) public String clusterId = ""; @Comment({"How many chars are needed for a command autocompletion", "Increase if you have a lot of players to list"}) @@ -39,14 +41,23 @@ public class Settings { @Comment("Minimum amount of money that can be paid") public double minPayAmount = 0.01; @Comment({"Currencies", "payTax is the tax on payments, 0.1 = 10% tax"}) - public List currencies = List.of(new CurrencySettings("vault", "euro", "euros", "#.##", "en-US", 0, 0, true, false), new CurrencySettings("dollar", "$", "$", "#.##", "en-US", 0, 0, false, false)); + public List currencies = List.of(new CurrencySettings("vault", "euro", "euros", "#.##", "en-US", 0, 100000000000000d, 0, true, true, false), new CurrencySettings("dollar", "$", "$", "#.##", "en-US", 0, 100000000000000d, 0, false, false, false)); public record CurrencySettings(String currencyName, String currencySingle, String currencyPlural, String decimalFormat, String languageTag, - double startingBalance, double payTax, boolean bankEnabled, boolean taxOnlyPay) { + double startingBalance, double maxBalance, double payTax, + boolean saveTransactions, boolean bankEnabled, boolean taxOnlyPay) { } public record RedisSettings(String host, int port, String user, String password, int database, int timeout, - String clientName) { + String clientName, int poolSize, int tryAgainCount) { + //Those checks are for new config files, if the user doesn't have the new settings + public int getPoolSize() { + return poolSize == 0 ? 5 : poolSize; + } + + public int getTryAgainCount() { + return tryAgainCount == 0 ? 3 : tryAgainCount; + } } } diff --git a/src/main/java/dev/unnm3d/rediseconomy/currency/CurrenciesManager.java b/src/main/java/dev/unnm3d/rediseconomy/currency/CurrenciesManager.java index e786bc3..80dbda8 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/currency/CurrenciesManager.java +++ b/src/main/java/dev/unnm3d/rediseconomy/currency/CurrenciesManager.java @@ -17,6 +17,7 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.server.PluginEnableEvent; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.ServicePriority; @@ -69,61 +70,63 @@ public CurrenciesManager(RedisManager redisManager, RedisEconomyPlugin plugin, C currencies.put(currencySettings.currencyName(), currency); }); if (currencies.get(configManager.getSettings().defaultCurrencyName) == null) { - currencies.put(configManager.getSettings().defaultCurrencyName, new Currency(this, new Settings.CurrencySettings(configManager.getSettings().defaultCurrencyName, "€", "€", "#.##", "en-US", 0.0, 0.0, true, false))); + currencies.put(configManager.getSettings().defaultCurrencyName, new Currency(this, new Settings.CurrencySettings(configManager.getSettings().defaultCurrencyName, "€", "€", "#.##", "en-US", 0.0, Double.POSITIVE_INFINITY, 0.0, true, true, false))); } registerPayMsgChannel(); registerBlockAccountChannel(); } - + /** + * Loads the default currency into the vault economy provider + * Unregisters the existent economy provider + * + * @param vaultPlugin the vault plugin + */ public void loadDefaultCurrency(Plugin vaultPlugin) { - Currency defaultCurrency = getDefaultCurrency(); - for (RegisteredServiceProvider registration : plugin.getServer().getServicesManager().getRegistrations(Economy.class)) { plugin.getServer().getServicesManager().unregister(Economy.class, registration.getProvider()); } + plugin.getServer().getServicesManager().register(Economy.class, getDefaultCurrency(), vaultPlugin, ServicePriority.High); + } - if (!configManager.getSettings().migrationEnabled) { - plugin.getServer().getServicesManager().register(Economy.class, defaultCurrency, vaultPlugin, ServicePriority.High); - return; - } - + /** + * Migrates the balances from the existent economy provider to the new one + * using the offline players + * + * @param offlinePlayers the offline players to migrate + */ + public void migrateFromOfflinePlayers(OfflinePlayer[] offlinePlayers) { + final Currency defaultCurrency = getDefaultCurrency(); RegisteredServiceProvider existentProvider = plugin.getServer().getServicesManager().getRegistration(Economy.class); if (existentProvider == null) { plugin.getLogger().severe("Vault economy provider not found!"); return; } - completeMigration.thenApply(voids -> { - plugin.getLogger().info("§aMigrating from " + existentProvider.getProvider().getName() + "..."); - if (existentProvider.getProvider() == defaultCurrency) { - plugin.getLogger().info("There's no other provider apart RedisEconomy!"); - return defaultCurrency; + plugin.getLogger().info("§aMigrating from " + existentProvider.getProvider().getName() + "..."); + + final List> balances = new ArrayList<>(); + final Map nameUniqueIds = new HashMap<>(); + for (int i = 0; i < offlinePlayers.length; i++) { + final OfflinePlayer offlinePlayer = offlinePlayers[i]; + try { + double bal = existentProvider.getProvider().getBalance(offlinePlayer); + balances.add(ScoredValue.just(bal, offlinePlayer.getUniqueId().toString())); + if (offlinePlayer.getName() != null) + nameUniqueIds.put(offlinePlayer.getName(), offlinePlayer.getUniqueId().toString()); + defaultCurrency.updateAccountLocal(offlinePlayer.getUniqueId(), offlinePlayer.getName() == null ? offlinePlayer.getUniqueId().toString() : offlinePlayer.getName(), bal); + } catch (Exception e) { + e.printStackTrace(); } - - List> balances = new ArrayList<>(); - Map nameUniqueIds = new HashMap<>(); - for (OfflinePlayer offlinePlayer : Bukkit.getOfflinePlayers()) { - try { - double bal = existentProvider.getProvider().getBalance(offlinePlayer); - balances.add(ScoredValue.just(bal, offlinePlayer.getUniqueId().toString())); - if (offlinePlayer.getName() != null) - nameUniqueIds.put(offlinePlayer.getName(), offlinePlayer.getUniqueId().toString()); - defaultCurrency.updateAccountLocal(offlinePlayer.getUniqueId(), offlinePlayer.getName() == null ? offlinePlayer.getUniqueId().toString() : offlinePlayer.getName(), bal); - } catch (Exception e) { - e.printStackTrace(); - } + if (i % 100 == 0) { + plugin.getLogger().info("Progress: " + i + "/" + offlinePlayers.length); } - - defaultCurrency.updateBulkAccountsCloudCache(balances, nameUniqueIds); - return defaultCurrency; - }).thenAccept((vaultCurrency) -> { - plugin.getServer().getServicesManager().register(Economy.class, vaultCurrency, vaultPlugin, ServicePriority.High); - plugin.getLogger().info("§aMigration completed!"); - configManager.getSettings().migrationEnabled = false; - configManager.saveConfigs(); - }); - + } + defaultCurrency.updateBulkAccountsCloudCache(balances, nameUniqueIds); + plugin.getLogger().info("§aMigration completed!"); + plugin.getLogger().info("§aRestart the server to apply the changes."); + configManager.getSettings().migrationEnabled = false; + configManager.saveConfigs(); } @Override @@ -190,6 +193,15 @@ public HashMap removeNamePattern(String namePattern, boolean reset */ public HashMap resetBalanceNamePattern(String namePattern, Currency currencyReset) { HashMap removed = new HashMap<>(); + currencyReset.getOrderedAccounts(Integer.MAX_VALUE).thenAccept(accounts -> { + for (ScoredValue account : accounts) { + UUID uuid = UUID.fromString(account.getValue()); + if (!nameUniqueIds.containsValue(uuid)) { + currencyReset.setPlayerBalance(uuid, null, 0.0); + } + } + }); + for (Map.Entry entry : nameUniqueIds.entrySet()) { if (entry.getKey().matches(namePattern)) { removed.put(entry.getKey(), entry.getValue()); @@ -258,6 +270,15 @@ private void onJoin(PlayerJoinEvent e) { })); } + @EventHandler + private void onPluginEnable(PluginEnableEvent pluginEnableEvent) { + if (plugin.settings().migrationEnabled) return; + if (!plugin.getServer().getServicesManager().getRegistrations(net.milkbowl.vault.economy.Economy.class) + .stream().allMatch(registration -> registration.getPlugin().equals(plugin))) { + loadDefaultCurrency(plugin.getVaultPlugin()); + } + } + private CompletionStage> loadRedisNameUniqueIds() { return redisManager.getConnectionAsync(connection -> connection.hgetall(NAME_UUID.toString()) @@ -403,6 +424,35 @@ public boolean isAccountLocked(UUID uuid, UUID target) { getLockedAccounts(uuid).contains(RedisKeys.getAllAccountUUID()); } + /** + * Formats the amount string to a double + * Parses suffixes (10k, 10M for 10 thousand and 10 million) + * Parses percentages ("10%" for 10% of the accountOwner balance) + * + * @param targetName The account that has to be modified + * @param currency the currency to format the amount + * @param amount the amount to format + * @return the formatted amount + */ + public double formatAmountString(String targetName, Currency currency, String amount) { + try { + //Check if last char of amount is number + if (Character.isDigit(amount.charAt(amount.length() - 1))) { + return Double.parseDouble(amount); + } + double parsedAmount = Double.parseDouble(amount.substring(0, amount.length() - 1)); + return amount.endsWith("%") && plugin.settings().allowPercentagePayments ? + //Percentage 20% + parsedAmount / 100 * currency.getBalance(targetName) : + //Parse suffixes from map + parsedAmount * plugin.langs().getSuffixes() + .getOrDefault(amount.substring(amount.length() - 1), -1L); + } catch (NumberFormatException e) { + plugin.langs().send(Bukkit.getConsoleSender(), plugin.langs().invalidAmount); + } + return -1; + } + public List getLockedAccounts(UUID uuid) { return lockedAccounts.getOrDefault(uuid, new ArrayList<>()); } diff --git a/src/main/java/dev/unnm3d/rediseconomy/currency/Currency.java b/src/main/java/dev/unnm3d/rediseconomy/currency/Currency.java index 6f57fcf..629fb31 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/currency/Currency.java +++ b/src/main/java/dev/unnm3d/rediseconomy/currency/Currency.java @@ -4,7 +4,6 @@ import dev.unnm3d.rediseconomy.config.Settings; import dev.unnm3d.rediseconomy.transaction.AccountID; import dev.unnm3d.rediseconomy.transaction.Transaction; -import io.lettuce.core.RedisFuture; import io.lettuce.core.ScoredValue; import io.lettuce.core.pubsub.StatefulRedisPubSubConnection; import lombok.AllArgsConstructor; @@ -19,7 +18,10 @@ import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.*; -import java.util.concurrent.*; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import static dev.unnm3d.rediseconomy.redis.RedisKeys.*; @@ -30,6 +32,8 @@ public class Currency implements Economy { @Getter protected final String currencyName; private final ConcurrentHashMap accounts; + private final ConcurrentHashMap maxPlayerBalances; + private boolean enabled; @Getter private String currencySingular; @@ -38,11 +42,15 @@ public class Currency implements Economy { @Getter private final DecimalFormat decimalFormat; @Getter - private double startingBalance; + private final double startingBalance; + @Getter + private final double maxBalance; + private boolean saveTransactions; @Getter private double transactionTax; @Getter private final boolean taxOnlyPay; + protected final ExecutorService updateExecutor; /** @@ -55,26 +63,39 @@ public class Currency implements Economy { public Currency(CurrenciesManager currenciesManager, Settings.CurrencySettings currencySettings) { this.currenciesManager = currenciesManager; this.enabled = true; + this.updateExecutor = Executors.newSingleThreadExecutor(); this.currencyName = currencySettings.currencyName(); this.currencySingular = currencySettings.currencySingle(); this.currencyPlural = currencySettings.currencyPlural(); this.startingBalance = currencySettings.startingBalance(); + this.maxBalance = currencySettings.maxBalance() == 0.0d ? Double.POSITIVE_INFINITY : currencySettings.maxBalance(); + this.saveTransactions = currencySettings.saveTransactions(); this.transactionTax = currencySettings.payTax(); this.taxOnlyPay = currencySettings.taxOnlyPay(); this.accounts = new ConcurrentHashMap<>(); + this.maxPlayerBalances = new ConcurrentHashMap<>(); this.decimalFormat = new DecimalFormat( currencySettings.decimalFormat() != null ? currencySettings.decimalFormat() : "#.##", new DecimalFormatSymbols(Locale.forLanguageTag(currencySettings.languageTag() != null ? currencySettings.languageTag() : "en-US")) ); + getOrderedAccounts(-1).thenApply(result -> { - result.forEach(t -> - accounts.put(UUID.fromString(t.getValue()), t.getScore())); - if (RedisEconomyPlugin.getInstance().settings().debug && accounts.size() > 0) { - Bukkit.getLogger().info("start1 Loaded " + accounts.size() + " accounts for currency " + currencyName); - } - return result; - } - ).toCompletableFuture().join(); //Wait to avoid API calls before accounts are loaded + result.forEach(t -> + accounts.put(UUID.fromString(t.getValue()), t.getScore())); + if (RedisEconomyPlugin.getInstance().settings().debug && !accounts.isEmpty()) { + Bukkit.getLogger().info("start1 Loaded " + accounts.size() + " accounts for currency " + currencyName); + } + return result; + }).toCompletableFuture().join(); //Wait to avoid API calls before accounts are loaded + + getPlayerMaxBalances().thenApply(result -> { + maxPlayerBalances.putAll(result); + if (RedisEconomyPlugin.getInstance().settings().debug && !maxPlayerBalances.isEmpty()) { + Bukkit.getLogger().info("start1 Loaded " + maxPlayerBalances.size() + " max balances for currency " + currencyName); + } + return result; + }); //Not as critical as accounts, so we don't wait + registerUpdateListener(); } @@ -85,21 +106,34 @@ private void registerUpdateListener() { @Override public void message(String channel, String message) { String[] split = message.split(";;"); - if (split.length != 4) { + if (split.length < 3) { Bukkit.getLogger().severe("Invalid message received from RedisEco channel, consider updating RedisEconomy"); - return; } - if (split[0].equals(RedisEconomyPlugin.getInstance().settings().serverId)) return; - UUID uuid = UUID.fromString(split[1]); - String playerName = split[2]; - double balance = Double.parseDouble(split[3]); - updateAccountLocal(uuid, playerName, balance); - if (RedisEconomyPlugin.getInstance().settings().debug) { - Bukkit.getLogger().info("01b Received balance update " + playerName + " to " + balance); + + if (split[0].equals(RedisEconomyPlugin.getInstanceUUID().toString())) return; + final UUID uuid = UUID.fromString(split[1]); + + if (channel.equals(UPDATE_PLAYER_CHANNEL_PREFIX + currencyName)) { + String playerName = split[2]; + double balance = Double.parseDouble(split[3]); + if (playerName == null) { + Bukkit.getLogger().severe("Player name not found for UUID " + uuid); + return; + } + updateAccountLocal(uuid, playerName, balance); + if (RedisEconomyPlugin.getInstance().settings().debug) { + Bukkit.getLogger().info("01b Received balance update " + playerName + " to " + balance); + } + } else if (channel.equals(UPDATE_MAX_BAL_PREFIX + currencyName)) { + double maxBal = Double.parseDouble(split[2]); + setPlayerMaxBalanceLocal(uuid, maxBal); + if (RedisEconomyPlugin.getInstance().settings().debug) { + Bukkit.getLogger().info("01b Received max balance update " + uuid + " to " + maxBal); + } } } }); - connection.async().subscribe(UPDATE_PLAYER_CHANNEL_PREFIX + currencyName); + connection.async().subscribe(UPDATE_PLAYER_CHANNEL_PREFIX + currencyName, UPDATE_MAX_BAL_PREFIX + currencyName); if (RedisEconomyPlugin.getInstance().settings().debug) { Bukkit.getLogger().info("start1b Listening to RedisEco channel " + UPDATE_PLAYER_CHANNEL_PREFIX + currencyName); } @@ -110,6 +144,14 @@ public boolean isEnabled() { return enabled; } + public boolean shouldSaveTransactions() { + return saveTransactions; + } + + public void setShouldSaveTransactions(boolean saveTransactions) { + this.saveTransactions = saveTransactions; + } + @Override public String getName() { return "RedisEconomy"; @@ -122,7 +164,7 @@ public boolean hasBankSupport() { @Override public int fractionalDigits() { - return 0; + return decimalFormat.getMaximumFractionDigits(); } @Override @@ -333,7 +375,7 @@ public boolean createPlayerAccount(@NotNull UUID playerUUID, @Nullable String pl if (hasAccount(playerUUID)) return false; updateAccount(playerUUID, playerName, startingBalance); - currenciesManager.getExchange().saveTransaction(new AccountID(playerUUID), new AccountID(), startingBalance, currencyName, "Account creation"); + currenciesManager.getExchange().saveTransaction(new AccountID(playerUUID), new AccountID(), startingBalance, this, "Account creation"); return true; } @@ -366,7 +408,7 @@ public EconomyResponse withdrawPlayer(@NotNull UUID playerUUID, @Nullable String return new EconomyResponse(amountToWithdraw, getBalance(playerUUID), EconomyResponse.ResponseType.FAILURE, "Insufficient funds"); updateAccount(playerUUID, playerName, getBalance(playerUUID) - amountToWithdraw); - currenciesManager.getExchange().saveTransaction(new AccountID(playerUUID), new AccountID(), -amountToWithdraw, currencyName, reason == null ? "Withdraw" : reason); + currenciesManager.getExchange().saveTransaction(new AccountID(playerUUID), new AccountID(), -amountToWithdraw, this, reason == null ? "Withdraw" : reason); return new EconomyResponse(amount, getBalance(playerUUID), EconomyResponse.ResponseType.SUCCESS, null); } @@ -387,10 +429,13 @@ public EconomyResponse payPlayer(@NotNull UUID sender, @NotNull UUID receiver, d if (!has(sender, amountToWithdraw)) return new EconomyResponse(0, getBalance(sender), EconomyResponse.ResponseType.FAILURE, "Insufficient funds"); + if (getBalance(receiver) + amount > getPlayerMaxBalance(receiver)) + return new EconomyResponse(0, getBalance(receiver), EconomyResponse.ResponseType.FAILURE, "The receiver has reached the maximum balance"); + updateAccount(sender, senderName, getBalance(sender) - amountToWithdraw); - currenciesManager.getExchange().saveTransaction(new AccountID(sender), new AccountID(receiver), -amountToWithdraw, currencyName, reason == null ? "Payment" : reason); + currenciesManager.getExchange().saveTransaction(new AccountID(sender), new AccountID(receiver), -amountToWithdraw, this, reason == null ? "Payment" : reason); updateAccount(sender, receiverName, getBalance(receiver) + amount); - currenciesManager.getExchange().saveTransaction(new AccountID(receiver), new AccountID(sender), amount, currencyName, reason == null ? "Payment" : reason); + currenciesManager.getExchange().saveTransaction(new AccountID(receiver), new AccountID(sender), amount, this, reason == null ? "Payment" : reason); return new EconomyResponse(amount, getBalance(sender), EconomyResponse.ResponseType.SUCCESS, null); } @@ -400,8 +445,11 @@ public EconomyResponse payPlayer(@NotNull String senderName, @NotNull String rec return new EconomyResponse(amount, getBalance(senderName), EconomyResponse.ResponseType.FAILURE, "Account not found"); if (!hasAccount(receiverName)) return new EconomyResponse(amount, getBalance(receiverName), EconomyResponse.ResponseType.FAILURE, "Account not found"); - UUID sender = currenciesManager.getUUIDFromUsernameCache(senderName); - UUID receiver = currenciesManager.getUUIDFromUsernameCache(receiverName); + + final UUID sender = currenciesManager.getUUIDFromUsernameCache(senderName); + final UUID receiver = currenciesManager.getUUIDFromUsernameCache(receiverName); + + //Calculate the amount to withdraw with the transaction tax double amountToWithdraw = amount + (amount * transactionTax); if (sender == null || receiver == null) return new EconomyResponse(amount, getBalance(senderName), EconomyResponse.ResponseType.FAILURE, "Account not found"); @@ -410,7 +458,10 @@ public EconomyResponse payPlayer(@NotNull String senderName, @NotNull String rec return new EconomyResponse(0, 0, EconomyResponse.ResponseType.FAILURE, "Invalid decimal amount format"); if (!has(senderName, amountToWithdraw)) - return new EconomyResponse(0, getBalance(senderName), EconomyResponse.ResponseType.FAILURE, "Insufficient funds"); + return new EconomyResponse(0, getBalance(sender), EconomyResponse.ResponseType.FAILURE, "Insufficient funds"); + + if (getBalance(receiver) + amount > getPlayerMaxBalance(receiver)) + return new EconomyResponse(0, getBalance(receiver), EconomyResponse.ResponseType.FAILURE, "The receiver has reached the maximum balance"); updateAccount(sender, senderName, getBalance(sender) - amountToWithdraw); updateAccount(receiver, receiverName, getBalance(receiver) + amount); @@ -440,9 +491,9 @@ public EconomyResponse setPlayerBalance(@NotNull OfflinePlayer player, double am public EconomyResponse setPlayerBalance(@NotNull UUID playerUUID, @Nullable String playerName, double amount) { if (amount == Double.POSITIVE_INFINITY || amount == Double.NEGATIVE_INFINITY || Double.isNaN(amount)) return new EconomyResponse(0, 0, EconomyResponse.ResponseType.FAILURE, "Invalid decimal amount format"); - currenciesManager.getExchange().saveTransaction(new AccountID(playerUUID), new AccountID(), -getBalance(playerUUID), currencyName, "Reset balance"); updateAccount(playerUUID, playerName, amount); - currenciesManager.getExchange().saveTransaction(new AccountID(playerUUID), new AccountID(), amount, currencyName, "Set balance"); + currenciesManager.getExchange().saveTransaction(new AccountID(playerUUID), new AccountID(), -getBalance(playerUUID), this, "Reset balance"); + currenciesManager.getExchange().saveTransaction(new AccountID(playerUUID), new AccountID(), amount, this, "Set balance"); return new EconomyResponse(amount, getBalance(playerUUID), EconomyResponse.ResponseType.SUCCESS, null); } @@ -463,7 +514,7 @@ public CompletionStage revertTransaction(int transactionId, @NotNull Tr if (RedisEconomyPlugin.getInstance().settings().debug) { Bukkit.getLogger().info("revert01a reverted on account " + transaction.getAccountIdentifier() + " amount " + transaction.getAmount()); } - return currenciesManager.getExchange().saveTransaction(transaction.getAccountIdentifier(), transaction.getActor(), -transaction.getAmount(), currencyName, "Revert #" + transactionId + ": " + transaction.getReason()); + return currenciesManager.getExchange().saveTransaction(transaction.getAccountIdentifier(), transaction.getActor(), -transaction.getAmount(), this, "Revert #" + transactionId + ": " + transaction.getReason()); } /** @@ -494,8 +545,11 @@ public EconomyResponse depositPlayer(@NotNull UUID playerUUID, @Nullable String if (amount == Double.POSITIVE_INFINITY || amount == Double.NEGATIVE_INFINITY || Double.isNaN(amount)) return new EconomyResponse(0, 0, EconomyResponse.ResponseType.FAILURE, "Invalid decimal amount format"); + if (getBalance(playerUUID) + amount > getPlayerMaxBalance(playerUUID)) + return new EconomyResponse(0, getBalance(playerUUID), EconomyResponse.ResponseType.FAILURE, "The player has reached the maximum balance"); + updateAccount(playerUUID, playerName, getBalance(playerUUID) + amount); - currenciesManager.getExchange().saveTransaction(new AccountID(playerUUID), new AccountID(), amount, currencyName, reason == null ? "Deposit" : reason); + currenciesManager.getExchange().saveTransaction(new AccountID(playerUUID), new AccountID(), amount, this, reason == null ? "Deposit" : reason); return new EconomyResponse(amount, getBalance(playerUUID), EconomyResponse.ResponseType.SUCCESS, null); } @@ -510,33 +564,53 @@ protected void updateAccount(@NotNull UUID uuid, @Nullable String playerName, do updateAccountLocal(uuid, playerName, balance); } - private void updateAccountCloudCache(@NotNull UUID uuid, @Nullable String playerName, double balance, int tries) { - try { - currenciesManager.getRedisManager().getConnectionPipeline(commands -> { - commands.zadd(BALANCE_PREFIX + currencyName, balance, uuid.toString()); - if (playerName != null) - commands.hset(NAME_UUID.toString(), playerName, uuid.toString()); - commands.publish(UPDATE_PLAYER_CHANNEL_PREFIX + currencyName, RedisEconomyPlugin.getInstance().settings().serverId + ";;" + uuid + ";;" + playerName + ";;" + balance).thenAccept((result) -> { - if (RedisEconomyPlugin.getInstance().settings().debug) { - Bukkit.getLogger().info("01 Sent update account " + playerName + " to " + balance); + private synchronized void updateAccountCloudCache(@NotNull UUID uuid, @Nullable String playerName, double balance, int tries) { + final RedisEconomyPlugin plugin = RedisEconomyPlugin.getInstance(); + updateExecutor.submit(() -> { + try { + if (plugin.settings().debugUpdateCache) { + Bukkit.getLogger().info("01a Starting update account " + playerName + " to " + balance + " currency " + currencyName); + } + currenciesManager.getRedisManager().executeTransaction(reactiveCommands -> { + reactiveCommands.zadd(BALANCE_PREFIX + currencyName, balance, uuid.toString()); + if (playerName != null) + reactiveCommands.hset(NAME_UUID.toString(), playerName, uuid.toString()); + reactiveCommands.publish(UPDATE_PLAYER_CHANNEL_PREFIX + currencyName, + RedisEconomyPlugin.getInstanceUUID().toString() + ";;" + uuid + ";;" + playerName + ";;" + balance); + if (plugin.settings().debugUpdateCache) { + plugin.getLogger().info("01b Publishing update account " + playerName + " to " + balance + " currency " + currencyName); } - }); - return null; - }); - - } catch (Exception e) { - if (tries < 3) { - e.printStackTrace(); - Bukkit.getLogger().severe("Failed to update account " + playerName + " after 3 tries"); - Bukkit.getLogger().severe("Player accounts are desynchronized"); - updateAccountCloudCache(uuid, playerName, balance, tries + 1); - } else { - e.printStackTrace(); + }).ifPresentOrElse(result -> { + if (RedisEconomyPlugin.getInstance().settings().debugUpdateCache) { + plugin.getLogger().info("01c Sent update account successfully " + playerName + " to " + balance + " currency " + currencyName); + } + }, () -> handleException(uuid, playerName, balance, tries, null)); + } catch (Exception e) { + handleException(uuid, playerName, balance, tries, e); } - } + }); + } + private void handleException(@NotNull UUID uuid, @Nullable String playerName, double balance, int tries, @Nullable Exception e) { + final RedisEconomyPlugin plugin = RedisEconomyPlugin.getInstance(); + if (tries < plugin.settings().redis.getTryAgainCount()) { + if (plugin.settings().debugUpdateCache) { + plugin.getLogger().warning("Player accounts are desynchronized. try: " + tries); + if (e != null) + e.printStackTrace(); + } + updateAccountCloudCache(uuid, playerName, balance, tries + 1); + return; + } + if (plugin.settings().debugUpdateCache) { + plugin.getLogger().severe("Failed to update account " + playerName + " after " + tries + " tries"); + currenciesManager.getRedisManager().printPool(); + if (e != null) + throw new RuntimeException(e); + } } + /** * Update the balances of all players and their nameuuids * Do not use this method unless you know what you are doing @@ -546,19 +620,16 @@ private void updateAccountCloudCache(@NotNull UUID uuid, @Nullable String player */ @SuppressWarnings("unchecked") public void updateBulkAccountsCloudCache(@NotNull List> balances, @NotNull Map nameUUIDs) { - currenciesManager.getRedisManager().getConnectionPipeline(commands -> { + currenciesManager.getRedisManager().executeTransaction(commands -> { ScoredValue[] balancesArray = new ScoredValue[balances.size()]; balances.toArray(balancesArray); - RedisFuture sortedAddFuture = commands.zadd(BALANCE_PREFIX + currencyName, balancesArray); - RedisFuture hstFuture = commands.hset(NAME_UUID.toString(), nameUUIDs); - try { - Bukkit.getLogger().info("migration01 updated balances into " + BALANCE_PREFIX + currencyName + " accounts. result " + sortedAddFuture.get(20, TimeUnit.SECONDS)); - Bukkit.getLogger().info("migration02 updated nameuuids into " + NAME_UUID + " accounts. result " + hstFuture.get(20, TimeUnit.SECONDS)); - } catch (ExecutionException | InterruptedException | TimeoutException e) { - e.printStackTrace(); - } - return null; + commands.zadd(BALANCE_PREFIX + currencyName, balancesArray); + commands.hset(NAME_UUID.toString(), nameUUIDs); + }).ifPresent(result -> { + Bukkit.getLogger().info("migration01 updated balances into " + BALANCE_PREFIX + currencyName + " accounts. result " + result.get(0)); + Bukkit.getLogger().info("migration02 updated nameuuids into " + NAME_UUID + " accounts. result " + result.get(1)); + }); } @@ -576,6 +647,40 @@ public CompletionStage>> getOrderedAccounts(int limit) } + public double getPlayerMaxBalance(UUID uuid) { + return maxPlayerBalances.getOrDefault(uuid, maxBalance); + } + + public void setPlayerMaxBalance(UUID uuid, double amount) { + setPlayerMaxBalanceCloud(uuid, amount); + setPlayerMaxBalanceLocal(uuid, amount); + } + + public void setPlayerMaxBalanceLocal(UUID uuid, double amount) { + maxPlayerBalances.put(uuid, amount); + } + + public void setPlayerMaxBalanceCloud(UUID uuid, double amount) { + currenciesManager.getRedisManager().getConnectionPipeline(asyncCommands -> { + if(amount == maxBalance) { + asyncCommands.hdel(MAX_PLAYER_BALANCES + currencyName, uuid.toString()); + } else { + asyncCommands.hset(MAX_PLAYER_BALANCES + currencyName, uuid.toString(), String.valueOf(amount)); + } + return asyncCommands.publish(UPDATE_MAX_BAL_PREFIX + currencyName, RedisEconomyPlugin.getInstanceUUID().toString() + ";;" + uuid + ";;" + amount); + }); + } + + public CompletionStage> getPlayerMaxBalances() { + return currenciesManager.getRedisManager().getConnectionAsync(accounts -> + accounts.hgetall(MAX_PLAYER_BALANCES + currencyName)) + .thenApply(result -> { + final Map maxBalances = new HashMap<>(); + result.forEach((key, value) -> maxBalances.put(UUID.fromString(key), Double.parseDouble(value))); + return maxBalances; + }); + } + /** * Get single ordered account from Redis * @@ -595,4 +700,6 @@ public CompletionStage getAccountRedis(UUID uuid) { public final Map getAccounts() { return Collections.unmodifiableMap(accounts); } + + } diff --git a/src/main/java/dev/unnm3d/rediseconomy/currency/CurrencyWithBanks.java b/src/main/java/dev/unnm3d/rediseconomy/currency/CurrencyWithBanks.java index 029f544..1d23c51 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/currency/CurrencyWithBanks.java +++ b/src/main/java/dev/unnm3d/rediseconomy/currency/CurrencyWithBanks.java @@ -10,12 +10,12 @@ import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.*; import static dev.unnm3d.rediseconomy.redis.RedisKeys.*; @@ -34,14 +34,14 @@ public CurrencyWithBanks(CurrenciesManager currenciesManager, Settings.CurrencyS for (ScoredValue scoredValue : list) { bankAccounts.put(scoredValue.getValue(), scoredValue.getScore()); } - if (RedisEconomyPlugin.getInstance().settings().debug && bankAccounts.size() > 0) { + if (RedisEconomyPlugin.getInstance().settings().debug && !bankAccounts.isEmpty()) { Bukkit.getLogger().info("start1bank Loaded " + bankAccounts.size() + " accounts for currency " + currencyName); } return list; }).toCompletableFuture().join(); getRedisBankOwners().thenApply(map -> { map.forEach((k, v) -> bankOwners.put(k, UUID.fromString(v))); - if (RedisEconomyPlugin.getInstance().settings().debug && bankOwners.size() > 0) { + if (RedisEconomyPlugin.getInstance().settings().debug && !bankOwners.isEmpty()) { Bukkit.getLogger().info("start1bankb Loaded " + bankOwners.size() + " accounts owners for currency " + currencyName); } return map; @@ -60,7 +60,7 @@ public void message(String channel, String message) { Bukkit.getLogger().severe("Invalid message received from RedisEco channel, consider updating RedisEconomy"); return; } - if (split[0].equals(RedisEconomyPlugin.getInstance().settings().serverId)) return; + if (split[0].equals(RedisEconomyPlugin.getInstanceUUID().toString())) return; String accountId = split[1]; UUID owner = UUID.fromString(split[2]); bankOwners.put(accountId, owner); @@ -85,7 +85,7 @@ public void message(String channel, String message) { Bukkit.getLogger().severe("Invalid message received from RedisEco channel, consider updating RedisEconomy"); return; } - if (split[0].equals(RedisEconomyPlugin.getInstance().settings().serverId)) return; + if (split[0].equals(RedisEconomyPlugin.getInstanceUUID().toString())) return; String accountId = split[1]; double balance = Double.parseDouble(split[2]); updateBankAccountLocal(accountId, balance); @@ -123,7 +123,7 @@ public EconomyResponse createBank(@NotNull String accountId, UUID playerOwner, S return new EconomyResponse(0, 0, EconomyResponse.ResponseType.FAILURE, "Bank account already exists"); setOwner(accountId, playerOwner); updateBankAccount(accountId, 0); - currenciesManager.getExchange().saveTransaction(new AccountID(accountId), new AccountID(playerOwner), 0, currencyName, reason); + currenciesManager.getExchange().saveTransaction(new AccountID(accountId), new AccountID(playerOwner), 0, this, reason); return new EconomyResponse(0, 0, EconomyResponse.ResponseType.SUCCESS, null); } @@ -146,7 +146,7 @@ public EconomyResponse deleteBank(@NotNull String accountId) { if (RedisEconomyPlugin.getInstance().settings().debug) { Bukkit.getLogger().info("Deleted bank account " + accountId + " with result " + result); } - currenciesManager.getExchange().saveTransaction(new AccountID(accountId), new AccountID(), 0, currencyName, "Bank account deletion"); + currenciesManager.getExchange().saveTransaction(new AccountID(accountId), new AccountID(), 0, this, "Bank account deletion"); }); return new EconomyResponse(0, 0, EconomyResponse.ResponseType.SUCCESS, null); } @@ -184,7 +184,7 @@ public EconomyResponse bankWithdraw(@NotNull String accountId, double amount, St return hasAmountResponse; } updateBankAccount(accountId, hasAmountResponse.balance);//Balance is the new balance with subtracted amount - currenciesManager.getExchange().saveTransaction(new AccountID(accountId), new AccountID(), -amount, currencyName, reason); + currenciesManager.getExchange().saveTransaction(new AccountID(accountId), new AccountID(), -amount, this, reason); return new EconomyResponse(amount, hasAmountResponse.balance, EconomyResponse.ResponseType.SUCCESS, null); } @@ -208,7 +208,7 @@ public EconomyResponse bankDeposit(@NotNull String accountId, double amount, Str } EconomyResponse balanceResponse = bankBalance(accountId); updateBankAccount(accountId, balanceResponse.balance + amount);//Balance is the new balance with subtracted amount - currenciesManager.getExchange().saveTransaction(new AccountID(accountId), new AccountID(), amount, currencyName, reason); + currenciesManager.getExchange().saveTransaction(new AccountID(accountId), new AccountID(), amount, this, reason); return new EconomyResponse(amount, balanceResponse.balance + amount, EconomyResponse.ResponseType.SUCCESS, null); } @@ -263,13 +263,13 @@ public CompletionStage revertTransaction(int transactionId, @NotNull Tr if (RedisEconomyPlugin.getInstance().settings().debug) { Bukkit.getLogger().info("revert01a reverted on account " + transaction.getAccountIdentifier() + " amount " + transaction.getAmount()); } - return currenciesManager.getExchange().saveTransaction(transaction.getAccountIdentifier(), transaction.getActor(), -transaction.getAmount(), currencyName, "Revert #" + transactionId + ": " + transaction.getReason()); + return currenciesManager.getExchange().saveTransaction(transaction.getAccountIdentifier(), transaction.getActor(), -transaction.getAmount(), this, "Revert #" + transactionId + ": " + transaction.getReason()); } private void setOwner(@NotNull String accountId, UUID ownerUUID) { currenciesManager.getRedisManager().getConnectionPipeline(connection -> { connection.hset(BANK_OWNERS.toString(), accountId, ownerUUID.toString()); - return connection.publish(UPDATE_BANK_OWNER_CHANNEL_PREFIX + currencyName, RedisEconomyPlugin.getInstance().settings().serverId + ";;" + accountId + ";;" + ownerUUID); + return connection.publish(UPDATE_BANK_OWNER_CHANNEL_PREFIX + currencyName, RedisEconomyPlugin.getInstanceUUID().toString() + ";;" + accountId + ";;" + ownerUUID); }).thenAccept((result) -> { if (RedisEconomyPlugin.getInstance().settings().debug) { Bukkit.getLogger().info("Set owner of bank " + accountId + " to " + ownerUUID + " with result " + result); @@ -287,26 +287,45 @@ private void updateBankAccount(@NotNull String accountId, double balance) { updateBankAccountLocal(accountId, balance); } - private void updateBankAccountCloudCache(@NotNull String accountId, double balance, int tries) { + private synchronized void updateBankAccountCloudCache(@NotNull String accountId, double balance, int tries) { try { - currenciesManager.getRedisManager().getConnectionPipeline(commands -> { - commands.zadd(BALANCE_BANK_PREFIX + currencyName, balance, accountId); - return commands.publish(UPDATE_BANK_CHANNEL_PREFIX + currencyName, RedisEconomyPlugin.getInstance().settings().serverId + ";;" + accountId + ";;" + balance).thenAccept((result) -> { - if (RedisEconomyPlugin.getInstance().settings().debug) { - Bukkit.getLogger().info("01 Sent bank update account " + accountId + " to " + balance); - } - }); - }); - - } catch (Exception e) { - if (tries < 3) { - e.printStackTrace(); - Bukkit.getLogger().severe("Failed to update bank account " + accountId + " after 3 tries"); - Bukkit.getLogger().severe("Bank accounts are desynchronized"); - updateBankAccountCloudCache(accountId, balance, tries + 1); - } else { - e.printStackTrace(); - } + updateExecutor.submit(() -> { + if (RedisEconomyPlugin.getInstance().settings().debug) { + Bukkit.getLogger().info("01a Starting update bank account " + accountId + " to " + balance + " currency " + currencyName); + } + try { + currenciesManager.getRedisManager().executeTransaction(reactiveCommands -> { + reactiveCommands.zadd(BALANCE_BANK_PREFIX + currencyName, balance, accountId); + reactiveCommands.publish(UPDATE_BANK_CHANNEL_PREFIX + currencyName, RedisEconomyPlugin.getInstanceUUID().toString() + ";;" + accountId + ";;" + balance); + if (RedisEconomyPlugin.getInstance().settings().debug) { + RedisEconomyPlugin.getInstance().getLogger().info("01b Publishing update bank account " + accountId + " to " + balance + " currency " + currencyName); + } + }).ifPresentOrElse(result -> { + if (RedisEconomyPlugin.getInstance().settings().debug) { + Bukkit.getLogger().info("01c Sent bank update account " + accountId + " to " + balance); + } + }, () -> handleException(accountId, balance, tries, null)); + } catch (Exception e) { + handleException(accountId, balance, tries, e); + } + }).get(RedisEconomyPlugin.getInstance().settings().redis.timeout(), TimeUnit.MILLISECONDS); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + handleException(accountId, balance, tries, e); + } + } + + private void handleException(@NotNull String accountId, double balance, int tries, @Nullable Exception e) { + final RedisEconomyPlugin plugin = RedisEconomyPlugin.getInstance(); + if (tries < plugin.settings().redis.getTryAgainCount()) { + plugin.getLogger().warning("Player accounts are desynchronized. try: " + tries); + if (e != null) + plugin.getLogger().warning(e.getMessage()); + updateBankAccountCloudCache(accountId, balance, tries + 1); + } else { + plugin.getLogger().severe("Failed to update bank account " + accountId + " after " + tries + " tries"); + currenciesManager.getRedisManager().printPool(); + if (e != null) + throw new RuntimeException(e); } } diff --git a/src/main/java/dev/unnm3d/rediseconomy/redis/RedisKeys.java b/src/main/java/dev/unnm3d/rediseconomy/redis/RedisKeys.java index 6865262..996d05a 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/redis/RedisKeys.java +++ b/src/main/java/dev/unnm3d/rediseconomy/redis/RedisKeys.java @@ -5,10 +5,12 @@ public enum RedisKeys { NAME_UUID("rediseco:nameuuid"), + MAX_PLAYER_BALANCES("rediseco:max_bals"), BALANCE_PREFIX("rediseco:balances_"), BALANCE_BANK_PREFIX("rediseco:b_balances_"), BANK_OWNERS("rediseco:b_owners"), UPDATE_PLAYER_CHANNEL_PREFIX("rediseco:update_"), + UPDATE_MAX_BAL_PREFIX("rediseco:update_max_"), UPDATE_BANK_CHANNEL_PREFIX("rediseco:b_update_"), UPDATE_BANK_OWNER_CHANNEL_PREFIX("rediseco:b_owner_update_"), MSG_CHANNEL("rediseco:paymsg"), diff --git a/src/main/java/dev/unnm3d/rediseconomy/redis/RedisManager.java b/src/main/java/dev/unnm3d/rediseconomy/redis/RedisManager.java index 2965065..1da0f61 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/redis/RedisManager.java +++ b/src/main/java/dev/unnm3d/rediseconomy/redis/RedisManager.java @@ -1,17 +1,22 @@ package dev.unnm3d.rediseconomy.redis; +import dev.unnm3d.rediseconomy.RedisEconomyPlugin; import io.lettuce.core.RedisClient; import io.lettuce.core.RedisFuture; +import io.lettuce.core.TransactionResult; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.async.RedisAsyncCommands; +import io.lettuce.core.api.sync.RedisCommands; import io.lettuce.core.pubsub.StatefulRedisPubSubConnection; import java.time.Duration; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletionStage; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Consumer; import java.util.function.Function; public class RedisManager { @@ -21,15 +26,18 @@ public class RedisManager { private final List> pubSubConnections; protected RedisClient lettuceRedisClient; - public RedisManager(RedisClient lettuceRedisClient) { + public RedisManager(RedisClient lettuceRedisClient, int poolSize) { this.lettuceRedisClient = lettuceRedisClient; - this.roundRobinConnectionPool = new RoundRobinConnectionPool<>(lettuceRedisClient::connect, 5); + this.roundRobinConnectionPool = new RoundRobinConnectionPool<>(lettuceRedisClient::connect, poolSize); pubSubConnections = new CopyOnWriteArrayList<>(); } public CompletionStage getConnectionAsync(Function, CompletionStage> redisCallBack) { return redisCallBack.apply(roundRobinConnectionPool.get().async()); } + public T getConnectionSync(Function, T> redisCallBack) { + return redisCallBack.apply(roundRobinConnectionPool.get().sync()); + } public CompletionStage getConnectionPipeline(Function, CompletionStage> redisCallBack) { StatefulRedisConnection connection = roundRobinConnectionPool.get(); @@ -40,12 +48,28 @@ public CompletionStage getConnectionPipeline(Function> executeTransaction(Consumer> redisCommandsConsumer) { + final RedisCommands syncCommands = roundRobinConnectionPool.get().sync(); + syncCommands.multi(); + redisCommandsConsumer.accept(syncCommands); + final TransactionResult transactionResult = syncCommands.exec(); + return Optional.ofNullable(transactionResult.wasDiscarded() ? null : transactionResult.stream().toList()); + } + public StatefulRedisPubSubConnection getPubSubConnection() { StatefulRedisPubSubConnection pubSubConnection = lettuceRedisClient.connectPubSub(); pubSubConnections.add(pubSubConnection); return pubSubConnection; } + public void expandPool(int expandBy) { + roundRobinConnectionPool.expandPool(expandBy); + } + + public void printPool() { + RedisEconomyPlugin.getInstance().getLogger().warning(roundRobinConnectionPool.printPool()); + } + public void close() { pubSubConnections.forEach(StatefulRedisPubSubConnection::close); lettuceRedisClient.shutdown(Duration.ofSeconds(1), Duration.ofSeconds(1)); diff --git a/src/main/java/dev/unnm3d/rediseconomy/redis/RoundRobinConnectionPool.java b/src/main/java/dev/unnm3d/rediseconomy/redis/RoundRobinConnectionPool.java index 6dd131e..ea81571 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/redis/RoundRobinConnectionPool.java +++ b/src/main/java/dev/unnm3d/rediseconomy/redis/RoundRobinConnectionPool.java @@ -41,6 +41,21 @@ public StatefulRedisConnection get() { return connection; } + public String printPool() { + StringBuilder sb = new StringBuilder(); + for (StatefulRedisConnection element : elements) { + sb.append("Open: ") + .append(element.isOpen()) + .append(", multi: ") + .append(element.isMulti()) + .append(", hash") + .append(element.hashCode()) + .append("\n"); + } + return sb.toString(); + } + + public void close() { for (StatefulRedisConnection element : elements) { element.closeAsync(); diff --git a/src/main/java/dev/unnm3d/rediseconomy/transaction/EconomyExchange.java b/src/main/java/dev/unnm3d/rediseconomy/transaction/EconomyExchange.java index 1b1e830..2ffbfeb 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/transaction/EconomyExchange.java +++ b/src/main/java/dev/unnm3d/rediseconomy/transaction/EconomyExchange.java @@ -9,13 +9,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.ExecutionException; +import java.util.*; +import java.util.concurrent.*; import static dev.unnm3d.rediseconomy.redis.RedisKeys.NEW_TRANSACTIONS; @@ -23,6 +18,7 @@ public class EconomyExchange { private final RedisEconomyPlugin plugin; + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); /** * Get transactions from an account id @@ -50,7 +46,7 @@ public CompletionStage removeAllTransactions() { return plugin.getCurrenciesManager().getRedisManager().getConnectionAsync(connection -> { try { List keys = connection.keys(NEW_TRANSACTIONS + "*").get(); - if (keys.size() == 0) { + if (keys.isEmpty()) { return CompletableFuture.completedFuture(0L); } return connection.del(keys.toArray(new String[0])); @@ -89,54 +85,56 @@ public CompletionStage getTransaction(AccountID accountId, int id) * @return List of ids: the first one is the id of the transaction on the sender side, the second one is the id of the transaction on the target side */ public CompletionStage> savePaymentTransaction(@NotNull UUID sender, @NotNull UUID target, double amount, @NotNull Currency currency, @NotNull String reason) { - long init = System.currentTimeMillis(); - reason += getCallerPluginString(); + if (!currency.shouldSaveTransactions()) return CompletableFuture.completedStage(List.of(-1, -1)); - TransactionEvent transactionSenderEvent = new TransactionEvent(new Transaction( - new AccountID(sender), - System.currentTimeMillis(), - new AccountID(target), - -amount, - currency.getCurrencyName(), - reason, - null)); - TransactionEvent transactionReceiverEvent = new TransactionEvent(new Transaction( - new AccountID(target), - System.currentTimeMillis(), - new AccountID(sender), - amount, - currency.getCurrencyName(), - reason, - null)); + final CompletableFuture> future = new CompletableFuture<>(); + long init = System.currentTimeMillis(); + final String stackTrace = getCallerPluginString(); + + executorService.submit(() -> { + TransactionEvent transactionSenderEvent = new TransactionEvent(new Transaction( + new AccountID(sender), + System.currentTimeMillis(), + new AccountID(target), + -amount, + currency.getCurrencyName(), + reason + stackTrace, + null)); + TransactionEvent transactionReceiverEvent = new TransactionEvent(new Transaction( + new AccountID(target), + System.currentTimeMillis(), + new AccountID(sender), + amount, + currency.getCurrencyName(), + reason + stackTrace, + null)); + + plugin.getScheduler().runTask(() -> { + plugin.getServer().getPluginManager().callEvent(transactionSenderEvent); + plugin.getServer().getPluginManager().callEvent(transactionReceiverEvent); + }); + future.complete(plugin.getCurrenciesManager().getRedisManager().getConnectionSync(connection -> + connection.eval( + "local senderCurrentId=redis.call('hlen', KEYS[1]);" + + "local receiverCurrentId=redis.call('hlen', KEYS[2]);" + + "redis.call('hset', KEYS[1], senderCurrentId, ARGV[1]);" + + "redis.call('hset', KEYS[2], receiverCurrentId, ARGV[2]);" + + "return {senderCurrentId,receiverCurrentId};", //Return the id of the new transaction + ScriptOutputType.MULTI, + new String[]{ + NEW_TRANSACTIONS + sender.toString(), + NEW_TRANSACTIONS + target.toString()}, //Key rediseco:transactions:playerUUID + transactionSenderEvent.getTransaction().toString(), + transactionReceiverEvent.getTransaction().toString()))); - plugin.getScheduler().runTask(() -> { - plugin.getServer().getPluginManager().callEvent(transactionSenderEvent); - plugin.getServer().getPluginManager().callEvent(transactionReceiverEvent); }); - - return plugin.getCurrenciesManager().getRedisManager().getConnectionAsync(connection -> - connection.>eval( - "local senderCurrentId=redis.call('hlen', KEYS[1]);" + - "local receiverCurrentId=redis.call('hlen', KEYS[2]);" + - "redis.call('hset', KEYS[1], senderCurrentId, ARGV[1]);" + - "redis.call('hset', KEYS[2], receiverCurrentId, ARGV[2]);" + - "return {senderCurrentId,receiverCurrentId};", //Return the id of the new transaction - ScriptOutputType.MULTI, - new String[]{ - NEW_TRANSACTIONS + sender.toString(), - NEW_TRANSACTIONS + target.toString()}, //Key rediseco:transactions:playerUUID - transactionSenderEvent.getTransaction().toString(), - transactionReceiverEvent.getTransaction().toString())) - .thenApply(response -> { - if (RedisEconomyPlugin.getInstance().settings().debug) { - Bukkit.getLogger().info("03payment Transaction for " + sender + " saved in " + (System.currentTimeMillis() - init) + " ms with id " + response.get(0) + " !"); - Bukkit.getLogger().info("03payment Transaction for " + target + " saved in " + (System.currentTimeMillis() - init) + " ms with id " + response.get(1) + " !"); - } - return response; - }).exceptionally(throwable -> { - throwable.printStackTrace(); - return null; - }); + return future.thenApply(response -> { + if (RedisEconomyPlugin.getInstance().settings().debug) { + Bukkit.getLogger().info("03payment Transaction for " + sender + " saved in " + (System.currentTimeMillis() - init) + " ms with id " + response.get(0) + " !"); + Bukkit.getLogger().info("03payment Transaction for " + target + " saved in " + (System.currentTimeMillis() - init) + " ms with id " + response.get(1) + " !"); + } + return response; + }); } @@ -146,38 +144,38 @@ public CompletionStage> savePaymentTransaction(@NotNull UUID sende * @param accountOwner The id of the account, could be a UUID or a bank id (string) * @param target The id of the target account, could be a UUID or a bank id (string) * @param amount The amount of money transferred - * @param currencyName The name of the currency + * @param currency The currency of the transaction * @param reason The reason of the transaction * @return The transaction id */ - public CompletionStage saveTransaction(@NotNull AccountID accountOwner, @NotNull AccountID target, double amount, @NotNull String currencyName, @NotNull String reason) { + public CompletionStage saveTransaction(@NotNull AccountID accountOwner, @NotNull AccountID target, double amount, @NotNull Currency currency, @NotNull String reason) { + if (!currency.shouldSaveTransactions()) return CompletableFuture.completedStage(-1); + final CompletableFuture future = new CompletableFuture<>(); long init = System.currentTimeMillis(); - return plugin.getCurrenciesManager().getRedisManager().getConnectionAsync(commands -> { - - TransactionEvent transactionEvent = new TransactionEvent(new Transaction( - accountOwner, - System.currentTimeMillis(), - target, //If target is null, it has been sent from the server - amount, currencyName, reason + getCallerPluginString(), null)); - plugin.getScheduler().runTask(() -> plugin.getServer().getPluginManager().callEvent(transactionEvent)); - return commands.eval( - "local currentId=redis.call('hlen', KEYS[1]);" + //Get the current size of the hash - "redis.call('hset', KEYS[1], currentId, ARGV[1]);" + //Add the new transaction - "return currentId;", //Return the id of the new transaction - ScriptOutputType.INTEGER, - new String[]{NEW_TRANSACTIONS + accountOwner.toString()}, //Key rediseco:transactions:playerUUID - transactionEvent.getTransaction().toString()).thenApply(response -> { - if (RedisEconomyPlugin.getInstance().settings().debug) { - Bukkit.getLogger().info("03 Transaction for " + accountOwner + " saved in " + (System.currentTimeMillis() - init) + " ms with id " + response + " !"); - } - return ((Long) response).intValue(); - }).exceptionally(throwable -> { - throwable.printStackTrace(); - return null; - }); - } - ); + final String stackTrace = getCallerPluginString(); + executorService.submit(() -> { + TransactionEvent transactionEvent = new TransactionEvent(new Transaction( + accountOwner, + System.currentTimeMillis(), + target, //If target is null, it has been sent from the server + amount, currency.getCurrencyName(), reason + stackTrace, null)); + plugin.getScheduler().runTask(() -> plugin.getServer().getPluginManager().callEvent(transactionEvent)); + Long longResult = plugin.getCurrenciesManager().getRedisManager().getConnectionSync(commands -> commands.eval( + "local currentId=redis.call('hlen', KEYS[1]);" + //Get the current size of the hash + "redis.call('hset', KEYS[1], currentId, ARGV[1]);" + //Add the new transaction + "return currentId;", //Return the id of the new transaction + ScriptOutputType.INTEGER, + new String[]{NEW_TRANSACTIONS + accountOwner.toString()}, //Key rediseco:transactions:playerUUID + transactionEvent.getTransaction().toString())); + future.complete(longResult.intValue()); + }); + return future.thenApply(response -> { + if (RedisEconomyPlugin.getInstance().settings().debug) { + Bukkit.getLogger().info("03 Transaction for " + accountOwner + " saved in " + (System.currentTimeMillis() - init) + " ms with id " + response + " !"); + } + return response; + }); } /** @@ -244,16 +242,12 @@ public CompletionStage revertTransaction(AccountID accountOwner, int tr public String getCallerPluginString() { if (!plugin.settings().registerCalls) return ""; - StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); - for (int i = 3; i < stackTraceElements.length; i++) { - if (!stackTraceElements[i].getClassName().startsWith("org.bukkit") && - !stackTraceElements[i].getClassName().startsWith("dev.unnm3d.rediseconomy") && - !stackTraceElements[i].getClassName().startsWith("com.mojang") - ) { - return "\nCall: " + stackTraceElements[i].getClassName() + ":" + stackTraceElements[i].getMethodName(); - } - } - return ""; + return Arrays.stream(Thread.currentThread().getStackTrace()) + .skip(3) + .filter(s -> plugin.settings().callBlacklistRegex.stream().noneMatch(blRegex -> s.getClassName().matches(blRegex))) + .findFirst() + .map(ste -> "\nCall: " + ste.getClassName() + ":" + ste.getMethodName()) + .orElse(""); } } diff --git a/src/main/java/dev/unnm3d/rediseconomy/utils/DecimalUtils.java b/src/main/java/dev/unnm3d/rediseconomy/utils/DecimalUtils.java new file mode 100644 index 0000000..ff55498 --- /dev/null +++ b/src/main/java/dev/unnm3d/rediseconomy/utils/DecimalUtils.java @@ -0,0 +1,27 @@ +package dev.unnm3d.rediseconomy.utils; + +import dev.unnm3d.rediseconomy.RedisEconomyPlugin; +import lombok.experimental.UtilityClass; + +import java.text.DecimalFormat; + +@UtilityClass +public class DecimalUtils { + + public static String shortAmount(double amount, DecimalFormat decimalFormat) { + if (amount >= 1000000000000.0) { + return decimalFormat.format(amount / 1000000000000.0) + + RedisEconomyPlugin.getInstance().getConfigManager().getLangs().unitSymbols.trillion(); + } else if (amount >= 1000000000.0) { + return decimalFormat.format(amount / 1000000000.0) + + RedisEconomyPlugin.getInstance().getConfigManager().getLangs().unitSymbols.billion(); + } else if (amount >= 1000000.0) { + return decimalFormat.format(amount / 1000000.0) + + RedisEconomyPlugin.getInstance().getConfigManager().getLangs().unitSymbols.million(); + } else if (amount >= 1000.0) { + return decimalFormat.format(amount / 1000.0) + + RedisEconomyPlugin.getInstance().getConfigManager().getLangs().unitSymbols.thousand(); + } + return decimalFormat.format(amount); + } +} diff --git a/src/main/java/dev/unnm3d/rediseconomy/utils/PlaceholderAPIHook.java b/src/main/java/dev/unnm3d/rediseconomy/utils/PlaceholderAPIHook.java index e12ce45..d4a9b25 100644 --- a/src/main/java/dev/unnm3d/rediseconomy/utils/PlaceholderAPIHook.java +++ b/src/main/java/dev/unnm3d/rediseconomy/utils/PlaceholderAPIHook.java @@ -10,6 +10,7 @@ import org.bukkit.plugin.RegisteredServiceProvider; import org.jetbrains.annotations.NotNull; +import java.text.DecimalFormat; import java.util.*; @@ -29,7 +30,7 @@ public PlaceholderAPIHook(RedisEconomyPlugin redisEconomyPlugin) { this.langs = redisEconomyPlugin.langs(); this.totalSupplyCache = new HashMap<>(); this.baltopCache = new HashMap<>(); - this.updateCachePeriod = 1000 * 20; // 1 minute + this.updateCachePeriod = 1000 * 5; // 5 secs this.lastUpdateTimestamp = 0; this.plugin = redisEconomyPlugin; this.prefixProvider = redisEconomyPlugin.getServer().getServicesManager().getRegistration(net.milkbowl.vault.chat.Chat.class); @@ -109,43 +110,47 @@ public String onRequest(OfflinePlayer player, String params) { updatePlaceholdersCache(); - if (splitted.get(0).equals("totsupply")) { - - return parseParams(totalSupplyCache.get(currency), splitted, currency); - } else if (splitted.get(0).equals("top")) { - + switch (splitted.get(0)) { + case "totsupply" -> { + return parseParams(totalSupplyCache.get(currency), splitted, currency); + } + case "maxbal" -> { + return String.valueOf(currency.getPlayerMaxBalance(player.getUniqueId())); + } + case "top" -> { - if (splitted.size() < 3) return null; //Insufficient parameters + if (splitted.size() < 3) return null; //Insufficient parameters - List user_balance_strings = baltopCache.get(currency); - if (user_balance_strings == null) return null; + List user_balance_strings = baltopCache.get(currency); + if (user_balance_strings == null) return null; - if (splitted.get(1).equals("position")) {//rediseco_top_position_ - for (int i = 0; i < user_balance_strings.size(); i++) { - if (user_balance_strings.get(i)[2].equals(player.getName())) { - return String.valueOf(i + 1); + if (splitted.get(1).equals("position")) {//rediseco_top_position_ + for (int i = 0; i < user_balance_strings.size(); i++) { + if (user_balance_strings.get(i)[2].equals(player.getName())) { + return String.valueOf(i + 1); + } } + return "10+"; } - return "10+"; - } - int position = Integer.parseInt(splitted.get(1)); - if (position < 1 || position > 10) return "N/A"; //Invalid positions - if (user_balance_strings.size() < position) return "N/A"; + int position = Integer.parseInt(splitted.get(1)); + if (position < 1 || position > 10) return "N/A"; //Invalid positions + if (user_balance_strings.size() < position) return "N/A"; - switch (splitted.get(2)) { - case "playerprefix" -> { - return user_balance_strings.get(position - 1)[0]; - } - case "playersuffix" -> { - return user_balance_strings.get(position - 1)[1]; - } - case "name" -> { - return user_balance_strings.get(position - 1)[2]; - } - case "bal" -> { - double balance = Double.parseDouble(user_balance_strings.get(position - 1)[3]); - return parseParams(balance, splitted, currency); + switch (splitted.get(2)) { + case "playerprefix" -> { + return user_balance_strings.get(position - 1)[0]; + } + case "playersuffix" -> { + return user_balance_strings.get(position - 1)[1]; + } + case "name" -> { + return user_balance_strings.get(position - 1)[2]; + } + case "bal" -> { + double balance = Double.parseDouble(user_balance_strings.get(position - 1)[3]); + return parseParams(balance, splitted, currency); + } } } } @@ -158,15 +163,24 @@ private String parseParams(double amount, List splitted, Currency curren if (splitted.contains("short")) { if (amount >= 1000000000000.0) { - formattedNumber = currency.format(amount / 1000000000000.0) + langs.unitSymbols.trillion(); + formattedNumber = currency.getDecimalFormat().format(amount / 1000000000000.0) + langs.unitSymbols.trillion(); } else if (amount >= 1000000000.0) { - formattedNumber = currency.format(amount / 1000000000.0) + langs.unitSymbols.billion(); + formattedNumber = currency.getDecimalFormat().format(amount / 1000000000.0) + langs.unitSymbols.billion(); } else if (amount >= 1000000.0) { - formattedNumber = currency.format(amount / 1000000.0) + langs.unitSymbols.million(); + formattedNumber = currency.getDecimalFormat().format(amount / 1000000.0) + langs.unitSymbols.million(); } else if (amount >= 1000.0) { - formattedNumber = currency.format(amount / 1000.0) + langs.unitSymbols.thousand(); + formattedNumber = currency.getDecimalFormat().format(amount / 1000.0) + langs.unitSymbols.thousand(); } } + + formattedNumber = splitted.stream() + .filter(s -> s.startsWith("decformat")) + .findFirst() + .map(s -> s.split("decformat")) + .filter(splittedFormat -> splittedFormat.length > 1) + .map(splittedFormat -> new DecimalFormat(splittedFormat[1]).format(amount)) + .orElse(formattedNumber); + if (splitted.contains("formatted")) { if (amount == 1) formattedNumber += currency.getCurrencySingular(); diff --git a/src/main/resources/de-DE.yml b/src/main/resources/de-DE.yml new file mode 100644 index 0000000..3173812 --- /dev/null +++ b/src/main/resources/de-DE.yml @@ -0,0 +1,66 @@ +noConsole: Der Befehl muss inGame eingegeben werden! +noPermission: Du hast keine berechtigung, den Befehl zu nutzen! +missingArguments: Zu wenig! Argumente +playerNotFound: Spieler nicht gefunden! +invalidAmount: Ungültiger Betrag! +tooSmallAmount: Der Betrag ist zu klein! +payCooldown: Deine vorherige Zahlung ist noch in Bearbeitung! Bitte warten +invalidCurrency: Ungültige Währung! +insufficientFunds: Du hast nicht genügend Geld! +balance: Du hast %balance%! +balanceSet: Du hast das Geld von %player% auf %balance% gesetzt! +balanceOther: %player% hat %balance% ! +balanceTop: Die reichsten Spieler:
%prevpage% %page% %nextpage% +blockedAccounts: Gesperrte Accounts:
%list% +blockedAccountSuccess: Account %player% ist gesperrt! +unblockedAccountSuccess: Account %player% wurde entsperrt! +blockedPayment: Deine Zahlung an %player% wurde gesperrt! +balanceTopFormat: %pos% - %player% %balance% +paySelf: Du kannst nichts an dich selbst zahlen! +# Use %tax_percentage% for tax percentage and %tax_applied% for tax applied to the transaction. +paySuccess: Du hast %player% %amount% gezahlt +payFail: Zahlung fehlgeschlagen! +payReceived: Du hast %amount% from %player%! +purgeUserSuccess: %player% wurde gelöscht! +purgeBalanceSuccess: Die Währung von %player% wurde auf %currency% zurückgesetzt! +switchCurrencySuccess: %currency% zu %switch-currency%. gewechselt
Bitte + starten Sie jeden Server mit
installiertem RedisEconomy neu, um ein Überschreiben + zu vermeiden!
+noTransactionFound: Keine Transaktion von %player% gefunden! +incorrectDate: Falsches Datumsformat ! +transactionsStart: Transactionen von Spieler %player% von %after% bis %before%! +transactionsEnd: Die Prüfung von %player%'s Transactionen dauerte %time% + ms +transactionsArchiveCompleted: Archiviere %size% Transactiosdaten nach %file% +transactionsArchiveProgress: 'Archivierungsstatus: %progress%% ' +editMessageError: Dieser Config-Eintrag ist keine Zeichenfolge oder existiert + nicht! +editMessageClickHere: Klicke hier um die Nachricht zu bearbeiten + %field%! +editMessageSuccess: Erfolgreich gespeichert %field%! +transactionItem: + outgoingFunds: '#%id% Click + to copy:
%timestamp%">[Timestamp⌛] + [←Revert]
%account-owner% + > %amount%%symbol% > %other-account%
Reason: + %reason%' + incomingFunds: '#%id% Click + to copy:
%timestamp%">[Timestamp⌛] + [←Revert]
%other-account% > %amount%%symbol% + > %account-owner%
Reason: + %reason%' +unitSymbols: + thousand: k + million: m + billion: b + trillion: t + quadrillion: q +backupRestoreFinished: Backup/restore der Datei %file% beendet! +invalidPath: Ungültiger Pfad! + +# Authors: Unnm3d diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 397b99f..4e7f1fd 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -6,7 +6,8 @@ load: STARTUP depend: - Vault libraries: - - io.lettuce:lettuce-core:6.2.4.RELEASE + - io.lettuce:lettuce-core:6.4.0.RELEASE + - de.exlll:configlib-yaml:4.5.0 softdepend: - PlaceholderAPI - Essentials @@ -17,6 +18,7 @@ softdepend: - BetterEconomy - ArcaneEconomy - EasyConomy + - Towny folia-supported: true commands: pay: @@ -71,6 +73,9 @@ permissions: rediseconomy.pay: description: Allows to use /pay default: true + rediseconomy.payall: + description: Allows to use /pay * + default: false rediseconomy.toggle-payments: description: Allows to use /toggle-payments default: true @@ -104,9 +109,14 @@ permissions: rediseconomy.admin.transaction: description: Allows to use /transaction default: false + rediseconomy.admin.expandpool: + description: Allows to expand the pool + default: false rediseconomy.pay.*: - description: Allows to use /pay all currencies - default: op + description: Allows to use /pay all currencies + default: op + children: + rediseconomy.payall: true rediseconomy.balance.*: description: Allows to use /balance with other currencies default: op @@ -121,3 +131,4 @@ permissions: rediseconomy.admin.editmessage: true rediseconomy.admin.backup-restore: true rediseconomy.admin.archive-transactions: true + rediseconomy.admin.expandpool: true diff --git a/src/main/resources/tr-TR.yml b/src/main/resources/tr-TR.yml new file mode 100644 index 0000000..9153add --- /dev/null +++ b/src/main/resources/tr-TR.yml @@ -0,0 +1,43 @@ +#Turkish language file by WinTone01 +noConsole: Oyuncu olmalısınız, bu komutu kullanmak için! +noPermission: Bu komutu kullanma izniniz yok! +missingArguments: Komut için eksik parametreler! +playerNotFound: Oyuncu bulunamadı! +invalidAmount: Girilen değer geçersiz! +invalidCurrency: Geçersiz para birimi! +insufficientFunds: Yeterli bakiyeniz yok! +balance: Bakiyeniz %balance%! +balanceSet: %player% adlı oyuncunun hesabına %balance% eklediniz! +balanceOther: %player%'in bakiyesi %balance%! +balanceTop: En zengin oyuncular:
%prevpage% %page% %nextpage% +balanceTopFormat: %pos% - %player% %balance% +paySelf: Kendinize ödeme yapamazsınız! +paySuccess: %amount% tutarındaki ödemeyi %player% adlı oyuncuya yaptınız. Komisyon oranı %tax_percentage% (%tax_applied%)! +payFail: Ödeme başarısız oldu! +payReceived: %player% adlı oyuncudan %amount% tutarında ödeme aldınız! +purgeUserSuccess: %player%'e eşleşen kullanıcı temizlendi! +switchCurrencySuccess: %currency% başarıyla %switch-currency% ile değiştirildi. Lütfen çakışmaları önlemek için RedisEconomy'nin her örneğini yeniden başlatın! +noTransactionFound: %player% adlı oyuncu için herhangi bir işlem bulunamadı! +transactionsStart: %player%'in işlemleri %after% - %before% arasında! +transactionsEnd: %player%'in işlemleri %time% ms içinde sona erdi +editMessageError: Bu yapılandırma alanı mevcut değil veya bir dize değil! +editMessageClickHere: %field% düzenlemek için buraya tıklayın! +editMessageSuccess: %field% başarıyla kaydedildi! +transactionItem: + outgoingFunds: '#%id% Kopyalamak için tıkla:
%timestamp%">[Zaman Damgası⌛] + [←Geri Al]
%account-owner% + > %amount%%symbol% > %other-account%
Neden: + %reason%' + incomingFunds: '#%id% Kopyalamak için tıkla:
%timestamp%">[Zaman Damgası⌛] + [←Geri Al]
%other-account% > %amount%%symbol% + > %account-owner%
Neden: + %reason%' +unitSymbols: + thousand: k + million: m + billion: M + trillion: T + quadrillion: Q