fix: resolve all remaining audit failures across rust, svelte, and java

This commit is contained in:
scoped committed 2026-09-29 15:51:15 -04:00
1 parent bc12a25a01
commit 590178e41a
35 files changed
+958 -300

No files matched your search

@@ -20,21 +20,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
getServer().getPluginManager().registerEvents(this, this);
saveDefaultConfig();
try {
Settings settings = Settings.of(
getConfig().getString("panel-url", ""),
getConfig().getString("token", ""),
getConfig().getBoolean("leveling.enabled", true),
getConfig().getDouble("leveling.global-xp-multiplier", 1.0),
getConfig().getDouble("leveling.server-xp-multiplier", 1.5),
getConfig().getBoolean("quests.enabled", true),
getConfig().getBoolean("achievements.enabled", true),
getConfig().getBoolean("guilds.enabled", true),
getConfig().getBoolean("guilds.land-claiming", true),
getConfig().getBoolean("social.enabled", true),
getConfig().getBoolean("social.chat-prefixes", true),
getConfig().getBoolean("essentials.enabled", true),
getConfig().getBoolean("economy.enabled", true)
);
Settings settings = readSettings();
integration = new Integration(
settings,
@@ -54,7 +40,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
bindCommand("scopenet", scopenetCmd);
// Register Essentials commands & GUI
if (settings.essentialsEnabled()) {
{
EssentialsHandler essentials = new EssentialsHandler(this);
getServer().getPluginManager().registerEvents(essentials, this);
String[] essCmds = {"spawn", "home", "sethome", "delhome", "back", "tpa", "tpaccept", "tpdeny", "rtp", "warp", "playtime"};
@@ -62,7 +48,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
}
// Register Economy commands & GUI
if (settings.economyEnabled()) {
{
EconomyHandler economy = new EconomyHandler(this, integration);
getServer().getPluginManager().registerEvents(economy, this);
String[] econCmds = {"balance", "pay", "baltop", "shop", "sell", "market", "orders", "trade", "transactions"};
@@ -70,7 +56,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
}
// Register Guilds & Land Claims commands & GUI
if (settings.guildsEnabled()) {
{
GuildHandler guilds = new GuildHandler(this, integration);
getServer().getPluginManager().registerEvents(guilds, this);
bindCommand("guild", guilds);
@@ -94,6 +80,35 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
}
}
private Settings readSettings() {
return Settings.of(
getConfig().getString("panel-url", ""),
getConfig().getString("token", ""),
getConfig().getBoolean("leveling.enabled", true),
getConfig().getDouble("leveling.global-xp-multiplier", 1.0),
getConfig().getDouble("leveling.server-xp-multiplier", 1.5),
getConfig().getBoolean("quests.enabled", true),
getConfig().getBoolean("achievements.enabled", true),
getConfig().getBoolean("guilds.enabled", true),
getConfig().getBoolean("guilds.land-claiming", true),
getConfig().getBoolean("social.enabled", true),
getConfig().getBoolean("social.chat-prefixes", true),
getConfig().getBoolean("essentials.enabled", true),
getConfig().getBoolean("economy.enabled", true)
);
}
public void reloadIntegrationSettings() {
reloadConfig();
Settings next = readSettings();
if (integration == null) throw new IllegalStateException("Restart the plugin after correcting its configuration");
integration.reload(next);
}
public static String dimension(World world) {
return world.getKey().toString();
}
private void bindCommand(String name, org.bukkit.command.CommandExecutor executor) {
org.bukkit.command.PluginCommand cmd = getCommand(name);
if (cmd != null) {
@@ -132,7 +147,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
}
private void add(Player player, String stat) {
if (integration != null && integration.settings().levelingEnabled()) {
if (integration != null) {
integration.activity.add(player.getUniqueId(), player.getName(), stat, 1);
}
}
@@ -141,7 +156,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
public void broken(BlockBreakEvent event) {
if (integration != null && integration.settings().guildsEnabled() && integration.settings().landClaimingEnabled()) {
Chunk chunk = event.getBlock().getChunk();
ChunkCheckResult check = integration.checkChunk(event.getBlock().getWorld().getName(), chunk.getX(), chunk.getZ(), event.getPlayer().getUniqueId());
ChunkCheckResult check = integration.checkChunk(dimension(event.getBlock().getWorld()), chunk.getX(), chunk.getZ(), event.getPlayer().getUniqueId());
if (check.claimed() && !check.allowed()) {
event.setCancelled(true);
event.getPlayer().sendMessage(ChatColor.RED + "This chunk is claimed by [" + check.guildTag() + "] " + check.guildName() + "!");
@@ -155,7 +170,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
public void placed(BlockPlaceEvent event) {
if (integration != null && integration.settings().guildsEnabled() && integration.settings().landClaimingEnabled()) {
Chunk chunk = event.getBlock().getChunk();
ChunkCheckResult check = integration.checkChunk(event.getBlock().getWorld().getName(), chunk.getX(), chunk.getZ(), event.getPlayer().getUniqueId());
ChunkCheckResult check = integration.checkChunk(dimension(event.getBlock().getWorld()), chunk.getX(), chunk.getZ(), event.getPlayer().getUniqueId());
if (check.claimed() && !check.allowed()) {
event.setCancelled(true);
event.getPlayer().sendMessage(ChatColor.RED + "This chunk is claimed by [" + check.guildTag() + "] " + check.guildName() + "!");
@@ -177,7 +192,7 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void statistic(PlayerStatisticIncrementEvent event) {
if (integration == null || !integration.settings().levelingEnabled()) return;
if (integration == null) return;
String item = event.getMaterial() != null ? ":" + event.getMaterial().name()
: event.getEntityType() != null ? ":" + event.getEntityType().name() : "";
integration.activity.action(event.getPlayer().getUniqueId(), event.getPlayer().getName(),
@@ -35,6 +35,11 @@ public final class EconomyHandler implements CommandExecutor, Listener {
// Trade sessions: maps player UUID to active trade session
private final Map<UUID, TradeSession> activeTrades = new ConcurrentHashMap<>();
// Marketplace: slot index → listing id (populated when the GUI is opened)
// Note: this is per-opening; multiple concurrent GUIs share the same map which is
// acceptable because listings are keyed by slot and players cannot open two markets.
private final Map<Integer, Long> activeMarketListings = new ConcurrentHashMap<>();
// Standard sell values for common items
private static final Map<Material, Double> ITEM_SELL_VALUES = new HashMap<>();
static {
@@ -219,16 +224,22 @@ public final class EconomyHandler implements CommandExecutor, Listener {
}
int amount = hand.getAmount();
double total = unitPrice * amount;
player.getInventory().setItemInMainHand(null);
player.sendMessage(ChatColor.GRAY + "Processing sale...");
// FIX #5: Credit FIRST, only remove item after the server confirms payment.
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
double currentBal = integration.client().getBalance(player.getUniqueId(), player.getName());
double newBal = currentBal + total;
integration.client().syncBalance(player.getUniqueId(), player.getName(), newBal);
player.sendMessage(ChatColor.GREEN + "Sold " + amount + "x " + hand.getType().name() + " for "
+ ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", total) + ChatColor.GREEN + "!");
// FIX #4: atomic addBalance – no read-modify-write race condition.
double newBal = integration.client().addBalance(
player.getUniqueId(), player.getName(), total, "Sell hand: " + hand.getType().name());
Bukkit.getScheduler().runTask(plugin, () -> {
player.getInventory().setItemInMainHand(null);
player.sendMessage(ChatColor.GREEN + "Sold " + amount + "x " + hand.getType().name() + " for "
+ ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", total) + ChatColor.GREEN
+ "! Balance: " + ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", newBal));
});
} catch (Exception e) {
// Item was NOT removed – nothing to restore.
player.sendMessage(ChatColor.RED + "Failed to credit sell balance: " + e.getMessage());
}
});
@@ -262,42 +273,72 @@ public final class EconomyHandler implements CommandExecutor, Listener {
String itemName = hand.hasItemMeta() && hand.getItemMeta().hasDisplayName()
? hand.getItemMeta().getDisplayName() : hand.getType().name().replace('_', ' ');
player.getInventory().setItemInMainHand(null);
player.sendMessage(ChatColor.GRAY + "Listing item on marketplace...");
// FIX #5: Only remove item after the API accepted the listing.
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
integration.client().marketList(player.getUniqueId(), player.getName(), itemId, itemName, amount, price);
player.sendMessage(ChatColor.GREEN + "Successfully listed " + ChatColor.YELLOW + amount + "x " + itemName
+ ChatColor.GREEN + " on the market for " + ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", price) + "!");
Bukkit.getScheduler().runTask(plugin, () -> {
player.getInventory().setItemInMainHand(null);
player.sendMessage(ChatColor.GREEN + "Successfully listed " + ChatColor.YELLOW + amount + "x " + itemName
+ ChatColor.GREEN + " on the market for " + ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", price) + "!");
});
} catch (Exception e) {
player.sendMessage(ChatColor.RED + "Market listing failed: " + e.getMessage());
// Return item to player on main thread
Bukkit.getScheduler().runTask(plugin, () -> player.getInventory().addItem(hand));
// Item was never removed; no restoration needed.
}
});
return;
}
// Open Market GUI
// FIX #2: Open Market GUI populated from the real API. All click slots are cancelled.
Inventory inv = Bukkit.createInventory(null, 54, GUI_MARKET_TITLE);
ItemStack border = GuiHelper.createBorder(Material.BLUE_STAINED_GLASS_PANE);
for (int i = 45; i < 54; i++) inv.setItem(i, border);
inv.setItem(49, GuiHelper.createItem(Material.EMERALD, "&aSell an Item", "&7Hold an item and type:", "&e/market sell <price>"));
ItemStack loading = GuiHelper.createItem(Material.GRAY_STAINED_GLASS_PANE, "&7Loading listings...", "&8Please wait");
for (int i = 0; i < 45; i++) inv.setItem(i, loading);
player.openInventory(inv);
player.sendMessage(ChatColor.GRAY + "Loading market listings...");
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
// In a production setup, market listings come from API
JsonArray listings = integration.client().getMarketListings();
Bukkit.getScheduler().runTask(plugin, () -> {
// Populate some interactive market slots
inv.setItem(11, GuiHelper.createItem(Material.ENCHANTED_GOLDEN_APPLE, 1, "&6Enchanted Golden Apple", "&7Seller: &fSCOPENET Vault", "&7Price: &a$2,500.00", "", "&eClick to Buy!"));
inv.setItem(13, GuiHelper.createItem(Material.ELYTRA, 1, "&bElytra Wings", "&7Seller: &fAeroPlayer", "&7Price: &a$5,000.00", "", "&eClick to Buy!"));
inv.setItem(15, GuiHelper.createItem(Material.BEACON, 1, "&bNether Beacon", "&7Seller: &fMiningLord", "&7Price: &a$3,200.00", "", "&eClick to Buy!"));
// Clear loading placeholders
for (int i = 0; i < 45; i++) inv.setItem(i, null);
activeMarketListings.clear();
if (listings.size() == 0) {
inv.setItem(22, GuiHelper.createItem(Material.BARRIER, "&cNo listings", "&7Be the first to sell something!"));
return;
}
int slot = 0;
for (JsonElement el : listings) {
if (slot >= 45) break;
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";
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;
double listPrice = listing.has("price") ? listing.get("price").getAsDouble() : 0;
Material mat;
try { mat = Material.valueOf(listItemId); } catch (Exception ex) { mat = Material.PAPER; }
activeMarketListings.put(slot, id);
inv.setItem(slot, GuiHelper.createItem(mat, listAmt,
"&f" + listItemName + " &7(x" + listAmt + ")",
"&7Seller: &f" + sellerName,
"&7Price: &a$" + String.format(Locale.US, "%,.2f", listPrice),
"",
"&eClick to Buy!"));
slot++;
}
});
} catch (Exception ignored) {}
} catch (Exception e) {
player.sendMessage(ChatColor.RED + "Could not load market listings: " + e.getMessage());
}
});
}
@@ -342,8 +383,8 @@ public final class EconomyHandler implements CommandExecutor, Listener {
return;
}
// Open trade session for both players
TradeSession session = new TradeSession(player, target);
// FIX #3: Use the fixed TradeSession that restricts each side and transfers items.
TradeSession session = new TradeSession(plugin, integration, player, target);
activeTrades.put(player.getUniqueId(), session);
activeTrades.put(target.getUniqueId(), session);
session.open();
@@ -355,9 +396,11 @@ public final class EconomyHandler implements CommandExecutor, Listener {
String title = event.getView().getTitle();
if (title.equals(GUI_SHOP_TITLE)) {
// FIX #2/#4: Always cancel – items cannot be taken from the shop GUI.
event.setCancelled(true);
ItemStack clicked = event.getCurrentItem();
if (clicked == null || !clicked.hasItemMeta() || clicked.getItemMeta().getLore() == null) return;
if (clicked == null || clicked.getType().isAir()
|| !clicked.hasItemMeta() || clicked.getItemMeta().getLore() == null) return;
// Extract price from lore
double price = -1;
@@ -372,22 +415,16 @@ public final class EconomyHandler implements CommandExecutor, Listener {
if (price <= 0) return;
final double finalPrice = price;
final ItemStack toBuy = clicked.clone();
player.sendMessage(ChatColor.GRAY + "Processing purchase...");
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
double bal = integration.client().getBalance(player.getUniqueId(), player.getName());
if (bal < finalPrice) {
player.sendMessage(ChatColor.RED + "Insufficient funds! Needed: $" + String.format(Locale.US, "%,.2f", finalPrice)
+ ", you have: $" + String.format(Locale.US, "%,.2f", bal));
return;
}
double newBal = bal - finalPrice;
integration.client().syncBalance(player.getUniqueId(), player.getName(), newBal);
// FIX #4: Atomic debit – the panel validates the balance and deducts atomically.
double newBal = integration.client().addBalance(
player.getUniqueId(), player.getName(), -finalPrice, "Shop purchase: " + toBuy.getType().name());
Bukkit.getScheduler().runTask(plugin, () -> {
ItemStack give = new ItemStack(clicked.getType(), clicked.getAmount());
ItemStack give = new ItemStack(toBuy.getType(), toBuy.getAmount());
player.getInventory().addItem(give);
player.sendMessage(ChatColor.GREEN + "Purchased " + give.getAmount() + "x " + give.getType().name()
+ " for " + ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", finalPrice) + ChatColor.GREEN
@@ -397,6 +434,39 @@ public final class EconomyHandler implements CommandExecutor, Listener {
player.sendMessage(ChatColor.RED + "Shop transaction failed: " + e.getMessage());
}
});
} else if (title.equals(GUI_MARKET_TITLE)) {
// FIX #2: Always cancel market clicks – purchase only via the API.
event.setCancelled(true);
int slot = event.getRawSlot();
Long listingId = activeMarketListings.get(slot);
if (listingId == null || listingId < 0) return;
player.sendMessage(ChatColor.GRAY + "Processing market purchase...");
final long fListingId = listingId;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
JsonObject result = integration.client().marketBuy(fListingId, player.getUniqueId(), player.getName());
String itemId = result.has("item_id") ? result.get("item_id").getAsString() : null;
int amt = result.has("amount") ? result.get("amount").getAsInt() : 1;
double newBal = result.has("new_balance") ? result.get("new_balance").getAsDouble() : 0;
if (itemId != null) {
Bukkit.getScheduler().runTask(plugin, () -> {
try {
Material mat = Material.valueOf(itemId);
player.getInventory().addItem(new ItemStack(mat, amt));
player.sendMessage(ChatColor.GREEN + "Purchased " + amt + "x " + mat.name()
+ " from the market! Balance: " + ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", newBal));
} catch (Exception ex) {
player.sendMessage(ChatColor.GREEN + "Purchase successful! Balance: " + ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", newBal));
}
});
} else {
player.sendMessage(ChatColor.GREEN + "Market purchase successful!");
}
} catch (Exception e) {
player.sendMessage(ChatColor.RED + "Market purchase failed: " + e.getMessage());
}
});
} else if (title.startsWith(GUI_TRADE_PREFIX)) {
TradeSession session = activeTrades.get(player.getUniqueId());
if (session != null) {
@@ -411,8 +481,10 @@ public final class EconomyHandler implements CommandExecutor, Listener {
String title = event.getView().getTitle();
if (title.equals(GUI_SELL_TITLE)) {
// Process sell chest items
// FIX #4/#5: Collect items, credit atomically, only confirm sale after success.
Inventory inv = event.getInventory();
List<ItemStack> soldItems = new ArrayList<>();
List<ItemStack> unsellable = new ArrayList<>();
double totalEarned = 0;
int countSold = 0;
@@ -422,42 +494,80 @@ public final class EconomyHandler implements CommandExecutor, Listener {
if (price != null) {
totalEarned += price * item.getAmount();
countSold += item.getAmount();
soldItems.add(item.clone());
} else {
// Return unsold item to player
player.getInventory().addItem(item);
unsellable.add(item.clone());
}
}
// Return unsellable items immediately
for (ItemStack it : unsellable) {
player.getInventory().addItem(it);
}
if (totalEarned > 0) {
final double earned = totalEarned;
final int itemsCount = countSold;
// FIX #4: addBalance is atomic – no read-modify-write race.
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
double cur = integration.client().getBalance(player.getUniqueId(), player.getName());
integration.client().syncBalance(player.getUniqueId(), player.getName(), cur + earned);
double newBal = integration.client().addBalance(
player.getUniqueId(), player.getName(), earned, "Sell chest: " + itemsCount + " items");
player.sendMessage(ChatColor.GREEN + "Sold " + itemsCount + " items for "
+ ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", earned) + ChatColor.GREEN + "!");
+ ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", earned) + ChatColor.GREEN + "!"
+ " Balance: " + ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", newBal));
} catch (Exception e) {
player.sendMessage(ChatColor.RED + "Failed to credit earnings: " + e.getMessage());
// FIX #5: Credit failed – return items to player.
player.sendMessage(ChatColor.RED + "Failed to credit earnings, items returned: " + e.getMessage());
Bukkit.getScheduler().runTask(plugin, () -> {
for (ItemStack it : soldItems) {
player.getInventory().addItem(it);
}
});
}
});
}
} else if (title.startsWith(GUI_TRADE_PREFIX)) {
TradeSession session = activeTrades.remove(player.getUniqueId());
TradeSession session = activeTrades.get(player.getUniqueId());
if (session != null) {
session.cancel();
// Only cancel if both players have closed
session.handleClose(player, activeTrades);
}
}
}
/**
* FIX #3: Trade session that:
* - Restricts each player to their own side of the shared inventory.
* - Transfers items between inventories on completion.
* - Returns items if the trade is cancelled.
*
* Layout: slots 0-3,9-12,18-21,27-30,36-39 = P1's offer side
* slots 5-8,14-17,23-26,32-35,41-44 = P2's offer side
* slot 4,13,22,31,40,49 = dividers (not interactable)
* slot 0 = P1 ready toggle, slot 8 = P2 ready toggle
*/
private static class TradeSession {
private final ScopenetPlugin plugin;
private final Integration integration;
final Player p1;
final Player p2;
final Inventory inv;
boolean p1Ready = false;
boolean p2Ready = false;
boolean completed = false;
boolean cancelled = false;
TradeSession(Player p1, Player p2) {
// Slots belonging to each side (excluding ready buttons at 0 and 8)
private static final Set<Integer> P1_SLOTS = new HashSet<>(Arrays.asList(
1, 2, 3, 9, 10, 11, 12, 18, 19, 20, 21, 27, 28, 29, 30, 36, 37, 38, 39));
private static final Set<Integer> P2_SLOTS = new HashSet<>(Arrays.asList(
5, 6, 7, 14, 15, 16, 17, 23, 24, 25, 26, 32, 33, 34, 35, 41, 42, 43, 44));
private static final Set<Integer> DIVIDERS = new HashSet<>(Arrays.asList(4, 13, 22, 31, 40, 49));
TradeSession(ScopenetPlugin plugin, Integration integration, Player p1, Player p2) {
this.plugin = plugin;
this.integration = integration;
this.p1 = p1;
this.p2 = p2;
this.inv = Bukkit.createInventory(null, 54, GUI_TRADE_PREFIX + p1.getName() + " & " + p2.getName());
@@ -466,8 +576,7 @@ public final class EconomyHandler implements CommandExecutor, Listener {
void buildDividers() {
ItemStack divider = GuiHelper.createBorder(Material.GRAY_STAINED_GLASS_PANE);
int[] dividerSlots = {4, 13, 22, 31, 40, 49};
for (int s : dividerSlots) inv.setItem(s, divider);
for (int s : DIVIDERS) inv.setItem(s, divider);
updateReadyButtons();
}
@@ -484,9 +593,12 @@ public final class EconomyHandler implements CommandExecutor, Listener {
}
void handleClick(Player clicker, InventoryClickEvent event) {
if (completed || cancelled) { event.setCancelled(true); return; }
int slot = event.getRawSlot();
boolean isP1 = clicker.getUniqueId().equals(p1.getUniqueId());
// Ready toggle buttons
if (slot == 0 && isP1) {
event.setCancelled(true);
p1Ready = !p1Ready;
@@ -502,24 +614,80 @@ public final class EconomyHandler implements CommandExecutor, Listener {
return;
}
// Divider slots are not interactable
if (slot == 4 || slot == 13 || slot == 22 || slot == 31 || slot == 40 || slot == 49) {
// Dividers and ready buttons on the wrong side are always blocked
if (DIVIDERS.contains(slot) || slot == 0 || slot == 8) {
event.setCancelled(true);
return;
}
// FIX #3: Each player can only interact with their own offer side.
if (isP1 && P2_SLOTS.contains(slot)) { event.setCancelled(true); return; }
if (!isP1 && P1_SLOTS.contains(slot)) { event.setCancelled(true); return; }
// If a player touches their side after being ready, reset ready state.
if (isP1 && p1Ready) { p1Ready = false; updateReadyButtons(); }
if (!isP1 && p2Ready) { p2Ready = false; updateReadyButtons(); }
}
void checkCompletion() {
if (p1Ready && p2Ready) {
if (!p1Ready || !p2Ready || completed || cancelled) return;
completed = true;
// FIX #3: Actually transfer items between players.
// Collect each side's offered items.
List<ItemStack> p1Offer = new ArrayList<>();
List<ItemStack> p2Offer = new ArrayList<>();
for (int s : P1_SLOTS) {
ItemStack it = inv.getItem(s);
if (it != null && !it.getType().isAir()) p1Offer.add(it.clone());
}
for (int s : P2_SLOTS) {
ItemStack it = inv.getItem(s);
if (it != null && !it.getType().isAir()) p2Offer.add(it.clone());
}
p1.closeInventory();
p2.closeInventory();
// Give each player the other's offered items on the main thread.
Bukkit.getScheduler().runTask(plugin, () -> {
for (ItemStack it : p2Offer) p1.getInventory().addItem(it);
for (ItemStack it : p1Offer) p2.getInventory().addItem(it);
p1.sendMessage(ChatColor.GREEN + "Trade completed successfully!");
p2.sendMessage(ChatColor.GREEN + "Trade completed successfully!");
p1.closeInventory();
p2.closeInventory();
}
});
}
void cancel() {
p1.sendMessage(ChatColor.RED + "Trade cancelled.");
p2.sendMessage(ChatColor.RED + "Trade cancelled.");
void handleClose(Player closer, Map<UUID, TradeSession> activeTrades) {
if (completed) {
activeTrades.remove(p1.getUniqueId());
activeTrades.remove(p2.getUniqueId());
return;
}
if (cancelled) return;
cancelled = true;
activeTrades.remove(p1.getUniqueId());
activeTrades.remove(p2.getUniqueId());
// FIX #3: Return items to their respective owners.
Bukkit.getScheduler().runTask(plugin, () -> {
for (int s : P1_SLOTS) {
ItemStack it = inv.getItem(s);
if (it != null && !it.getType().isAir()) p1.getInventory().addItem(it.clone());
}
for (int s : P2_SLOTS) {
ItemStack it = inv.getItem(s);
if (it != null && !it.getType().isAir()) p2.getInventory().addItem(it.clone());
}
if (p1.isOnline()) {
p1.closeInventory();
p1.sendMessage(ChatColor.RED + "Trade cancelled. Your items have been returned.");
}
if (p2.isOnline()) {
p2.closeInventory();
p2.sendMessage(ChatColor.RED + "Trade cancelled. Your items have been returned.");
}
});
}
}
}
@@ -0,0 +1,128 @@
package net.scopenet.paper.commands;
import com.google.gson.*;
import net.scopenet.integration.Integration;
import net.scopenet.integration.PanelClient;
import net.scopenet.paper.ScopenetPlugin;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.io.File;
import java.util.*;
/** Durable escrow plus idempotent panel requests. All inventory/file access is on the server thread. */
final class EconomyOperations {
private final ScopenetPlugin plugin;
private final Integration integration;
private final File file;
private final Map<String, JsonObject> jobs = new LinkedHashMap<>();
private final Set<String> running = new HashSet<>();
EconomyOperations(ScopenetPlugin plugin, Integration integration) {
this.plugin = plugin;
this.integration = integration;
file = new File(plugin.getDataFolder(), "economy-pending.yml");
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
for (String id : yaml.getKeys(false)) jobs.put(id, JsonParser.parseString(yaml.getString(id)).getAsJsonObject());
Bukkit.getScheduler().runTaskTimer(plugin, this::tick, 20, 100);
}
static String encode(ItemStack item) {
YamlConfiguration yaml = new YamlConfiguration();
yaml.set("item", item);
return yaml.saveToString();
}
static ItemStack decode(String data) throws Exception {
YamlConfiguration yaml = new YamlConfiguration();
yaml.loadFromString(data);
ItemStack item = yaml.getItemStack("item");
if (item == null) throw new IllegalArgumentException("Missing item data");
return item;
}
static void give(Player player, ItemStack item) {
player.getInventory().addItem(item).values().forEach(left -> player.getWorld().dropItemNaturally(player.getLocation(), left));
}
boolean enqueue(Player player, String endpoint, JsonObject payload, List<ItemStack> escrow, ItemStack purchase) {
String id = UUID.randomUUID().toString();
payload.addProperty("operation_id", id);
JsonObject job = new JsonObject();
job.addProperty("player", player.getUniqueId().toString());
job.addProperty("endpoint", endpoint);
job.add("payload", payload);
JsonArray items = new JsonArray();
for (ItemStack item : escrow) items.add(encode(item));
job.add("escrow", items);
if (purchase != null) job.addProperty("purchase", encode(purchase));
jobs.put(id, job);
try { save(); } catch (Exception e) {
jobs.remove(id);
player.sendMessage(ChatColor.RED + "Cannot save transaction; nothing was purchased or sold.");
return false;
}
player.sendMessage(ChatColor.GRAY + "Transaction queued. Items are kept safe while the panel responds.");
return true;
}
private void save() throws Exception {
YamlConfiguration yaml = new YamlConfiguration();
jobs.forEach((id, job) -> yaml.set(id, job.toString()));
File temp = new File(file.getParentFile(), file.getName() + ".tmp");
yaml.save(temp);
java.nio.file.Files.move(temp.toPath(), file.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
private void tick() {
for (var entry : new ArrayList<>(jobs.entrySet())) {
String id = entry.getKey();
JsonObject job = entry.getValue();
if (job.has("result") || job.has("error")) { deliver(id, job); continue; }
if (!running.add(id)) continue;
String endpoint = job.get("endpoint").getAsString();
JsonObject payload = job.getAsJsonObject("payload").deepCopy();
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
JsonObject result = null;
String error = null;
try { result = integration.client().post(endpoint, payload); }
catch (PanelClient.HttpFailure e) { if (e.status >= 400 && e.status < 500) error = e.getMessage(); }
catch (Exception e) { /* Unknown outcome: retry the same operation ID; never duplicate a credit. */ }
final JsonObject response = result;
final String failure = error;
if (!plugin.isEnabled()) return;
Bukkit.getScheduler().runTask(plugin, () -> {
running.remove(id);
if (response != null) job.add("result", response);
if (failure != null) job.addProperty("error", failure);
try { save(); } catch (Exception e) { plugin.getLogger().severe("Could not persist economy result: " + e.getMessage()); return; }
if (response != null || failure != null) deliver(id, job);
});
});
}
}
private void deliver(String id, JsonObject job) {
Player player = Bukkit.getPlayer(UUID.fromString(job.get("player").getAsString()));
if (player == null || !player.isOnline()) return;
try {
List<ItemStack> items = new ArrayList<>();
if (job.has("error")) {
for (JsonElement encoded : job.getAsJsonArray("escrow")) items.add(decode(encoded.getAsString()));
} else if (job.has("purchase")) {
items.add(decode(job.get("purchase").getAsString()));
} else if (job.get("endpoint").getAsString().equals("economy/market/buy")) {
JsonObject result = job.getAsJsonObject("result");
if (result.has("item_data") && !result.get("item_data").isJsonNull()) items.add(decode(result.get("item_data").getAsString()));
else items.add(new ItemStack(org.bukkit.Material.valueOf(result.get("item_id").getAsString()), result.get("amount").getAsInt()));
}
for (ItemStack item : items) give(player, item);
jobs.remove(id);
save();
player.sendMessage(job.has("error") ? ChatColor.RED + "Transaction rejected; held items returned: " + job.get("error").getAsString()
: ChatColor.GREEN + "Transaction completed.");
} catch (Exception e) { plugin.getLogger().severe("Economy delivery requires attention for " + id + ": " + e.getMessage()); }
}
}
@@ -147,6 +147,7 @@ public final class EssentialsHandler implements CommandExecutor, Listener {
return true;
}
if (!plugin.getConfig().getBoolean("essentials.enabled", true)) { player.sendMessage(ChatColor.RED + "Essentials are disabled."); return true; }
String cmd = command.getName().toLowerCase();
switch (cmd) {
case "spawn" -> handleSpawn(player);
@@ -71,6 +71,7 @@ public final class GuildHandler implements CommandExecutor, Listener {
return true;
}
if (!integration.settings().guildsEnabled()) { player.sendMessage(ChatColor.RED + "Guilds are disabled."); return true; }
String cmd = command.getName().toLowerCase();
if (cmd.equals("claim")) {
handleClaim(player);
@@ -120,7 +121,7 @@ public final class GuildHandler implements CommandExecutor, Listener {
private void handleClaim(Player player) {
Chunk chunk = player.getLocation().getChunk();
String dim = player.getWorld().getName();
String dim = ScopenetPlugin.dimension(player.getWorld());
player.sendMessage(ChatColor.GRAY + "Claiming chunk [" + chunk.getX() + ", " + chunk.getZ() + "]...");
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
@@ -136,7 +137,7 @@ public final class GuildHandler implements CommandExecutor, Listener {
private void handleUnclaim(Player player) {
Chunk chunk = player.getLocation().getChunk();
String dim = player.getWorld().getName();
String dim = ScopenetPlugin.dimension(player.getWorld());
player.sendMessage(ChatColor.GRAY + "Unclaiming chunk [" + chunk.getX() + ", " + chunk.getZ() + "]...");
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
@@ -255,7 +256,7 @@ public final class GuildHandler implements CommandExecutor, Listener {
Chunk center = pLoc.getChunk();
int cx = center.getX();
int cz = center.getZ();
String dim = pLoc.getWorld().getName();
String dim = ScopenetPlugin.dimension(pLoc.getWorld());
player.sendMessage(ChatColor.GOLD + "========= " + ChatColor.YELLOW + "Territory Map (" + cx + ", " + cz + ")" + ChatColor.GOLD + " =========");
player.sendMessage(ChatColor.GRAY + " N (-Z)");
@@ -124,7 +124,9 @@ public final class ScopenetCommandHandler implements CommandExecutor {
return;
}
plugin.reloadConfig();
sender.sendMessage(ChatColor.GREEN + "SCOPENET configuration reloaded successfully.");
try {
plugin.reloadIntegrationSettings();
sender.sendMessage(ChatColor.GREEN + "SCOPENET configuration reloaded successfully.");
} catch (Exception e) { sender.sendMessage(ChatColor.RED + "Reload failed; existing integration settings retained: " + e.getMessage()); }
}
}