Guild bank: in-game commands, guild market listings and purchases, shop sales to the bank, guild payments
This commit is contained in:
14 files changed
+680
-82
No files matched your search
@@ -16,6 +16,7 @@ import java.util.UUID;
|
||||
|
||||
public final class ScopenetPlugin extends JavaPlugin implements Listener {
|
||||
private Integration integration;
|
||||
private EconomyHandler economy;
|
||||
|
||||
@Override public void onEnable() {
|
||||
// Install the deny-by-default listener even if configuration is invalid.
|
||||
@@ -59,7 +60,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
|
||||
|
||||
// Register Economy commands & GUI
|
||||
{
|
||||
EconomyHandler economy = new EconomyHandler(this, integration);
|
||||
economy = new EconomyHandler(this, integration);
|
||||
getServer().getPluginManager().registerEvents(economy, this);
|
||||
String[] econCmds = {"balance", "pay", "baltop", "shop", "sell", "market", "orders", "trade", "transactions"};
|
||||
for (String c : econCmds) bindCommand(c, economy);
|
||||
@@ -67,7 +68,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
|
||||
|
||||
// Register Guilds & Land Claims commands & GUI
|
||||
{
|
||||
GuildHandler guilds = new GuildHandler(this, integration);
|
||||
GuildHandler guilds = new GuildHandler(this, integration, economy);
|
||||
getServer().getPluginManager().registerEvents(guilds, this);
|
||||
bindCommand("guild", guilds);
|
||||
bindCommand("claim", guilds);
|
||||
|
||||
@@ -35,6 +35,12 @@ public final class EconomyHandler implements CommandExecutor, Listener {
|
||||
// Trade sessions: maps player UUID to active trade session
|
||||
private final Map<UUID, TradeSession> activeTrades = new ConcurrentHashMap<>();
|
||||
|
||||
// Sell chests opened with /guild sell pay the guild bank.
|
||||
private final Set<Inventory> guildSellChests = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
|
||||
/** Durable, idempotent panel requests; also used by the guild bank. */
|
||||
EconomyOperations operations() { return operations; }
|
||||
|
||||
// Each open inventory must retain its own snapshot of listing IDs.
|
||||
private final Map<Inventory, Map<Integer, Long>> activeMarketListings = new IdentityHashMap<>();
|
||||
|
||||
@@ -216,6 +222,11 @@ public final class EconomyHandler implements CommandExecutor, Listener {
|
||||
}
|
||||
|
||||
private void handleSell(Player player, String[] args) {
|
||||
handleSell(player, args, false);
|
||||
}
|
||||
|
||||
/** {@code guild}: the proceeds go to the player's guild bank instead of their balance. */
|
||||
void handleSell(Player player, String[] args, boolean guild) {
|
||||
if (args.length > 0 && args[0].equalsIgnoreCase("hand")) {
|
||||
ItemStack hand = player.getInventory().getItemInMainHand();
|
||||
if (hand.getType().isAir()) {
|
||||
@@ -233,20 +244,41 @@ public final class EconomyHandler implements CommandExecutor, Listener {
|
||||
JsonObject payload = new JsonObject();
|
||||
payload.addProperty("uuid", player.getUniqueId().toString());
|
||||
payload.addProperty("username", player.getName());
|
||||
payload.addProperty("delta", total);
|
||||
payload.addProperty(guild ? "amount" : "delta", total);
|
||||
payload.addProperty("description", "Sell hand: " + sold.getType().name());
|
||||
player.getInventory().setItemInMainHand(null);
|
||||
if (!operations.enqueue(player, "economy/adjust", payload, List.of(sold), null)) giveOrDrop(player, sold);
|
||||
if (!operations.enqueue(player, guild ? "guilds/bank/credit" : "economy/adjust", payload, List.of(sold), null)) giveOrDrop(player, sold);
|
||||
return;
|
||||
}
|
||||
|
||||
// Open sell chest GUI
|
||||
Inventory sellInv = Bukkit.createInventory(null, 54, GUI_SELL_TITLE);
|
||||
if (guild) guildSellChests.add(sellInv);
|
||||
player.openInventory(sellInv);
|
||||
player.sendMessage(ChatColor.YELLOW + "Place items to sell in the chest. Close the chest to confirm your sale!");
|
||||
player.sendMessage(ChatColor.YELLOW + "Place items to sell in the chest. Close the chest to confirm your sale!"
|
||||
+ (guild ? ChatColor.GOLD + " The money goes to your guild bank." : ""));
|
||||
}
|
||||
|
||||
private void handleMarket(Player player, String[] args) {
|
||||
handleMarket(player, args, false);
|
||||
}
|
||||
|
||||
/** {@code asGuild}: list on behalf of the player's guild, or buy with its bank (see /guild market). */
|
||||
void handleMarket(Player player, String[] args, boolean asGuild) {
|
||||
if (asGuild && args.length >= 2 && args[0].equalsIgnoreCase("buy")) {
|
||||
long listing;
|
||||
try { listing = Long.parseLong(args[1].replace("#", "")); } catch (NumberFormatException e) {
|
||||
player.sendMessage(ChatColor.RED + "Usage: /guild market buy <listing number> (shown as Listing #n in /market)");
|
||||
return;
|
||||
}
|
||||
JsonObject payload = new JsonObject();
|
||||
payload.addProperty("listing_id", listing);
|
||||
payload.addProperty("buyer_uuid", player.getUniqueId().toString());
|
||||
payload.addProperty("buyer_name", player.getName());
|
||||
payload.addProperty("as_guild", true);
|
||||
operations.enqueue(player, "economy/market/buy", payload, Collections.emptyList(), null);
|
||||
return;
|
||||
}
|
||||
if (args.length >= 2 && args[0].equalsIgnoreCase("sell")) {
|
||||
ItemStack hand = player.getInventory().getItemInMainHand();
|
||||
if (hand.getType().isAir()) {
|
||||
@@ -275,6 +307,7 @@ public final class EconomyHandler implements CommandExecutor, Listener {
|
||||
payload.addProperty("amount", amount);
|
||||
payload.addProperty("price", price);
|
||||
payload.addProperty("item_data", EconomyOperations.encode(listed));
|
||||
if (asGuild) payload.addProperty("as_guild", true);
|
||||
|
||||
// Escrow the exact stack before persisting a retryable operation.
|
||||
player.getInventory().setItemInMainHand(null);
|
||||
@@ -314,6 +347,9 @@ public final class EconomyHandler implements CommandExecutor, Listener {
|
||||
JsonObject listing = el.getAsJsonObject();
|
||||
long id = listing.has("id") ? listing.get("id").getAsLong() : -1;
|
||||
String sellerName = listing.has("seller_name") ? listing.get("seller_name").getAsString() : "Unknown";
|
||||
// Guild listings show the guild, and are paid into its bank.
|
||||
String guildTag = listing.has("seller_guild") && listing.get("seller_guild").isJsonPrimitive()
|
||||
? listing.get("seller_guild").getAsString() : null;
|
||||
String listItemId = listing.has("item_id") ? listing.get("item_id").getAsString() : "STONE";
|
||||
String listItemName = listing.has("item_name") ? listing.get("item_name").getAsString() : listItemId;
|
||||
int listAmt = listing.has("amount") ? listing.get("amount").getAsInt() : 1;
|
||||
@@ -323,8 +359,9 @@ public final class EconomyHandler implements CommandExecutor, Listener {
|
||||
inventoryListings.put(slot, id);
|
||||
inv.setItem(slot, GuiHelper.createItem(mat, listAmt,
|
||||
"&f" + listItemName + " &7(x" + listAmt + ")",
|
||||
"&7Seller: &f" + sellerName,
|
||||
guildTag != null ? "&7Seller: &6[" + guildTag + "] &fGuild" : "&7Seller: &f" + sellerName,
|
||||
"&7Price: &a$" + String.format(Locale.US, "%,.2f", listPrice),
|
||||
"&8Listing #" + id,
|
||||
"",
|
||||
"&eClick to Buy!"));
|
||||
slot++;
|
||||
@@ -447,6 +484,7 @@ public final class EconomyHandler implements CommandExecutor, Listener {
|
||||
if (title.equals(GUI_SELL_TITLE)) {
|
||||
// Move sellable items to durable escrow before crediting the balance.
|
||||
Inventory inv = event.getInventory();
|
||||
boolean toGuild = guildSellChests.remove(inv);
|
||||
List<ItemStack> soldItems = new ArrayList<>();
|
||||
List<ItemStack> unsellable = new ArrayList<>();
|
||||
double totalEarned = 0;
|
||||
@@ -472,9 +510,9 @@ public final class EconomyHandler implements CommandExecutor, Listener {
|
||||
JsonObject payload = new JsonObject();
|
||||
payload.addProperty("uuid", player.getUniqueId().toString());
|
||||
payload.addProperty("username", player.getName());
|
||||
payload.addProperty("delta", totalEarned);
|
||||
payload.addProperty(toGuild ? "amount" : "delta", totalEarned);
|
||||
payload.addProperty("description", "Sell chest: " + countSold + " items");
|
||||
if (!operations.enqueue(player, "economy/adjust", payload, soldItems, null)) {
|
||||
if (!operations.enqueue(player, toGuild ? "guilds/bank/credit" : "economy/adjust", payload, soldItems, null)) {
|
||||
for (ItemStack item : soldItems) giveOrDrop(player, item);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-3
@@ -125,9 +125,17 @@ final class EconomyOperations {
|
||||
player.sendMessage(ChatColor.RED + "Transaction rejected; held items returned: " + job.get("error").getAsString());
|
||||
} else {
|
||||
JsonObject result = job.getAsJsonObject("result");
|
||||
JsonElement balanceValue = result.has("new_balance") ? result.get("new_balance") : result.get("balance");
|
||||
String balance = balanceValue != null ? " Balance: $" + String.format(Locale.US, "%,.2f", balanceValue.getAsDouble()) : "";
|
||||
player.sendMessage(ChatColor.GREEN + "Transaction completed." + balance);
|
||||
if (result.has("message") && result.get("message").isJsonPrimitive()) {
|
||||
// Guild bank operations explain themselves ("Deposited $50.00 into [IRON] bank.").
|
||||
JsonElement balanceValue = result.get("balance");
|
||||
String bank = balanceValue != null && balanceValue.isJsonPrimitive()
|
||||
? " Bank: $" + String.format(Locale.US, "%,.2f", balanceValue.getAsDouble()) : "";
|
||||
player.sendMessage(ChatColor.GREEN + result.get("message").getAsString() + bank);
|
||||
} else {
|
||||
JsonElement balanceValue = result.has("new_balance") ? result.get("new_balance") : result.get("balance");
|
||||
String balance = balanceValue != null ? " Balance: $" + String.format(Locale.US, "%,.2f", balanceValue.getAsDouble()) : "";
|
||||
player.sendMessage(ChatColor.GREEN + "Transaction completed." + balance);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) { plugin.getLogger().severe("Economy delivery requires attention for " + id + ": " + e.getMessage()); }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package net.scopenet.paper.commands;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import net.scopenet.integration.Integration;
|
||||
import net.scopenet.paper.ScopenetPlugin;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.entity.Player;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* /guild bank, /guild pay: the guild's money as seen in game. Balances are
|
||||
* kept by the panel (and shown in the launcher); spending rights are checked
|
||||
* there too, so the plugin only has to pass on who is asking.
|
||||
*/
|
||||
final class GuildBank {
|
||||
private final ScopenetPlugin plugin;
|
||||
private final Integration integration;
|
||||
private final EconomyOperations operations;
|
||||
|
||||
GuildBank(ScopenetPlugin plugin, Integration integration, EconomyOperations operations) {
|
||||
this.plugin = plugin;
|
||||
this.integration = integration;
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
private static String money(double value) {
|
||||
return "$" + String.format(Locale.US, "%,.2f", value);
|
||||
}
|
||||
|
||||
private static Double amount(Player player, String text) {
|
||||
try {
|
||||
double value = Double.parseDouble(text.replace(",", ""));
|
||||
if (Double.isNaN(value) || Double.isInfinite(value) || value < 0.01) throw new NumberFormatException();
|
||||
return Math.round(value * 100.0) / 100.0;
|
||||
} catch (NumberFormatException e) {
|
||||
player.sendMessage(ChatColor.RED + "Enter an amount of at least 0.01, for example 25 or 12.50.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** /guild bank [deposit|withdraw <amount>] */
|
||||
void bank(Player player, String[] args) {
|
||||
if (args.length >= 2 && (args[0].equalsIgnoreCase("deposit") || args[0].equalsIgnoreCase("withdraw"))) {
|
||||
String action = args[0].toLowerCase();
|
||||
if (!player.hasPermission("scopenet.command.guild.bank." + action)) {
|
||||
player.sendMessage(ChatColor.RED + "You do not have permission to " + action + " guild money.");
|
||||
return;
|
||||
}
|
||||
Double value = amount(player, args[1]);
|
||||
if (value == null) return;
|
||||
JsonObject payload = new JsonObject();
|
||||
payload.addProperty("uuid", player.getUniqueId().toString());
|
||||
payload.addProperty("username", player.getName());
|
||||
payload.addProperty("amount", value);
|
||||
payload.addProperty("action", action);
|
||||
operations.enqueue(player, "guilds/bank/transfer", payload, Collections.emptyList(), null);
|
||||
return;
|
||||
}
|
||||
if (args.length > 0 && !args[0].equalsIgnoreCase("balance") && !args[0].equalsIgnoreCase("info")) {
|
||||
player.sendMessage(ChatColor.RED + "Usage: /guild bank [deposit|withdraw <amount>]");
|
||||
return;
|
||||
}
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
JsonObject info = integration.client().post("guilds/bank", who(player));
|
||||
if (!info.has("guild") || info.get("guild").isJsonNull()) {
|
||||
player.sendMessage(ChatColor.RED + "You are not in a guild on this server.");
|
||||
return;
|
||||
}
|
||||
JsonObject guild = info.getAsJsonObject("guild");
|
||||
player.sendMessage(ChatColor.GOLD + "=== [" + guild.get("tag").getAsString() + "] " + guild.get("name").getAsString() + " Bank ===");
|
||||
player.sendMessage(ChatColor.YELLOW + "Balance: " + ChatColor.GREEN + money(info.get("balance").getAsDouble())
|
||||
+ ChatColor.GRAY + " Your wallet: " + money(info.get("my_balance").getAsDouble())
|
||||
+ " Role: " + info.get("role").getAsString());
|
||||
JsonArray recent = info.getAsJsonArray("recent");
|
||||
for (JsonElement element : recent) {
|
||||
JsonObject row = element.getAsJsonObject();
|
||||
String kind = row.get("kind").getAsString();
|
||||
boolean out = kind.equals("withdraw") || kind.equals("purchase") || kind.equals("transfer_out");
|
||||
player.sendMessage(ChatColor.GRAY + " " + (out ? ChatColor.RED + "-" : ChatColor.GREEN + "+")
|
||||
+ money(row.get("amount").getAsDouble()) + ChatColor.GRAY + " " + kind.replace('_', ' ') + " by "
|
||||
+ row.get("who").getAsString());
|
||||
}
|
||||
player.sendMessage(ChatColor.GRAY + "/guild bank deposit|withdraw <amount>, /guild sell, /guild market, /guild pay <tag> <amount>");
|
||||
} catch (Exception e) {
|
||||
player.sendMessage(ChatColor.RED + "Could not load the guild bank: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** /guild pay <tag> <amount> */
|
||||
void pay(Player player, String[] args) {
|
||||
if (args.length < 2) {
|
||||
player.sendMessage(ChatColor.RED + "Usage: /guild pay <guild tag> <amount>");
|
||||
return;
|
||||
}
|
||||
Double value = amount(player, args[1]);
|
||||
if (value == null) return;
|
||||
JsonObject payload = new JsonObject();
|
||||
payload.addProperty("uuid", player.getUniqueId().toString());
|
||||
payload.addProperty("to_tag", args[0]);
|
||||
payload.addProperty("amount", value);
|
||||
operations.enqueue(player, "guilds/bank/pay", payload, Collections.emptyList(), null);
|
||||
}
|
||||
|
||||
private static JsonObject who(Player player) {
|
||||
JsonObject body = new JsonObject();
|
||||
body.addProperty("uuid", player.getUniqueId().toString());
|
||||
return body;
|
||||
}
|
||||
}
|
||||
@@ -37,9 +37,14 @@ public final class GuildHandler implements CommandExecutor, Listener {
|
||||
|
||||
private static final String GUI_GUILD_TITLE = ChatColor.DARK_AQUA + "Guild Dashboard";
|
||||
|
||||
public GuildHandler(ScopenetPlugin plugin, Integration integration) {
|
||||
private final EconomyHandler economy;
|
||||
private final GuildBank bank;
|
||||
|
||||
public GuildHandler(ScopenetPlugin plugin, Integration integration, EconomyHandler economy) {
|
||||
this.plugin = plugin;
|
||||
this.integration = integration;
|
||||
this.economy = economy;
|
||||
this.bank = new GuildBank(plugin, integration, economy.operations());
|
||||
this.guildHomesFile = new File(plugin.getDataFolder(), "guild_homes.yml");
|
||||
this.guildHomesConfig = YamlConfiguration.loadConfiguration(guildHomesFile);
|
||||
loadGuildHomes();
|
||||
@@ -90,7 +95,7 @@ public final class GuildHandler implements CommandExecutor, Listener {
|
||||
|
||||
String sub = args[0].toLowerCase();
|
||||
String permission = sub.equals("c") ? "chat" : sub;
|
||||
if (java.util.Set.of("create", "leave", "claim", "unclaim", "map", "chat", "sethome", "home", "members").contains(permission)
|
||||
if (java.util.Set.of("create", "leave", "claim", "unclaim", "map", "chat", "sethome", "home", "members", "bank", "sell", "market", "pay").contains(permission)
|
||||
&& !player.hasPermission("scopenet.command.guild." + permission)) {
|
||||
player.sendMessage(ChatColor.RED + "You do not have permission to use /guild " + permission + ".");
|
||||
return true;
|
||||
@@ -105,6 +110,10 @@ public final class GuildHandler implements CommandExecutor, Listener {
|
||||
case "sethome" -> handleSetHome(player);
|
||||
case "home" -> handleHome(player);
|
||||
case "members" -> handleMembers(player);
|
||||
case "bank" -> bank.bank(player, Arrays.copyOfRange(args, 1, args.length));
|
||||
case "pay" -> bank.pay(player, Arrays.copyOfRange(args, 1, args.length));
|
||||
case "sell" -> economy.handleSell(player, Arrays.copyOfRange(args, 1, args.length), true);
|
||||
case "market" -> economy.handleMarket(player, Arrays.copyOfRange(args, 1, args.length), true);
|
||||
default -> sendGuildHelp(player);
|
||||
}
|
||||
return true;
|
||||
@@ -123,6 +132,10 @@ public final class GuildHandler implements CommandExecutor, Listener {
|
||||
player.sendMessage(ChatColor.YELLOW + "/guild map" + ChatColor.GRAY + " - Show nearby land claims radar map");
|
||||
player.sendMessage(ChatColor.YELLOW + "/guild chat <msg>" + ChatColor.GRAY + " - Send message to guild members");
|
||||
player.sendMessage(ChatColor.YELLOW + "/guild sethome / /guild home" + ChatColor.GRAY + " - Guild waypoint base");
|
||||
player.sendMessage(ChatColor.YELLOW + "/guild bank [deposit|withdraw <amount>]" + ChatColor.GRAY + " - Guild money");
|
||||
player.sendMessage(ChatColor.YELLOW + "/guild sell [hand]" + ChatColor.GRAY + " - Sell items to the shop for the guild bank");
|
||||
player.sendMessage(ChatColor.YELLOW + "/guild market sell <price> | buy <#id>" + ChatColor.GRAY + " - Trade on the market as a guild");
|
||||
player.sendMessage(ChatColor.YELLOW + "/guild pay <tag> <amount>" + ChatColor.GRAY + " - Pay another guild");
|
||||
}
|
||||
|
||||
private void handleClaim(Player player) {
|
||||
|
||||
Reference in new issue
Block a user