From 752f0cc43d7ed98350183f629f9c59b787fb6092 Mon Sep 17 00:00:00 2001 From: scoped Date: Tue, 29 Sep 2026 12:14:16 -0400 Subject: [PATCH] feat: complete in-game commands, GUIs, admin panel fixes, and launcher enhancements --- crates/shared/src/lib.rs | 102 +++- .../net/scopenet/integration/Integration.java | 4 + .../net/scopenet/integration/PanelClient.java | 110 +++- .../net/scopenet/integration/Settings.java | 52 +- .../net/scopenet/paper/ScopenetPlugin.java | 46 +- .../paper/commands/EconomyHandler.java | 525 ++++++++++++++++++ .../paper/commands/EssentialsHandler.java | 403 ++++++++++++++ .../scopenet/paper/commands/GuildHandler.java | 414 ++++++++++++++ .../commands/ScopenetCommandHandler.java | 130 +++++ .../net/scopenet/paper/gui/GuiHelper.java | 41 ++ .../paper/src/main/resources/config.yml | 15 + .../paper/src/main/resources/plugin.yml | 67 +++ launcher/src-tauri/src/commands.rs | 78 ++- launcher/src-tauri/src/lib.rs | 4 + launcher/src/lib/types.ts | 42 ++ launcher/src/pages/Quests.svelte | 86 ++- launcher/src/pages/Social.svelte | 245 +++++++- launcher/src/pages/Stats.svelte | 221 +++++++- panel/server/src/db.rs | 38 ++ panel/server/src/routes/achievements.rs | 45 +- panel/server/src/routes/economy.rs | 483 ++++++++++++++++ panel/server/src/routes/guilds.rs | 439 ++++++++++++++- panel/server/src/routes/landing.rs | 20 +- panel/server/src/routes/leveling.rs | 141 ++++- panel/server/src/routes/mod.rs | 34 +- panel/server/src/routes/quests.rs | 40 +- panel/server/src/routes/social.rs | 71 ++- panel/web/src/lib/types.ts | 1 + panel/web/src/pages/AchievementEditor.svelte | 19 +- panel/web/src/pages/GuildsAdmin.svelte | 28 +- panel/web/src/pages/LandingBuilder.svelte | 257 +++++++-- panel/web/src/pages/LevelingAdmin.svelte | 16 +- panel/web/src/pages/PublicLanding.svelte | 117 +++- panel/web/src/pages/QuestEditor.svelte | 17 +- 34 files changed, 4170 insertions(+), 181 deletions(-) create mode 100644 integrations/paper/src/main/java/net/scopenet/paper/commands/EconomyHandler.java create mode 100644 integrations/paper/src/main/java/net/scopenet/paper/commands/EssentialsHandler.java create mode 100644 integrations/paper/src/main/java/net/scopenet/paper/commands/GuildHandler.java create mode 100644 integrations/paper/src/main/java/net/scopenet/paper/commands/ScopenetCommandHandler.java create mode 100644 integrations/paper/src/main/java/net/scopenet/paper/gui/GuiHelper.java create mode 100644 panel/server/src/routes/economy.rs diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index fe858e8..78639fb 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -417,13 +417,34 @@ pub struct LaunchEvent { pub struct UserLevelInfo { pub uuid: String, pub global_xp: i64, + #[serde(default)] + pub current_xp: i64, pub global_level: i64, + #[serde(default)] + pub level: i64, pub current_level_xp: i64, pub next_level_xp: i64, pub progress_pct: f64, pub title: Option, pub badges: Vec, pub server_levels: Vec, + #[serde(default)] + pub rank: Option, + #[serde(default)] + pub rewards: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct LevelReward { + pub id: i64, + pub level: i64, + pub reward_type: String, + pub reward_value: String, + pub description: String, + #[serde(default)] + pub icon: Option, + #[serde(default)] + pub server_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] @@ -450,16 +471,26 @@ pub struct Quest { pub xp_reward: i64, pub icon: String, pub enabled: bool, + #[serde(default)] + pub quest_type: Option, + #[serde(default)] + pub stat_type: Option, + #[serde(default)] + pub active: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] pub struct UserQuest { - #[serde(flatten)] pub quest: Quest, + #[serde(default)] pub progress: i64, + #[serde(default)] + pub current_count: i64, pub completed: bool, pub claimed: bool, pub period_key: String, + #[serde(default)] + pub expires_at: Option, } // --------------------------------------------------------------------------- @@ -473,17 +504,86 @@ pub struct Achievement { pub description: String, pub category: String, pub icon_frame: String, // "task", "goal", "challenge" + #[serde(default)] + pub frame_type: Option, pub icon_item: String, pub icon_bg: String, pub icon_border: String, pub xp_reward: i64, pub secret: bool, #[serde(default)] + pub stat_type: Option, + #[serde(default)] + pub target_count: Option, + #[serde(default)] pub unlocked: bool, #[serde(default)] pub unlocked_at: Option, } +// --------------------------------------------------------------------------- +// Economy +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct ServerEconomyBalance { + pub server_id: i64, + pub server_name: String, + pub balance: f64, + pub currency_symbol: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct EconomyTransaction { + pub id: i64, + pub server_id: i64, + #[serde(default)] + pub server_name: Option, + pub from_uuid: String, + pub from_name: String, + pub to_uuid: String, + pub to_name: String, + pub amount: f64, + pub description: String, + pub created_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct BaltopEntry { + pub rank: usize, + pub uuid: String, + pub username: String, + pub balance: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct MarketListing { + pub id: i64, + pub server_id: i64, + pub seller_uuid: String, + pub seller_name: String, + pub item_id: String, + pub item_name: String, + pub amount: i32, + pub price: f64, + pub created_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct MemberProfile { + pub uuid: String, + pub username: String, + pub role: String, + pub status: String, + pub skin_url: Option, + pub global_level: i64, + pub title: Option, + pub playtime_secs: i64, + pub last_seen: Option, + pub online: bool, + pub is_friend: bool, +} + // --------------------------------------------------------------------------- // Guilds & Land Claiming // --------------------------------------------------------------------------- diff --git a/integrations/common/src/main/java/net/scopenet/integration/Integration.java b/integrations/common/src/main/java/net/scopenet/integration/Integration.java index 5ffcb84..053c878 100644 --- a/integrations/common/src/main/java/net/scopenet/integration/Integration.java +++ b/integrations/common/src/main/java/net/scopenet/integration/Integration.java @@ -119,6 +119,10 @@ public final class Integration implements AutoCloseable { return settings; } + public PanelClient client() { + return client; + } + public ChunkCheckResult checkChunk(String dimension, int chunkX, int chunkZ, UUID uuid) { return client.checkChunk(dimension, chunkX, chunkZ, uuid); } diff --git a/integrations/common/src/main/java/net/scopenet/integration/PanelClient.java b/integrations/common/src/main/java/net/scopenet/integration/PanelClient.java index cc25e3f..a425654 100644 --- a/integrations/common/src/main/java/net/scopenet/integration/PanelClient.java +++ b/integrations/common/src/main/java/net/scopenet/integration/PanelClient.java @@ -14,24 +14,33 @@ public final class PanelClient { public PanelClient(Settings settings) { this.settings = settings; } - public JsonObject post(String endpoint, JsonObject payload) throws IOException, InterruptedException { + public JsonElement postElement(String endpoint, JsonObject payload) throws IOException, InterruptedException { HttpRequest request = HttpRequest.newBuilder(URI.create(settings.panel() + "/api/server/v1/" + endpoint)) .timeout(Duration.ofSeconds(5)) .header("Authorization", "Bearer " + settings.token()) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(payload.toString())).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() != 200) { - // Never log the token, request body, or an untrusted HTML error page. - throw new IOException("Panel " + endpoint + " returned HTTP " + response.statusCode()); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + String msg = "HTTP " + response.statusCode(); + try { + JsonObject err = JsonParser.parseString(response.body()).getAsJsonObject(); + 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); } try { - return JsonParser.parseString(response.body()).getAsJsonObject(); + return JsonParser.parseString(response.body()); } catch (RuntimeException e) { throw new IOException("Invalid panel response", e); } } + public JsonObject post(String endpoint, JsonObject payload) throws IOException, InterruptedException { + return postElement(endpoint, payload).getAsJsonObject(); + } + public static String verdict(JsonObject response) throws IOException { JsonElement allowed = response.get("allowed"); if (allowed == null || !allowed.isJsonPrimitive() || !allowed.getAsJsonPrimitive().isBoolean()) { @@ -63,5 +72,96 @@ public final class PanelClient { return new ChunkCheckResult(false, true, null, null); } } + + public JsonObject claimChunk(String dimension, int chunkX, int chunkZ, java.util.UUID uuid) throws IOException, InterruptedException { + JsonObject req = new JsonObject(); + req.addProperty("uuid", uuid.toString()); + req.addProperty("dimension", dimension); + req.addProperty("chunk_x", chunkX); + req.addProperty("chunk_z", chunkZ); + return post("guilds/claim", req); + } + + public JsonObject unclaimChunk(String dimension, int chunkX, int chunkZ, java.util.UUID uuid) throws IOException, InterruptedException { + JsonObject req = new JsonObject(); + req.addProperty("uuid", uuid.toString()); + req.addProperty("dimension", dimension); + req.addProperty("chunk_x", chunkX); + req.addProperty("chunk_z", chunkZ); + return post("guilds/unclaim", req); + } + + public JsonObject getPlayerGuild(java.util.UUID uuid) throws IOException, InterruptedException { + JsonObject req = new JsonObject(); + req.addProperty("uuid", uuid.toString()); + return post("guilds/player", req); + } + + public JsonObject createGuild(java.util.UUID uuid, String username, String name, String tag) throws IOException, InterruptedException { + JsonObject req = new JsonObject(); + req.addProperty("uuid", uuid.toString()); + req.addProperty("username", username); + req.addProperty("name", name); + req.addProperty("tag", tag); + return post("guilds/create", req); + } + + public JsonObject leaveGuild(java.util.UUID uuid) throws IOException, InterruptedException { + JsonObject req = new JsonObject(); + req.addProperty("uuid", uuid.toString()); + return post("guilds/leave", req); + } + + public double getBalance(java.util.UUID uuid, String username) throws IOException, InterruptedException { + JsonObject req = new JsonObject(); + req.addProperty("uuid", uuid.toString()); + req.addProperty("username", username); + JsonObject res = post("economy/balance", req); + return res.has("balance") ? res.get("balance").getAsDouble() : 0.0; + } + + public double transfer(java.util.UUID fromUuid, String fromName, java.util.UUID toUuid, String toName, double amount, String description) throws IOException, InterruptedException { + JsonObject req = new JsonObject(); + req.addProperty("from_uuid", fromUuid.toString()); + req.addProperty("from_name", fromName); + req.addProperty("to_uuid", toUuid.toString()); + req.addProperty("to_name", toName); + req.addProperty("amount", amount); + req.addProperty("description", description); + JsonObject res = post("economy/transfer", req); + return res.has("from_balance") ? res.get("from_balance").getAsDouble() : 0.0; + } + + public void syncBalance(java.util.UUID uuid, String username, double newBalance) throws IOException, InterruptedException { + JsonObject req = new JsonObject(); + req.addProperty("uuid", uuid.toString()); + req.addProperty("username", username); + req.addProperty("new_balance", newBalance); + post("economy/sync-balance", req); + } + + public JsonArray getBaltop() throws IOException, InterruptedException { + JsonElement el = postElement("economy/baltop", new JsonObject()); + return el.isJsonArray() ? el.getAsJsonArray() : new JsonArray(); + } + + 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("seller_uuid", sellerUuid.toString()); + req.addProperty("seller_name", sellerName); + req.addProperty("item_id", itemId); + req.addProperty("item_name", itemName); + req.addProperty("amount", amount); + req.addProperty("price", price); + return post("economy/market/list", req); + } + + public JsonObject marketBuy(long listingId, java.util.UUID buyerUuid, String buyerName) throws IOException, InterruptedException { + JsonObject req = new JsonObject(); + req.addProperty("listing_id", listingId); + req.addProperty("buyer_uuid", buyerUuid.toString()); + req.addProperty("buyer_name", buyerName); + return post("economy/market/buy", req); + } } diff --git a/integrations/common/src/main/java/net/scopenet/integration/Settings.java b/integrations/common/src/main/java/net/scopenet/integration/Settings.java index 8f3f25e..857f5d4 100644 --- a/integrations/common/src/main/java/net/scopenet/integration/Settings.java +++ b/integrations/common/src/main/java/net/scopenet/integration/Settings.java @@ -19,7 +19,9 @@ public record Settings( boolean guildsEnabled, boolean landClaimingEnabled, boolean socialEnabled, - boolean chatPrefixesEnabled + boolean chatPrefixesEnabled, + boolean essentialsEnabled, + boolean economyEnabled ) { public Settings { if (panel == null || panel.getHost() == null || panel.getRawUserInfo() != null @@ -34,11 +36,27 @@ public record Settings( } public Settings(URI panel, String token) { - this(panel, token, true, 1.0, 1.5, true, true, true, true, true, true); + this(panel, token, true, 1.0, 1.5, true, true, true, true, true, true, true, true); + } + + public Settings( + URI panel, + String token, + boolean levelingEnabled, + double globalXpMultiplier, + double serverXpMultiplier, + boolean questsEnabled, + boolean achievementsEnabled, + boolean guildsEnabled, + boolean landClaimingEnabled, + boolean socialEnabled, + boolean chatPrefixesEnabled + ) { + this(panel, token, levelingEnabled, globalXpMultiplier, serverXpMultiplier, questsEnabled, achievementsEnabled, guildsEnabled, landClaimingEnabled, socialEnabled, chatPrefixesEnabled, true, true); } public static Settings of(String panel, String token) { - return of(panel, token, true, 1.0, 1.5, true, true, true, true, true, true); + return of(panel, token, true, 1.0, 1.5, true, true, true, true, true, true, true, true); } public static Settings of( @@ -53,6 +71,24 @@ public record Settings( boolean landClaimingEnabled, boolean socialEnabled, boolean chatPrefixesEnabled + ) { + return of(panel, token, levelingEnabled, globalXpMultiplier, serverXpMultiplier, questsEnabled, achievementsEnabled, guildsEnabled, landClaimingEnabled, socialEnabled, chatPrefixesEnabled, true, true); + } + + public static Settings of( + String panel, + String token, + boolean levelingEnabled, + double globalXpMultiplier, + double serverXpMultiplier, + boolean questsEnabled, + boolean achievementsEnabled, + boolean guildsEnabled, + boolean landClaimingEnabled, + boolean socialEnabled, + boolean chatPrefixesEnabled, + boolean essentialsEnabled, + boolean economyEnabled ) { return new Settings( URI.create(panel.trim().replaceAll("/+$", "")), @@ -65,7 +101,9 @@ public record Settings( guildsEnabled, landClaimingEnabled, socialEnabled, - chatPrefixesEnabled + chatPrefixesEnabled, + essentialsEnabled, + economyEnabled ); } @@ -87,6 +125,8 @@ public record Settings( guilds.land_claiming=true social.enabled=true social.chat_prefixes=true + essentials.enabled=true + economy.enabled=true """; Files.writeString(path, defaultProps, StandardCharsets.UTF_8); } @@ -105,7 +145,9 @@ public record Settings( Boolean.parseBoolean(properties.getProperty("guilds.enabled", "true")), Boolean.parseBoolean(properties.getProperty("guilds.land_claiming", "true")), Boolean.parseBoolean(properties.getProperty("social.enabled", "true")), - Boolean.parseBoolean(properties.getProperty("social.chat_prefixes", "true")) + Boolean.parseBoolean(properties.getProperty("social.chat_prefixes", "true")), + Boolean.parseBoolean(properties.getProperty("essentials.enabled", "true")), + Boolean.parseBoolean(properties.getProperty("economy.enabled", "true")) ); } } diff --git a/integrations/paper/src/main/java/net/scopenet/paper/ScopenetPlugin.java b/integrations/paper/src/main/java/net/scopenet/paper/ScopenetPlugin.java index b421a4b..7642efb 100644 --- a/integrations/paper/src/main/java/net/scopenet/paper/ScopenetPlugin.java +++ b/integrations/paper/src/main/java/net/scopenet/paper/ScopenetPlugin.java @@ -1,6 +1,7 @@ package net.scopenet.paper; import net.scopenet.integration.*; +import net.scopenet.paper.commands.*; import org.bukkit.*; import org.bukkit.entity.Player; import org.bukkit.event.*; @@ -30,7 +31,9 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener { getConfig().getBoolean("guilds.enabled", true), getConfig().getBoolean("guilds.land-claiming", true), getConfig().getBoolean("social.enabled", true), - getConfig().getBoolean("social.chat-prefixes", true) + getConfig().getBoolean("social.chat-prefixes", true), + getConfig().getBoolean("essentials.enabled", true), + getConfig().getBoolean("economy.enabled", true) ); integration = new Integration( @@ -45,6 +48,36 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener { }); }); Bukkit.getScheduler().runTaskTimer(this, integration::tick, 1, 1); + + // Register /scopenet admin & help command + ScopenetCommandHandler scopenetCmd = new ScopenetCommandHandler(this, integration); + 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"}; + for (String c : essCmds) bindCommand(c, essentials); + } + + // 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"}; + for (String c : econCmds) bindCommand(c, economy); + } + + // Register Guilds & Land Claims commands & GUI + if (settings.guildsEnabled()) { + GuildHandler guilds = new GuildHandler(this, integration); + getServer().getPluginManager().registerEvents(guilds, this); + bindCommand("guild", guilds); + bindCommand("claim", guilds); + bindCommand("unclaim", guilds); + } + // Reloading an auth plugin cannot silently admit existing sessions. for (Player player : Bukkit.getOnlinePlayers()) player.kickPlayer("SCOPENET restarted. Please reconnect."); getLogger().info("SCOPENET integration initialized. Configured modules:"); @@ -53,12 +86,23 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener { getLogger().info(" - Achievements: " + (settings.achievementsEnabled() ? "ENABLED" : "DISABLED")); getLogger().info(" - Guilds & Land Claiming: " + (settings.guildsEnabled() && settings.landClaimingEnabled() ? "ENABLED" : "DISABLED")); getLogger().info(" - Social: " + (settings.socialEnabled() ? "ENABLED" : "DISABLED")); + getLogger().info(" - Essentials: " + (settings.essentialsEnabled() ? "ENABLED" : "DISABLED")); + getLogger().info(" - Economy: " + (settings.economyEnabled() ? "ENABLED" : "DISABLED")); } catch (Exception e) { getLogger().severe("SCOPENET is blocking joins: " + e.getMessage()); for (Player player : Bukkit.getOnlinePlayers()) player.kickPlayer(Integration.UNAVAILABLE); } } + private void bindCommand(String name, org.bukkit.command.CommandExecutor executor) { + org.bukkit.command.PluginCommand cmd = getCommand(name); + if (cmd != null) { + cmd.setExecutor(executor); + } else { + getLogger().warning("Could not bind command '/" + name + "' - not found in plugin.yml"); + } + } + @Override public void onDisable() { if (integration != null) integration.close(); } diff --git a/integrations/paper/src/main/java/net/scopenet/paper/commands/EconomyHandler.java b/integrations/paper/src/main/java/net/scopenet/paper/commands/EconomyHandler.java new file mode 100644 index 0000000..beb9860 --- /dev/null +++ b/integrations/paper/src/main/java/net/scopenet/paper/commands/EconomyHandler.java @@ -0,0 +1,525 @@ +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 net.scopenet.paper.gui.GuiHelper; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public final class EconomyHandler implements CommandExecutor, Listener { + private final ScopenetPlugin plugin; + private final Integration integration; + + private static final String GUI_SHOP_TITLE = ChatColor.DARK_GREEN + "SCOPENET Server Shop"; + private static final String GUI_SELL_TITLE = ChatColor.GOLD + "Sell Chest (Place Items Here)"; + private static final String GUI_MARKET_TITLE = ChatColor.BLUE + "Player Marketplace"; + private static final String GUI_TRADE_PREFIX = ChatColor.DARK_PURPLE + "Trade: "; + + // Trade sessions: maps player UUID to active trade session + private final Map activeTrades = new ConcurrentHashMap<>(); + + // Standard sell values for common items + private static final Map ITEM_SELL_VALUES = new HashMap<>(); + static { + ITEM_SELL_VALUES.put(Material.COBBLESTONE, 0.1); + ITEM_SELL_VALUES.put(Material.STONE, 0.2); + ITEM_SELL_VALUES.put(Material.OAK_LOG, 0.5); + ITEM_SELL_VALUES.put(Material.BIRCH_LOG, 0.5); + ITEM_SELL_VALUES.put(Material.SPRUCE_LOG, 0.5); + ITEM_SELL_VALUES.put(Material.COAL, 1.0); + ITEM_SELL_VALUES.put(Material.RAW_COPPER, 1.5); + ITEM_SELL_VALUES.put(Material.COPPER_INGOT, 2.0); + ITEM_SELL_VALUES.put(Material.RAW_IRON, 3.0); + ITEM_SELL_VALUES.put(Material.IRON_INGOT, 4.0); + ITEM_SELL_VALUES.put(Material.RAW_GOLD, 6.0); + ITEM_SELL_VALUES.put(Material.GOLD_INGOT, 8.0); + ITEM_SELL_VALUES.put(Material.REDSTONE, 1.5); + ITEM_SELL_VALUES.put(Material.LAPIS_LAZULI, 2.0); + ITEM_SELL_VALUES.put(Material.DIAMOND, 35.0); + ITEM_SELL_VALUES.put(Material.EMERALD, 20.0); + ITEM_SELL_VALUES.put(Material.NETHERITE_SCRAP, 150.0); + ITEM_SELL_VALUES.put(Material.NETHERITE_INGOT, 650.0); + ITEM_SELL_VALUES.put(Material.WHEAT, 0.5); + ITEM_SELL_VALUES.put(Material.CARROT, 0.5); + ITEM_SELL_VALUES.put(Material.POTATO, 0.5); + ITEM_SELL_VALUES.put(Material.BEEF, 1.0); + ITEM_SELL_VALUES.put(Material.COOKED_BEEF, 2.0); + ITEM_SELL_VALUES.put(Material.PORKCHOP, 1.0); + ITEM_SELL_VALUES.put(Material.COOKED_PORKCHOP, 2.0); + } + + public EconomyHandler(ScopenetPlugin plugin, Integration integration) { + this.plugin = plugin; + this.integration = integration; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage(ChatColor.RED + "Only in-game players can execute economy commands."); + return true; + } + + String cmd = command.getName().toLowerCase(); + switch (cmd) { + case "balance" -> handleBalance(player); + case "pay" -> handlePay(player, args); + case "baltop" -> handleBaltop(player); + case "shop" -> handleShop(player); + case "sell" -> handleSell(player, args); + case "market" -> handleMarket(player, args); + case "orders" -> handleOrders(player); + case "trade" -> handleTrade(player, args); + case "transactions" -> handleTransactions(player); + default -> { return false; } + } + return true; + } + + private void handleBalance(Player player) { + player.sendMessage(ChatColor.GRAY + "Fetching your balance..."); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + double bal = integration.client().getBalance(player.getUniqueId(), player.getName()); + player.sendMessage(ChatColor.GOLD + "=== SCOPENET Economy ==="); + player.sendMessage(ChatColor.YELLOW + "Your Balance: " + ChatColor.GREEN + "$" + String.format(Locale.US, "%,.2f", bal)); + } catch (Exception e) { + player.sendMessage(ChatColor.RED + "Could not reach economy server: " + e.getMessage()); + } + }); + } + + private void handlePay(Player player, String[] args) { + if (args.length < 2) { + player.sendMessage(ChatColor.RED + "Usage: /pay "); + return; + } + + Player target = Bukkit.getPlayer(args[0]); + if (target == null || !target.isOnline()) { + player.sendMessage(ChatColor.RED + "Player '" + args[0] + "' is not online."); + return; + } + + if (target.getUniqueId().equals(player.getUniqueId())) { + player.sendMessage(ChatColor.RED + "You cannot pay yourself."); + return; + } + + double amount; + try { + amount = Double.parseDouble(args[1]); + if (amount <= 0 || Double.isNaN(amount)) throw new NumberFormatException(); + } catch (NumberFormatException e) { + player.sendMessage(ChatColor.RED + "Invalid amount specified. Must be positive number."); + return; + } + + player.sendMessage(ChatColor.GRAY + "Processing payment..."); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + double newBal = integration.client().transfer( + player.getUniqueId(), player.getName(), + target.getUniqueId(), target.getName(), + amount, "Player payment to " + target.getName() + ); + player.sendMessage(ChatColor.GREEN + "Sent " + ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", amount) + + ChatColor.GREEN + " to " + ChatColor.YELLOW + target.getName() + ChatColor.GREEN + ". New Balance: " + + ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", newBal)); + target.sendMessage(ChatColor.GREEN + "Received " + ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", amount) + + ChatColor.GREEN + " from " + ChatColor.YELLOW + player.getName() + ChatColor.GREEN + "!"); + } catch (Exception e) { + player.sendMessage(ChatColor.RED + "Transfer failed: " + e.getMessage()); + } + }); + } + + private void handleBaltop(Player player) { + player.sendMessage(ChatColor.GRAY + "Loading leaderboard..."); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + JsonArray baltop = integration.client().getBaltop(); + player.sendMessage(ChatColor.GOLD + "================= " + ChatColor.YELLOW + "BALTOP (Richest Players)" + ChatColor.GOLD + " ================="); + if (baltop.size() == 0) { + player.sendMessage(ChatColor.GRAY + "No players found on the leaderboard."); + return; + } + for (int i = 0; i < Math.min(10, baltop.size()); i++) { + JsonObject entry = baltop.get(i).getAsJsonObject(); + int rank = entry.has("rank") ? entry.get("rank").getAsInt() : (i + 1); + String name = entry.has("username") ? entry.get("username").getAsString() : "Unknown"; + double bal = entry.has("balance") ? entry.get("balance").getAsDouble() : 0.0; + ChatColor rankColor = rank == 1 ? ChatColor.GOLD : (rank == 2 ? ChatColor.WHITE : (rank == 3 ? ChatColor.YELLOW : ChatColor.GRAY)); + player.sendMessage(rankColor + "#" + rank + " " + ChatColor.WHITE + name + ": " + ChatColor.GREEN + "$" + String.format(Locale.US, "%,.2f", bal)); + } + } catch (Exception e) { + player.sendMessage(ChatColor.RED + "Could not fetch baltop: " + e.getMessage()); + } + }); + } + + private void handleShop(Player player) { + Inventory inv = Bukkit.createInventory(null, 54, GUI_SHOP_TITLE); + + // Fill border + ItemStack border = GuiHelper.createBorder(Material.BLACK_STAINED_GLASS_PANE); + for (int i = 0; i < 9; i++) inv.setItem(i, border); + for (int i = 45; i < 54; i++) inv.setItem(i, border); + + // Category / Popular items in shop + inv.setItem(10, GuiHelper.createItem(Material.DIAMOND, "&bDiamond &7(x1)", "&7Price: &a$100.00", "", "&eClick to Buy!")); + inv.setItem(11, GuiHelper.createItem(Material.NETHERITE_INGOT, "&5Netherite Ingot &7(x1)", "&7Price: &a$1,500.00", "", "&eClick to Buy!")); + inv.setItem(12, GuiHelper.createItem(Material.IRON_INGOT, 16, "&fIron Ingot &7(x16)", "&7Price: &a$80.00", "", "&eClick to Buy!")); + inv.setItem(13, GuiHelper.createItem(Material.GOLD_INGOT, 16, "&6Gold Ingot &7(x16)", "&7Price: &a$150.00", "", "&eClick to Buy!")); + inv.setItem(14, GuiHelper.createItem(Material.EMERALD, 16, "&aEmerald &7(x16)", "&7Price: &a$250.00", "", "&eClick to Buy!")); + inv.setItem(15, GuiHelper.createItem(Material.OAK_LOG, 32, "&6Oak Wood &7(x32)", "&7Price: &a$20.00", "", "&eClick to Buy!")); + inv.setItem(16, GuiHelper.createItem(Material.STONE, 64, "&7Stone &7(x64)", "&7Price: &a$25.00", "", "&eClick to Buy!")); + + inv.setItem(19, GuiHelper.createItem(Material.COOKED_BEEF, 16, "&cCooked Steak &7(x16)", "&7Price: &a$40.00", "", "&eClick to Buy!")); + inv.setItem(20, GuiHelper.createItem(Material.GOLDEN_APPLE, 4, "&eGolden Apple &7(x4)", "&7Price: &a$300.00", "", "&eClick to Buy!")); + inv.setItem(21, GuiHelper.createItem(Material.EXPERIENCE_BOTTLE, 16, "&aBottle o' Enchanting &7(x16)", "&7Price: &a$200.00", "", "&eClick to Buy!")); + inv.setItem(22, GuiHelper.createItem(Material.ENDER_PEARL, 8, "&3Ender Pearl &7(x8)", "&7Price: &a$120.00", "", "&eClick to Buy!")); + inv.setItem(23, GuiHelper.createItem(Material.OBSIDIAN, 14, "&8Obsidian &7(x14)", "&7Price: &a$150.00", "", "&eClick to Buy!")); + inv.setItem(24, GuiHelper.createItem(Material.TORCH, 64, "&eTorches &7(x64)", "&7Price: &a$10.00", "", "&eClick to Buy!")); + inv.setItem(25, GuiHelper.createItem(Material.BOW, "&bPower Bow", "&7Price: &a$100.00", "", "&eClick to Buy!")); + + inv.setItem(49, GuiHelper.createItem(Material.GOLD_INGOT, "&eCheck Balance / Sell Items", "&7Type &a/sell &7to sell your loot!", "&7Type &a/market &7for player listings.")); + + player.openInventory(inv); + } + + private void handleSell(Player player, String[] args) { + if (args.length > 0 && args[0].equalsIgnoreCase("hand")) { + ItemStack hand = player.getInventory().getItemInMainHand(); + if (hand.getType().isAir()) { + player.sendMessage(ChatColor.RED + "You are not holding any item to sell."); + return; + } + Double unitPrice = ITEM_SELL_VALUES.get(hand.getType()); + if (unitPrice == null) { + player.sendMessage(ChatColor.RED + "This item (" + hand.getType().name() + ") cannot be sold to the server shop."); + return; + } + int amount = hand.getAmount(); + double total = unitPrice * amount; + player.getInventory().setItemInMainHand(null); + + 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 + "!"); + } catch (Exception e) { + player.sendMessage(ChatColor.RED + "Failed to credit sell balance: " + e.getMessage()); + } + }); + return; + } + + // Open sell chest GUI + Inventory sellInv = Bukkit.createInventory(null, 54, GUI_SELL_TITLE); + player.openInventory(sellInv); + player.sendMessage(ChatColor.YELLOW + "Place items to sell in the chest. Close the chest to confirm your sale!"); + } + + private void handleMarket(Player player, String[] args) { + if (args.length >= 2 && args[0].equalsIgnoreCase("sell")) { + ItemStack hand = player.getInventory().getItemInMainHand(); + if (hand.getType().isAir()) { + player.sendMessage(ChatColor.RED + "Hold the item you want to sell in your main hand."); + return; + } + double price; + try { + price = Double.parseDouble(args[1]); + if (price <= 0 || Double.isNaN(price)) throw new NumberFormatException(); + } catch (NumberFormatException e) { + player.sendMessage(ChatColor.RED + "Invalid price. Usage: /market sell "); + return; + } + + int amount = hand.getAmount(); + String itemId = hand.getType().name(); + 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..."); + + 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) + "!"); + } 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)); + } + }); + return; + } + + // Open Market GUI + 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 ")); + + player.openInventory(inv); + player.sendMessage(ChatColor.GRAY + "Loading market listings..."); + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + // In a production setup, market listings come from API + 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!")); + }); + } catch (Exception ignored) {} + }); + } + + private void handleOrders(Player player) { + player.sendMessage(ChatColor.GOLD + "=== Your Active Market Orders ==="); + player.sendMessage(ChatColor.GRAY + "Use " + ChatColor.YELLOW + "/market sell " + ChatColor.GRAY + " to list items, or visit the " + ChatColor.YELLOW + "/market" + ChatColor.GRAY + " GUI."); + } + + private void handleTransactions(Player player) { + player.sendMessage(ChatColor.GOLD + "=== Recent Transactions ==="); + player.sendMessage(ChatColor.GRAY + "Loading transaction history..."); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + double bal = integration.client().getBalance(player.getUniqueId(), player.getName()); + player.sendMessage(ChatColor.GREEN + "Current Balance: " + ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", bal)); + player.sendMessage(ChatColor.GRAY + "For full transaction logs and charts, check the SCOPENET Launcher Stats page!"); + } catch (Exception e) { + player.sendMessage(ChatColor.RED + "Error: " + e.getMessage()); + } + }); + } + + private void handleTrade(Player player, String[] args) { + if (args.length < 1) { + player.sendMessage(ChatColor.RED + "Usage: /trade "); + return; + } + + Player target = Bukkit.getPlayer(args[0]); + if (target == null || !target.isOnline()) { + player.sendMessage(ChatColor.RED + "Player '" + args[0] + "' is not online."); + return; + } + + if (target.getUniqueId().equals(player.getUniqueId())) { + player.sendMessage(ChatColor.RED + "You cannot trade with yourself."); + return; + } + + if (activeTrades.containsKey(player.getUniqueId()) || activeTrades.containsKey(target.getUniqueId())) { + player.sendMessage(ChatColor.RED + "Either you or the other player is already in a trade."); + return; + } + + // Open trade session for both players + TradeSession session = new TradeSession(player, target); + activeTrades.put(player.getUniqueId(), session); + activeTrades.put(target.getUniqueId(), session); + session.open(); + } + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) return; + String title = event.getView().getTitle(); + + if (title.equals(GUI_SHOP_TITLE)) { + event.setCancelled(true); + ItemStack clicked = event.getCurrentItem(); + if (clicked == null || !clicked.hasItemMeta() || clicked.getItemMeta().getLore() == null) return; + + // Extract price from lore + double price = -1; + for (String loreLine : clicked.getItemMeta().getLore()) { + String raw = ChatColor.stripColor(loreLine); + if (raw.startsWith("Price: $")) { + try { + price = Double.parseDouble(raw.replace("Price: $", "").replace(",", "").trim()); + } catch (Exception ignored) {} + } + } + + if (price <= 0) return; + final double finalPrice = price; + + 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); + + Bukkit.getScheduler().runTask(plugin, () -> { + ItemStack give = new ItemStack(clicked.getType(), clicked.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 + + "! Balance: " + ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", newBal)); + }); + } catch (Exception e) { + player.sendMessage(ChatColor.RED + "Shop transaction failed: " + e.getMessage()); + } + }); + } else if (title.startsWith(GUI_TRADE_PREFIX)) { + TradeSession session = activeTrades.get(player.getUniqueId()); + if (session != null) { + session.handleClick(player, event); + } + } + } + + @EventHandler + public void onInventoryClose(InventoryCloseEvent event) { + if (!(event.getPlayer() instanceof Player player)) return; + String title = event.getView().getTitle(); + + if (title.equals(GUI_SELL_TITLE)) { + // Process sell chest items + Inventory inv = event.getInventory(); + double totalEarned = 0; + int countSold = 0; + + for (ItemStack item : inv.getContents()) { + if (item == null || item.getType().isAir()) continue; + Double price = ITEM_SELL_VALUES.get(item.getType()); + if (price != null) { + totalEarned += price * item.getAmount(); + countSold += item.getAmount(); + } else { + // Return unsold item to player + player.getInventory().addItem(item); + } + } + + if (totalEarned > 0) { + final double earned = totalEarned; + final int itemsCount = countSold; + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + double cur = integration.client().getBalance(player.getUniqueId(), player.getName()); + integration.client().syncBalance(player.getUniqueId(), player.getName(), cur + earned); + player.sendMessage(ChatColor.GREEN + "Sold " + itemsCount + " items for " + + ChatColor.YELLOW + "$" + String.format(Locale.US, "%,.2f", earned) + ChatColor.GREEN + "!"); + } catch (Exception e) { + player.sendMessage(ChatColor.RED + "Failed to credit earnings: " + e.getMessage()); + } + }); + } + } else if (title.startsWith(GUI_TRADE_PREFIX)) { + TradeSession session = activeTrades.remove(player.getUniqueId()); + if (session != null) { + session.cancel(); + } + } + } + + private static class TradeSession { + final Player p1; + final Player p2; + final Inventory inv; + boolean p1Ready = false; + boolean p2Ready = false; + + TradeSession(Player p1, Player p2) { + this.p1 = p1; + this.p2 = p2; + this.inv = Bukkit.createInventory(null, 54, GUI_TRADE_PREFIX + p1.getName() + " & " + p2.getName()); + buildDividers(); + } + + 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); + updateReadyButtons(); + } + + void updateReadyButtons() { + inv.setItem(0, GuiHelper.createItem(p1Ready ? Material.LIME_CONCRETE : Material.RED_CONCRETE, + p1.getName() + ": " + (p1Ready ? "&aREADY" : "&cNOT READY"), "&7Click to toggle ready status.")); + inv.setItem(8, GuiHelper.createItem(p2Ready ? Material.LIME_CONCRETE : Material.RED_CONCRETE, + p2.getName() + ": " + (p2Ready ? "&aREADY" : "&cNOT READY"), "&7Click to toggle ready status.")); + } + + void open() { + p1.openInventory(inv); + p2.openInventory(inv); + } + + void handleClick(Player clicker, InventoryClickEvent event) { + int slot = event.getRawSlot(); + boolean isP1 = clicker.getUniqueId().equals(p1.getUniqueId()); + + if (slot == 0 && isP1) { + event.setCancelled(true); + p1Ready = !p1Ready; + updateReadyButtons(); + checkCompletion(); + return; + } + if (slot == 8 && !isP1) { + event.setCancelled(true); + p2Ready = !p2Ready; + updateReadyButtons(); + checkCompletion(); + return; + } + + // Divider slots are not interactable + if (slot == 4 || slot == 13 || slot == 22 || slot == 31 || slot == 40 || slot == 49) { + event.setCancelled(true); + } + } + + void checkCompletion() { + if (p1Ready && p2Ready) { + 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."); + } + } +} diff --git a/integrations/paper/src/main/java/net/scopenet/paper/commands/EssentialsHandler.java b/integrations/paper/src/main/java/net/scopenet/paper/commands/EssentialsHandler.java new file mode 100644 index 0000000..70879b3 --- /dev/null +++ b/integrations/paper/src/main/java/net/scopenet/paper/commands/EssentialsHandler.java @@ -0,0 +1,403 @@ +package net.scopenet.paper.commands; + +import net.scopenet.paper.ScopenetPlugin; +import net.scopenet.paper.gui.GuiHelper; +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public final class EssentialsHandler implements CommandExecutor, Listener { + private final ScopenetPlugin plugin; + private final File homesFile; + private final File warpsFile; + private final YamlConfiguration homesConfig; + private final YamlConfiguration warpsConfig; + + private final Map> homes = new ConcurrentHashMap<>(); + private final Map warps = new ConcurrentHashMap<>(); + private final Map lastLocations = new ConcurrentHashMap<>(); + private final Map pendingTpa = new ConcurrentHashMap<>(); // target -> requester + private final Map tpaTimestamp = new ConcurrentHashMap<>(); + + private static final String GUI_HOMES_TITLE = ChatColor.DARK_GRAY + "Homes Manager"; + private static final String GUI_WARPS_TITLE = ChatColor.DARK_GRAY + "Server Warps"; + + public EssentialsHandler(ScopenetPlugin plugin) { + this.plugin = plugin; + this.homesFile = new File(plugin.getDataFolder(), "homes.yml"); + this.warpsFile = new File(plugin.getDataFolder(), "warps.yml"); + this.homesConfig = YamlConfiguration.loadConfiguration(homesFile); + this.warpsConfig = YamlConfiguration.loadConfiguration(warpsFile); + loadHomes(); + loadWarps(); + } + + private void loadHomes() { + if (!homesFile.exists()) return; + for (String uuidStr : homesConfig.getKeys(false)) { + try { + UUID uuid = UUID.fromString(uuidStr); + Map playerHomes = new HashMap<>(); + if (homesConfig.isConfigurationSection(uuidStr)) { + for (String homeName : homesConfig.getConfigurationSection(uuidStr).getKeys(false)) { + Location loc = homesConfig.getLocation(uuidStr + "." + homeName); + if (loc != null) playerHomes.put(homeName.toLowerCase(), loc); + } + } + homes.put(uuid, playerHomes); + } catch (Exception ignored) {} + } + } + + private void saveHomes() { + try { + for (Map.Entry> entry : homes.entrySet()) { + String uuidStr = entry.getKey().toString(); + homesConfig.set(uuidStr, null); + for (Map.Entry home : entry.getValue().entrySet()) { + homesConfig.set(uuidStr + "." + home.getKey(), home.getValue()); + } + } + homesConfig.save(homesFile); + } catch (IOException e) { + plugin.getLogger().warning("Failed to save homes: " + e.getMessage()); + } + } + + private void loadWarps() { + if (!warpsFile.exists()) return; + for (String warpName : warpsConfig.getKeys(false)) { + Location loc = warpsConfig.getLocation(warpName); + if (loc != null) warps.put(warpName.toLowerCase(), loc); + } + } + + private void saveWarps() { + try { + for (Map.Entry entry : warps.entrySet()) { + warpsConfig.set(entry.getKey(), entry.getValue()); + } + warpsConfig.save(warpsFile); + } catch (IOException e) { + plugin.getLogger().warning("Failed to save warps: " + e.getMessage()); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onTeleport(PlayerTeleportEvent event) { + if (event.getFrom() != null && event.getCause() != PlayerTeleportEvent.TeleportCause.UNKNOWN) { + lastLocations.put(event.getPlayer().getUniqueId(), event.getFrom()); + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onDeath(PlayerDeathEvent event) { + lastLocations.put(event.getEntity().getUniqueId(), event.getEntity().getLocation()); + } + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) return; + String title = event.getView().getTitle(); + if (title.equals(GUI_HOMES_TITLE)) { + event.setCancelled(true); + ItemStack item = event.getCurrentItem(); + if (item == null || !item.hasItemMeta() || item.getItemMeta().getDisplayName() == null) return; + String homeName = ChatColor.stripColor(item.getItemMeta().getDisplayName()).replace("Home: ", "").trim().toLowerCase(); + Map playerHomes = homes.get(player.getUniqueId()); + if (playerHomes != null && playerHomes.containsKey(homeName)) { + player.closeInventory(); + player.teleport(playerHomes.get(homeName)); + player.sendMessage(ChatColor.GREEN + "Teleported to home: " + ChatColor.YELLOW + homeName); + } + } else if (title.equals(GUI_WARPS_TITLE)) { + event.setCancelled(true); + ItemStack item = event.getCurrentItem(); + if (item == null || !item.hasItemMeta() || item.getItemMeta().getDisplayName() == null) return; + String warpName = ChatColor.stripColor(item.getItemMeta().getDisplayName()).replace("Warp: ", "").trim().toLowerCase(); + if (warps.containsKey(warpName)) { + player.closeInventory(); + player.teleport(warps.get(warpName)); + player.sendMessage(ChatColor.GREEN + "Teleported to warp: " + ChatColor.YELLOW + warpName); + } + } + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage(ChatColor.RED + "Only in-game players can execute essentials commands."); + return true; + } + + String cmd = command.getName().toLowerCase(); + switch (cmd) { + case "spawn" -> handleSpawn(player); + case "home" -> handleHome(player, args); + case "sethome" -> handleSetHome(player, args); + case "delhome" -> handleDelHome(player, args); + case "back" -> handleBack(player); + case "tpa" -> handleTpa(player, args); + case "tpaccept" -> handleTpAccept(player); + case "tpdeny" -> handleTpDeny(player); + case "rtp" -> handleRtp(player); + case "warp" -> handleWarp(player, args); + case "playtime" -> handlePlaytime(player, args); + default -> { return false; } + } + return true; + } + + private void handleSpawn(Player player) { + Location spawnLoc = player.getWorld().getSpawnLocation().add(0.5, 0, 0.5); + player.teleport(spawnLoc); + player.sendMessage(ChatColor.GREEN + "Teleported to spawn."); + } + + private void handleHome(Player player, String[] args) { + Map playerHomes = homes.computeIfAbsent(player.getUniqueId(), k -> new HashMap<>()); + if (args.length == 0) { + if (playerHomes.isEmpty()) { + player.sendMessage(ChatColor.RED + "You have no homes set! Set one with " + ChatColor.YELLOW + "/sethome [name]"); + return; + } + if (playerHomes.size() == 1) { + Location loc = playerHomes.values().iterator().next(); + player.teleport(loc); + player.sendMessage(ChatColor.GREEN + "Teleported to home."); + return; + } + openHomePicker(player, playerHomes); + return; + } + + String homeName = args[0].toLowerCase(); + Location loc = playerHomes.get(homeName); + if (loc == null) { + player.sendMessage(ChatColor.RED + "Home '" + homeName + "' not found. Your homes: " + ChatColor.YELLOW + String.join(", ", playerHomes.keySet())); + return; + } + player.teleport(loc); + player.sendMessage(ChatColor.GREEN + "Teleported to home " + ChatColor.YELLOW + homeName + ChatColor.GREEN + "."); + } + + private void openHomePicker(Player player, Map playerHomes) { + Inventory inv = Bukkit.createInventory(null, 27, GUI_HOMES_TITLE); + int slot = 10; + Material[] bedColors = { + Material.RED_BED, Material.BLUE_BED, Material.LIME_BED, + Material.YELLOW_BED, Material.PURPLE_BED, Material.ORANGE_BED, Material.CYAN_BED + }; + int idx = 0; + for (Map.Entry entry : playerHomes.entrySet()) { + if (slot > 16) break; + Material mat = bedColors[idx % bedColors.length]; + Location l = entry.getValue(); + String world = l.getWorld() != null ? l.getWorld().getName() : "world"; + ItemStack item = GuiHelper.createItem( + mat, + "&aHome: &f" + entry.getKey(), + "&7World: &f" + world, + "&7Coords: &f" + l.getBlockX() + ", " + l.getBlockY() + ", " + l.getBlockZ(), + "", + "&eClick to teleport!" + ); + inv.setItem(slot++, item); + idx++; + } + player.openInventory(inv); + } + + private void handleSetHome(Player player, String[] args) { + String homeName = args.length > 0 ? args[0].toLowerCase() : "home"; + int maxHomes = plugin.getConfig().getInt("essentials.max-homes", 5); + Map playerHomes = homes.computeIfAbsent(player.getUniqueId(), k -> new HashMap<>()); + + if (!playerHomes.containsKey(homeName) && playerHomes.size() >= maxHomes) { + player.sendMessage(ChatColor.RED + "You have reached your limit of " + maxHomes + " homes."); + return; + } + + playerHomes.put(homeName, player.getLocation()); + saveHomes(); + player.sendMessage(ChatColor.GREEN + "Home " + ChatColor.YELLOW + homeName + ChatColor.GREEN + " set successfully."); + } + + private void handleDelHome(Player player, String[] args) { + String homeName = args.length > 0 ? args[0].toLowerCase() : "home"; + Map playerHomes = homes.get(player.getUniqueId()); + if (playerHomes == null || playerHomes.remove(homeName) == null) { + player.sendMessage(ChatColor.RED + "Home '" + homeName + "' does not exist."); + return; + } + saveHomes(); + player.sendMessage(ChatColor.GREEN + "Home " + ChatColor.YELLOW + homeName + ChatColor.GREEN + " deleted."); + } + + private void handleBack(Player player) { + Location last = lastLocations.get(player.getUniqueId()); + if (last == null) { + player.sendMessage(ChatColor.RED + "No previous location found to return to."); + return; + } + player.teleport(last); + player.sendMessage(ChatColor.GREEN + "Teleported back to your previous location."); + } + + private void handleTpa(Player player, String[] args) { + if (args.length < 1) { + player.sendMessage(ChatColor.RED + "Usage: /tpa "); + return; + } + Player target = Bukkit.getPlayer(args[0]); + if (target == null || !target.isOnline()) { + player.sendMessage(ChatColor.RED + "Player '" + args[0] + "' is not online."); + return; + } + if (target.getUniqueId().equals(player.getUniqueId())) { + player.sendMessage(ChatColor.RED + "You cannot teleport to yourself."); + return; + } + + pendingTpa.put(target.getUniqueId(), player.getUniqueId()); + tpaTimestamp.put(target.getUniqueId(), System.currentTimeMillis()); + + player.sendMessage(ChatColor.GREEN + "Teleport request sent to " + ChatColor.YELLOW + target.getName() + ChatColor.GREEN + "."); + target.sendMessage(ChatColor.YELLOW + player.getName() + ChatColor.GREEN + " has sent you a teleport request."); + target.sendMessage(ChatColor.GRAY + "Type " + ChatColor.YELLOW + "/tpaccept" + ChatColor.GRAY + " to accept or " + ChatColor.RED + "/tpdeny" + ChatColor.GRAY + " to deny (expires in 60s)."); + } + + private void handleTpAccept(Player player) { + UUID requesterId = pendingTpa.get(player.getUniqueId()); + Long time = tpaTimestamp.get(player.getUniqueId()); + if (requesterId == null || time == null || System.currentTimeMillis() - time > 60_000) { + pendingTpa.remove(player.getUniqueId()); + tpaTimestamp.remove(player.getUniqueId()); + player.sendMessage(ChatColor.RED + "You have no active teleport requests."); + return; + } + + Player requester = Bukkit.getPlayer(requesterId); + if (requester == null || !requester.isOnline()) { + player.sendMessage(ChatColor.RED + "The player who requested to teleport is no longer online."); + pendingTpa.remove(player.getUniqueId()); + return; + } + + requester.teleport(player.getLocation()); + requester.sendMessage(ChatColor.GREEN + "Teleport request accepted by " + ChatColor.YELLOW + player.getName() + ChatColor.GREEN + "!"); + player.sendMessage(ChatColor.GREEN + "Accepted teleport request from " + ChatColor.YELLOW + requester.getName() + ChatColor.GREEN + "."); + pendingTpa.remove(player.getUniqueId()); + tpaTimestamp.remove(player.getUniqueId()); + } + + private void handleTpDeny(Player player) { + UUID requesterId = pendingTpa.remove(player.getUniqueId()); + tpaTimestamp.remove(player.getUniqueId()); + if (requesterId == null) { + player.sendMessage(ChatColor.RED + "You have no active teleport requests."); + return; + } + Player requester = Bukkit.getPlayer(requesterId); + if (requester != null && requester.isOnline()) { + requester.sendMessage(ChatColor.RED + player.getName() + " denied your teleport request."); + } + player.sendMessage(ChatColor.YELLOW + "Teleport request denied."); + } + + private void handleRtp(Player player) { + int radius = plugin.getConfig().getInt("essentials.rtp-radius", 2500); + World world = player.getWorld(); + Random rng = new Random(); + + player.sendMessage(ChatColor.GRAY + "Searching for a safe wilderness destination..."); + Bukkit.getScheduler().runTask(plugin, () -> { + for (int i = 0; i < 15; i++) { + int x = rng.nextInt(radius * 2) - radius; + int z = rng.nextInt(radius * 2) - radius; + int y = world.getHighestBlockYAt(x, z); + Block block = world.getBlockAt(x, y - 1, z); + if (block.getType().isSolid() && block.getType() != Material.LAVA && block.getType() != Material.WATER) { + Location target = new Location(world, x + 0.5, y, z + 0.5); + player.teleport(target); + player.sendMessage(ChatColor.GREEN + "Wilderness RTP: Teleported to " + ChatColor.YELLOW + x + ", " + y + ", " + z + ChatColor.GREEN + "!"); + return; + } + } + player.sendMessage(ChatColor.RED + "Could not find a safe RTP spot. Please try again."); + }); + } + + private void handleWarp(Player player, String[] args) { + if (args.length == 0) { + openWarpPicker(player); + return; + } + + String warpName = args[0].toLowerCase(); + Location loc = warps.get(warpName); + if (loc == null) { + player.sendMessage(ChatColor.RED + "Warp '" + warpName + "' does not exist."); + return; + } + player.teleport(loc); + player.sendMessage(ChatColor.GREEN + "Teleported to warp " + ChatColor.YELLOW + warpName + ChatColor.GREEN + "."); + } + + private void openWarpPicker(Player player) { + Inventory inv = Bukkit.createInventory(null, 27, GUI_WARPS_TITLE); + int slot = 11; + // Default built-in warps if empty + if (warps.isEmpty()) { + warps.put("spawn", player.getWorld().getSpawnLocation()); + saveWarps(); + } + + for (Map.Entry entry : warps.entrySet()) { + if (slot > 15) break; + ItemStack item = GuiHelper.createItem( + Material.ENDER_PEARL, + "&bWarp: &f" + entry.getKey(), + "&7World: &f" + (entry.getValue().getWorld() != null ? entry.getValue().getWorld().getName() : "world"), + "", + "&eClick to teleport!" + ); + inv.setItem(slot++, item); + } + player.openInventory(inv); + } + + private void handlePlaytime(Player player, String[] args) { + Player target = args.length > 0 ? Bukkit.getPlayer(args[0]) : player; + if (target == null) { + player.sendMessage(ChatColor.RED + "Player not found."); + return; + } + + int ticksPlayed = target.getStatistic(Statistic.PLAY_ONE_MINUTE); + long seconds = ticksPlayed / 20; + long hours = seconds / 3600; + long minutes = (seconds % 3600) / 60; + long secs = seconds % 60; + + player.sendMessage(ChatColor.GOLD + "=== Playtime: " + ChatColor.YELLOW + target.getName() + ChatColor.GOLD + " ==="); + player.sendMessage(ChatColor.GRAY + "Total Time: " + ChatColor.GREEN + hours + "h " + minutes + "m " + secs + "s"); + } +} diff --git a/integrations/paper/src/main/java/net/scopenet/paper/commands/GuildHandler.java b/integrations/paper/src/main/java/net/scopenet/paper/commands/GuildHandler.java new file mode 100644 index 0000000..e9099e0 --- /dev/null +++ b/integrations/paper/src/main/java/net/scopenet/paper/commands/GuildHandler.java @@ -0,0 +1,414 @@ +package net.scopenet.paper.commands; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import net.scopenet.integration.ChunkCheckResult; +import net.scopenet.integration.Integration; +import net.scopenet.paper.ScopenetPlugin; +import net.scopenet.paper.gui.GuiHelper; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public final class GuildHandler implements CommandExecutor, Listener { + private final ScopenetPlugin plugin; + private final Integration integration; + private final File guildHomesFile; + private final YamlConfiguration guildHomesConfig; + private final Map guildHomes = new ConcurrentHashMap<>(); + + private static final String GUI_GUILD_TITLE = ChatColor.DARK_AQUA + "Guild Dashboard"; + + public GuildHandler(ScopenetPlugin plugin, Integration integration) { + this.plugin = plugin; + this.integration = integration; + this.guildHomesFile = new File(plugin.getDataFolder(), "guild_homes.yml"); + this.guildHomesConfig = YamlConfiguration.loadConfiguration(guildHomesFile); + loadGuildHomes(); + } + + private void loadGuildHomes() { + if (!guildHomesFile.exists()) return; + for (String gid : guildHomesConfig.getKeys(false)) { + Location loc = guildHomesConfig.getLocation(gid); + if (loc != null) guildHomes.put(gid, loc); + } + } + + private void saveGuildHomes() { + try { + for (Map.Entry entry : guildHomes.entrySet()) { + guildHomesConfig.set(entry.getKey(), entry.getValue()); + } + guildHomesConfig.save(guildHomesFile); + } catch (IOException e) { + plugin.getLogger().warning("Failed to save guild homes: " + e.getMessage()); + } + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage(ChatColor.RED + "Only players can execute guild commands."); + return true; + } + + String cmd = command.getName().toLowerCase(); + if (cmd.equals("claim")) { + handleClaim(player); + return true; + } + if (cmd.equals("unclaim")) { + handleUnclaim(player); + return true; + } + + if (cmd.equals("guild") || cmd.equals("g")) { + if (args.length == 0 || args[0].equalsIgnoreCase("info") || args[0].equalsIgnoreCase("gui")) { + handleGuildDashboard(player); + return true; + } + + String sub = args[0].toLowerCase(); + switch (sub) { + case "create" -> handleCreate(player, args); + case "leave" -> handleLeave(player); + case "claim" -> handleClaim(player); + case "unclaim" -> handleUnclaim(player); + case "map" -> handleMap(player); + case "chat", "c" -> handleChat(player, args); + case "sethome" -> handleSetHome(player); + case "home" -> handleHome(player); + case "members" -> handleMembers(player); + default -> sendGuildHelp(player); + } + return true; + } + + return false; + } + + private void sendGuildHelp(Player player) { + player.sendMessage(ChatColor.GOLD + "=== SCOPENET Guild Commands ==="); + player.sendMessage(ChatColor.YELLOW + "/guild" + ChatColor.GRAY + " - Open interactive Guild Dashboard GUI"); + player.sendMessage(ChatColor.YELLOW + "/guild create " + ChatColor.GRAY + " - Create a new guild"); + player.sendMessage(ChatColor.YELLOW + "/guild leave" + ChatColor.GRAY + " - Leave your current guild"); + player.sendMessage(ChatColor.YELLOW + "/claim" + ChatColor.GRAY + " (or /guild claim) - Claim current chunk"); + player.sendMessage(ChatColor.YELLOW + "/unclaim" + ChatColor.GRAY + " (or /guild unclaim) - Unclaim current chunk"); + player.sendMessage(ChatColor.YELLOW + "/guild map" + ChatColor.GRAY + " - Show nearby land claims radar map"); + player.sendMessage(ChatColor.YELLOW + "/guild chat " + ChatColor.GRAY + " - Send message to guild members"); + player.sendMessage(ChatColor.YELLOW + "/guild sethome / /guild home" + ChatColor.GRAY + " - Guild waypoint base"); + } + + private void handleClaim(Player player) { + Chunk chunk = player.getLocation().getChunk(); + String dim = player.getWorld().getName(); + player.sendMessage(ChatColor.GRAY + "Claiming chunk [" + chunk.getX() + ", " + chunk.getZ() + "]..."); + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + JsonObject res = integration.client().claimChunk(dim, chunk.getX(), chunk.getZ(), player.getUniqueId()); + String gname = res.has("guild_name") ? res.get("guild_name").getAsString() : "your guild"; + player.sendMessage(ChatColor.GREEN + "Chunk claimed successfully for " + ChatColor.YELLOW + gname + ChatColor.GREEN + "!"); + } catch (Exception e) { + player.sendMessage(ChatColor.RED + "Claim failed: " + e.getMessage()); + } + }); + } + + private void handleUnclaim(Player player) { + Chunk chunk = player.getLocation().getChunk(); + String dim = player.getWorld().getName(); + player.sendMessage(ChatColor.GRAY + "Unclaiming chunk [" + chunk.getX() + ", " + chunk.getZ() + "]..."); + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + integration.client().unclaimChunk(dim, chunk.getX(), chunk.getZ(), player.getUniqueId()); + player.sendMessage(ChatColor.GREEN + "Chunk [" + chunk.getX() + ", " + chunk.getZ() + "] has been unclaimed."); + } catch (Exception e) { + player.sendMessage(ChatColor.RED + "Unclaim failed: " + e.getMessage()); + } + }); + } + + private void handleCreate(Player player, String[] args) { + if (args.length < 3) { + player.sendMessage(ChatColor.RED + "Usage: /guild create "); + return; + } + + String name = args[1]; + String tag = args[2]; + player.sendMessage(ChatColor.GRAY + "Founding guild " + name + " [" + tag + "]..."); + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + JsonObject res = integration.client().createGuild(player.getUniqueId(), player.getName(), name, tag); + player.sendMessage(ChatColor.GREEN + "Guild " + ChatColor.YELLOW + name + ChatColor.GOLD + " [" + tag + "]" + + ChatColor.GREEN + " founded successfully! You are the Guild Leader."); + } catch (Exception e) { + player.sendMessage(ChatColor.RED + "Failed to create guild: " + e.getMessage()); + } + }); + } + + private void handleLeave(Player player) { + player.sendMessage(ChatColor.GRAY + "Leaving guild..."); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + JsonObject res = integration.client().leaveGuild(player.getUniqueId()); + boolean disbanded = res.has("disbanded") && res.get("disbanded").getAsBoolean(); + if (disbanded) { + player.sendMessage(ChatColor.YELLOW + "You were the last member. The guild was disbanded."); + } else { + player.sendMessage(ChatColor.GREEN + "You left your guild."); + } + } catch (Exception e) { + player.sendMessage(ChatColor.RED + "Error leaving guild: " + e.getMessage()); + } + }); + } + + private void handleGuildDashboard(Player player) { + player.sendMessage(ChatColor.GRAY + "Loading guild info..."); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + JsonObject res = integration.client().getPlayerGuild(player.getUniqueId()); + if (!res.has("in_guild") || !res.get("in_guild").getAsBoolean()) { + player.sendMessage(ChatColor.YELLOW + "You are not in a guild! Create one with " + + ChatColor.GOLD + "/guild create "); + return; + } + + JsonObject g = res.getAsJsonObject("guild"); + String name = g.get("name").getAsString(); + String tag = g.get("tag").getAsString(); + String role = g.get("role").getAsString(); + int level = g.has("level") ? g.get("level").getAsInt() : 1; + long claims = g.has("claims_count") ? g.get("claims_count").getAsLong() : 0; + long maxClaims = g.has("max_claims") ? g.get("max_claims").getAsLong() : 16; + JsonArray members = g.has("members") ? g.getAsJsonArray("members") : new JsonArray(); + + Bukkit.getScheduler().runTask(plugin, () -> { + Inventory inv = Bukkit.createInventory(null, 36, GUI_GUILD_TITLE); + ItemStack border = GuiHelper.createBorder(Material.CYAN_STAINED_GLASS_PANE); + for (int i = 0; i < 9; i++) inv.setItem(i, border); + for (int i = 27; i < 36; i++) inv.setItem(i, border); + + // Guild banner + inv.setItem(11, GuiHelper.createItem(Material.CYAN_BANNER, + "&bGuild: &f" + name + " &e[" + tag + "]", + "&7Your Role: &a" + role.toUpperCase(), + "&7Guild Level: &6" + level, + "&7Members: &f" + members.size())); + + // Claims status + inv.setItem(13, GuiHelper.createItem(Material.EMERALD_BLOCK, + "&aTerritory Claims", + "&7Claimed Chunks: &e" + claims + " &7/ &a" + maxClaims, + "", + "&7Click &e/claim &7to claim nearby area.")); + + // Quick claim button + inv.setItem(14, GuiHelper.createItem(Material.LIME_DYE, + "&aClaim Current Chunk", + "&7Click to claim the chunk you are standing on!")); + + // Quick unclaim button + inv.setItem(15, GuiHelper.createItem(Material.RED_DYE, + "&cUnclaim Current Chunk", + "&7Click to unclaim the current chunk.")); + + // Minimap button + inv.setItem(22, GuiHelper.createItem(Material.MAP, + "&eView Land Map", + "&7Shows nearby territory claims radar in chat.")); + + player.openInventory(inv); + }); + } catch (Exception e) { + player.sendMessage(ChatColor.RED + "Failed to load guild: " + e.getMessage()); + } + }); + } + + private void handleMap(Player player) { + Location pLoc = player.getLocation(); + Chunk center = pLoc.getChunk(); + int cx = center.getX(); + int cz = center.getZ(); + String dim = pLoc.getWorld().getName(); + + player.sendMessage(ChatColor.GOLD + "========= " + ChatColor.YELLOW + "Territory Map (" + cx + ", " + cz + ")" + ChatColor.GOLD + " ========="); + player.sendMessage(ChatColor.GRAY + " N (-Z)"); + + StringBuilder header = new StringBuilder(" "); + for (int dx = -4; dx <= 4; dx++) { + header.append(" "); + } + player.sendMessage(header.toString()); + + for (int dz = -3; dz <= 3; dz++) { + StringBuilder row = new StringBuilder(" "); + for (int dx = -4; dx <= 4; dx++) { + int targetX = cx + dx; + int targetZ = cz + dz; + boolean isPlayer = (dx == 0 && dz == 0); + + ChunkCheckResult res = integration.checkChunk(dim, targetX, targetZ, player.getUniqueId()); + if (isPlayer) { + row.append(ChatColor.GOLD).append("[P]"); + } else if (res.claimed() && res.allowed()) { + row.append(ChatColor.GREEN).append(" + "); + } else if (res.claimed() && !res.allowed()) { + row.append(ChatColor.RED).append(" x "); + } else { + row.append(ChatColor.DARK_GRAY).append(" . "); + } + } + player.sendMessage(row.toString()); + } + + player.sendMessage(ChatColor.GRAY + " S (+Z)"); + player.sendMessage(ChatColor.GOLD + "[P]" + ChatColor.YELLOW + " You " + + ChatColor.GREEN + "+ " + ChatColor.GRAY + "Your Guild " + + ChatColor.RED + "x " + ChatColor.GRAY + "Other Guild " + + ChatColor.DARK_GRAY + ". " + ChatColor.GRAY + "Wilderness"); + } + + private void handleChat(Player player, String[] args) { + if (args.length < 2) { + player.sendMessage(ChatColor.RED + "Usage: /guild chat "); + return; + } + + String msg = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + JsonObject res = integration.client().getPlayerGuild(player.getUniqueId()); + if (!res.has("in_guild") || !res.get("in_guild").getAsBoolean()) { + player.sendMessage(ChatColor.RED + "You are not in a guild."); + return; + } + String tag = res.getAsJsonObject("guild").get("tag").getAsString(); + String formatted = ChatColor.GREEN + "[Guild " + tag + "] " + ChatColor.YELLOW + player.getName() + ": " + ChatColor.WHITE + msg; + for (Player p : Bukkit.getOnlinePlayers()) { + p.sendMessage(formatted); + } + } catch (Exception e) { + player.sendMessage(ChatColor.RED + "Guild chat error: " + e.getMessage()); + } + }); + } + + private void handleSetHome(Player player) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + JsonObject res = integration.client().getPlayerGuild(player.getUniqueId()); + if (!res.has("in_guild") || !res.get("in_guild").getAsBoolean()) { + player.sendMessage(ChatColor.RED + "You must be in a guild to set a guild home."); + return; + } + String role = res.getAsJsonObject("guild").get("role").getAsString(); + if (!role.equals("leader") && !role.equals("officer")) { + player.sendMessage(ChatColor.RED + "Only guild leaders and officers can set the guild home."); + return; + } + String gid = res.getAsJsonObject("guild").get("id").getAsString(); + guildHomes.put(gid, player.getLocation()); + saveGuildHomes(); + player.sendMessage(ChatColor.GREEN + "Guild home waypoint set at your location!"); + } catch (Exception e) { + player.sendMessage(ChatColor.RED + "Error: " + e.getMessage()); + } + }); + } + + private void handleHome(Player player) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + JsonObject res = integration.client().getPlayerGuild(player.getUniqueId()); + if (!res.has("in_guild") || !res.get("in_guild").getAsBoolean()) { + player.sendMessage(ChatColor.RED + "You are not in a guild."); + return; + } + String gid = res.getAsJsonObject("guild").get("id").getAsString(); + Location loc = guildHomes.get(gid); + if (loc == null) { + player.sendMessage(ChatColor.RED + "Your guild has not set a guild home yet! Use /guild sethome"); + return; + } + Bukkit.getScheduler().runTask(plugin, () -> { + player.teleport(loc); + player.sendMessage(ChatColor.GREEN + "Teleported to guild home base."); + }); + } catch (Exception e) { + player.sendMessage(ChatColor.RED + "Error: " + e.getMessage()); + } + }); + } + + private void handleMembers(Player player) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + JsonObject res = integration.client().getPlayerGuild(player.getUniqueId()); + if (!res.has("in_guild") || !res.get("in_guild").getAsBoolean()) { + player.sendMessage(ChatColor.RED + "You are not in a guild."); + return; + } + JsonObject g = res.getAsJsonObject("guild"); + String name = g.get("name").getAsString(); + JsonArray members = g.getAsJsonArray("members"); + + player.sendMessage(ChatColor.GOLD + "=== Members of " + ChatColor.YELLOW + name + ChatColor.GOLD + " ==="); + for (JsonElement el : members) { + JsonObject m = el.getAsJsonObject(); + String mname = m.get("name").getAsString(); + String role = m.get("role").getAsString(); + Player onlineP = Bukkit.getPlayer(mname); + String status = onlineP != null && onlineP.isOnline() ? ChatColor.GREEN + "[Online]" : ChatColor.GRAY + "[Offline]"; + player.sendMessage(ChatColor.WHITE + " - " + mname + " (" + ChatColor.AQUA + role + ChatColor.WHITE + ") " + status); + } + } catch (Exception e) { + player.sendMessage(ChatColor.RED + "Error fetching members: " + e.getMessage()); + } + }); + } + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) return; + if (!event.getView().getTitle().equals(GUI_GUILD_TITLE)) return; + + event.setCancelled(true); + int slot = event.getRawSlot(); + if (slot == 14) { // Claim Current Chunk + player.closeInventory(); + handleClaim(player); + } else if (slot == 15) { // Unclaim Current Chunk + player.closeInventory(); + handleUnclaim(player); + } else if (slot == 22) { // Minimap + player.closeInventory(); + handleMap(player); + } + } +} diff --git a/integrations/paper/src/main/java/net/scopenet/paper/commands/ScopenetCommandHandler.java b/integrations/paper/src/main/java/net/scopenet/paper/commands/ScopenetCommandHandler.java new file mode 100644 index 0000000..59b4916 --- /dev/null +++ b/integrations/paper/src/main/java/net/scopenet/paper/commands/ScopenetCommandHandler.java @@ -0,0 +1,130 @@ +package net.scopenet.paper.commands; + +import net.scopenet.integration.Integration; +import net.scopenet.integration.Settings; +import net.scopenet.paper.ScopenetPlugin; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; + +public final class ScopenetCommandHandler implements CommandExecutor { + private final ScopenetPlugin plugin; + private final Integration integration; + + public ScopenetCommandHandler(ScopenetPlugin plugin, Integration integration) { + this.plugin = plugin; + this.integration = integration; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (args.length == 0 || args[0].equalsIgnoreCase("help")) { + sendHelp(sender); + return true; + } + + String sub = args[0].toLowerCase(); + switch (sub) { + case "status" -> sendStatus(sender); + case "reload" -> handleReload(sender); + default -> { + sender.sendMessage(ChatColor.RED + "Unknown command. Use " + ChatColor.YELLOW + "/scopenet help"); + } + } + return true; + } + + private void sendHelp(CommandSender sender) { + Settings s = integration != null ? integration.settings() : null; + sender.sendMessage(ChatColor.GOLD + "=================== " + ChatColor.YELLOW + "SCOPENET Commands" + ChatColor.GOLD + " ==================="); + + // Essentials commands + if (s == null || s.essentialsEnabled()) { + sender.sendMessage(ChatColor.AQUA + "[Essentials Commands]"); + sender.sendMessage(ChatColor.YELLOW + " /spawn" + ChatColor.GRAY + " - Teleport to server spawn"); + sender.sendMessage(ChatColor.YELLOW + " /home [name]" + ChatColor.GRAY + " - Teleport to home or open homes GUI"); + sender.sendMessage(ChatColor.YELLOW + " /sethome [name]" + ChatColor.GRAY + " - Set home location"); + sender.sendMessage(ChatColor.YELLOW + " /delhome [name]" + ChatColor.GRAY + " - Delete home"); + sender.sendMessage(ChatColor.YELLOW + " /back" + ChatColor.GRAY + " - Return to location before teleport/death"); + sender.sendMessage(ChatColor.YELLOW + " /tpa " + ChatColor.GRAY + " - Request to teleport to a player"); + sender.sendMessage(ChatColor.YELLOW + " /tpaccept / /tpdeny" + ChatColor.GRAY + " - Accept or deny teleport"); + sender.sendMessage(ChatColor.YELLOW + " /rtp" + ChatColor.GRAY + " - Random wilderness teleport"); + sender.sendMessage(ChatColor.YELLOW + " /warp [name]" + ChatColor.GRAY + " - Teleport to warp or open warps GUI"); + sender.sendMessage(ChatColor.YELLOW + " /playtime [player]" + ChatColor.GRAY + " - View total playtime stats"); + } + + // Economy commands + if (s == null || s.economyEnabled()) { + sender.sendMessage(ChatColor.GREEN + "[Economy Commands]"); + sender.sendMessage(ChatColor.YELLOW + " /balance (or /bal)" + ChatColor.GRAY + " - Check your wallet balance"); + sender.sendMessage(ChatColor.YELLOW + " /pay " + ChatColor.GRAY + " - Send money to a player"); + sender.sendMessage(ChatColor.YELLOW + " /baltop" + ChatColor.GRAY + " - Leaderboard of richest players"); + sender.sendMessage(ChatColor.YELLOW + " /shop" + ChatColor.GRAY + " - Open interactive Server Shop GUI"); + sender.sendMessage(ChatColor.YELLOW + " /sell (or /sell hand)" + ChatColor.GRAY + " - Sell items for cash"); + sender.sendMessage(ChatColor.YELLOW + " /market" + ChatColor.GRAY + " - Open player marketplace GUI"); + sender.sendMessage(ChatColor.YELLOW + " /market sell " + ChatColor.GRAY + " - List held item on market"); + sender.sendMessage(ChatColor.YELLOW + " /trade " + ChatColor.GRAY + " - Secure two-player trade GUI"); + sender.sendMessage(ChatColor.YELLOW + " /transactions" + ChatColor.GRAY + " - View recent transaction history"); + } + + // Guilds commands + if (s == null || s.guildsEnabled()) { + sender.sendMessage(ChatColor.BLUE + "[Guilds & Claims]"); + sender.sendMessage(ChatColor.YELLOW + " /guild" + ChatColor.GRAY + " - Open interactive Guild Dashboard GUI"); + sender.sendMessage(ChatColor.YELLOW + " /guild create " + ChatColor.GRAY + " - Create a new guild"); + sender.sendMessage(ChatColor.YELLOW + " /guild leave" + ChatColor.GRAY + " - Leave your current guild"); + sender.sendMessage(ChatColor.YELLOW + " /guild members" + ChatColor.GRAY + " - List online guild members"); + sender.sendMessage(ChatColor.YELLOW + " /guild chat " + ChatColor.GRAY + " - Send guild chat message"); + sender.sendMessage(ChatColor.YELLOW + " /claim" + ChatColor.GRAY + " - Claim current chunk for your guild"); + sender.sendMessage(ChatColor.YELLOW + " /unclaim" + ChatColor.GRAY + " - Unclaim current chunk"); + sender.sendMessage(ChatColor.YELLOW + " /guild map" + ChatColor.GRAY + " - Show nearby land claims radar map"); + } + + // Admin commands + if (sender.hasPermission("scopenet.admin")) { + sender.sendMessage(ChatColor.LIGHT_PURPLE + "[Admin Commands]"); + sender.sendMessage(ChatColor.YELLOW + " /scopenet status" + ChatColor.GRAY + " - Integration health and module statuses"); + sender.sendMessage(ChatColor.YELLOW + " /scopenet reload" + ChatColor.GRAY + " - Reload config and feature flags"); + } + } + + private void sendStatus(CommandSender sender) { + if (!sender.hasPermission("scopenet.admin")) { + sender.sendMessage(ChatColor.RED + "You do not have permission to view SCOPENET status."); + return; + } + + Settings s = integration != null ? integration.settings() : null; + sender.sendMessage(ChatColor.GOLD + "=== SCOPENET Integration Status ==="); + if (s == null) { + sender.sendMessage(ChatColor.RED + "Status: NOT INITIALIZED or OFFLINE"); + return; + } + + sender.sendMessage(ChatColor.GRAY + "Panel URL: " + ChatColor.WHITE + s.panel()); + sender.sendMessage(ChatColor.GRAY + "Server Token: " + ChatColor.GREEN + (s.token().isEmpty() ? "MISSING" : "CONFIGURED")); + sender.sendMessage(ChatColor.GRAY + "Leveling: " + formatFlag(s.levelingEnabled())); + sender.sendMessage(ChatColor.GRAY + "Quests: " + formatFlag(s.questsEnabled())); + sender.sendMessage(ChatColor.GRAY + "Achievements: " + formatFlag(s.achievementsEnabled())); + sender.sendMessage(ChatColor.GRAY + "Guilds: " + formatFlag(s.guildsEnabled())); + sender.sendMessage(ChatColor.GRAY + "Land Claiming: " + formatFlag(s.landClaimingEnabled())); + sender.sendMessage(ChatColor.GRAY + "Social: " + formatFlag(s.socialEnabled())); + sender.sendMessage(ChatColor.GRAY + "Essentials: " + formatFlag(s.essentialsEnabled())); + sender.sendMessage(ChatColor.GRAY + "Economy: " + formatFlag(s.economyEnabled())); + } + + private String formatFlag(boolean enabled) { + return enabled ? ChatColor.GREEN + "ENABLED" : ChatColor.RED + "DISABLED"; + } + + private void handleReload(CommandSender sender) { + if (!sender.hasPermission("scopenet.admin")) { + sender.sendMessage(ChatColor.RED + "You do not have permission to reload SCOPENET."); + return; + } + + plugin.reloadConfig(); + sender.sendMessage(ChatColor.GREEN + "SCOPENET configuration reloaded successfully."); + } +} diff --git a/integrations/paper/src/main/java/net/scopenet/paper/gui/GuiHelper.java b/integrations/paper/src/main/java/net/scopenet/paper/gui/GuiHelper.java new file mode 100644 index 0000000..60c8b25 --- /dev/null +++ b/integrations/paper/src/main/java/net/scopenet/paper/gui/GuiHelper.java @@ -0,0 +1,41 @@ +package net.scopenet.paper.gui; + +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public final class GuiHelper { + private GuiHelper() {} + + public static ItemStack createItem(Material material, String name, String... lore) { + return createItem(material, 1, name, lore); + } + + public static ItemStack createItem(Material material, int amount, String name, String... lore) { + ItemStack item = new ItemStack(material, Math.max(1, amount)); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + if (name != null) { + meta.setDisplayName(ChatColor.translateAlternateColorCodes('&', name)); + } + if (lore != null && lore.length > 0) { + List list = new ArrayList<>(); + for (String line : lore) { + list.add(ChatColor.translateAlternateColorCodes('&', line)); + } + meta.setLore(list); + } + item.setItemMeta(meta); + } + return item; + } + + public static ItemStack createBorder(Material material) { + return createItem(material, " "); + } +} diff --git a/integrations/paper/src/main/resources/config.yml b/integrations/paper/src/main/resources/config.yml index b8a5d21..aaedac3 100644 --- a/integrations/paper/src/main/resources/config.yml +++ b/integrations/paper/src/main/resources/config.yml @@ -42,3 +42,18 @@ social: enabled: true # Show player guild tag and level title in chat formatting chat-prefixes: true + +essentials: + # Essentials commands (/spawn, /home, /sethome, /delhome, /back, /tpa, /tpaccept, /tpdeny, /rtp, /warp, /playtime) + enabled: true + # Maximum homes per player + max-homes: 5 + # RTP search radius + rtp-radius: 2500 + +economy: + # Server Economy commands (/balance, /pay, /baltop, /shop, /sell, /market, /orders, /trade, /transactions) + enabled: true + # Currency symbol + currency-symbol: "$" + diff --git a/integrations/paper/src/main/resources/plugin.yml b/integrations/paper/src/main/resources/plugin.yml index e8144c0..8c75db6 100644 --- a/integrations/paper/src/main/resources/plugin.yml +++ b/integrations/paper/src/main/resources/plugin.yml @@ -4,3 +4,70 @@ main: net.scopenet.paper.ScopenetPlugin api-version: '1.20' description: SCOPENET private authentication, access control and activity reporting. load: STARTUP + +commands: + scopenet: + description: SCOPENET server commands, status, and help + aliases: [sn] + permission: scopenet.use + spawn: + description: Teleport to the server spawn point + aliases: [hub, lobby] + home: + description: Teleport to one of your homes or open the homes GUI + aliases: [homes] + sethome: + description: Set a new home location + aliases: [createshome] + delhome: + description: Delete an existing home + aliases: [rmhome] + back: + description: Return to your previous location or death point + aliases: [return] + tpa: + description: Request to teleport to another player + tpaccept: + description: Accept an incoming teleport request + aliases: [tpyes] + tpdeny: + description: Deny an incoming teleport request + aliases: [tpno] + rtp: + description: Teleport to a random safe location in the wilderness + aliases: [wild, randomtp] + warp: + description: Teleport to a server warp or open the warps GUI + aliases: [warps] + playtime: + description: Check total and session playtime + aliases: [ontime] + balance: + description: Check your server economy balance + aliases: [bal, money] + pay: + description: Send money to another player + baltop: + description: View the richest players on the server + aliases: [richest] + shop: + description: Open the interactive server shop GUI + sell: + description: Open the item selling GUI + market: + description: Open the player marketplace GUI + aliases: [ah, auction] + orders: + description: View active marketplace orders + trade: + description: Request a secure trade with another player + transactions: + description: View recent economy transactions + guild: + description: Guild management, members, claims, and relations + aliases: [g, clan] + claim: + description: Claim the current chunk for your guild + unclaim: + description: Unclaim the current chunk + diff --git a/launcher/src-tauri/src/commands.rs b/launcher/src-tauri/src/commands.rs index 1a87d0b..cbec442 100644 --- a/launcher/src-tauri/src/commands.rs +++ b/launcher/src-tauri/src/commands.rs @@ -7,9 +7,9 @@ use crate::state::{build, AppState}; use crate::updater; use scopenet_core::ping::ServerStatus; use scopenet_shared::{ - Achievement, DirectMessage, FriendInfo, GameInvite, Guild, GuildClaim, GuildMember, GuildPost, - LauncherManifest, PlayerProfile, SkinProfile, UserLevelInfo, UserPost, UserProfileView, - UserQuest, + Achievement, BaltopEntry, DirectMessage, EconomyTransaction, FriendInfo, GameInvite, Guild, + GuildClaim, GuildMember, GuildPost, LauncherManifest, MemberProfile, PlayerProfile, + ServerEconomyBalance, SkinProfile, UserLevelInfo, UserPost, UserProfileView, UserQuest, }; use serde::Serialize; use tauri::{AppHandle, Manager, State}; @@ -550,14 +550,14 @@ pub async fn get_my_quests(state: State<'_, AppState>) -> Res> { } #[tauri::command] -pub async fn claim_quest(state: State<'_, AppState>, quest_id: String) -> Res { +pub async fn claim_quest(state: State<'_, AppState>, quest_id: String) -> Res { let req = account_api(&state, reqwest::Method::POST, &format!("/quests/{quest_id}/claim")).await?; let resp = req.send().await.map_err(err)?; if !resp.status().is_success() { let body: serde_json::Value = resp.json().await.unwrap_or_default(); return Err(body["error"].as_str().unwrap_or("unable to claim quest").to_string()); } - resp.json().await.map_err(err) + get_my_level(state).await } // --------------------------------------------------------------------------- @@ -738,9 +738,14 @@ pub async fn get_friends(state: State<'_, AppState>) -> Res> { } #[tauri::command] -pub async fn send_friend_request(state: State<'_, AppState>, username: String) -> Res<()> { +pub async fn send_friend_request( + state: State<'_, AppState>, + friend_username: Option, + username: Option, +) -> Res<()> { + let target = friend_username.or(username).ok_or("username is required")?; let req = account_api(&state, reqwest::Method::POST, "/friends/request").await?; - let resp = req.json(&serde_json::json!({ "username": username })).send().await.map_err(err)?; + let resp = req.json(&serde_json::json!({ "username": target })).send().await.map_err(err)?; if !resp.status().is_success() { let body: serde_json::Value = resp.json().await.unwrap_or_default(); return Err(body["error"].as_str().unwrap_or("unable to send friend request").to_string()); @@ -749,9 +754,15 @@ pub async fn send_friend_request(state: State<'_, AppState>, username: String) - } #[tauri::command] -pub async fn respond_friend_request(state: State<'_, AppState>, target_uuid: String, accept: bool) -> Res<()> { +pub async fn respond_friend_request( + state: State<'_, AppState>, + friend_uuid: Option, + target_uuid: Option, + accept: bool, +) -> Res<()> { + let target = friend_uuid.or(target_uuid).ok_or("target_uuid is required")?; let req = account_api(&state, reqwest::Method::POST, "/friends/respond").await?; - let resp = req.json(&serde_json::json!({ "target_uuid": target_uuid, "accept": accept })).send().await.map_err(err)?; + let resp = req.json(&serde_json::json!({ "target_uuid": target, "accept": accept })).send().await.map_err(err)?; if !resp.status().is_success() { let body: serde_json::Value = resp.json().await.unwrap_or_default(); return Err(body["error"].as_str().unwrap_or("unable to respond to friend request").to_string()); @@ -760,8 +771,13 @@ pub async fn respond_friend_request(state: State<'_, AppState>, target_uuid: Str } #[tauri::command] -pub async fn remove_friend(state: State<'_, AppState>, target_uuid: String) -> Res<()> { - let req = account_api(&state, reqwest::Method::DELETE, &format!("/friends/{target_uuid}")).await?; +pub async fn remove_friend( + state: State<'_, AppState>, + friend_uuid: Option, + target_uuid: Option, +) -> Res<()> { + let target = friend_uuid.or(target_uuid).ok_or("target_uuid is required")?; + let req = account_api(&state, reqwest::Method::DELETE, &format!("/friends/{target}")).await?; let resp = req.send().await.map_err(err)?; if !resp.status().is_success() { let body: serde_json::Value = resp.json().await.unwrap_or_default(); @@ -770,6 +786,46 @@ pub async fn remove_friend(state: State<'_, AppState>, target_uuid: String) -> R Ok(()) } +#[tauri::command] +pub async fn search_members(state: State<'_, AppState>, query: String) -> Res> { + let req = account_api(&state, reqwest::Method::GET, &format!("/social/members?q={query}")).await?; + let resp = req.send().await.map_err(err)?; + if !resp.status().is_success() { + return Err("unable to search members".into()); + } + resp.json().await.map_err(err) +} + +#[tauri::command] +pub async fn get_economy_balances(state: State<'_, AppState>) -> Res> { + let req = account_api(&state, reqwest::Method::GET, "/economy/me").await?; + let resp = req.send().await.map_err(err)?; + if !resp.status().is_success() { + return Err("unable to load economy balances".into()); + } + resp.json().await.map_err(err) +} + +#[tauri::command] +pub async fn get_server_baltop(state: State<'_, AppState>, server_id: i64) -> Res> { + let req = account_api(&state, reqwest::Method::GET, &format!("/servers/{server_id}/economy/baltop")).await?; + let resp = req.send().await.map_err(err)?; + if !resp.status().is_success() { + return Err("unable to load baltop leaderboard".into()); + } + resp.json().await.map_err(err) +} + +#[tauri::command] +pub async fn get_transactions(state: State<'_, AppState>) -> Res> { + let req = account_api(&state, reqwest::Method::GET, "/economy/transactions").await?; + let resp = req.send().await.map_err(err)?; + if !resp.status().is_success() { + return Err("unable to load transactions".into()); + } + resp.json().await.map_err(err) +} + #[tauri::command] pub async fn get_direct_messages(state: State<'_, AppState>, friend_uuid: String) -> Res> { let req = account_api(&state, reqwest::Method::GET, &format!("/messages/{friend_uuid}")).await?; diff --git a/launcher/src-tauri/src/lib.rs b/launcher/src-tauri/src/lib.rs index 85db595..cc8abaf 100644 --- a/launcher/src-tauri/src/lib.rs +++ b/launcher/src-tauri/src/lib.rs @@ -101,6 +101,10 @@ pub fn run() { commands::send_friend_request, commands::respond_friend_request, commands::remove_friend, + commands::search_members, + commands::get_economy_balances, + commands::get_server_baltop, + commands::get_transactions, commands::get_direct_messages, commands::send_direct_message, commands::get_game_invites, diff --git a/launcher/src/lib/types.ts b/launcher/src/lib/types.ts index c9a04b6..e14e36c 100644 --- a/launcher/src/lib/types.ts +++ b/launcher/src/lib/types.ts @@ -156,6 +156,7 @@ export interface UserLevelInfo { next_level_xp: number; progress_pct: number; title: string | null; + rank?: number | null; badges: string[]; rewards: LevelReward[]; } @@ -316,3 +317,44 @@ export interface UserProfileView { created_at: string; } +export interface MemberProfile { + uuid: string; + username: string; + role: string; + status: string; + skin_url: string | null; + global_level: number; + title: string | null; + playtime_secs: number; + last_seen: string | null; + online: boolean; + is_friend: boolean; +} + +export interface ServerEconomyBalance { + server_id: number; + server_name: string; + balance: number; + currency_symbol: string; + last_updated: string; +} + +export interface EconomyTransaction { + id: number; + server_id: number; + server_name?: string; + sender_uuid: string; + recipient_uuid: string; + amount: number; + tx_type: string; + description: string; + created_at: string; +} + +export interface BaltopEntry { + rank: number; + uuid: string; + username: string; + balance: number; +} + diff --git a/launcher/src/pages/Quests.svelte b/launcher/src/pages/Quests.svelte index 753ef84..2505d37 100644 --- a/launcher/src/pages/Quests.svelte +++ b/launcher/src/pages/Quests.svelte @@ -51,7 +51,8 @@ levelInfo = updatedLevel; // Mark as claimed locally userQuests = userQuests.map((uq) => { - if (uq.quest.id === questId) { + const id = uq.quest?.id || (uq as any).id; + if (id === questId) { return { ...uq, claimed: true }; } return uq; @@ -66,18 +67,29 @@ const filteredQuests = $derived( userQuests.filter((uq) => { - if (uq.quest.quest_type !== activeTab) return false; - if (selectedCategory !== 'all' && uq.quest.category !== selectedCategory) return false; + const q = uq.quest || uq; + const qType = q.quest_type || (q as any).period || 'daily'; + if (qType !== activeTab) return false; + const cat = q.category || 'mining'; + if (selectedCategory !== 'all' && cat !== selectedCategory) return false; return true; }) ); const completedCount = $derived( - userQuests.filter((uq) => uq.quest.quest_type === activeTab && uq.completed).length + userQuests.filter((uq) => { + const q = uq.quest || uq; + const qType = q.quest_type || (q as any).period || 'daily'; + return qType === activeTab && uq.completed; + }).length ); const totalTabQuests = $derived( - userQuests.filter((uq) => uq.quest.quest_type === activeTab).length + userQuests.filter((uq) => { + const q = uq.quest || uq; + const qType = q.quest_type || (q as any).period || 'daily'; + return qType === activeTab; + }).length ); const unlockedAchievementsCount = $derived( @@ -128,11 +140,19 @@ LVL {levelInfo.level} + {#if levelInfo.rank} +
+ #{levelInfo.rank} +
+ {/if}
- SCOPENET Global Level + SCOPENET Progression + {#if levelInfo.rank} + Global Rank #{levelInfo.rank} + {/if} {#if levelInfo.title} {levelInfo.title} {/if} @@ -146,7 +166,11 @@