fix: resolve all remaining audit failures across rust, svelte, and java
This commit is contained in:
1 parent
bc12a25a01
commit
590178e41a
35 files changed
+958
-300
No files matched your search
@@ -9,7 +9,7 @@ import java.util.function.*;
|
||||
public final class Integration implements AutoCloseable {
|
||||
public static final String UNAVAILABLE = "SCOPENET sign-in is unavailable. Please try again shortly.";
|
||||
public final Activity activity = new Activity();
|
||||
private final Settings settings;
|
||||
private volatile Settings settings;
|
||||
private final PanelClient client;
|
||||
private final JsonObject hello;
|
||||
private final Consumer<String> log;
|
||||
@@ -115,6 +115,11 @@ public final class Integration implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
public void reload(Settings next) {
|
||||
client.updateSettings(next);
|
||||
settings = next;
|
||||
}
|
||||
|
||||
public Settings settings() {
|
||||
return settings;
|
||||
}
|
||||
@@ -129,6 +134,7 @@ public final class Integration implements AutoCloseable {
|
||||
|
||||
@Override public void close() {
|
||||
closed = true;
|
||||
client.close();
|
||||
loginWorker.shutdownNow();
|
||||
activity.leaveAll();
|
||||
Future<?> flush = syncWorker.submit(() -> {
|
||||
|
||||
@@ -7,7 +7,23 @@ import java.net.http.*;
|
||||
import java.time.Duration;
|
||||
|
||||
public final class PanelClient {
|
||||
private final Settings settings;
|
||||
private volatile Settings settings;
|
||||
private record ChunkKey(String dimension, int x, int z, java.util.UUID uuid) {}
|
||||
private record Cached(ChunkCheckResult result, long expires) {}
|
||||
private final java.util.Map<ChunkKey, Cached> claims = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
private final java.util.Set<ChunkKey> pendingClaims = java.util.concurrent.ConcurrentHashMap.newKeySet();
|
||||
private final java.util.concurrent.ThreadPoolExecutor claimWorker = new java.util.concurrent.ThreadPoolExecutor(
|
||||
2, 2, 0, java.util.concurrent.TimeUnit.SECONDS, new java.util.concurrent.ArrayBlockingQueue<>(64),
|
||||
task -> { Thread t = new Thread(task, "scopenet-claims"); t.setDaemon(true); return t; });
|
||||
private static final ChunkCheckResult UNKNOWN = new ChunkCheckResult(true, false, "Protection check pending or unavailable", "WAIT");
|
||||
private final java.util.concurrent.atomic.AtomicLong claimGeneration = new java.util.concurrent.atomic.AtomicLong();
|
||||
public void invalidateClaims() { claimGeneration.incrementAndGet(); claims.clear(); }
|
||||
public void updateSettings(Settings next) { settings = next; invalidateClaims(); }
|
||||
public void close() { claimWorker.shutdownNow(); }
|
||||
public static class HttpFailure extends IOException {
|
||||
public final int status;
|
||||
public HttpFailure(int status, String message) { super(message); this.status = status; }
|
||||
}
|
||||
private final HttpClient client = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(3))
|
||||
.followRedirects(HttpClient.Redirect.NEVER).build();
|
||||
@@ -28,7 +44,7 @@ public final class PanelClient {
|
||||
if (err.has("message")) msg = err.get("message").getAsString();
|
||||
else if (err.has("error")) msg = err.get("error").getAsString();
|
||||
} catch (Exception ignored) {}
|
||||
throw new IOException(msg);
|
||||
throw new HttpFailure(response.statusCode(), msg);
|
||||
}
|
||||
try {
|
||||
return JsonParser.parseString(response.body());
|
||||
@@ -53,6 +69,24 @@ public final class PanelClient {
|
||||
}
|
||||
|
||||
public ChunkCheckResult checkChunk(String dimension, int chunkX, int chunkZ, java.util.UUID uuid) {
|
||||
if (!settings.guildsEnabled() || !settings.landClaimingEnabled()) return new ChunkCheckResult(false, true, null, null);
|
||||
ChunkKey key = new ChunkKey(dimension, chunkX, chunkZ, uuid);
|
||||
Cached cached = claims.get(key);
|
||||
if (cached != null && cached.expires() > System.nanoTime()) return cached.result();
|
||||
if (pendingClaims.add(key)) {
|
||||
long generation = claimGeneration.get();
|
||||
try { claimWorker.execute(() -> {
|
||||
try {
|
||||
ChunkCheckResult result = checkChunkRemote(dimension, chunkX, chunkZ, uuid);
|
||||
if (claims.size() > 4096) claims.clear();
|
||||
if (generation == claimGeneration.get()) claims.put(key, new Cached(result, System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(2)));
|
||||
} finally { pendingClaims.remove(key); }
|
||||
}); } catch (java.util.concurrent.RejectedExecutionException e) { pendingClaims.remove(key); }
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
private ChunkCheckResult checkChunkRemote(String dimension, int chunkX, int chunkZ, java.util.UUID uuid) {
|
||||
if (!settings.guildsEnabled() || !settings.landClaimingEnabled()) {
|
||||
return new ChunkCheckResult(false, true, null, null);
|
||||
}
|
||||
@@ -64,12 +98,12 @@ public final class PanelClient {
|
||||
try {
|
||||
JsonObject res = post("guilds/check-chunk", req);
|
||||
boolean claimed = res.has("claimed") && res.get("claimed").getAsBoolean();
|
||||
boolean allowed = !res.has("allowed") || res.get("allowed").getAsBoolean();
|
||||
boolean allowed = res.has("allowed") && res.get("allowed").getAsBoolean();
|
||||
String guildName = res.has("guild_name") && !res.get("guild_name").isJsonNull() ? res.get("guild_name").getAsString() : null;
|
||||
String guildTag = res.has("guild_tag") && !res.get("guild_tag").isJsonNull() ? res.get("guild_tag").getAsString() : null;
|
||||
return new ChunkCheckResult(claimed, allowed, guildName, guildTag);
|
||||
} catch (Exception e) {
|
||||
return new ChunkCheckResult(false, true, null, null);
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +113,9 @@ public final class PanelClient {
|
||||
req.addProperty("dimension", dimension);
|
||||
req.addProperty("chunk_x", chunkX);
|
||||
req.addProperty("chunk_z", chunkZ);
|
||||
return post("guilds/claim", req);
|
||||
JsonObject result = post("guilds/claim", req);
|
||||
invalidateClaims();
|
||||
return result;
|
||||
}
|
||||
|
||||
public JsonObject unclaimChunk(String dimension, int chunkX, int chunkZ, java.util.UUID uuid) throws IOException, InterruptedException {
|
||||
@@ -88,7 +124,9 @@ public final class PanelClient {
|
||||
req.addProperty("dimension", dimension);
|
||||
req.addProperty("chunk_x", chunkX);
|
||||
req.addProperty("chunk_z", chunkZ);
|
||||
return post("guilds/unclaim", req);
|
||||
JsonObject result = post("guilds/unclaim", req);
|
||||
invalidateClaims();
|
||||
return result;
|
||||
}
|
||||
|
||||
public JsonObject getPlayerGuild(java.util.UUID uuid) throws IOException, InterruptedException {
|
||||
@@ -147,6 +185,7 @@ public final class PanelClient {
|
||||
|
||||
public JsonObject marketList(java.util.UUID sellerUuid, String sellerName, String itemId, String itemName, int amount, double price) throws IOException, InterruptedException {
|
||||
JsonObject req = new JsonObject();
|
||||
req.addProperty("operation_id", java.util.UUID.randomUUID().toString());
|
||||
req.addProperty("seller_uuid", sellerUuid.toString());
|
||||
req.addProperty("seller_name", sellerName);
|
||||
req.addProperty("item_id", itemId);
|
||||
@@ -158,10 +197,33 @@ public final class PanelClient {
|
||||
|
||||
public JsonObject marketBuy(long listingId, java.util.UUID buyerUuid, String buyerName) throws IOException, InterruptedException {
|
||||
JsonObject req = new JsonObject();
|
||||
req.addProperty("operation_id", java.util.UUID.randomUUID().toString());
|
||||
req.addProperty("listing_id", listingId);
|
||||
req.addProperty("buyer_uuid", buyerUuid.toString());
|
||||
req.addProperty("buyer_name", buyerName);
|
||||
return post("economy/market/buy", req);
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically add (or subtract, if amount is negative) to a player's balance.
|
||||
* Returns the new balance. This avoids the read-modify-write race in the old
|
||||
* getBalance + syncBalance pattern.
|
||||
*/
|
||||
public double addBalance(java.util.UUID uuid, String username, double amount, String description) throws IOException, InterruptedException {
|
||||
JsonObject req = new JsonObject();
|
||||
req.addProperty("operation_id", java.util.UUID.randomUUID().toString());
|
||||
req.addProperty("uuid", uuid.toString());
|
||||
req.addProperty("username", username);
|
||||
req.addProperty("delta", amount);
|
||||
req.addProperty("description", description);
|
||||
JsonObject res = post("economy/adjust", req);
|
||||
return res.has("balance") ? res.get("balance").getAsDouble() : 0.0;
|
||||
}
|
||||
|
||||
/** Fetch active player marketplace listings. */
|
||||
public JsonArray getMarketListings() throws IOException, InterruptedException {
|
||||
JsonElement el = postElement("economy/market", new JsonObject());
|
||||
return el.isJsonArray() ? el.getAsJsonArray() : new JsonArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,16 +63,13 @@ public final class Bridge {
|
||||
public static void stat(ServerPlayer player, String key, int amount) {
|
||||
Integration current = integration;
|
||||
if (current == null) return;
|
||||
if (!current.settings().levelingEnabled() && (key.equals("blocks_broken") || key.equals("blocks_placed") || key.equals("player_kills") || key.equals("mob_kills"))) {
|
||||
return;
|
||||
}
|
||||
current.activity.add(player.getUUID(), player.getName().getString(), key, amount);
|
||||
if (key.equals("deaths")) current.activity.event(player.getUUID(), player.getName().getString(), "death", null);
|
||||
}
|
||||
|
||||
public static void action(ServerPlayer player, String action, int amount) {
|
||||
Integration current = integration;
|
||||
if (current != null && current.settings().levelingEnabled()) current.activity.action(player.getUUID(), player.getName().getString(), action, amount);
|
||||
if (current != null) current.activity.action(player.getUUID(), player.getName().getString(), action, amount);
|
||||
}
|
||||
|
||||
public static void command(ServerPlayer player, String command) {
|
||||
|
||||
@@ -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)");
|
||||
|
||||
+4
-2
@@ -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()); }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user