diff --git a/common/src/main/java/online/mineroo/common/request/CurrencyRequest.java b/common/src/main/java/online/mineroo/common/request/CurrencyRequest.java index 6b66bb4..ef0c2fe 100644 --- a/common/src/main/java/online/mineroo/common/request/CurrencyRequest.java +++ b/common/src/main/java/online/mineroo/common/request/CurrencyRequest.java @@ -1,5 +1,14 @@ package online.mineroo.common.request; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import online.mineroo.common.NetworkServiceInterface; import org.slf4j.Logger; @@ -7,8 +16,207 @@ public class CurrencyRequest { private final Logger logger; private final NetworkServiceInterface networkService; + private final ConcurrentHashMap currencyCache; + public CurrencyRequest(Logger logger, NetworkServiceInterface networkService) { this.logger = logger; this.networkService = networkService; + this.currencyCache = new ConcurrentHashMap<>(); + } + + public static class PlayerWallet { + private final ConcurrentHashMap currencies; + + public PlayerWallet() { + this.currencies = new ConcurrentHashMap<>(); + } + + public long getBalance(String currency) { + return currencies.getOrDefault(currency, 0L); + } + + public void setBalance(String currency, long amount) { + currencies.put(currency, amount); + } + + public void add(String currency, long delta) { + currencies.merge(currency, delta, Long::sum); + } + + public void updateAll(JsonObject json) { + for (Map.Entry entry : json.entrySet()) { + try { + this.currencies.put(entry.getKey(), entry.getValue().getAsLong()); + } catch (Exception ignored) { + } + } + } + } + + public static class LoadPlayerCurrencyResponse { + @SerializedName("amount") private long amount; + @SerializedName("slug") private String slug; + + public LoadPlayerCurrencyResponse(long amount, String slug) { + this.amount = amount; + this.slug = slug; + } + + public long getAmount() { + return amount; + } + + public String getSlug() { + return slug; + } + } + + public static class ModifyPlayerCurrencyResponse { + @SerializedName("message") private String message; + @SerializedName("new_balance") private long newBalance; + @SerializedName("success") private boolean success; + + public ModifyPlayerCurrencyResponse(String message, long newBalance, boolean success) { + this.message = message; + this.newBalance = newBalance; + this.success = success; + } + + public String getMessage() { + return message; + } + + public long getNewBalance() { + return newBalance; + } + + public boolean isSuccess() { + return success; + } + } + + private LoadPlayerCurrencyResponse createLoadPlayerCurrencyErrorResponse() { + return new LoadPlayerCurrencyResponse(0, ""); + } + + private ModifyPlayerCurrencyResponse createModifyPlayerCurrencyErrorResponse() { + return new ModifyPlayerCurrencyResponse("", 0, false); + } + + // INFO: the amount need `/ 100` and convert to double + public CompletableFuture LoadPlayerCurrency( + UUID playerUuid, String slug + ) { + String path = "/server/currency/user"; + + Map params = new HashMap<>(); + + if (slug != null) { + params.put("currency_slug", slug); + } + + if (playerUuid != null) { + String simpleUuid = playerUuid.toString().replace("-", ""); + params.put("mc_uuid", simpleUuid); + } + + return networkService.getData(path, params) + .thenApply(response -> { + Gson gson = new Gson(); + String responseBody = response.getBody(); + + if (response.getStatusCode() != 200) { + try { + LoadPlayerCurrencyResponse res = + gson.fromJson(responseBody, LoadPlayerCurrencyResponse.class); + PlayerWallet wallet = + currencyCache.computeIfAbsent(playerUuid, k -> new PlayerWallet()); + wallet.setBalance(res.getSlug(), res.getAmount()); + return res; + } catch (Exception ignored) { + logger.error("API Returned: " + responseBody); + return createLoadPlayerCurrencyErrorResponse(); + } + } + + try { + LoadPlayerCurrencyResponse res = + gson.fromJson(responseBody, LoadPlayerCurrencyResponse.class); + PlayerWallet wallet = + currencyCache.computeIfAbsent(playerUuid, k -> new PlayerWallet()); + wallet.setBalance(res.getSlug(), res.getAmount()); + return res; + } catch (Exception e) { + logger.error("API Returned: " + responseBody); + return createLoadPlayerCurrencyErrorResponse(); + } + }) + .exceptionally(e -> { return createLoadPlayerCurrencyErrorResponse(); }); + } + + public CompletableFuture ModifyPlayerCurrency( + long amount, UUID playerUuid, String requestId, String currencySlug, String reason + ) { + String path = "/server/currency/user"; + + JsonObject json = new JsonObject(); + + json.addProperty("amount", amount); + + if (playerUuid != null) { + String simpleUuid = playerUuid.toString().replace("-", ""); + json.addProperty("mc_uuid", simpleUuid); + } + + if (requestId != null) { + json.addProperty("request_id", requestId); + } + + if (currencySlug != null) { + json.addProperty("currency", currencySlug); + } + + if (reason != null) { + json.addProperty("reason", reason); + } + + return networkService.postData(path, json) + .thenApply(response -> { + Gson gson = new Gson(); + String responseBody = response.getBody(); + + if (response.getStatusCode() != 200) { + try { + return gson.fromJson(responseBody, ModifyPlayerCurrencyResponse.class); + } catch (Exception ignored) { + return createModifyPlayerCurrencyErrorResponse(); + } + } + + try { + return gson.fromJson(responseBody, ModifyPlayerCurrencyResponse.class); + } catch (Exception e) { + return createModifyPlayerCurrencyErrorResponse(); + } + }) + .exceptionally(e -> { return createModifyPlayerCurrencyErrorResponse(); }); + } + + // sync functions + + public double getCachedBalance(UUID uuid, String currencySlug) { + PlayerWallet wallet = currencyCache.get(uuid); + if (wallet == null) + return 0.0; + return wallet.getBalance(currencySlug) / 100; + } + + public void modifyCachedBalance(UUID uuid, String currencySlug, double amount) { + long delta = (long) (amount * 100); + currencyCache.computeIfAbsent(uuid, k -> new PlayerWallet()).add(currencySlug, delta); + } + + public void unloadPlayer(UUID uuid) { + currencyCache.remove(uuid); } } diff --git a/common/src/main/java/online/mineroo/common/request/RequestClient.java b/common/src/main/java/online/mineroo/common/request/RequestClient.java index 6218c5b..898547b 100644 --- a/common/src/main/java/online/mineroo/common/request/RequestClient.java +++ b/common/src/main/java/online/mineroo/common/request/RequestClient.java @@ -6,10 +6,12 @@ import org.slf4j.Logger; public class RequestClient { private final ManagementRequest managementRequest; private final UserRequest userRequest; + private final CurrencyRequest currencyRequest; public RequestClient(Logger logger, NetworkServiceInterface networkService) { this.managementRequest = new ManagementRequest(logger, networkService); this.userRequest = new UserRequest(logger, networkService); + this.currencyRequest = new CurrencyRequest(logger, networkService); } public ManagementRequest management() { @@ -19,4 +21,8 @@ public class RequestClient { public UserRequest user() { return this.userRequest; } + + public CurrencyRequest currency() { + return this.currencyRequest; + } } diff --git a/common/src/main/java/online/mineroo/common/request/Serializable.java b/common/src/main/java/online/mineroo/common/request/Serializable.java deleted file mode 100644 index e69de29..0000000 diff --git a/paper/src/main/java/online/mineroo/paper/Config.java b/paper/src/main/java/online/mineroo/paper/Config.java index e4affff..432c3f2 100644 --- a/paper/src/main/java/online/mineroo/paper/Config.java +++ b/paper/src/main/java/online/mineroo/paper/Config.java @@ -3,17 +3,18 @@ package online.mineroo.paper; import org.bukkit.configuration.file.FileConfiguration; public class Config { - private final ServerSection server; private final PlayersSection player; private final ProxySection proxy; private final TestSection test; + private final CurrenciesSection currencies; public Config(FileConfiguration config) { this.test = new TestSection(config); this.proxy = new ProxySection(config); this.server = new ServerSection(config); this.player = new PlayersSection(config); + this.currencies = new CurrenciesSection(config); } public TestSection getTest() { @@ -32,6 +33,10 @@ public class Config { return player; } + public CurrenciesSection getCurrencies() { + return currencies; + } + public static class TestSection { private final String apiHostname; @@ -67,7 +72,10 @@ public class Config { this.bind = new ServerBindSection( config.getString("server.bind.address", ""), config.getObject("server.bind.port", Integer.class, null), - config.getString("server.bind.token", "get token from `mineroo.online/resources/servers` page!")); + config.getString( + "server.bind.token", "get token from `mineroo.online/resources/servers` page!" + ) + ); } public String getServerName() { @@ -113,7 +121,8 @@ public class Config { public PlayersSection(FileConfiguration config) { this.bind = new PlayerBindSection( config.getBoolean("players.bind.required", false), - config.getBoolean("players.bind.share_player_info", true)); + config.getBoolean("players.bind.share_player_info", true) + ); } public PlayerBindSection getPlayerBind() { @@ -138,4 +147,15 @@ public class Config { return sharePlayerInfo; } } + + public static class CurrenciesSection { + private final String primary; + public CurrenciesSection(FileConfiguration config) { + this.primary = config.getString("currencies.primary", "money"); + } + + public String getPrimary() { + return primary; + } + } } diff --git a/paper/src/main/java/online/mineroo/paper/MinerooCore.java b/paper/src/main/java/online/mineroo/paper/MinerooCore.java index e936554..0cc9b88 100644 --- a/paper/src/main/java/online/mineroo/paper/MinerooCore.java +++ b/paper/src/main/java/online/mineroo/paper/MinerooCore.java @@ -7,9 +7,11 @@ import online.mineroo.common.NetworkServiceInterface; import online.mineroo.common.cache.UserInfoCache; import online.mineroo.common.request.RequestClient; import online.mineroo.paper.commands.MainCommand; +import online.mineroo.paper.economy.MinerooEconomy; import online.mineroo.paper.expansions.MinerooExpansion; import online.mineroo.paper.listeners.BindListener; import online.mineroo.paper.listeners.PlayerBindListener; +import online.mineroo.paper.listeners.PlayerCurrencyListener; import org.bukkit.Bukkit; import org.bukkit.event.Listener; import org.bukkit.plugin.java.JavaPlugin; @@ -38,6 +40,7 @@ public class MinerooCore extends JavaPlugin implements Listener { }); getServer().getPluginManager().registerEvents(new PlayerBindListener(this), this); + getServer().getPluginManager().registerEvents(new PlayerCurrencyListener(this), this); messageManager = new MessageManager(); @@ -49,6 +52,22 @@ public class MinerooCore extends JavaPlugin implements Listener { if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { new MinerooExpansion(this).register(); } + + // register vault + if (getServer().getPluginManager().getPlugin("Vault") != null) { + MinerooEconomy economy = new MinerooEconomy(this); + + getServer().getServicesManager().register( + net.milkbowl.vault.economy.Economy.class, + economy, + this, + org.bukkit.plugin.ServicePriority.Highest + ); + + getLogger().info("✅ Mineroo Vault Economy has been hooked!"); + } else { + getLogger().warning("❌ Vault not found! Economy features will be disabled."); + } } @Override diff --git a/paper/src/main/java/online/mineroo/paper/economy/MinerooEconomy.java b/paper/src/main/java/online/mineroo/paper/economy/MinerooEconomy.java index 43c396d..66ddbfd 100644 --- a/paper/src/main/java/online/mineroo/paper/economy/MinerooEconomy.java +++ b/paper/src/main/java/online/mineroo/paper/economy/MinerooEconomy.java @@ -1,16 +1,278 @@ package online.mineroo.paper.economy; +import java.util.Collections; import java.util.List; +import java.util.UUID; import net.milkbowl.vault.economy.AbstractEconomy; import net.milkbowl.vault.economy.EconomyResponse; -import online.mineroo.common.request.UserRequest.SimpleUserInfoResponse; +import online.mineroo.common.request.CurrencyRequest; import online.mineroo.paper.MinerooCore; +import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; -public class MinerooEconomy { +@SuppressWarnings("deprecation") +public class MinerooEconomy extends AbstractEconomy { private final MinerooCore plugin; + private final String PRIMARY_CURRENCY; public MinerooEconomy(MinerooCore plugin) { this.plugin = plugin; + this.PRIMARY_CURRENCY = this.plugin.getConfigObject().getCurrencies().getPrimary(); + } + + private CurrencyRequest currency() { + return plugin.getRequestClient().currency(); + } + + @Override + public boolean isEnabled() { + return plugin.isEnabled(); + } + + @Override + public String getName() { + return "Mineroo"; + } + + @Override + public boolean hasBankSupport() { + return false; + } + + @Override + public int fractionalDigits() { + return 2; + } + + @Override + public String format(double amount) { + return String.format("%.2f", amount); + } + + @Override + public String currencyNamePlural() { + return ""; + } + + @Override + public String currencyNameSingular() { + return ""; + } + + @Override + public boolean hasAccount(String playerName) { + return hasAccount(Bukkit.getOfflinePlayer(playerName)); + } + + @Override + public boolean hasAccount(OfflinePlayer player) { + return true; + } + + @Override + public boolean hasAccount(String playerName, String worldName) { + return hasAccount(playerName); + } + + @Override + public double getBalance(String playerName) { + return getBalance(Bukkit.getOfflinePlayer(playerName)); + } + + @Override + public double getBalance(OfflinePlayer player) { + return currency().getCachedBalance(player.getUniqueId(), PRIMARY_CURRENCY); + } + + @Override + public double getBalance(String playerName, String world) { + return getBalance(playerName); + } + + @Override + public double getBalance(OfflinePlayer player, String world) { + return getBalance(player); + } + + @Override + public boolean has(String playerName, double amount) { + return getBalance(playerName) >= amount; + } + + @Override + public boolean has(OfflinePlayer player, double amount) { + return getBalance(player) >= amount; + } + + @Override + public boolean has(String playerName, String worldName, double amount) { + return has(playerName, amount); + } + + @Override + public boolean has(OfflinePlayer player, String worldName, double amount) { + return has(player, amount); + } + + // --- 核心:扣款 (Withdraw) --- + + @Override + public EconomyResponse withdrawPlayer(String playerName, double amount) { + return withdrawPlayer(Bukkit.getOfflinePlayer(playerName), amount); + } + + @Override + public EconomyResponse withdrawPlayer(OfflinePlayer player, double amount) { + if (amount < 0) { + return new EconomyResponse( + 0, 0, EconomyResponse.ResponseType.FAILURE, "Cannot withdraw negative funds" + ); + } + + double balance = getBalance(player); + if (balance < amount) { + return new EconomyResponse( + 0, balance, EconomyResponse.ResponseType.FAILURE, "Insufficient funds" + ); + } + + currency().modifyCachedBalance(player.getUniqueId(), PRIMARY_CURRENCY, -amount); + + long deltaCents = (long) (-amount * 100); + currency().ModifyPlayerCurrency( + deltaCents, + player.getUniqueId(), + UUID.randomUUID().toString(), + PRIMARY_CURRENCY, + "Plugin Withdraw" + ); + + return new EconomyResponse( + amount, balance - amount, EconomyResponse.ResponseType.SUCCESS, null + ); + } + + @Override + public EconomyResponse withdrawPlayer(String playerName, String worldName, double amount) { + return withdrawPlayer(playerName, amount); + } + + @Override + public EconomyResponse withdrawPlayer(OfflinePlayer player, String worldName, double amount) { + return withdrawPlayer(player, amount); + } + + // --- 核心:存款 (Deposit) --- + + @Override + public EconomyResponse depositPlayer(String playerName, double amount) { + return depositPlayer(Bukkit.getOfflinePlayer(playerName), amount); + } + + @Override + public EconomyResponse depositPlayer(OfflinePlayer player, double amount) { + if (amount < 0) { + return new EconomyResponse( + 0, 0, EconomyResponse.ResponseType.FAILURE, "Cannot deposit negative funds" + ); + } + + currency().modifyCachedBalance(player.getUniqueId(), PRIMARY_CURRENCY, amount); + + long deltaCents = (long) (amount * 100); + currency().ModifyPlayerCurrency( + deltaCents, + player.getUniqueId(), + UUID.randomUUID().toString(), + PRIMARY_CURRENCY, + "Plugin Deposit" + ); + + return new EconomyResponse( + amount, getBalance(player), EconomyResponse.ResponseType.SUCCESS, null + ); + } + + @Override + public EconomyResponse depositPlayer(String playerName, String worldName, double amount) { + return depositPlayer(playerName, amount); + } + + @Override + public EconomyResponse depositPlayer(OfflinePlayer player, String worldName, double amount) { + return depositPlayer(player, amount); + } + + // --- 银行功能 (不支持) --- + + @Override + public EconomyResponse createBank(String name, String player) { + return new EconomyResponse( + 0, 0, EconomyResponse.ResponseType.NOT_IMPLEMENTED, "Banks not supported" + ); + } + + @Override + public EconomyResponse deleteBank(String name) { + return new EconomyResponse( + 0, 0, EconomyResponse.ResponseType.NOT_IMPLEMENTED, "Banks not supported" + ); + } + + @Override + public EconomyResponse bankBalance(String name) { + return new EconomyResponse( + 0, 0, EconomyResponse.ResponseType.NOT_IMPLEMENTED, "Banks not supported" + ); + } + + @Override + public EconomyResponse bankHas(String name, double amount) { + return new EconomyResponse( + 0, 0, EconomyResponse.ResponseType.NOT_IMPLEMENTED, "Banks not supported" + ); + } + + @Override + public EconomyResponse bankWithdraw(String name, double amount) { + return new EconomyResponse( + 0, 0, EconomyResponse.ResponseType.NOT_IMPLEMENTED, "Banks not supported" + ); + } + + @Override + public EconomyResponse bankDeposit(String name, double amount) { + return new EconomyResponse( + 0, 0, EconomyResponse.ResponseType.NOT_IMPLEMENTED, "Banks not supported" + ); + } + + @Override + public EconomyResponse isBankOwner(String name, String playerName) { + return new EconomyResponse( + 0, 0, EconomyResponse.ResponseType.NOT_IMPLEMENTED, "Banks not supported" + ); + } + + @Override + public EconomyResponse isBankMember(String name, String playerName) { + return new EconomyResponse( + 0, 0, EconomyResponse.ResponseType.NOT_IMPLEMENTED, "Banks not supported" + ); + } + + @Override + public List getBanks() { + return Collections.emptyList(); + } + + @Override + public boolean createPlayerAccount(String playerName) { + return true; + } + + @Override + public boolean createPlayerAccount(String playerName, String worldName) { + return true; } } diff --git a/paper/src/main/java/online/mineroo/paper/listeners/PlayerCurrencyListener.java b/paper/src/main/java/online/mineroo/paper/listeners/PlayerCurrencyListener.java new file mode 100644 index 0000000..b698f67 --- /dev/null +++ b/paper/src/main/java/online/mineroo/paper/listeners/PlayerCurrencyListener.java @@ -0,0 +1,46 @@ +package online.mineroo.paper.listeners; + +import java.util.UUID; +import online.mineroo.paper.MinerooCore; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +public class PlayerCurrencyListener implements Listener { + private final MinerooCore plugin; + + public PlayerCurrencyListener(MinerooCore plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + UUID uuid = event.getPlayer().getUniqueId(); + + plugin.getRequestClient().currency().unloadPlayer(uuid); + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + UUID uuid = player.getUniqueId(); + + String currency_slug = plugin.getConfigObject().getCurrencies().getPrimary(); + + plugin.getRequestClient() + .currency() + .LoadPlayerCurrency(uuid, currency_slug) + .thenAccept(resp -> { + if (resp.getSlug().equals(currency_slug)) { + plugin.getLogger().info( + "Synced balance for " + player.getName() + " [" + resp.getSlug() + " : " + + resp.getAmount() + "]" + ); + } else { + plugin.getLogger().warning("Failed to sync balance for " + player.getName()); + } + }); + } +} diff --git a/paper/src/main/resources/config.yml b/paper/src/main/resources/config.yml index fcafae9..e38f0fa 100644 --- a/paper/src/main/resources/config.yml +++ b/paper/src/main/resources/config.yml @@ -12,6 +12,10 @@ server: port: 0 token: "get token from `mineroo.online/resources/servers` page!" +currencies: + # which currency slug should bound into `VaultApi` + primary: "money" + players: # for player bind bind: diff --git a/paper/src/main/resources/paper-plugin.yml b/paper/src/main/resources/paper-plugin.yml index d4d81a3..01d6fae 100644 --- a/paper/src/main/resources/paper-plugin.yml +++ b/paper/src/main/resources/paper-plugin.yml @@ -10,3 +10,6 @@ dependencies: PlaceholderAPI: load: BEFORE required: false + Vault: + load: BEFORE + required: false diff --git a/paper/src/main/resources/plugin.yml b/paper/src/main/resources/plugin.yml index 40171b9..d2e7b38 100644 --- a/paper/src/main/resources/plugin.yml +++ b/paper/src/main/resources/plugin.yml @@ -6,7 +6,7 @@ author: YuKun Liu website: https://mineroo.online description: Mineroo Base Plugin api-version: "1.21.10" -softdepend: ["PlaceholderAPI"] +softdepend: ["PlaceholderAPI", "Vault"] permissions: mineroo.admin.reload: description: "Reload plugin config files."