Guild bank: in-game commands, guild market listings and purchases, shop sales to the bank, guild payments

This commit is contained in:
Claude committed 2026-09-30 17:57:58 +00:00
1 parent 5defda1128
commit e6c6073eae
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);
}
}
@@ -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) {