diff --git a/api/src/main/java/dev/xhyrom/lighteco/api/LightEco.java b/api/src/main/java/dev/xhyrom/lighteco/api/LightEco.java index 3732b3a..451c6e7 100644 --- a/api/src/main/java/dev/xhyrom/lighteco/api/LightEco.java +++ b/api/src/main/java/dev/xhyrom/lighteco/api/LightEco.java @@ -16,10 +16,33 @@ public interface LightEco { */ @NonNull Platform getPlatform(); + /** + * Gets the {@link UserManager}, which manages the users. + * + * @return the user manager + */ @NonNull UserManager getUserManager(); + /** + * Gets the {@link CurrencyManager}, which manages the currencies. + * + * @return the currency manager + */ @NonNull CurrencyManager getCurrencyManager(); + + /** + * Gets the {@link CommandManager}, which manages the commands. + * + * @return the command manager + */ @NonNull CommandManager getCommandManager(); + /** + * Gets the {@link PlayerAdapter} for a player class. + * + * @param playerClass the player class + * @return the player adapter + * @param the player class + */ @NonNull PlayerAdapter getPlayerAdapter(@NonNull Class playerClass); } diff --git a/api/src/main/java/dev/xhyrom/lighteco/api/LightEcoProvider.java b/api/src/main/java/dev/xhyrom/lighteco/api/LightEcoProvider.java index c5bdfee..b321054 100644 --- a/api/src/main/java/dev/xhyrom/lighteco/api/LightEcoProvider.java +++ b/api/src/main/java/dev/xhyrom/lighteco/api/LightEcoProvider.java @@ -13,7 +13,7 @@ public final class LightEcoProvider { LightEco instance = LightEcoProvider.instance; if (instance == null) { throw new NotLoadedException(); - }; + } return instance; } @@ -22,7 +22,7 @@ public final class LightEcoProvider { public static void set(LightEco instance) { if (LightEcoProvider.instance != null) { throw new IllegalStateException("LightEco is already loaded!"); - }; + } LightEcoProvider.instance = instance; } diff --git a/api/src/main/java/dev/xhyrom/lighteco/api/manager/CommandManager.java b/api/src/main/java/dev/xhyrom/lighteco/api/manager/CommandManager.java index 34c0550..9d7401f 100644 --- a/api/src/main/java/dev/xhyrom/lighteco/api/manager/CommandManager.java +++ b/api/src/main/java/dev/xhyrom/lighteco/api/manager/CommandManager.java @@ -5,6 +5,23 @@ import org.checkerframework.checker.nullness.qual.NonNull; public interface CommandManager { // Make more transparent and freedom way to do this (more abstract) + + /** + * Registers commands for a currency. + * + * @param currency the currency to register the command for + */ void registerCurrencyCommand(@NonNull Currency currency); + + /** + * Registers commands for a currency. + *

+ * If main is true, the pay and balance commands will be exposed as main commands. + * You can also use {@link #registerCurrencyCommand(Currency)} to register the commands as subcommands. + *

+ * + * @param currency the currency to register the command for + * @param main if pay and balance should be exposed as main commands + */ void registerCurrencyCommand(@NonNull Currency currency, boolean main); } diff --git a/api/src/main/java/dev/xhyrom/lighteco/api/manager/ContextManager.java b/api/src/main/java/dev/xhyrom/lighteco/api/manager/ContextManager.java index 6f0643f..b24ec8b 100644 --- a/api/src/main/java/dev/xhyrom/lighteco/api/manager/ContextManager.java +++ b/api/src/main/java/dev/xhyrom/lighteco/api/manager/ContextManager.java @@ -5,6 +5,18 @@ import org.checkerframework.checker.nullness.qual.NonNull; import java.util.UUID; public interface ContextManager { + /** + * Gets the unique id of a player. + * + * @param context the context to get the unique id of + * @return the unique id of the player + */ @NonNull UUID getPlayerUniqueId(@NonNull T context); + + /** + * Gets the player class of the platform. + * + * @return the player class + */ @NonNull Class getPlayerClass(); } diff --git a/api/src/main/java/dev/xhyrom/lighteco/api/manager/CurrencyManager.java b/api/src/main/java/dev/xhyrom/lighteco/api/manager/CurrencyManager.java index 89c0c4c..db35d2f 100644 --- a/api/src/main/java/dev/xhyrom/lighteco/api/manager/CurrencyManager.java +++ b/api/src/main/java/dev/xhyrom/lighteco/api/manager/CurrencyManager.java @@ -3,16 +3,42 @@ package dev.xhyrom.lighteco.api.manager; import dev.xhyrom.lighteco.api.model.currency.Currency; import dev.xhyrom.lighteco.api.model.user.User; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Collection; import java.util.List; +import java.util.concurrent.CompletableFuture; public interface CurrencyManager { + /** + * Gets all registered currencies. + * + * @return a collection of all registered currencies + */ @NonNull Collection getRegisteredCurrencies(); - Currency getCurrency(@NonNull String identifier); + /** + * Gets a currency by its identifier. + * + * @param identifier the identifier of the currency + * @return the currency, or null if not found + */ + @Nullable Currency getCurrency(@NonNull String identifier); + /** + * Registers a currency. + * + * @param currency the currency to register + */ void registerCurrency(@NonNull Currency currency); - List getTopUsers(@NonNull Currency currency, int length); + /** + * Gets the top users for a currency. + * + * @implNote This method is not cached. It fetches the data from the database every time it is called. + * @param currency the currency to get the top users for + * @param length the length of the list + * @return a future that completes with the top users (sorted from highest to lowest balance) + */ + CompletableFuture> getTopUsers(@NonNull Currency currency, int length); } diff --git a/api/src/main/java/dev/xhyrom/lighteco/api/manager/UserManager.java b/api/src/main/java/dev/xhyrom/lighteco/api/manager/UserManager.java index 9a5d855..eb02103 100644 --- a/api/src/main/java/dev/xhyrom/lighteco/api/manager/UserManager.java +++ b/api/src/main/java/dev/xhyrom/lighteco/api/manager/UserManager.java @@ -13,6 +13,20 @@ public interface UserManager { @NonNull CompletableFuture saveUser(@NonNull User user); + /** + * Saves multiple users at once. + *

+ * Uses TRANSACTION clause under the hood. + * If one of the users fails to save, the entire transaction will be rolled back. + * This is to ensure data integrity. + * If you want to save users individually, use {@link #saveUser(User)} instead. + *

+ * + * @param users the users to save + * @return a future that completes when the users have been saved + */ + @NonNull CompletableFuture saveUsers(@NonNull User... users); + @Nullable User getUser(@NonNull UUID uniqueId); boolean isLoaded(@NonNull UUID uniqueId); diff --git a/api/src/main/java/dev/xhyrom/lighteco/api/model/currency/Currency.java b/api/src/main/java/dev/xhyrom/lighteco/api/model/currency/Currency.java index 34a19fe..8f06741 100644 --- a/api/src/main/java/dev/xhyrom/lighteco/api/model/currency/Currency.java +++ b/api/src/main/java/dev/xhyrom/lighteco/api/model/currency/Currency.java @@ -3,32 +3,57 @@ package dev.xhyrom.lighteco.api.model.currency; import dev.xhyrom.lighteco.api.model.user.User; import java.math.BigDecimal; +import java.text.NumberFormat; public interface Currency { - String getIdentifier(); /** - * Get the type of the currency, either {@link Type#LOCAL} or {@link Type#GLOBAL} + * Returns the identifier of the currency. + * + * @return the identifier + */ + String getIdentifier(); + + /** + * Returns the type of the currency, either {@link Type#LOCAL} or {@link Type#GLOBAL} * * @see Type * @return The type of the currency */ Type getType(); + /** + * Format the given amount to a string + * + * @param amount The amount to format + * @return The formatted amount + */ + default String format(BigDecimal amount) { + NumberFormat format = NumberFormat.getInstance(); + format.setMaximumFractionDigits(fractionalDigits()); + + return format.format(amount); + } + + /** + * Determine if this currency is payable + * + * @return `true` if this currency is payable, `false` otherwise + */ boolean isPayable(); /** - * Get the number of fractional digits this currency has + * Returns the number of fractional digits this currency has * * @return The number of fractional digits */ default int fractionalDigits() { return 0; - }; + } /** - * Get the users that have a balance in this currency + * Returns the default balance for this currency * - * @return The users + * @return The default balance */ BigDecimal getDefaultBalance(); @@ -46,7 +71,7 @@ public interface Currency { /** * Represents the type of currency */ - public enum Type { + enum Type { /** * A currency that is only available on a single server */ diff --git a/api/src/main/java/dev/xhyrom/lighteco/api/model/user/User.java b/api/src/main/java/dev/xhyrom/lighteco/api/model/user/User.java index bf937d8..715dada 100644 --- a/api/src/main/java/dev/xhyrom/lighteco/api/model/user/User.java +++ b/api/src/main/java/dev/xhyrom/lighteco/api/model/user/User.java @@ -32,6 +32,10 @@ public interface User { /** * Set the balance of this user for the specified currency. + *

+ * Save the user after setting the balance using {@link dev.xhyrom.lighteco.api.manager.UserManager#saveUser(User)}.
+ * If you're doing multiple changes, use {@link dev.xhyrom.lighteco.api.manager.UserManager#saveUsers(User...)} + *

* * @param currency the currency * @param balance the balance diff --git a/api/src/main/java/dev/xhyrom/lighteco/api/platform/Platform.java b/api/src/main/java/dev/xhyrom/lighteco/api/platform/Platform.java index a2aeee2..0a77057 100644 --- a/api/src/main/java/dev/xhyrom/lighteco/api/platform/Platform.java +++ b/api/src/main/java/dev/xhyrom/lighteco/api/platform/Platform.java @@ -3,6 +3,11 @@ package dev.xhyrom.lighteco.api.platform; import org.checkerframework.checker.nullness.qual.NonNull; public interface Platform { + /** + * Get the type of this platform. + * + * @return + */ @NonNull Type getType(); enum Type { diff --git a/api/src/main/java/dev/xhyrom/lighteco/api/platform/PlayerAdapter.java b/api/src/main/java/dev/xhyrom/lighteco/api/platform/PlayerAdapter.java index 4ceebb9..ddd8efc 100644 --- a/api/src/main/java/dev/xhyrom/lighteco/api/platform/PlayerAdapter.java +++ b/api/src/main/java/dev/xhyrom/lighteco/api/platform/PlayerAdapter.java @@ -6,7 +6,19 @@ import org.checkerframework.checker.nullness.qual.NonNull; import java.util.concurrent.CompletableFuture; public interface PlayerAdapter { + /** + * Gets the user of a player. + * + * @param player the player to get the user of + * @return the user + */ @NonNull User getUser(@NonNull T player); + /** + * Loads the user of a player. + * + * @param player the player to load the user of + * @return a future that completes with the user + */ @NonNull CompletableFuture loadUser(@NonNull T player); } diff --git a/api/src/main/java/dev/xhyrom/lighteco/api/storage/StorageProvider.java b/api/src/main/java/dev/xhyrom/lighteco/api/storage/StorageProvider.java index 2bedb77..fbc7732 100644 --- a/api/src/main/java/dev/xhyrom/lighteco/api/storage/StorageProvider.java +++ b/api/src/main/java/dev/xhyrom/lighteco/api/storage/StorageProvider.java @@ -1,15 +1,31 @@ package dev.xhyrom.lighteco.api.storage; +import dev.xhyrom.lighteco.api.model.currency.Currency; import dev.xhyrom.lighteco.api.model.user.User; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.List; import java.util.UUID; public interface StorageProvider { + /** + * Initialize the storage provider. + * + * @throws Exception + */ void init() throws Exception; + + /** + * Shutdown the storage provider. + * + * @throws Exception + */ void shutdown() throws Exception; @NonNull User loadUser(@NonNull UUID uniqueId, @Nullable String username) throws Exception; void saveUser(@NonNull User user) throws Exception; + void saveUsers(@NonNull User... users) throws Exception; + + @NonNull List getTopUsers(Currency currency, int length) throws Exception; } diff --git a/bukkittest/src/main/java/dev/xhyrom/lighteco/bukkittest/TestCurrency2.java b/bukkittest/src/main/java/dev/xhyrom/lighteco/bukkittest/TestCurrency2.java index 2b961ac..63de38a 100644 --- a/bukkittest/src/main/java/dev/xhyrom/lighteco/bukkittest/TestCurrency2.java +++ b/bukkittest/src/main/java/dev/xhyrom/lighteco/bukkittest/TestCurrency2.java @@ -18,7 +18,7 @@ public class TestCurrency2 implements Currency { @Override public boolean isPayable() { - return false; + return true; } @Override diff --git a/bukkittest/src/main/java/dev/xhyrom/lighteco/bukkittest/TestPlugin.java b/bukkittest/src/main/java/dev/xhyrom/lighteco/bukkittest/TestPlugin.java index f98d847..45f5d96 100644 --- a/bukkittest/src/main/java/dev/xhyrom/lighteco/bukkittest/TestPlugin.java +++ b/bukkittest/src/main/java/dev/xhyrom/lighteco/bukkittest/TestPlugin.java @@ -3,9 +3,17 @@ package dev.xhyrom.lighteco.bukkittest; import dev.xhyrom.lighteco.api.LightEco; import dev.xhyrom.lighteco.api.LightEcoProvider; import dev.xhyrom.lighteco.api.manager.CurrencyManager; +import dev.xhyrom.lighteco.api.model.user.User; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerChatEvent; import org.bukkit.plugin.java.JavaPlugin; -public class TestPlugin extends JavaPlugin { +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class TestPlugin extends JavaPlugin implements Listener { @Override public void onEnable() { getLogger().info("TestPlugin loaded!"); @@ -15,6 +23,7 @@ public class TestPlugin extends JavaPlugin { currencyManager.registerCurrency(new TestCurrency()); currencyManager.registerCurrency(new TestCurrency2()); + getServer().getPluginManager().registerEvents(this, this); getLogger().info("TestCurrency registered!"); @@ -23,4 +32,15 @@ public class TestPlugin extends JavaPlugin { provider.getCommandManager().registerCurrencyCommand(currencyManager.getCurrency("test")); provider.getCommandManager().registerCurrencyCommand(currencyManager.getCurrency("test2")); } + + @EventHandler + public void onChat(AsyncPlayerChatEvent event) { + LightEco provider = LightEcoProvider.get(); + CurrencyManager currencyManager = provider.getCurrencyManager(); + CompletableFuture> topusers = currencyManager.getTopUsers(currencyManager.getCurrency("money"), 5); + + for (User user : topusers.join()) { + event.getPlayer().sendMessage(user.getUniqueId() + " ("+ user.getUsername() +") " + ": " + user.getBalance(currencyManager.getCurrency("money"))); + } + } } diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/api/impl/ApiCurrencyManager.java b/common/src/main/java/dev/xhyrom/lighteco/common/api/impl/ApiCurrencyManager.java index 4f19d76..8ed1e56 100644 --- a/common/src/main/java/dev/xhyrom/lighteco/common/api/impl/ApiCurrencyManager.java +++ b/common/src/main/java/dev/xhyrom/lighteco/common/api/impl/ApiCurrencyManager.java @@ -8,7 +8,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Collection; import java.util.List; -import java.util.stream.Collectors; +import java.util.concurrent.CompletableFuture; public class ApiCurrencyManager extends ApiAbstractManager implements CurrencyManager { public ApiCurrencyManager(LightEcoPlugin plugin, dev.xhyrom.lighteco.common.manager.currency.CurrencyManager handler) { @@ -38,10 +38,8 @@ public class ApiCurrencyManager extends ApiAbstractManager getTopUsers(@NonNull Currency currency, int length) { + public CompletableFuture> getTopUsers(@NonNull Currency currency, int length) { dev.xhyrom.lighteco.common.model.currency.Currency internal = this.handler.getIfLoaded(currency.getIdentifier()); - return this.handler.getTopUsers(internal, length) - .stream().map(dev.xhyrom.lighteco.common.model.user.User::getProxy) - .collect(Collectors.toList()); + return this.handler.getTopUsers(internal, length); } } diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/api/impl/ApiUserManager.java b/common/src/main/java/dev/xhyrom/lighteco/common/api/impl/ApiUserManager.java index 4c0a495..91915d2 100644 --- a/common/src/main/java/dev/xhyrom/lighteco/common/api/impl/ApiUserManager.java +++ b/common/src/main/java/dev/xhyrom/lighteco/common/api/impl/ApiUserManager.java @@ -5,6 +5,7 @@ import dev.xhyrom.lighteco.api.model.user.User; import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -34,6 +35,11 @@ public class ApiUserManager extends ApiAbstractManager saveUsers(@NotNull @NonNull User... users) { + return this.plugin.getStorage().saveUsers(users); + } + @Override public @Nullable User getUser(@NonNull UUID uniqueId) { return wrap(this.handler.getIfLoaded(uniqueId)); diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/config/message/CurrencyMessageConfig.java b/common/src/main/java/dev/xhyrom/lighteco/common/config/message/CurrencyMessageConfig.java index 4f48fea..3bc4c4b 100644 --- a/common/src/main/java/dev/xhyrom/lighteco/common/config/message/CurrencyMessageConfig.java +++ b/common/src/main/java/dev/xhyrom/lighteco/common/config/message/CurrencyMessageConfig.java @@ -12,13 +12,10 @@ public class CurrencyMessageConfig extends OkaeriConfig { public String give = " | Gave | "; public String take = " | Took from "; - public String pay = " | Paid to ( after tax)"; + public String pay = " | Paid to "; + public String payWithTax = " | Paid to ( after tax)"; public String wait = "Please wait a moment before using this command again."; public String notEnoughMoney = "You don't have enough money!"; - public String cannotSetNegative = "Cannot set negative money!"; - public String cannotGiveNegative = "Cannot give negative money!"; - public String cannotTakeNegative = "Cannot take negative money!"; - public String cannotPayNegative = "Cannot pay negative money!"; public String cannotPaySelf = "You cannot pay yourself!"; } diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/config/storage/StorageDataConfig.java b/common/src/main/java/dev/xhyrom/lighteco/common/config/storage/StorageDataConfig.java index 0ff6d3c..614eef6 100644 --- a/common/src/main/java/dev/xhyrom/lighteco/common/config/storage/StorageDataConfig.java +++ b/common/src/main/java/dev/xhyrom/lighteco/common/config/storage/StorageDataConfig.java @@ -13,4 +13,19 @@ public class StorageDataConfig extends OkaeriConfig { @Comment("Credentials for connecting to the database.") public String username = "root"; public String password = "password"; + + @Comment("Maximum number of connections in the pool.") + public int maximumPoolSize = 10; + + @Comment("Minimum idle connections in the pool.") + public int minimumIdle = 5; + + @Comment("Maximum lifetime of a connection in the pool.") + public long maxLifetime = 1800000; + + @Comment("Keep alive interval in milliseconds.") + public long keepAliveTime = 0; + + @Comment("Connection timeout in milliseconds.") + public long connectionTimeout = 5000; } diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/manager/ConcurrentManager.java b/common/src/main/java/dev/xhyrom/lighteco/common/manager/ConcurrentManager.java new file mode 100644 index 0000000..530bcf6 --- /dev/null +++ b/common/src/main/java/dev/xhyrom/lighteco/common/manager/ConcurrentManager.java @@ -0,0 +1,39 @@ +package dev.xhyrom.lighteco.common.manager; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public abstract class ConcurrentManager implements Manager { + public final Map map = new ConcurrentHashMap<>(); + + @Override + public Collection keys() { + return this.map.keySet(); + } + + @Override + public Collection values() { + return this.map.values(); + } + + @Override + public T getOrMake(I identifier) { + return this.map.computeIfAbsent(identifier, this::apply); + } + + @Override + public T getIfLoaded(I identifier) { + return this.map.get(identifier); + } + + @Override + public boolean isLoaded(I identifier) { + return this.map.containsKey(identifier); + } + + @Override + public void unload(I identifier) { + this.map.remove(identifier); + } +} diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/manager/Manager.java b/common/src/main/java/dev/xhyrom/lighteco/common/manager/Manager.java index 3b20cb8..7734c22 100644 --- a/common/src/main/java/dev/xhyrom/lighteco/common/manager/Manager.java +++ b/common/src/main/java/dev/xhyrom/lighteco/common/manager/Manager.java @@ -15,6 +15,4 @@ public interface Manager { boolean isLoaded(I identifier); void unload(I identifier); - - void unload(Collection identifiers); } diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/manager/AbstractManager.java b/common/src/main/java/dev/xhyrom/lighteco/common/manager/SingleManager.java similarity index 80% rename from common/src/main/java/dev/xhyrom/lighteco/common/manager/AbstractManager.java rename to common/src/main/java/dev/xhyrom/lighteco/common/manager/SingleManager.java index 024b202..528e637 100644 --- a/common/src/main/java/dev/xhyrom/lighteco/common/manager/AbstractManager.java +++ b/common/src/main/java/dev/xhyrom/lighteco/common/manager/SingleManager.java @@ -4,7 +4,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; -public abstract class AbstractManager implements Manager { +public abstract class SingleManager implements Manager { public final Map map = new HashMap<>(); @Override @@ -36,9 +36,4 @@ public abstract class AbstractManager implements Manager { public void unload(I identifier) { this.map.remove(identifier); } - - @Override - public void unload(Collection identifiers) { - identifiers.forEach(this::unload); - } } diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/manager/command/AbstractCommandManager.java b/common/src/main/java/dev/xhyrom/lighteco/common/manager/command/AbstractCommandManager.java index f48cc80..2a815a1 100644 --- a/common/src/main/java/dev/xhyrom/lighteco/common/manager/command/AbstractCommandManager.java +++ b/common/src/main/java/dev/xhyrom/lighteco/common/manager/command/AbstractCommandManager.java @@ -13,7 +13,6 @@ import java.math.RoundingMode; import java.util.ArrayList; import java.util.Map; import java.util.UUID; -import java.util.concurrent.CompletableFuture; public abstract class AbstractCommandManager implements CommandManager { public final LightEcoPlugin plugin; @@ -102,16 +101,7 @@ public abstract class AbstractCommandManager implements CommandManager { addToMustWait(sender.getUniqueId(), target.getUniqueId()); amount = amount.setScale(currency.getProxy().fractionalDigits(), RoundingMode.DOWN); - try { - target.setBalance(currency, amount); - } catch (IllegalArgumentException e) { - sender.sendMessage( - miniMessage.deserialize(this.getConfig(currency).cannotSetNegative) - ); - - removeFromMustWait(target.getUniqueId(), sender.getUniqueId()); - return; - } + target.setBalance(currency, amount); sender.sendMessage( miniMessage.deserialize( @@ -131,16 +121,7 @@ public abstract class AbstractCommandManager implements CommandManager { addToMustWait(sender.getUniqueId(), target.getUniqueId()); amount = amount.setScale(currency.getProxy().fractionalDigits(), RoundingMode.DOWN); - try { - target.deposit(currency, amount); - } catch (IllegalArgumentException e) { - sender.sendMessage( - miniMessage.deserialize(this.getConfig(currency).cannotGiveNegative) - ); - - removeFromMustWait(target.getUniqueId(), sender.getUniqueId()); - return; - } + target.deposit(currency, amount); sender.sendMessage( miniMessage.deserialize( @@ -161,16 +142,7 @@ public abstract class AbstractCommandManager implements CommandManager { addToMustWait(sender.getUniqueId(), target.getUniqueId()); amount = amount.setScale(currency.getProxy().fractionalDigits(), RoundingMode.DOWN); - try { - target.withdraw(currency, amount); - } catch (IllegalArgumentException e) { - sender.sendMessage( - miniMessage.deserialize(this.getConfig(currency).cannotTakeNegative) - ); - - removeFromMustWait(target.getUniqueId(), sender.getUniqueId()); - return; - } + target.withdraw(currency, amount); sender.sendMessage( miniMessage.deserialize( @@ -196,14 +168,6 @@ public abstract class AbstractCommandManager implements CommandManager { return; } - if (amount.compareTo(BigDecimal.ZERO) < 0) { - sender.sendMessage( - miniMessage.deserialize(this.getConfig(currency).cannotPayNegative) - ); - - return; - } - amount = amount.setScale(currency.getProxy().fractionalDigits(), RoundingMode.DOWN); User user = this.plugin.getUserManager().getIfLoaded(sender.getUniqueId()); @@ -220,6 +184,7 @@ public abstract class AbstractCommandManager implements CommandManager { // calculate tax using Currency#calculateTax BigDecimal tax = currency.getProxy().calculateTax(user.getProxy(), amount); + tax = tax.setScale(currency.getProxy().fractionalDigits(), RoundingMode.DOWN); // subtract tax from amount BigDecimal taxedAmount = amount.subtract(tax); @@ -227,9 +192,14 @@ public abstract class AbstractCommandManager implements CommandManager { target.deposit(currency, taxedAmount); user.withdraw(currency, amount); + String template = tax.compareTo(BigDecimal.ZERO) > 0 + ? this.getConfig(currency).payWithTax + : this.getConfig(currency).pay; + + sender.sendMessage( miniMessage.deserialize( - this.getConfig(currency).pay, + template, Placeholder.parsed("currency", currency.getIdentifier()), Placeholder.parsed("target", target.getUsername()), Placeholder.parsed("amount", amount.toPlainString()), @@ -239,9 +209,7 @@ public abstract class AbstractCommandManager implements CommandManager { ) ); - CompletableFuture.allOf( - this.plugin.getUserManager().saveUser(user), - this.plugin.getUserManager().saveUser(target) - ).thenAccept(v -> removeFromMustWait(sender.getUniqueId(), target.getUniqueId())); + this.plugin.getUserManager().saveUsers(user, target) + .thenAccept(v -> removeFromMustWait(sender.getUniqueId(), target.getUniqueId())); } } diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/manager/currency/CurrencyManager.java b/common/src/main/java/dev/xhyrom/lighteco/common/manager/currency/CurrencyManager.java index 632c2e1..350669f 100644 --- a/common/src/main/java/dev/xhyrom/lighteco/common/manager/currency/CurrencyManager.java +++ b/common/src/main/java/dev/xhyrom/lighteco/common/manager/currency/CurrencyManager.java @@ -1,17 +1,18 @@ package dev.xhyrom.lighteco.common.manager.currency; +import dev.xhyrom.lighteco.api.model.user.User; import dev.xhyrom.lighteco.common.manager.Manager; import dev.xhyrom.lighteco.common.model.currency.Currency; -import dev.xhyrom.lighteco.common.model.user.User; import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Collection; import java.util.List; +import java.util.concurrent.CompletableFuture; public interface CurrencyManager extends Manager { @NonNull Collection getRegisteredCurrencies(); void registerCurrency(@NonNull Currency currency); - List getTopUsers(@NonNull Currency currency, int length); + CompletableFuture> getTopUsers(@NonNull Currency currency, int length); } diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/manager/currency/StandardCurrencyManager.java b/common/src/main/java/dev/xhyrom/lighteco/common/manager/currency/StandardCurrencyManager.java index 2064c5d..1577d11 100644 --- a/common/src/main/java/dev/xhyrom/lighteco/common/manager/currency/StandardCurrencyManager.java +++ b/common/src/main/java/dev/xhyrom/lighteco/common/manager/currency/StandardCurrencyManager.java @@ -1,15 +1,16 @@ package dev.xhyrom.lighteco.common.manager.currency; -import dev.xhyrom.lighteco.common.manager.AbstractManager; +import dev.xhyrom.lighteco.api.model.user.User; +import dev.xhyrom.lighteco.common.manager.SingleManager; import dev.xhyrom.lighteco.common.model.currency.Currency; -import dev.xhyrom.lighteco.common.model.user.User; import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin; import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Collection; import java.util.List; +import java.util.concurrent.CompletableFuture; -public class StandardCurrencyManager extends AbstractManager implements CurrencyManager { +public class StandardCurrencyManager extends SingleManager implements CurrencyManager { private final LightEcoPlugin plugin; public StandardCurrencyManager(LightEcoPlugin plugin) { @@ -18,7 +19,7 @@ public class StandardCurrencyManager extends AbstractManager i @Override public Currency apply(String identifier) { - return null; + throw new UnsupportedOperationException(); } @Override @@ -39,9 +40,8 @@ public class StandardCurrencyManager extends AbstractManager i this.map.put(currency.getIdentifier(), currency); } - // TODO: finish @Override - public List getTopUsers(@NonNull Currency currency, int length) { - return null; + public CompletableFuture> getTopUsers(@NonNull Currency currency, int length) { + return this.plugin.getStorage().getTopUsers(currency.getProxy(), length); } } diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/manager/user/StandardUserManager.java b/common/src/main/java/dev/xhyrom/lighteco/common/manager/user/StandardUserManager.java index dc61424..3735de0 100644 --- a/common/src/main/java/dev/xhyrom/lighteco/common/manager/user/StandardUserManager.java +++ b/common/src/main/java/dev/xhyrom/lighteco/common/manager/user/StandardUserManager.java @@ -1,15 +1,14 @@ package dev.xhyrom.lighteco.common.manager.user; -import dev.xhyrom.lighteco.common.manager.AbstractManager; +import dev.xhyrom.lighteco.common.manager.ConcurrentManager; import dev.xhyrom.lighteco.common.model.user.User; import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin; -import java.util.HashSet; -import java.util.Set; +import java.util.Arrays; import java.util.UUID; import java.util.concurrent.CompletableFuture; -public class StandardUserManager extends AbstractManager implements UserManager { +public class StandardUserManager extends ConcurrentManager implements UserManager { private final LightEcoPlugin plugin; public StandardUserManager(LightEcoPlugin plugin) { @@ -31,23 +30,17 @@ public class StandardUserManager extends AbstractManager implements return this.plugin.getStorage().loadUser(uniqueId, username); } - @Override - public CompletableFuture load() { - Set uniqueIds = new HashSet<>(keys()); - uniqueIds.addAll(this.plugin.getBootstrap().getOnlinePlayers()); - - return uniqueIds.stream() - .map(id -> this.plugin.getStorage().loadUser(id)) - .collect(CompletableFuture::allOf, (future, userFuture) -> future.join(), (future, userFuture) -> future.join()); - } - @Override public CompletableFuture saveUser(User user) { return this.plugin.getStorage().saveUser(user.getProxy()); } @Override - public void invalidateCaches() { - values().forEach(User::invalidateCaches); + public CompletableFuture saveUsers(User... users) { + return this.plugin.getStorage().saveUsers( + Arrays.stream(users) + .map(User::getProxy) + .toArray(dev.xhyrom.lighteco.api.model.user.User[]::new) + ); } } \ No newline at end of file diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/manager/user/UserManager.java b/common/src/main/java/dev/xhyrom/lighteco/common/manager/user/UserManager.java index 917f91a..b5cb08c 100644 --- a/common/src/main/java/dev/xhyrom/lighteco/common/manager/user/UserManager.java +++ b/common/src/main/java/dev/xhyrom/lighteco/common/manager/user/UserManager.java @@ -8,11 +8,8 @@ import java.util.concurrent.CompletableFuture; public interface UserManager extends Manager { CompletableFuture saveUser(User user); - - CompletableFuture load(); + CompletableFuture saveUsers(User... users); CompletableFuture loadUser(UUID uniqueId); CompletableFuture loadUser(UUID uniqueId, String username); - - void invalidateCaches(); } diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/storage/Storage.java b/common/src/main/java/dev/xhyrom/lighteco/common/storage/Storage.java index d9c82e9..18a0b06 100644 --- a/common/src/main/java/dev/xhyrom/lighteco/common/storage/Storage.java +++ b/common/src/main/java/dev/xhyrom/lighteco/common/storage/Storage.java @@ -5,6 +5,8 @@ import dev.xhyrom.lighteco.api.storage.StorageProvider; import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin; import dev.xhyrom.lighteco.common.util.ThrowableRunnable; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; @@ -80,4 +82,14 @@ public class Storage { public CompletableFuture saveUser(dev.xhyrom.lighteco.api.model.user.User user) { return future(() -> this.provider.saveUser(user)); } + + public CompletableFuture saveUsers(dev.xhyrom.lighteco.api.model.user.User... users) { + return future(() -> this.provider.saveUsers(users)); + } + + // Return ApiUser instead of User + // We don't do anything with this + public CompletableFuture> getTopUsers(dev.xhyrom.lighteco.api.model.currency.Currency currency, int length) { + return future(() -> this.provider.getTopUsers(currency, length)); + } } diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/storage/provider/memory/MemoryStorageProvider.java b/common/src/main/java/dev/xhyrom/lighteco/common/storage/provider/memory/MemoryStorageProvider.java index 3337af6..607c99b 100644 --- a/common/src/main/java/dev/xhyrom/lighteco/common/storage/provider/memory/MemoryStorageProvider.java +++ b/common/src/main/java/dev/xhyrom/lighteco/common/storage/provider/memory/MemoryStorageProvider.java @@ -1,12 +1,17 @@ package dev.xhyrom.lighteco.common.storage.provider.memory; +import dev.xhyrom.lighteco.api.model.currency.Currency; import dev.xhyrom.lighteco.api.model.user.User; import dev.xhyrom.lighteco.api.storage.StorageProvider; import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; +import java.math.BigDecimal; import java.util.HashMap; +import java.util.List; +import java.util.Set; import java.util.UUID; public class MemoryStorageProvider implements StorageProvider { @@ -41,6 +46,23 @@ public class MemoryStorageProvider implements StorageProvider { this.userDatabase.put(user.getUniqueId(), user); } + @Override + public void saveUsers(@NotNull @NonNull User... users) { + for (User user : users) { + this.userDatabase.put(user.getUniqueId(), user); + } + } + + @Override + public @NonNull List getTopUsers(Currency currency, int length) throws Exception { + return userDatabase.values().stream().sorted((user1, user2) -> { + BigDecimal balance1 = user1.getBalance(currency); + BigDecimal balance2 = user2.getBalance(currency); + + return balance1.compareTo(balance2); + }).limit(length).toList(); + } + private User createUser(UUID uniqueId, String username, User data) { dev.xhyrom.lighteco.common.model.user.User user = this.plugin.getUserManager().getOrMake(uniqueId); if (username != null) diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/storage/provider/sql/SqlStatements.java b/common/src/main/java/dev/xhyrom/lighteco/common/storage/provider/sql/SqlStatements.java index 14bdb40..572221a 100644 --- a/common/src/main/java/dev/xhyrom/lighteco/common/storage/provider/sql/SqlStatements.java +++ b/common/src/main/java/dev/xhyrom/lighteco/common/storage/provider/sql/SqlStatements.java @@ -19,7 +19,7 @@ public enum SqlStatements { "SELECT currency_identifier, balance FROM ( SELECT currency_identifier, balance FROM '{prefix}_users' WHERE uuid = ?1 UNION ALL SELECT currency_identifier, balance FROM '{prefix}_{context}_users' WHERE uuid = ?1 ) AS combined_currencies;", "SELECT currency_identifier, balance FROM ( SELECT currency_identifier, balance FROM '{prefix}_users' WHERE uuid = ?1 UNION ALL SELECT currency_identifier, balance FROM '{prefix}_{context}_users' WHERE uuid = ?1 ) AS combined_currencies;", "SELECT currency_identifier, balance FROM ( SELECT currency_identifier, balance FROM '{prefix}_users' WHERE uuid = ? UNION ALL SELECT currency_identifier, balance FROM '{prefix}_{context}_users' WHERE uuid = ? ) AS combined_currencies;", - null + null // same as mariadb ); public final String sqlite; diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/storage/provider/sql/SqlStorageProvider.java b/common/src/main/java/dev/xhyrom/lighteco/common/storage/provider/sql/SqlStorageProvider.java index c2aedfd..42d11d5 100644 --- a/common/src/main/java/dev/xhyrom/lighteco/common/storage/provider/sql/SqlStorageProvider.java +++ b/common/src/main/java/dev/xhyrom/lighteco/common/storage/provider/sql/SqlStorageProvider.java @@ -8,11 +8,13 @@ import dev.xhyrom.lighteco.common.storage.StorageType; import dev.xhyrom.lighteco.common.storage.provider.sql.connection.ConnectionFactory; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; import java.sql.*; +import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.function.Function; @@ -21,7 +23,9 @@ public class SqlStorageProvider implements StorageProvider { private static String SAVE_USER_LOCAL_CURRENCY; private static String SAVE_USER_GLOBAL_CURRENCY; private static String LOAD_WHOLE_USER; - + private static final String GET_TOP_X_USERS_LOCAL = "SELECT uuid, balance FROM {prefix}_{context}_users WHERE currency_identifier = ? ORDER BY balance DESC LIMIT ?;"; + private static final String GET_TOP_X_USERS_GLOBAL = "SELECT uuid, balance FROM {prefix}_users WHERE currency_identifier = ? ORDER BY balance DESC LIMIT ?;"; + private final LightEcoPlugin plugin; private final ConnectionFactory connectionFactory; private final Function statementProcessor; @@ -109,37 +113,103 @@ public class SqlStorageProvider implements StorageProvider { try (PreparedStatement psGlobal = c.prepareStatement(this.statementProcessor.apply(SAVE_USER_GLOBAL_CURRENCY)); PreparedStatement psLocal = c.prepareStatement(this.statementProcessor.apply(SAVE_USER_LOCAL_CURRENCY))) { - for (Currency currency : this.plugin.getCurrencyManager().getRegisteredCurrencies()) { - BigDecimal balance = user.getBalance(currency.getProxy()); - - if (balance.compareTo(BigDecimal.ZERO) == 0) continue; - - switch (currency.getType()) { - case GLOBAL -> { - psGlobal.setString(1, uniqueIdString); - psGlobal.setString(2, currency.getIdentifier()); - psGlobal.setBigDecimal(3, balance); - if (SqlStatements.mustDuplicateParameters(this.connectionFactory.getImplementationName())) - psGlobal.setBigDecimal(4, balance); - - psGlobal.addBatch(); - } - case LOCAL -> { - psLocal.setString(1, uniqueIdString); - psLocal.setString(2, currency.getIdentifier()); - psLocal.setBigDecimal(3, balance); - if (SqlStatements.mustDuplicateParameters(this.connectionFactory.getImplementationName())) - psLocal.setBigDecimal(4, balance); - - psLocal.addBatch(); - } - } - } + saveBalances(psGlobal, psLocal, user, uniqueIdString); psGlobal.executeBatch(); psLocal.executeBatch(); } catch (SQLException e) { - throw new RuntimeException("Failed to save user " + user.getUniqueId(), e); + throw new SQLException("Failed to save user " + user.getUniqueId(), e); + } + } + } + + @Override + public void saveUsers(@NotNull @NonNull User... users) throws Exception { + // use transaction + try (Connection c = this.connectionFactory.getConnection()) { + try { + c.setAutoCommit(false); + + try (PreparedStatement psGlobal = c.prepareStatement(this.statementProcessor.apply(SAVE_USER_GLOBAL_CURRENCY)); + PreparedStatement psLocal = c.prepareStatement(this.statementProcessor.apply(SAVE_USER_LOCAL_CURRENCY))) { + + for (User user : users) { + String uniqueIdString = user.getUniqueId().toString(); + + saveBalances(psGlobal, psLocal, user, uniqueIdString); + } + + psGlobal.executeBatch(); + psLocal.executeBatch(); + } catch (SQLException e) { + throw new SQLException("Failed to save users", e); + } + + c.commit(); + } catch (SQLException e) { + c.rollback(); + throw e; + } + } + } + + @Override + public @NonNull List getTopUsers(dev.xhyrom.lighteco.api.model.currency.Currency apiCurrency, int length) throws Exception { + Currency currency = this.plugin.getCurrencyManager().getIfLoaded(apiCurrency.getIdentifier()); + String statement = currency.getType() == dev.xhyrom.lighteco.api.model.currency.Currency.Type.GLOBAL + ? GET_TOP_X_USERS_GLOBAL + : GET_TOP_X_USERS_LOCAL; + + try (Connection c = this.connectionFactory.getConnection()) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(statement))) { + ps.setString(1, currency.getIdentifier()); + ps.setInt(2, length); + + ResultSet rs = ps.executeQuery(); + + List users = new ArrayList<>(); + while (rs.next()) { + String uniqueIdString = rs.getString("uuid"); + UUID uniqueId = UUID.fromString(uniqueIdString); + + BigDecimal balance = rs.getBigDecimal("balance"); + + dev.xhyrom.lighteco.common.model.user.User user = this.plugin.getUserManager().getOrMake(uniqueId); + user.setBalance(currency, balance); + + users.add(user.getProxy()); + } + + return users; + } + } + } + + private void saveBalances(PreparedStatement psGlobal, PreparedStatement psLocal, User user, String uniqueIdString) throws SQLException { + for (Currency currency : this.plugin.getCurrencyManager().getRegisteredCurrencies()) { + BigDecimal balance = user.getBalance(currency.getProxy()); + + if (balance.compareTo(BigDecimal.ZERO) == 0) continue; + + switch (currency.getType()) { + case GLOBAL -> { + psGlobal.setString(1, uniqueIdString); + psGlobal.setString(2, currency.getIdentifier()); + psGlobal.setBigDecimal(3, balance); + if (SqlStatements.mustDuplicateParameters(this.connectionFactory.getImplementationName())) + psGlobal.setBigDecimal(4, balance); + + psGlobal.addBatch(); + } + case LOCAL -> { + psLocal.setString(1, uniqueIdString); + psLocal.setString(2, currency.getIdentifier()); + psLocal.setBigDecimal(3, balance); + if (SqlStatements.mustDuplicateParameters(this.connectionFactory.getImplementationName())) + psLocal.setBigDecimal(4, balance); + + psLocal.addBatch(); + } } } } diff --git a/common/src/main/java/dev/xhyrom/lighteco/common/storage/provider/sql/connection/hikari/HikariConnectionFactory.java b/common/src/main/java/dev/xhyrom/lighteco/common/storage/provider/sql/connection/hikari/HikariConnectionFactory.java index adc5b84..303b9cc 100644 --- a/common/src/main/java/dev/xhyrom/lighteco/common/storage/provider/sql/connection/hikari/HikariConnectionFactory.java +++ b/common/src/main/java/dev/xhyrom/lighteco/common/storage/provider/sql/connection/hikari/HikariConnectionFactory.java @@ -64,12 +64,12 @@ public abstract class HikariConnectionFactory implements ConnectionFactory { this.setProperties(config, properties); // configure the connection pool -// config.setMaximumPoolSize(1); -// config.setMinimumIdle(10); -// config.setMaxLifetime(1800000); -// config.setKeepaliveTime(0); -// config.setConnectionTimeout(5000); -// config.setInitializationFailTimeout(-1); + config.setMaximumPoolSize(this.configuration.maximumPoolSize); + config.setMinimumIdle(this.configuration.minimumIdle); + config.setMaxLifetime(this.configuration.maxLifetime); + config.setKeepaliveTime(this.configuration.keepAliveTime); + config.setConnectionTimeout(this.configuration.connectionTimeout); + config.setInitializationFailTimeout(-1); this.hikari = new HikariDataSource(config);