From 38c51a9beb1a703ff7d63f8cbccbf33b81133cf8 Mon Sep 17 00:00:00 2001 From: Git Manager GUI Date: Fri, 24 Apr 2026 09:13:04 +0200 Subject: [PATCH] Upload folder via GUI - src --- .../de/mviper/spigot/IngameShopSpigot.java | 1832 +++++++++++++++-- .../src/main/resources/config.yml | 23 +- .../src/main/resources/plugin.yml | 44 +- 3 files changed, 1697 insertions(+), 202 deletions(-) diff --git a/IngameShopSpigot/src/main/java/de/mviper/spigot/IngameShopSpigot.java b/IngameShopSpigot/src/main/java/de/mviper/spigot/IngameShopSpigot.java index 27cc4a8..c0466c8 100644 --- a/IngameShopSpigot/src/main/java/de/mviper/spigot/IngameShopSpigot.java +++ b/IngameShopSpigot/src/main/java/de/mviper/spigot/IngameShopSpigot.java @@ -11,10 +11,17 @@ 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.event.inventory.InventoryType; +import org.bukkit.event.inventory.PrepareAnvilEvent; +import org.bukkit.inventory.AnvilInventory; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.boss.BossBar; import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.scheduler.BukkitRunnable; @@ -28,10 +35,20 @@ import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import com.google.gson.Gson; @@ -45,23 +62,33 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { private static Economy econ = null; private String wpBase; private String wpUrlPending; - private String wpUrlPendingOffline; // NEU: Offline-Queue + private String wpUrlPendingOffline; private String wpUrlExecute; private String wpUrlComplete; private String wpUrlCancel; - private Gson gson = new Gson(); + private Gson gson = new Gson(); private boolean debug = false; private BukkitTask task; - private String currency = "Coins"; - private String targetServer = "survival"; + private String currency = "Coins"; + private String targetServer = "survival"; + private String apiKey = ""; + private boolean flyRedeemDisabled = false; + private String incomeReceiver = ""; - // NEU: API-Key für gesicherte Endpunkte - private String apiKey = ""; + private Map orderCache = new HashMap<>(); + private Map activeOrderIds = new ConcurrentHashMap<>(); - private Map orderCache = new HashMap<>(); - private Map activeOrderIds = new HashMap<>(); + private FlyManager flyManager; + private FlyCodeManager flyCodeManager; + private RankManager rankManager; + + private static final String GUI_FLYCODES = ChatColor.GOLD + "✈ Deine Fly-Gutscheine"; + private final Map flyCodesPage = new HashMap<>(); + private final Map> flyCodesCache = new HashMap<>(); + private final Map pendingTransferAnvil = new HashMap<>(); + private final Map anvilTypedText = new HashMap<>(); // =========================================================== // LIFECYCLE @@ -83,46 +110,62 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { wpBase = domain + "/wp-json/wis/v1"; wpUrlPending = wpBase + "/pending_orders"; - wpUrlPendingOffline = wpBase + "/pending_offline"; // NEU + wpUrlPendingOffline = wpBase + "/pending_offline"; wpUrlExecute = wpBase + "/execute_order"; wpUrlComplete = wpBase + "/complete_order"; wpUrlCancel = wpBase + "/cancel_order"; - targetServer = getConfig().getString("server-name", "survival").toLowerCase(); - currency = getConfig().getString("currency-name", "Coins"); - apiKey = getConfig().getString("api-key", ""); // NEU - debug = getConfig().getBoolean("debug-mode", false); + targetServer = getConfig().getString("server-name", "survival").toLowerCase(); + currency = getConfig().getString("currency-name", "Coins"); + apiKey = getConfig().getString("api-key", ""); + debug = getConfig().getBoolean("debug-mode", false); + flyRedeemDisabled = getConfig().getBoolean("fly-redeem-disabled", false); + incomeReceiver = getConfig().getString("income-receiver", ""); if (apiKey.isEmpty()) { - getLogger().warning("⚠️ Kein api-key in config.yml gesetzt! Geschützte Endpunkte sind nicht erreichbar."); - getLogger().warning(" Trage den Key aus den WordPress-Einstellungen (Ingame Shop → Einstellungen → 🔑 API-Key) ein."); + getLogger().warning("⚠️ Kein api-key in config.yml gesetzt!"); } - int intervalSeconds = getConfig().getInt("check-interval", 10); - long pollInterval = intervalSeconds * 20L; + flyCodeManager = new FlyCodeManager(this); + if (!flyCodeManager.connect()) { + getLogger().severe("❌ MySQL-Verbindung fehlgeschlagen! Plugin deaktiviert."); + getServer().getPluginManager().disablePlugin(this); + return; + } + flyCodeManager.createTable(); + flyManager = new FlyManager(this); + rankManager = new RankManager(this, flyCodeManager); + rankManager.startExpiryChecker(); + + int intervalSeconds = getConfig().getInt("check-interval", 10); + long pollInterval = intervalSeconds * 20L; getServer().getPluginManager().registerEvents(this, this); - - // /orders Befehl registrieren getCommand("orders").setExecutor(new OrdersCommand()); + getCommand("flytime").setExecutor(new FlyTimeCommand()); + getCommand("flyredeem").setExecutor(new FlyRedeemCommand()); + getCommand("flycodes").setExecutor(new FlyCodesCommand()); + getCommand("flygive").setExecutor(new FlyGiveCommand()); + getCommand("flypause").setExecutor(new FlyPauseCommand()); + getCommand("rankinfo").setExecutor(new RankInfoCommand()); startPolling(pollInterval); + flyManager.startSessionPersist(); - getLogger().info("=== IngameShopSpigot v6.3 (API-Key + Offline-Queue) ==="); - getLogger().info("Domain: " + domain); - getLogger().info("Target Server: " + targetServer); - getLogger().info("Currency: " + currency); - getLogger().info("API-Key: " + (apiKey.isEmpty() ? "❌ NICHT GESETZT" : "✅ gesetzt")); + getLogger().info("IngameShopSpigot v6.4 aktiv."); } @Override public void onDisable() { - if (task != null) task.cancel(); - getLogger().info("IngameShopSpigot gestoppt"); + if (task != null) task.cancel(); + if (flyManager != null) flyManager.cancelAll(); + if (flyCodeManager != null) flyCodeManager.disconnect(); + getLogger().info("IngameShopSpigot gestoppt."); } private boolean setupEconomy() { - RegisteredServiceProvider rsp = getServer().getServicesManager().getRegistration(Economy.class); + RegisteredServiceProvider rsp = + getServer().getServicesManager().getRegistration(Economy.class); if (rsp == null) return false; econ = rsp.getProvider(); return econ != null; @@ -134,16 +177,20 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { private void startPolling(long intervalTicks) { this.task = new BukkitRunnable() { - @Override - public void run() { + @Override public void run() { new BukkitRunnable() { - @Override - public void run() { + @Override public void run() { for (Player p : Bukkit.getOnlinePlayers()) { - // RACE CONDITION FIX: Spieler der bereits eine aktive Order im GUI hat - // bekommt keine neue Order angezeigt bis die vorherige abgeschlossen ist if (activeOrderIds.containsKey(p.getUniqueId())) { - if (debug) getLogger().info("Spieler " + p.getName() + " hat bereits aktive Order – überspringe Poll."); + if (debug) getLogger().info( + "Spieler " + p.getName() + " hat aktive Order – überspringe."); + continue; + } + // Join-Fetch läuft noch – nicht parallel pollen + Long cooldown = joinFetchCooldown.get(p.getUniqueId()); + if (cooldown != null && System.currentTimeMillis() < cooldown) { + if (debug) getLogger().info( + "Spieler " + p.getName() + " im Join-Cooldown – überspringe."); continue; } fetchPendingOrders(p, wpUrlPending); @@ -155,38 +202,55 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { } private void fetchPendingOrders(Player p, String endpointUrl) { + // Vor dem API-Call prüfen – verhindert doppelte GUI bei Race Condition + if (activeOrderIds.containsKey(p.getUniqueId())) { + if (debug) getLogger().info( + "fetchPendingOrders: " + p.getName() + " hat bereits aktive Order – abgebrochen."); + return; + } try { - String urlString = endpointUrl + "?player=" + p.getName(); - HttpURLConnection conn = openAuthConnection(urlString, "GET"); - - if (conn.getResponseCode() == 200) { - String body = readResponse(conn); + HttpURLConnection conn = + openAuthConnection(endpointUrl + "?player=" + p.getName(), "GET"); + int responseCode = conn.getResponseCode(); + if (responseCode == 200) { + String body = readResponse(conn); JsonObject json = JsonParser.parseString(body).getAsJsonObject(); JsonArray orders = json.getAsJsonArray("orders"); if (orders != null && orders.size() > 0) { - JsonObject order = orders.get(0).getAsJsonObject(); // immer nur erste Order zeigen - - int id = order.get("id").getAsInt(); - String orderServer = order.has("server") ? order.get("server").getAsString().toLowerCase() : ""; + JsonObject order = orders.get(0).getAsJsonObject(); + int id = order.get("id").getAsInt(); + String orderServer = order.has("server") + ? order.get("server").getAsString().toLowerCase() : ""; if (!orderServer.equals(targetServer)) { - if (debug) getLogger().info("Order #" + id + " ist für Server '" + orderServer + "'. Ignoriere."); + if (debug) getLogger().info( + "Order #" + id + " für '" + orderServer + "'. Ignoriere."); return; } - String jsonResponse = order.has("response") ? order.get("response").getAsString() : "[]"; - String itemTitle = order.get("item_title").getAsString(); - double price = order.get("price").getAsDouble(); + String jsonResponse = order.has("response") + ? order.get("response").getAsString() : "[]"; + String itemTitle = order.get("item_title").getAsString(); + double price = order.get("price").getAsDouble(); - OrderData data = new OrderData(id, "multi_item", itemTitle, price, 1, jsonResponse); - orderCache.put(id, data); - activeOrderIds.put(p.getUniqueId(), id); - - Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> openConfirmGUI(p, id, itemTitle, price, jsonResponse)); + // Atomar: nur wenn noch keine aktive Order → eintragen und GUI öffnen + // synchronized auf activeOrderIds verhindert Race Condition zwischen zwei Async-Threads + synchronized (activeOrderIds) { + if (activeOrderIds.containsKey(p.getUniqueId())) { + if (debug) getLogger().info( + "Order #" + id + " für " + p.getName() + " ignoriert (bereits aktiv)."); + return; + } + OrderData data = new OrderData(id, "multi_item", itemTitle, price, 1, jsonResponse); + orderCache.put(id, data); + activeOrderIds.put(p.getUniqueId(), id); + } + Bukkit.getScheduler().runTask(IngameShopSpigot.this, + () -> openConfirmGUI(p, id, itemTitle, price, jsonResponse)); } - } else if (conn.getResponseCode() == 401) { - getLogger().warning("❌ API-Key ungültig oder nicht gesetzt! HTTP 401 von " + endpointUrl); + } else if (responseCode == 401) { + getLogger().warning("❌ API-Key ungültig! HTTP 401 von " + endpointUrl); } } catch (Exception e) { if (debug) getLogger().log(Level.WARNING, "Polling Fehler für " + p.getName(), e); @@ -194,89 +258,197 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { } // =========================================================== - // OFFLINE-QUEUE: beim Login ausstehende Orders liefern + // OFFLINE-QUEUE & FLY-RESTORE // =========================================================== + // Spieler die gerade frisch gejoined sind – Polling überspringen bis Join-Fetch fertig + private final Map joinFetchCooldown = new ConcurrentHashMap<>(); + @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { Player p = event.getPlayer(); + // Polling für diesen Spieler sperren bis der Join-Fetch durch ist + joinFetchCooldown.put(p.getUniqueId(), System.currentTimeMillis() + 10_000L); + + // Offline-Bestellungen abholen new BukkitRunnable() { - @Override - public void run() { + @Override public void run() { fetchPendingOrders(p, wpUrlPendingOffline); + // Sperre aufheben sobald Join-Fetch fertig + joinFetchCooldown.remove(p.getUniqueId()); + } + }.runTaskAsynchronously(this); + + // Fly-Zeit wiederherstellen + flyManager.restoreOnJoin(p); + + // Abgelaufene Ränge prüfen + new BukkitRunnable() { + @Override public void run() { rankManager.checkExpiredRanksForPlayer(p); } + }.runTaskAsynchronously(this); + + // Ausstehende Fly-Codes anzeigen + new BukkitRunnable() { + @Override public void run() { + List pending = + flyCodeManager.getUnusedCodes(p.getName()); + if (!pending.isEmpty()) { + Bukkit.getScheduler().runTaskLater(IngameShopSpigot.this, () -> + p.sendMessage(ChatColor.GOLD + "✈ Du hast " + + ChatColor.YELLOW + pending.size() + + ChatColor.GOLD + " ungenutzten Fly-Gutschein(e)! " + + ChatColor.GRAY + "Öffne sie mit: " + + ChatColor.WHITE + "/flycodes"), + 40L); + } } }.runTaskAsynchronously(this); } + @EventHandler + public void onPlayerQuit(org.bukkit.event.player.PlayerQuitEvent event) { + joinFetchCooldown.remove(event.getPlayer().getUniqueId()); + flyManager.saveOnQuit(event.getPlayer()); + } + // =========================================================== - // GUI: zeigt echtes Item-Icon aus dem Warenkorb-Payload + // ANVIL – Text verfolgen & Kosten auf 0 setzen // =========================================================== - private void openConfirmGUI(Player player, int orderId, String itemTitle, double price, String jsonPayload) { - Inventory gui = Bukkit.createInventory(null, 27, ChatColor.YELLOW + "Kauf bestätigen?"); + // =========================================================== + // ANVIL – Text verfolgen & Kosten auf 0 setzen + // =========================================================== - // NEU: erstes Item aus dem Payload als Icon verwenden - Material iconMaterial = getFirstItemMaterial(jsonPayload); - ItemStack info = new ItemStack(iconMaterial); - ItemMeta infoMeta = info.getItemMeta(); + @EventHandler + public void onPrepareAnvil(PrepareAnvilEvent event) { + if (!(event.getView().getPlayer() instanceof Player)) return; + Player p = (Player) event.getView().getPlayer(); + if (!pendingTransferAnvil.containsKey(p.getUniqueId())) return; + + AnvilInventory inv = event.getInventory(); + String text = inv.getRenameText(); + if (text == null) text = ""; + + // Eingetippten Text immer speichern (auch wenn leer – dann alten behalten) + if (!text.isEmpty()) { + anvilTypedText.put(p.getUniqueId(), text); + } + + // Result-Item mit dem aktuellen Text setzen, damit der Spieler es anklicken kann + String display = !text.isEmpty() + ? text + : anvilTypedText.getOrDefault(p.getUniqueId(), ""); + + if (!display.isEmpty()) { + ItemStack result = new ItemStack(Material.PAPER); + ItemMeta rm = result.getItemMeta(); + rm.setDisplayName(display); + result.setItemMeta(rm); + event.setResult(result); + } + + inv.setRepairCost(0); + } + + private static final String GUI_CONFIRM_TITLE = ChatColor.YELLOW + "Kauf bestätigen?"; + + @EventHandler + public void onInventoryClose(InventoryCloseEvent event) { + if (!(event.getPlayer() instanceof Player)) return; + Player p = (Player) event.getPlayer(); + + // Confirm-GUI: bei ESC sofort wieder öffnen + String title = event.getView().getTitle(); + if (GUI_CONFIRM_TITLE.equals(title) && activeOrderIds.containsKey(p.getUniqueId())) { + int orderId = activeOrderIds.get(p.getUniqueId()); + OrderData data = orderCache.get(orderId); + if (data != null) { + p.sendMessage(ChatColor.YELLOW + "⚠ Bitte bestätige oder storniere deinen Kauf zuerst!"); + Bukkit.getScheduler().runTaskLater(this, + () -> openConfirmGUI(p, orderId, data.itemTitle, data.price, data.jsonPayload), + 2L); + return; + } + } + + // Anvil-Transfer: verzögert aufräumen + if (event.getInventory().getType() == InventoryType.ANVIL + && pendingTransferAnvil.containsKey(p.getUniqueId())) { + Bukkit.getScheduler().runTaskLater(this, () -> { + pendingTransferAnvil.remove(p.getUniqueId()); + anvilTypedText.remove(p.getUniqueId()); + }, 2L); + } + } + + // =========================================================== + // CONFIRM GUI + // =========================================================== + + private void openConfirmGUI(Player player, int orderId, + String itemTitle, double price, String jsonPayload) { + Inventory gui = Bukkit.createInventory(null, 27, GUI_CONFIRM_TITLE); + Material icon = getFirstItemMaterial(jsonPayload); + + ItemStack info = new ItemStack(icon); + ItemMeta infoMeta = info.getItemMeta(); infoMeta.setDisplayName(ChatColor.GOLD + "" + ChatColor.BOLD + itemTitle); infoMeta.setLore(Arrays.asList( - ChatColor.GRAY + "──────────────────", + ChatColor.GRAY + "──────────────────", ChatColor.WHITE + "Preis: " + ChatColor.YELLOW + price + " " + currency, - ChatColor.GRAY + "──────────────────", - ChatColor.GRAY + "Klicke Grün zum Bestätigen", - ChatColor.GRAY + "Klicke Rot zum Abbrechen" + ChatColor.GRAY + "──────────────────", + ChatColor.GRAY + "Klicke Grün zum Bestätigen", + ChatColor.GRAY + "Klicke Rot zum Abbrechen" )); info.setItemMeta(infoMeta); ItemStack yes = new ItemStack(Material.LIME_WOOL); - ItemMeta yesMeta = yes.getItemMeta(); - yesMeta.setDisplayName(ChatColor.GREEN + "" + ChatColor.BOLD + "✔ JA, kaufen!"); - yesMeta.setLore(Arrays.asList(ChatColor.GRAY + "" + price + " " + currency + " werden abgezogen")); - yes.setItemMeta(yesMeta); + ItemMeta yM = yes.getItemMeta(); + yM.setDisplayName(ChatColor.GREEN + "" + ChatColor.BOLD + "✔ JA, kaufen!"); + yM.setLore(Arrays.asList( + ChatColor.GRAY + "" + price + " " + currency + " werden abgezogen")); + yes.setItemMeta(yM); - ItemStack no = new ItemStack(Material.RED_WOOL); - ItemMeta noMeta = no.getItemMeta(); - noMeta.setDisplayName(ChatColor.RED + "" + ChatColor.BOLD + "✘ NEIN, abbrechen"); - noMeta.setLore(Arrays.asList(ChatColor.GRAY + "Bestellung wird storniert")); - no.setItemMeta(noMeta); + ItemStack no = new ItemStack(Material.RED_WOOL); + ItemMeta nM = no.getItemMeta(); + nM.setDisplayName(ChatColor.RED + "" + ChatColor.BOLD + "✘ NEIN, abbrechen"); + nM.setLore(Arrays.asList(ChatColor.GRAY + "Bestellung wird storniert")); + no.setItemMeta(nM); ItemStack pane = new ItemStack(Material.BLACK_STAINED_GLASS_PANE); - ItemMeta paneMeta = pane.getItemMeta(); - paneMeta.setDisplayName(" "); - pane.setItemMeta(paneMeta); + ItemMeta pM = pane.getItemMeta(); + pM.setDisplayName(" "); + pane.setItemMeta(pM); for (int i = 0; i < 27; i++) gui.setItem(i, pane); gui.setItem(13, info); gui.setItem(11, yes); gui.setItem(15, no); - player.openInventory(gui); } - /** - * NEU: Liest das erste Item aus dem JSON-Payload und gibt das passende Material zurück. - * Fällt auf CHEST zurück wenn nichts parsebar ist. - */ private Material getFirstItemMaterial(String jsonPayload) { try { - JsonElement root = gson.fromJson(jsonPayload, JsonElement.class); - JsonArray items; + JsonElement root = gson.fromJson(jsonPayload, JsonElement.class); + JsonArray items = null; if (root.isJsonObject()) { - items = root.getAsJsonObject().getAsJsonArray("items"); - } else { + JsonObject obj = root.getAsJsonObject(); + if (obj.has("items")) items = obj.getAsJsonArray("items"); + if ((items == null || items.size() == 0) && obj.has("commands")) + return Material.FEATHER; + } else if (root.isJsonArray()) { items = root.getAsJsonArray(); } if (items != null && items.size() > 0) { - String itemId = items.get(0).getAsJsonObject().get("id").getAsString(); - ItemStack test = parseItem(itemId); + String id = items.get(0).getAsJsonObject().get("id").getAsString(); + ItemStack test = parseItem(id); if (test != null) return test.getType(); } } catch (Exception e) { if (debug) getLogger().log(Level.WARNING, "getFirstItemMaterial Fehler", e); } - return Material.CHEST; // sicherer Fallback + return Material.CHEST; } // =========================================================== @@ -285,33 +457,216 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { @EventHandler public void onInventoryClick(InventoryClickEvent event) { - if (!event.getView().getTitle().contains("Kauf bestätigen?")) return; + String title = event.getView().getTitle(); + + // ── Fly-Codes GUI ────────────────────────────────────────────────── + if (title.equals(GUI_FLYCODES)) { + event.setCancelled(true); + if (!(event.getWhoClicked() instanceof Player)) return; + Player p = (Player) event.getWhoClicked(); + int slot = event.getRawSlot(); + + List codes = flyCodesCache.get(p.getUniqueId()); + if (codes == null) return; + int page = flyCodesPage.getOrDefault(p.getUniqueId(), 0); + int total = (int) Math.ceil(codes.size() / 45.0); + + if (slot == 45 && page > 0) { + openFlyCodesGUI(p, codes, page - 1); + return; + } + if (slot == 53 && page < total - 1) { + openFlyCodesGUI(p, codes, page + 1); + return; + } + if (slot < 0 || slot > 44) return; + int codeIndex = page * 45 + slot; + if (codeIndex >= codes.size()) return; + + FlyCodeManager.FlyCodeEntry entry = codes.get(codeIndex); + boolean isRight = event.getClick().isRightClick(); + + if (!isRight) { + if (flyRedeemDisabled) { + p.sendMessage(ChatColor.RED + + "✗ Fly-Codes können auf diesem Server nicht eingelöst werden."); + p.closeInventory(); + return; + } + p.closeInventory(); + new BukkitRunnable() { + @Override public void run() { + int sec = flyCodeManager.redeemCode(entry.code); + Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> { + if (sec < 0) + p.sendMessage(ChatColor.RED + "❌ Code ungültig oder bereits eingelöst."); + else + flyManager.grantFly(p, sec); + }); + } + }.runTaskAsynchronously(this); + } else { + pendingTransferAnvil.put(p.getUniqueId(), entry.code); + anvilTypedText.remove(p.getUniqueId()); + p.closeInventory(); + Bukkit.getScheduler().runTaskLater(IngameShopSpigot.this, () -> { + org.bukkit.inventory.InventoryView view = p.openAnvil(p.getLocation(), true); + if (view != null) { + // Slot 0 mit einem namenlosen Paper befüllen – + // nur so kann der Spieler im Amboss tippen. + // Kein DisplayName setzen, damit das Textfeld leer startet. + ItemStack paper = new ItemStack(Material.PAPER); + view.getTopInventory().setItem(0, paper); + p.updateInventory(); + } + }, 2L); + } + return; + } + + + // ── Anvil GUI – Spielername für Weitergabe ───────────────────────── + if (event.getInventory().getType() == InventoryType.ANVIL + && event.getWhoClicked() instanceof Player) { + Player p = (Player) event.getWhoClicked(); + if (!pendingTransferAnvil.containsKey(p.getUniqueId())) return; + if (event.getRawSlot() != 2) { event.setCancelled(true); return; } + event.setCancelled(true); + + // Text aus anvilTypedText – wird in PrepareAnvilEvent zuverlässig befüllt + String targetName = anvilTypedText.getOrDefault(p.getUniqueId(), "").trim(); + + String code = pendingTransferAnvil.remove(p.getUniqueId()); + anvilTypedText.remove(p.getUniqueId()); + p.closeInventory(); + + if (targetName.isEmpty()) { + p.sendMessage(ChatColor.RED + "❌ Kein Spielername eingegeben."); + return; + } + if (targetName.equalsIgnoreCase(p.getName())) { + p.sendMessage(ChatColor.RED + + "❌ Du kannst dir den Gutschein nicht selbst übertragen."); + return; + } + executeTransfer(p, code, targetName); + return; + } + + // ── Kauf-Bestätigen GUI ───────────────────────────────────────────── + if (!title.contains("Kauf bestätigen?")) return; event.setCancelled(true); if (!(event.getWhoClicked() instanceof Player)) return; Player p = (Player) event.getWhoClicked(); int slot = event.getRawSlot(); - if (slot == 11) { // JA - Integer orderId = activeOrderIds.get(p.getUniqueId()); + if (slot == 11) { + Integer orderId = activeOrderIds.remove(p.getUniqueId()); if (orderId != null) { processOrder(p, orderId); p.closeInventory(); - activeOrderIds.remove(p.getUniqueId()); } else { p.sendMessage(ChatColor.RED + "❌ Fehler: Kauf abgelaufen."); p.closeInventory(); } - } else if (slot == 15) { // NEIN - Integer orderId = activeOrderIds.get(p.getUniqueId()); + } else if (slot == 15) { + Integer orderId = activeOrderIds.remove(p.getUniqueId()); if (orderId != null) { cancelOrder(p, orderId); p.closeInventory(); - activeOrderIds.remove(p.getUniqueId()); } } } + // =========================================================== + // FLY-CODES GUI + // =========================================================== + + private void openFlyCodesGUI(Player player, + List codes, int page) { + final int pageSize = 45; + final int totalPages = Math.max(1, (int) Math.ceil(codes.size() / (double) pageSize)); + page = Math.max(0, Math.min(page, totalPages - 1)); + + flyCodesPage.put(player.getUniqueId(), page); + flyCodesCache.put(player.getUniqueId(), codes); + + Inventory gui = Bukkit.createInventory(null, 54, GUI_FLYCODES); + + ItemStack pane = new ItemStack(Material.BLACK_STAINED_GLASS_PANE); + ItemMeta pM = pane.getItemMeta(); + pM.setDisplayName(" "); + pane.setItemMeta(pM); + for (int i = 45; i < 54; i++) gui.setItem(i, pane); + + int start = page * pageSize; + int end = Math.min(start + pageSize, codes.size()); + for (int i = start; i < end; i++) { + FlyCodeManager.FlyCodeEntry e = codes.get(i); + ItemStack item = new ItemStack(Material.FEATHER); + ItemMeta meta = item.getItemMeta(); + meta.setDisplayName(ChatColor.YELLOW + "" + ChatColor.BOLD + e.label); + + List lore = new ArrayList<>(Arrays.asList( + ChatColor.GRAY + "──────────────────────", + ChatColor.GRAY + "Code: " + ChatColor.AQUA + ChatColor.BOLD + e.code, + ChatColor.GRAY + "Dauer: " + ChatColor.WHITE + flyManager.formatTime(e.durationSec), + ChatColor.GRAY + "Erhalten: " + ChatColor.WHITE + e.createdAt, + ChatColor.GRAY + "──────────────────────" + )); + if (flyRedeemDisabled) { + lore.add(ChatColor.RED + "✗ Einlösen auf diesem Server nicht möglich"); + lore.add(ChatColor.YELLOW + "▶ Rechtsklick: " + ChatColor.WHITE + "An Spieler weitergeben"); + } else { + lore.add(ChatColor.GREEN + "▶ Linksklick: " + ChatColor.WHITE + "Selbst einlösen"); + lore.add(ChatColor.YELLOW + "▶ Rechtsklick: " + ChatColor.WHITE + "An Spieler weitergeben"); + } + meta.setLore(lore); + item.setItemMeta(meta); + gui.setItem(i - start, item); + } + + ItemStack info = new ItemStack(Material.PAPER); + ItemMeta im = info.getItemMeta(); + im.setDisplayName(ChatColor.WHITE + "Seite " + (page + 1) + " / " + totalPages); + im.setLore(Arrays.asList( + ChatColor.GRAY + (codes.size() + " Gutschein(e) insgesamt"), + ChatColor.DARK_GRAY + "Codes gehören dir und sind übertragbar" + )); + info.setItemMeta(im); + gui.setItem(49, info); + + if (page > 0) { + ItemStack prev = new ItemStack(Material.ARROW); + ItemMeta pm2 = prev.getItemMeta(); + pm2.setDisplayName(ChatColor.YELLOW + "◀ Vorherige Seite"); + prev.setItemMeta(pm2); + gui.setItem(45, prev); + } + if (page < totalPages - 1) { + ItemStack next = new ItemStack(Material.ARROW); + ItemMeta nm2 = next.getItemMeta(); + nm2.setDisplayName(ChatColor.YELLOW + "Nächste Seite ▶"); + next.setItemMeta(nm2); + gui.setItem(53, next); + } + + if (codes.isEmpty()) { + ItemStack empty = new ItemStack(Material.BARRIER); + ItemMeta em = empty.getItemMeta(); + em.setDisplayName(ChatColor.RED + "Keine Gutscheine vorhanden"); + em.setLore(Arrays.asList( + ChatColor.GRAY + "Kaufe Fly-Zeit im Shop,", + ChatColor.GRAY + "um Gutscheine zu erhalten." + )); + empty.setItemMeta(em); + gui.setItem(22, empty); + } + + player.openInventory(gui); + } + // =========================================================== // ORDER PROCESSING // =========================================================== @@ -322,30 +677,28 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { player.sendMessage(ChatColor.RED + "❌ Fehler: Daten nicht gefunden."); return; } - new BukkitRunnable() { - @Override - public void run() { + @Override public void run() { try { HttpURLConnection conn = openAuthConnection(wpUrlExecute, "POST"); writeJson(conn, "{\"id\":\"" + orderId + "\"}"); - int code = conn.getResponseCode(); if (code == 200) { - Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> executeShopLogic(player, data, orderId)); + Bukkit.getScheduler().runTask(IngameShopSpigot.this, + () -> executeShopLogic(player, data, orderId)); } else if (code == 401) { getLogger().warning("❌ API-Key ungültig bei /execute_order"); - Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> - player.sendMessage(ChatColor.RED + "❌ Server-Konfigurationsfehler (Auth).")); + Bukkit.getScheduler().runTask(IngameShopSpigot.this, + () -> player.sendMessage(ChatColor.RED + "❌ Konfigurationsfehler (Auth).")); } else { if (debug) getLogger().warning("Execute Order HTTP " + code); - Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> - player.sendMessage(ChatColor.RED + "❌ Server-Fehler beim Starten des Kaufs.")); + Bukkit.getScheduler().runTask(IngameShopSpigot.this, + () -> player.sendMessage(ChatColor.RED + "❌ Server-Fehler beim Starten des Kaufs.")); } } catch (Exception e) { getLogger().log(Level.SEVERE, "Fehler bei /execute_order", e); - Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> - player.sendMessage(ChatColor.RED + "❌ Interner Fehler beim Kauf.")); + Bukkit.getScheduler().runTask(IngameShopSpigot.this, + () -> player.sendMessage(ChatColor.RED + "❌ Interner Fehler beim Kauf.")); } } }.runTaskAsynchronously(this); @@ -353,21 +706,17 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { private void cancelOrder(Player player, int orderId) { new BukkitRunnable() { - @Override - public void run() { + @Override public void run() { try { HttpURLConnection conn = openAuthConnection(wpUrlCancel, "POST"); writeJson(conn, "{\"id\":\"" + orderId + "\"}"); - int code = conn.getResponseCode(); orderCache.remove(orderId); - if (code == 200) { player.sendMessage(ChatColor.YELLOW + "❌ Kauf abgebrochen."); - if (debug) getLogger().info("✅ Order #" + orderId + " cancelled"); } else { if (debug) getLogger().warning("⚠️ Cancel HTTP " + code); - player.sendMessage(ChatColor.RED + "❌ Fehler beim Abbrechen – Order wird lokal verworfen."); + player.sendMessage(ChatColor.RED + "❌ Fehler beim Abbrechen."); activeOrderIds.remove(player.getUniqueId()); } } catch (Exception e) { @@ -383,7 +732,8 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { private void executeShopLogic(Player player, OrderData data, int orderId) { try { if (econ.getBalance(player) < data.price) { - player.sendMessage(ChatColor.RED + "❌ Nicht genug " + currency + "! (Benötigt: " + data.price + ")"); + player.sendMessage(ChatColor.RED + "❌ Nicht genug " + + currency + "! (Benötigt: " + data.price + ")"); cancelOrder(player, orderId); return; } @@ -391,26 +741,118 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { econ.withdrawPlayer(player, data.price); player.sendMessage(ChatColor.GREEN + "💰 " + data.price + " " + currency + " abgezogen."); - JsonElement root = gson.fromJson(data.jsonPayload, JsonElement.class); - JsonArray items; - if (root.isJsonObject()) { - items = root.getAsJsonObject().getAsJsonArray("items"); - } else { - items = root.getAsJsonArray(); + // Einnahmen an Empfänger-Account gutschreiben + if (!incomeReceiver.isEmpty()) { + try { + @SuppressWarnings("deprecation") + org.bukkit.OfflinePlayer receiver = + Bukkit.getOfflinePlayerIfCached(incomeReceiver); + if (receiver == null) { + for (org.bukkit.OfflinePlayer op : Bukkit.getOfflinePlayers()) { + if (op.getName() != null + && op.getName().equalsIgnoreCase(incomeReceiver)) { + receiver = op; + break; + } + } + } + if (receiver != null && receiver.hasPlayedBefore()) { + econ.depositPlayer(receiver, data.price); + if (debug) getLogger().info("💰 " + data.price + " " + currency + + " an " + incomeReceiver + " gutgeschrieben (Order #" + data.id + ")"); + } else { + getLogger().warning("⚠ income-receiver '" + incomeReceiver + + "' nicht gefunden – Einnahmen nicht gutgeschrieben!"); + } + } catch (Exception e) { + getLogger().log(Level.WARNING, "Fehler beim Gutschreiben der Einnahmen", e); + } } - int totalGiven = 0; - for (JsonElement e : items) { - JsonObject itemObj = e.getAsJsonObject(); - String itemId = itemObj.get("id").getAsString(); - int amount = itemObj.get("amount").getAsInt(); + JsonElement root = gson.fromJson(data.jsonPayload, JsonElement.class); + JsonObject rootObj = root.isJsonObject() ? root.getAsJsonObject() : null; - ItemStack item = parseItem(itemId); - if (item != null) { - giveItems(player, item, amount); - totalGiven++; - } else { - player.sendMessage(ChatColor.RED + "❌ Item '" + itemId + "' nicht gefunden."); + // --- Normale Items --- + JsonArray items = null; + if (rootObj != null && rootObj.has("items")) { + items = rootObj.getAsJsonArray("items"); + } else if (root.isJsonArray()) { + items = root.getAsJsonArray(); + } + int totalGiven = 0; + if (items != null) { + for (JsonElement e : items) { + JsonObject itemObj = e.getAsJsonObject(); + String itemId = itemObj.get("id").getAsString(); + int amount = itemObj.get("amount").getAsInt(); + ItemStack item = parseItem(itemId); + if (item != null) { giveItems(player, item, amount); totalGiven++; } + else player.sendMessage(ChatColor.RED + "❌ Item '" + itemId + "' nicht gefunden."); + } + } + + // --- Commands (Fly / Rang / Generic) --- + boolean anyCode = false; + if (rootObj != null && rootObj.has("commands")) { + JsonArray commands = rootObj.getAsJsonArray("commands"); + for (JsonElement ce : commands) { + JsonObject cmdObj = ce.getAsJsonObject(); + String type = cmdObj.has("type") + ? cmdObj.get("type").getAsString() : "generic"; + + if ("fly".equals(type)) { + int durationSec = cmdObj.has("duration_sec") + ? cmdObj.get("duration_sec").getAsInt() : 300; + String label = cmdObj.has("label") + ? cmdObj.get("label").getAsString() + : flyManager.formatTime(durationSec) + " Fly"; + final String pName = player.getName(); + new BukkitRunnable() { + @Override public void run() { + String code = flyCodeManager.generateCode(pName, durationSec, label); + Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> { + player.sendMessage(""); + player.sendMessage(ChatColor.GOLD + "✈ ════════════════════════════"); + player.sendMessage(ChatColor.YELLOW + " Fly-Gutschein erhalten!"); + player.sendMessage(ChatColor.GRAY + " Dauer: " + + ChatColor.WHITE + label); + player.sendMessage(ChatColor.GRAY + " Code: " + + ChatColor.AQUA + ChatColor.BOLD + code); + player.sendMessage(ChatColor.GRAY + " Einlösen: " + + ChatColor.WHITE + "/flyredeem " + code); + player.sendMessage(ChatColor.GOLD + " ════════════════════════════"); + player.sendMessage(""); + player.playSound(player.getLocation(), + Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.2F); + }); + } + }.runTaskAsynchronously(IngameShopSpigot.this); + anyCode = true; + + } else if ("rank".equals(type)) { + String rankId = cmdObj.has("rank_id") + ? cmdObj.get("rank_id").getAsString() : ""; + String lpGroup = cmdObj.has("lp_group") + ? cmdObj.get("lp_group").getAsString() : rankId; + String label = cmdObj.has("label") + ? cmdObj.get("label").getAsString() : rankId; + String defaultGroup = cmdObj.has("default_group") + ? cmdObj.get("default_group").getAsString() : "default"; + int days = cmdObj.has("days") + ? cmdObj.get("days").getAsInt() : 0; + if (rankId.isEmpty() || lpGroup.isEmpty()) { + player.sendMessage(ChatColor.RED + "❌ Rang-Konfiguration fehlt im Shop-Eintrag."); + } else { + rankManager.grantRank(player, rankId, lpGroup, label, defaultGroup, days); + } + anyCode = true; + + } else if ("generic".equals(type) && cmdObj.has("cmd")) { + String cmd = cmdObj.get("cmd").getAsString() + .replace("{player}", player.getName()); + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), cmd); + anyCode = true; + } } } @@ -418,20 +860,20 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.0F); player.sendMessage(ChatColor.GREEN + "✅ Kauf erfolgreich abgeschlossen!"); } + if (!anyCode && totalGiven == 0) { + player.sendMessage(ChatColor.RED + "⚠ Nichts verteilt – Admin kontaktieren."); + } markOrderCompleted(orderId); orderCache.remove(orderId); } catch (Exception e) { getLogger().log(Level.SEVERE, "Fehler bei Shop-Logik", e); - player.sendMessage(ChatColor.RED + "❌ Interner Fehler beim Verteilen. Admin kontaktieren."); + player.sendMessage(ChatColor.RED + + "❌ Interner Fehler beim Verteilen. Admin kontaktieren."); } } - /** - * NEU: Gibt Items an den Spieler – volles Inventar → natürlicher Drop - * (bestehende Logik unverändert, aber in eigene Methode ausgelagert) - */ private void giveItems(Player player, ItemStack template, int amount) { int remaining = amount; while (remaining > 0) { @@ -450,13 +892,13 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { private void markOrderCompleted(int orderId) { new BukkitRunnable() { - @Override - public void run() { + @Override public void run() { try { HttpURLConnection conn = openAuthConnection(wpUrlComplete, "POST"); writeJson(conn, "{\"id\":\"" + orderId + "\"}"); int code = conn.getResponseCode(); - if (debug) getLogger().info("Complete Order #" + orderId + " → HTTP " + code); + if (debug) getLogger().info( + "Complete Order #" + orderId + " → HTTP " + code); } catch (Exception e) { getLogger().log(Level.WARNING, "Complete Order Error #" + orderId, e); } @@ -465,73 +907,68 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { } // =========================================================== - // /orders BEFEHL – Bestellhistorie im Chat + // BEFEHLE // =========================================================== + /** /orders */ private class OrdersCommand implements CommandExecutor { @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!(sender instanceof Player)) { - sender.sendMessage("Nur für Spieler verfügbar."); - return true; - } + public boolean onCommand(CommandSender sender, Command command, + String label, String[] args) { + if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; } Player p = (Player) sender; - p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); p.sendMessage(ChatColor.YELLOW + "📦 Deine letzten Bestellungen:"); - p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); - + p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); new BukkitRunnable() { - @Override - public void run() { + @Override public void run() { try { - String urlString = wpBase + "/orders_history?player=" + p.getName(); - HttpURLConnection conn = openAuthConnection(urlString, "GET"); + HttpURLConnection conn = openAuthConnection( + wpBase + "/orders_history?player=" + p.getName(), "GET"); int code = conn.getResponseCode(); - if (code == 200) { - String body = readResponse(conn); - JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + String body = readResponse(conn); + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); JsonArray orders = json.getAsJsonArray("orders"); - Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> { if (orders == null || orders.size() == 0) { p.sendMessage(ChatColor.GRAY + "Noch keine Bestellungen vorhanden."); } else { for (int i = 0; i < Math.min(orders.size(), 10); i++) { JsonObject o = orders.get(i).getAsJsonObject(); - String title = o.get("item_title").getAsString(); - double price = o.get("price").getAsDouble(); + String t = o.get("item_title").getAsString(); + double pr = o.get("price").getAsDouble(); String status = o.get("status").getAsString(); - String date = o.has("created_at") ? o.get("created_at").getAsString().substring(0,10) : "?"; - - String statusColor; + String date = o.has("created_at") + ? o.get("created_at").getAsString().substring(0, 10) : "?"; + String sc; switch (status) { - case "completed": statusColor = ChatColor.GREEN + "✔ Geliefert"; break; - case "cancelled": statusColor = ChatColor.RED + "✘ Storniert"; break; - case "processing": statusColor = ChatColor.AQUA + "⟳ In Arbeit"; break; - default: statusColor = ChatColor.YELLOW + "⌛ Ausstehend"; break; + case "completed": sc = ChatColor.GREEN + "✔ Geliefert"; break; + case "cancelled": sc = ChatColor.RED + "✘ Storniert"; break; + case "processing": sc = ChatColor.AQUA + "⟳ In Arbeit"; break; + default: sc = ChatColor.YELLOW + "⌛ Ausstehend"; break; } - - // Titel kürzen wenn zu lang - String display = title.length() > 35 ? title.substring(0, 32) + "…" : title; + String display = t.length() > 35 + ? t.substring(0, 32) + "…" : t; p.sendMessage( - ChatColor.WHITE + " #" + o.get("id").getAsInt() + - " " + ChatColor.AQUA + display + - ChatColor.GRAY + " | " + ChatColor.YELLOW + price + " " + currency + - ChatColor.GRAY + " | " + statusColor + - ChatColor.GRAY + " (" + date + ")" - ); + ChatColor.WHITE + " #" + o.get("id").getAsInt() + " " + + ChatColor.AQUA + display + + ChatColor.GRAY + " | " + + ChatColor.YELLOW + pr + " " + currency + + ChatColor.GRAY + " | " + sc + + ChatColor.GRAY + " (" + date + ")"); } } p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); }); } else if (code == 404) { - // Endpunkt noch nicht implementiert Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> - p.sendMessage(ChatColor.GRAY + "Bestellhistorie nicht verfügbar (WP-Plugin zu alt?).")); + p.sendMessage(ChatColor.GRAY + + "Bestellhistorie nicht verfügbar (WP-Plugin zu alt?).")); } else { Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> - p.sendMessage(ChatColor.RED + "Fehler beim Laden der Bestellungen (HTTP " + code + ").")); + p.sendMessage(ChatColor.RED + + "Fehler beim Laden (HTTP " + code + ").")); } } catch (Exception e) { getLogger().log(Level.WARNING, "/orders Fehler", e); @@ -544,19 +981,260 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { } } + /** /flytime */ + private class FlyTimeCommand implements CommandExecutor { + @Override + public boolean onCommand(CommandSender sender, Command command, + String label, String[] args) { + if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; } + Player p = (Player) sender; + int remaining = flyManager.getRemainingSeconds(p.getUniqueId()); + if (remaining <= 0) + p.sendMessage(ChatColor.GRAY + + "✈ Kein aktiver Fly. Codes einlösen: /flyredeem "); + else + p.sendMessage(ChatColor.AQUA + "✈ Verbleibende Fly-Zeit: " + + ChatColor.YELLOW + flyManager.formatTime(remaining)); + return true; + } + } + + /** /flyredeem */ + private class FlyRedeemCommand implements CommandExecutor { + @Override + public boolean onCommand(CommandSender sender, Command command, + String label, String[] args) { + if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; } + if (args.length < 1) { + sender.sendMessage(ChatColor.RED + "Verwendung: /flyredeem "); + return true; + } + Player p = (Player) sender; + if (flyRedeemDisabled) { + p.sendMessage(ChatColor.RED + + "✗ Fly-Codes können auf diesem Server nicht eingelöst werden."); + return true; + } + String code = args[0].toUpperCase().trim(); + new BukkitRunnable() { + @Override public void run() { + int durationSec = flyCodeManager.redeemCode(code); + Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> { + if (durationSec < 0) + p.sendMessage(ChatColor.RED + + "❌ Ungültiger oder bereits eingelöster Code."); + else + flyManager.grantFly(p, durationSec); + }); + } + }.runTaskAsynchronously(IngameShopSpigot.this); + return true; + } + } + + /** /flycodes */ + private class FlyCodesCommand implements CommandExecutor { + @Override + public boolean onCommand(CommandSender sender, Command command, + String label, String[] args) { + if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; } + Player p = (Player) sender; + new BukkitRunnable() { + @Override public void run() { + List codes = + flyCodeManager.getUnusedCodes(p.getName()); + Bukkit.getScheduler().runTask(IngameShopSpigot.this, + () -> openFlyCodesGUI(p, codes, 0)); + } + }.runTaskAsynchronously(IngameShopSpigot.this); + return true; + } + } + + /** /flygive [Label] */ + private class FlyGiveCommand implements CommandExecutor { + @Override + public boolean onCommand(CommandSender sender, Command command, + String label, String[] args) { + if (!sender.hasPermission("ingameshop.flygive")) { + sender.sendMessage(ChatColor.RED + "✗ Keine Berechtigung."); + return true; + } + if (args.length < 2) { + sender.sendMessage(ChatColor.RED + + "Verwendung: /flygive [Label]"); + return true; + } + String targetName = args[0]; + int durationSec; + try { + durationSec = Integer.parseInt(args[1]); + if (durationSec <= 0) throw new NumberFormatException(); + } catch (NumberFormatException e) { + sender.sendMessage(ChatColor.RED + + "✗ Ungültige Sekundenangabe. Bitte eine positive Zahl eingeben."); + return true; + } + String customLabel = args.length >= 3 + ? String.join(" ", Arrays.copyOfRange(args, 2, args.length)) + : flyManager.formatTime(durationSec) + " Fly"; + + final int finalSec = durationSec; + final String finalLabel = customLabel; + Player online = Bukkit.getPlayer(targetName); + + if (online != null) { + final String resolvedName = online.getName(); + new BukkitRunnable() { + @Override public void run() { + String code = flyCodeManager.generateCode(resolvedName, finalSec, finalLabel); + Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> { + sender.sendMessage(ChatColor.GREEN + "✅ Fly-Gutschein an " + + ChatColor.YELLOW + resolvedName + ChatColor.GREEN + " vergeben!"); + sender.sendMessage(ChatColor.GRAY + " Code: " + + ChatColor.AQUA + ChatColor.BOLD + code); + sender.sendMessage(ChatColor.GRAY + " Dauer: " + + ChatColor.WHITE + flyManager.formatTime(finalSec)); + sender.sendMessage(ChatColor.GRAY + " Label: " + + ChatColor.WHITE + finalLabel); + online.sendMessage(""); + online.sendMessage(ChatColor.GOLD + "✈ ════════════════════════════"); + online.sendMessage(ChatColor.YELLOW + " Du hast einen Fly-Gutschein erhalten!"); + online.sendMessage(ChatColor.GRAY + " Von: " + + ChatColor.WHITE + sender.getName()); + online.sendMessage(ChatColor.GRAY + " Dauer: " + + ChatColor.WHITE + flyManager.formatTime(finalSec)); + online.sendMessage(ChatColor.GRAY + " Code: " + + ChatColor.AQUA + ChatColor.BOLD + code); + online.sendMessage(ChatColor.GRAY + " Einlösen: " + + ChatColor.WHITE + "/flyredeem " + code); + online.sendMessage(ChatColor.GOLD + " ════════════════════════════"); + online.sendMessage(""); + online.playSound(online.getLocation(), + Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.2F); + }); + } + }.runTaskAsynchronously(IngameShopSpigot.this); + } else { + new BukkitRunnable() { + @Override public void run() { + boolean knownTemp = false; + for (org.bukkit.OfflinePlayer op : Bukkit.getOfflinePlayers()) { + if (op.getName() != null + && op.getName().equalsIgnoreCase(targetName)) { + knownTemp = true; + break; + } + } + final boolean known = knownTemp; + String resolved = targetName; + for (org.bukkit.OfflinePlayer op : Bukkit.getOfflinePlayers()) { + if (op.getName() != null + && op.getName().equalsIgnoreCase(targetName)) { + resolved = op.getName(); + break; + } + } + final String finalResolved = resolved; + String code = flyCodeManager.generateCode(finalResolved, finalSec, finalLabel); + Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> { + sender.sendMessage(ChatColor.GREEN + "✅ Fly-Gutschein für " + + ChatColor.YELLOW + finalResolved + + ChatColor.GREEN + " gespeichert" + + (known ? "" : ChatColor.GRAY + " (Spieler noch nie online)") + + ChatColor.GREEN + "!"); + sender.sendMessage(ChatColor.GRAY + " Code: " + + ChatColor.AQUA + ChatColor.BOLD + code); + sender.sendMessage(ChatColor.GRAY + " Dauer: " + + ChatColor.WHITE + flyManager.formatTime(finalSec)); + sender.sendMessage(ChatColor.GRAY + " Label: " + + ChatColor.WHITE + finalLabel); + sender.sendMessage(ChatColor.GRAY + + " → Spieler wird beim nächsten Login benachrichtigt."); + }); + } + }.runTaskAsynchronously(IngameShopSpigot.this); + } + return true; + } + } + + /** /flypause */ + private class FlyPauseCommand implements CommandExecutor { + @Override + public boolean onCommand(CommandSender sender, Command command, + String label, String[] args) { + if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; } + Player p = (Player) sender; + if (flyManager.getRemainingSeconds(p.getUniqueId()) <= 0) { + p.sendMessage(ChatColor.RED + "✗ Du hast keine aktive Fly-Zeit."); + return true; + } + boolean nowPaused = flyManager.togglePause(p); + if (nowPaused) + p.sendMessage(ChatColor.YELLOW + "⏸ Fly-Zeit pausiert. " + + ChatColor.GRAY + "Verbleibend: " + + ChatColor.WHITE + flyManager.formatTime( + flyManager.getRemainingSeconds(p.getUniqueId()))); + else + p.sendMessage(ChatColor.GREEN + "▶ Fly-Zeit fortgesetzt. " + + ChatColor.GRAY + "Verbleibend: " + + ChatColor.WHITE + flyManager.formatTime( + flyManager.getRemainingSeconds(p.getUniqueId()))); + return true; + } + } + + /** /rankinfo */ + private class RankInfoCommand implements CommandExecutor { + @Override + public boolean onCommand(CommandSender sender, Command command, + String label, String[] args) { + if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; } + Player p = (Player) sender; + new BukkitRunnable() { + @Override public void run() { + List ranks = + rankManager.getActiveRanks(p.getName()); + Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> { + p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + p.sendMessage(ChatColor.YELLOW + "👑 Deine aktiven Ränge:"); + p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + if (ranks.isEmpty()) { + p.sendMessage(ChatColor.GRAY + "Keine zeitbasierten Ränge aktiv."); + } else { + for (RankManager.RankEntry r : ranks) { + if (r.permanent) + p.sendMessage(ChatColor.AQUA + " ▶ " + + ChatColor.WHITE + r.rankId + + ChatColor.GRAY + " | " + + ChatColor.GREEN + "Dauerhaft"); + else + p.sendMessage(ChatColor.AQUA + " ▶ " + + ChatColor.WHITE + r.rankId + + ChatColor.GRAY + " | Läuft ab: " + + ChatColor.YELLOW + r.expiresAt); + } + } + p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + }); + } + }.runTaskAsynchronously(IngameShopSpigot.this); + return true; + } + } + // =========================================================== // HTTP HILFSMETHODEN // =========================================================== - /** - * Öffnet eine HTTP-Verbindung mit gesetztem API-Key Header. - */ - private HttpURLConnection openAuthConnection(String urlString, String method) throws Exception { - URL url = new URL(urlString); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + private HttpURLConnection openAuthConnection(String urlString, String method) + throws Exception { + URL url = new URL(urlString); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod(method); conn.setRequestProperty("Content-Type", "application/json"); - conn.setRequestProperty("X-WIS-Key", apiKey); // NEU: API-Key bei jedem Request + conn.setRequestProperty("X-WIS-Key", apiKey); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); if ("POST".equals(method)) conn.setDoOutput(true); @@ -585,13 +1263,12 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { private ItemStack parseItem(String itemId) { try { - String clean = itemId.toUpperCase().trim() + String clean = itemId.toUpperCase().trim() .replace("MINECRAFT:", "").replace("MC:", ""); String materialName = numericIdToMaterial(clean); if (materialName == null) materialName = clean; - - if (materialName.equalsIgnoreCase("VIP") || materialName.startsWith("VIP")) return null; - + if (materialName.equalsIgnoreCase("VIP") || materialName.startsWith("VIP")) + return null; Material material = Material.matchMaterial(materialName); if (material != null && material.isItem()) return new ItemStack(material, 1); return null; @@ -623,19 +1300,778 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { } } + // =========================================================== + // FLY TRANSFER + // =========================================================== + + private void executeTransfer(Player player, String code, String targetName) { + // Bukkit.getPlayer() MUSS auf dem Main-Thread aufgerufen werden + Player targetOnline = Bukkit.getPlayer(targetName); + String resolvedName = (targetOnline != null) ? targetOnline.getName() : targetName; + new BukkitRunnable() { + @Override public void run() { + boolean ok = flyCodeManager.transferCode(code, player.getName(), resolvedName); + Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> { + if (ok) { + player.sendMessage(ChatColor.GREEN + "✅ Gutschein " + + ChatColor.AQUA + ChatColor.BOLD + code + + ChatColor.GREEN + " wurde an " + + ChatColor.YELLOW + resolvedName + + ChatColor.GREEN + " übertragen!"); + if (targetOnline != null && targetOnline.isOnline()) { + targetOnline.sendMessage( + ChatColor.GOLD + "✈ ════════════════════════════"); + targetOnline.sendMessage( + ChatColor.YELLOW + " Du hast einen Fly-Gutschein erhalten!"); + targetOnline.sendMessage( + ChatColor.GRAY + " Von: " + + ChatColor.WHITE + player.getName()); + targetOnline.sendMessage( + ChatColor.GRAY + " Code: " + + ChatColor.AQUA + ChatColor.BOLD + code); + targetOnline.sendMessage( + ChatColor.GRAY + " Einlösen: " + + ChatColor.WHITE + "/flyredeem " + code); + targetOnline.sendMessage( + ChatColor.GOLD + " ════════════════════════════"); + targetOnline.playSound(targetOnline.getLocation(), + Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.2F); + } + } else { + player.sendMessage(ChatColor.RED + + "❌ Übertragung fehlgeschlagen. " + + "Spielernamen prüfen oder Code bereits eingelöst."); + } + }); + } + }.runTaskAsynchronously(this); + } + + // =========================================================== + // FLY MANAGER + // =========================================================== + + private class FlyManager { + + private final IngameShopSpigot plugin; + private final Map flySeconds = new HashMap<>(); + private final Map flyTasks = new HashMap<>(); + private final Map flyBossBars = new HashMap<>(); + private final Map flyStartSecs = new HashMap<>(); + private final java.util.Set paused = new java.util.HashSet<>(); + private final Map savedSeconds = new HashMap<>(); + + FlyManager(IngameShopSpigot plugin) { this.plugin = plugin; } + + void grantFly(Player player, int seconds) { + UUID uuid = player.getUniqueId(); + int existing = flySeconds.getOrDefault(uuid, 0); + int total = existing + seconds; + flySeconds.put(uuid, total); + flyStartSecs.put(uuid, total); + paused.remove(uuid); + + player.setAllowFlight(true); + player.setFlying(true); + player.sendMessage(ChatColor.AQUA + "✈ Fly aktiviert! Verbleibend: " + + ChatColor.YELLOW + formatTime(total)); + + BossBar bar = flyBossBars.get(uuid); + if (bar == null) { + bar = Bukkit.createBossBar( + buildBarTitle(total, false), BarColor.BLUE, BarStyle.SEGMENTED_10); + bar.addPlayer(player); + flyBossBars.put(uuid, bar); + } else { + bar.setTitle(buildBarTitle(total, false)); + bar.setProgress(1.0); + bar.setColor(BarColor.BLUE); + } + + if (flyTasks.containsKey(uuid)) flyTasks.get(uuid).cancel(); + + final BossBar finalBar = bar; + + BukkitTask t = new BukkitRunnable() { + @Override public void run() { + Player p = Bukkit.getPlayer(uuid); + if (p == null || !p.isOnline()) { + removeBossBar(uuid); + flyTasks.remove(uuid); + paused.remove(uuid); + cancel(); + return; + } + if (paused.contains(uuid)) { + int rem = flySeconds.getOrDefault(uuid, 0); + finalBar.setTitle(buildBarTitle(rem, true)); + finalBar.setColor(BarColor.WHITE); + return; + } + int remaining = flySeconds.getOrDefault(uuid, 0) - 1; + if (remaining <= 0) { + flySeconds.remove(uuid); + flyTasks.remove(uuid); + flyStartSecs.remove(uuid); + removeBossBar(uuid); + p.setFlying(false); + p.setAllowFlight(false); + p.sendMessage(ChatColor.RED + "✈ Fly-Zeit abgelaufen!"); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_BASS, 1.0F, 0.5F); + cancel(); + return; + } + flySeconds.put(uuid, remaining); + int start = flyStartSecs.getOrDefault(uuid, remaining); + double progress = Math.max(0.0, Math.min(1.0, + (double) remaining / start)); + finalBar.setProgress(progress); + finalBar.setTitle(buildBarTitle(remaining, false)); + if (remaining <= 30) finalBar.setColor(BarColor.RED); + else if (remaining <= 60) finalBar.setColor(BarColor.YELLOW); + else finalBar.setColor(BarColor.BLUE); + if (remaining == 300 || remaining == 60 + || remaining == 30 || remaining == 10) { + p.sendMessage(ChatColor.YELLOW + + "✈ Fly noch " + formatTime(remaining) + "!"); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_PLING, 0.5F, 1.2F); + } + } + }.runTaskTimer(plugin, 20L, 20L); + flyTasks.put(uuid, t); + } + + boolean togglePause(Player player) { + UUID uuid = player.getUniqueId(); + BossBar bar = flyBossBars.get(uuid); + if (paused.contains(uuid)) { + paused.remove(uuid); + player.setAllowFlight(true); + player.setFlying(true); + if (bar != null) { + int rem = flySeconds.getOrDefault(uuid, 0); + bar.setTitle(buildBarTitle(rem, false)); + bar.setColor(rem <= 30 ? BarColor.RED + : rem <= 60 ? BarColor.YELLOW : BarColor.BLUE); + } + return false; + } else { + paused.add(uuid); + player.setFlying(false); + player.setAllowFlight(false); + if (bar != null) { + bar.setTitle(buildBarTitle( + flySeconds.getOrDefault(uuid, 0), true)); + bar.setColor(BarColor.WHITE); + } + return true; + } + } + + private String buildBarTitle(int seconds, boolean isPaused) { + if (isPaused) + return ChatColor.GRAY + "⏸ Fly pausiert: " + + ChatColor.WHITE + formatTime(seconds); + return ChatColor.AQUA + "✈ Fly-Zeit: " + + ChatColor.YELLOW + formatTime(seconds); + } + + private void removeBossBar(UUID uuid) { + BossBar bar = flyBossBars.remove(uuid); + if (bar != null) bar.removeAll(); + } + + int getRemainingSeconds(UUID uuid) { + return flySeconds.getOrDefault(uuid, 0); + } + + void saveOnQuit(Player player) { + UUID uuid = player.getUniqueId(); + int rem = flySeconds.getOrDefault(uuid, 0); + BukkitTask t = flyTasks.remove(uuid); + if (t != null) t.cancel(); + removeBossBar(uuid); + flySeconds.remove(uuid); + flyStartSecs.remove(uuid); + paused.remove(uuid); + savedSeconds.remove(uuid); + // Synchron speichern – beim Server-Stop werden async Tasks nicht mehr ausgeführt + if (rem > 0) { + flyCodeManager.saveFlySession(uuid, player.getName(), rem); + } + } + + void restoreOnJoin(Player player) { + UUID uuid = player.getUniqueId(); + int ramRem = savedSeconds.remove(uuid) != null + ? savedSeconds.getOrDefault(uuid, 0) : 0; + if (ramRem > 0) { + Bukkit.getScheduler().runTaskLater(plugin, () -> { + grantFly(player, ramRem); + togglePause(player); + player.sendMessage(ChatColor.AQUA + + "✈ Deine Fly-Zeit wurde wiederhergestellt: " + + ChatColor.YELLOW + formatTime(ramRem)); + player.sendMessage(ChatColor.GRAY + + " Fly ist pausiert. Fortsetzen: " + + ChatColor.WHITE + "/flypause"); + }, 20L); + return; + } + new BukkitRunnable() { + @Override public void run() { + int rem = flyCodeManager.loadAndDeleteFlySession(uuid); + if (rem > 0) { + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (player.isOnline()) { + grantFly(player, rem); + togglePause(player); + player.sendMessage(ChatColor.AQUA + + "✈ Deine Fly-Zeit wurde wiederhergestellt: " + + ChatColor.YELLOW + formatTime(rem)); + player.sendMessage(ChatColor.GRAY + + " Fly ist pausiert. Fortsetzen: " + + ChatColor.WHITE + "/flypause"); + } + }, 20L); + } + } + }.runTaskAsynchronously(plugin); + } + + void cancelAll() { + for (Map.Entry entry : flySeconds.entrySet()) { + UUID uuid = entry.getKey(); + int rem = entry.getValue(); + Player p = Bukkit.getPlayer(uuid); + // Name aus OfflinePlayer holen falls Spieler schon disconnected + String name; + if (p != null) { + name = p.getName(); + } else { + org.bukkit.OfflinePlayer op = Bukkit.getOfflinePlayer(uuid); + name = (op.getName() != null) ? op.getName() : uuid.toString(); + } + // Synchron speichern als letzte Absicherung beim Server-Stop + if (rem > 0) flyCodeManager.saveFlySession(uuid, name, rem); + if (p != null && !p.isOp()) { + p.setFlying(false); + p.setAllowFlight(false); + } + } + for (BukkitTask bt : flyTasks.values()) bt.cancel(); + for (BossBar bar : flyBossBars.values()) bar.removeAll(); + flySeconds.clear(); + flyTasks.clear(); + flyBossBars.clear(); + flyStartSecs.clear(); + paused.clear(); + savedSeconds.clear(); + } + + /** + * Speichert alle aktiven Fly-Sessions alle 30 Sekunden in die DB. + * Schützt vor Datenverlust bei Absturz / Kill des Servers. + */ + void startSessionPersist() { + new BukkitRunnable() { + @Override public void run() { + if (flySeconds.isEmpty()) return; + for (Map.Entry entry : flySeconds.entrySet()) { + UUID uuid = entry.getKey(); + int rem = entry.getValue(); + if (rem <= 0) continue; + Player p = Bukkit.getPlayer(uuid); + String name = (p != null) ? p.getName() + : Bukkit.getOfflinePlayer(uuid).getName(); + if (name == null) name = uuid.toString(); + final String finalName = name; + new BukkitRunnable() { + @Override public void run() { + flyCodeManager.saveFlySession(uuid, finalName, rem); + } + }.runTaskAsynchronously(plugin); + } + } + }.runTaskTimer(plugin, 20L * 30, 20L * 30); // alle 30 Sekunden + } + + String formatTime(int seconds) { + if (seconds >= 3600) { + int h = seconds / 3600, m = (seconds % 3600) / 60; + return h + "h" + (m > 0 ? " " + m + "min" : ""); + } else if (seconds >= 60) { + int m = seconds / 60, s = seconds % 60; + return m + "min" + (s > 0 ? " " + s + "s" : ""); + } + return seconds + "s"; + } + } + + // =========================================================== + // FLY CODE MANAGER + // =========================================================== + + private class FlyCodeManager { + + private final IngameShopSpigot plugin; + private Connection connection; + private final String dbHost, dbPort, dbName, dbUser, dbPass; + + private static final String CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + private final SecureRandom rng = new SecureRandom(); + + FlyCodeManager(IngameShopSpigot plugin) { + this.plugin = plugin; + dbHost = plugin.getConfig().getString("mysql.host", "localhost"); + dbPort = plugin.getConfig().getString("mysql.port", "3306"); + dbName = plugin.getConfig().getString("mysql.database", "minecraft"); + dbUser = plugin.getConfig().getString("mysql.username", "root"); + dbPass = plugin.getConfig().getString("mysql.password", ""); + } + + boolean connect() { + try { + Class.forName("com.mysql.cj.jdbc.Driver"); + String url = "jdbc:mysql://" + dbHost + ":" + dbPort + "/" + dbName + + "?useSSL=false&autoReconnect=true&characterEncoding=utf8"; + connection = DriverManager.getConnection(url, dbUser, dbPass); + return true; + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "MySQL Verbindungsfehler", e); + return false; + } + } + + void disconnect() { + try { + if (connection != null && !connection.isClosed()) connection.close(); + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "MySQL Disconnect Fehler", e); + } + } + + void createTable() { + String sqlCodes = + "CREATE TABLE IF NOT EXISTS wis_fly_codes (" + + " id INT AUTO_INCREMENT PRIMARY KEY," + + " code VARCHAR(20) NOT NULL UNIQUE," + + " player_name VARCHAR(64) NOT NULL COLLATE utf8mb4_general_ci," + + " duration_sec INT NOT NULL," + + " label VARCHAR(64) NOT NULL," + + " used TINYINT(1) NOT NULL DEFAULT 0," + + " redeemed_by VARCHAR(64) DEFAULT NULL COLLATE utf8mb4_general_ci," + + " created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," + + " redeemed_at DATETIME DEFAULT NULL" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; + String sqlSessions = + "CREATE TABLE IF NOT EXISTS wis_fly_sessions (" + + " uuid VARCHAR(36) NOT NULL PRIMARY KEY," + + " player_name VARCHAR(64) NOT NULL COLLATE utf8mb4_general_ci," + + " remaining_sec INT NOT NULL," + + " saved_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" + + " ON UPDATE CURRENT_TIMESTAMP" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; + String sqlRanks = + "CREATE TABLE IF NOT EXISTS wis_rank_sessions (" + + " id INT AUTO_INCREMENT PRIMARY KEY," + + " player_name VARCHAR(64) NOT NULL COLLATE utf8mb4_general_ci," + + " rank_id VARCHAR(64) NOT NULL," + + " lp_group VARCHAR(64) NOT NULL," + + " default_group VARCHAR(64) NOT NULL DEFAULT 'default'," + + " permanent TINYINT(1) NOT NULL DEFAULT 0," + + " expires_at DATETIME DEFAULT NULL," + + " granted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," + + " UNIQUE KEY uq_player_rank (player_name, rank_id)" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; + String sqlRanksMigrate = + "ALTER TABLE wis_rank_sessions" + + " ADD COLUMN IF NOT EXISTS default_group VARCHAR(64) NOT NULL DEFAULT 'default'" + + " AFTER lp_group;"; + try (Statement stmt = connection.createStatement()) { + stmt.execute(sqlCodes); + stmt.execute(sqlSessions); + stmt.execute(sqlRanks); + // Migration: default_group-Spalte ergänzen falls noch nicht vorhanden + try { stmt.execute(sqlRanksMigrate); } catch (SQLException ignored) {} + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Tabellen-Erstellung fehlgeschlagen", e); + } + } + + void saveFlySession(UUID uuid, String playerName, int remainingSec) { + String sql = + "INSERT INTO wis_fly_sessions (uuid, player_name, remaining_sec) " + + "VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE " + + "player_name = ?, remaining_sec = ?, saved_at = NOW()"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, uuid.toString()); + ps.setString(2, playerName); + ps.setInt(3, remainingSec); + ps.setString(4, playerName); + ps.setInt(5, remainingSec); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "saveFlySession SQL Fehler", e); + } + } + + int loadAndDeleteFlySession(UUID uuid) { + String selectSql = "SELECT remaining_sec FROM wis_fly_sessions WHERE uuid = ?"; + String deleteSql = "DELETE FROM wis_fly_sessions WHERE uuid = ?"; + try { + int remaining = -1; + try (PreparedStatement ps = connection.prepareStatement(selectSql)) { + ps.setString(1, uuid.toString()); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) remaining = rs.getInt("remaining_sec"); + } + } + if (remaining > 0) { + try (PreparedStatement ps = connection.prepareStatement(deleteSql)) { + ps.setString(1, uuid.toString()); + ps.executeUpdate(); + } + } + return remaining; + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "loadFlySession SQL Fehler", e); + return -1; + } + } + + void deleteFlySession(UUID uuid) { + String sql = "DELETE FROM wis_fly_sessions WHERE uuid = ?"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, uuid.toString()); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "deleteFlySession SQL Fehler", e); + } + } + + // ── Rang-Sessions ──────────────────────────────────────────────── + + void saveRankSession(String playerName, String rankId, String group, + String defaultGroup, int days) { + String sql = days == 0 + ? "INSERT INTO wis_rank_sessions (player_name, rank_id, lp_group, default_group, permanent, expires_at)" + + " VALUES (?, ?, ?, ?, 1, NULL)" + + " ON DUPLICATE KEY UPDATE lp_group=?, default_group=?, permanent=1, expires_at=NULL, granted_at=NOW()" + : "INSERT INTO wis_rank_sessions (player_name, rank_id, lp_group, default_group, permanent, expires_at)" + + " VALUES (?, ?, ?, ?, 0, DATE_ADD(NOW(), INTERVAL ? DAY))" + + " ON DUPLICATE KEY UPDATE lp_group=?, default_group=?, permanent=0," + + " expires_at=DATE_ADD(NOW(), INTERVAL ? DAY), granted_at=NOW()"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, playerName); + ps.setString(2, rankId); + ps.setString(3, group); + ps.setString(4, defaultGroup); + if (days == 0) { + ps.setString(5, group); + ps.setString(6, defaultGroup); + } else { + ps.setInt(5, days); + ps.setString(6, group); + ps.setString(7, defaultGroup); + ps.setInt(8, days); + } + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "saveRankSession SQL Fehler", e); + } + } + + List getExpiredRanks(String playerName) { + List list = new ArrayList<>(); + String sql = + "SELECT id, rank_id, lp_group, default_group," + + " DATE_FORMAT(expires_at, '%d.%m.%Y') AS expires_fmt" + + " FROM wis_rank_sessions" + + " WHERE player_name = ? AND permanent = 0 AND expires_at <= NOW()"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, playerName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String dg = rs.getString("default_group"); + list.add(new RankManager.RankEntry( + rs.getInt("id"), + rs.getString("rank_id"), + rs.getString("lp_group"), + dg != null ? dg : "default", + rs.getString("expires_fmt"), + false)); + } + } + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "getExpiredRanks SQL Fehler", e); + } + return list; + } + + List getActiveRanks(String playerName) { + List list = new ArrayList<>(); + String sql = + "SELECT id, rank_id, lp_group, default_group, permanent," + + " DATE_FORMAT(expires_at, '%d.%m.%Y %H:%i') AS expires_fmt" + + " FROM wis_rank_sessions" + + " WHERE player_name = ? AND (permanent = 1 OR expires_at > NOW())" + + " ORDER BY granted_at DESC"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, playerName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String dg = rs.getString("default_group"); + list.add(new RankManager.RankEntry( + rs.getInt("id"), + rs.getString("rank_id"), + rs.getString("lp_group"), + dg != null ? dg : "default", + rs.getString("expires_fmt") != null + ? rs.getString("expires_fmt") : "–", + rs.getInt("permanent") == 1)); + } + } + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "getActiveRanks SQL Fehler", e); + } + return list; + } + + void deleteRankSession(int id) { + String sql = "DELETE FROM wis_rank_sessions WHERE id = ?"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setInt(1, id); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "deleteRankSession SQL Fehler", e); + } + } + + // ── Fly-Code CRUD ──────────────────────────────────────────────── + + String generateCode(String playerName, int durationSec, String label) { + String code; + int attempts = 0; + do { + code = "FLY-" + randomSegment(4) + "-" + randomSegment(4); + attempts++; + } while (!insertCode(code, playerName, durationSec, label) && attempts < 20); + return code; + } + + private boolean insertCode(String code, String playerName, + int durationSec, String label) { + String sql = + "INSERT INTO wis_fly_codes (code, player_name, duration_sec, label)" + + " VALUES (?, ?, ?, ?)"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, code); + ps.setString(2, playerName); + ps.setInt(3, durationSec); + ps.setString(4, label); + ps.executeUpdate(); + return true; + } catch (SQLException e) { + if (debug) plugin.getLogger().log( + Level.WARNING, "Code-Insert Kollision: " + code, e); + return false; + } + } + + int redeemCode(String code) { + String checkSql = + "SELECT id, duration_sec, used FROM wis_fly_codes WHERE code = ?"; + String updateSql = + "UPDATE wis_fly_codes SET used = 1, redeemed_at = NOW()" + + " WHERE id = ? AND used = 0"; + try { + int id = -1, durationSec = -1; + try (PreparedStatement ps = connection.prepareStatement(checkSql)) { + ps.setString(1, code); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return -1; + if (rs.getInt("used") == 1) return -1; + id = rs.getInt("id"); + durationSec = rs.getInt("duration_sec"); + } + } + try (PreparedStatement ps = connection.prepareStatement(updateSql)) { + ps.setInt(1, id); + if (ps.executeUpdate() == 0) return -1; + } + return durationSec; + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "redeemCode SQL Fehler", e); + return -1; + } + } + + List getUnusedCodes(String playerName) { + List list = new ArrayList<>(); + String sql = + "SELECT code, label, duration_sec," + + " DATE_FORMAT(created_at, '%d.%m.%Y') AS created_at" + + " FROM wis_fly_codes WHERE player_name = ? AND used = 0" + + " ORDER BY created_at DESC"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, playerName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) + list.add(new FlyCodeEntry( + rs.getString("code"), + rs.getString("label"), + rs.getString("created_at"), + rs.getInt("duration_sec"))); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "getUnusedCodes SQL Fehler", e); + } + return list; + } + + boolean transferCode(String code, String fromPlayer, String toPlayer) { + String sql = + "UPDATE wis_fly_codes SET player_name = ?" + + " WHERE code = ? AND player_name = ? AND used = 0"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, toPlayer); + ps.setString(2, code); + ps.setString(3, fromPlayer); + return ps.executeUpdate() == 1; + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "transferCode SQL Fehler", e); + return false; + } + } + + private String randomSegment(int length) { + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) + sb.append(CHARS.charAt(rng.nextInt(CHARS.length()))); + return sb.toString(); + } + + static class FlyCodeEntry { + final String code, label, createdAt; + final int durationSec; + FlyCodeEntry(String code, String label, String createdAt, int durationSec) { + this.code = code; + this.label = label; + this.createdAt = createdAt; + this.durationSec = durationSec; + } + } + } + + // =========================================================== + // RANK MANAGER + // =========================================================== + + private class RankManager { + + private final IngameShopSpigot plugin; + private final FlyCodeManager db; + + RankManager(IngameShopSpigot plugin, FlyCodeManager db) { + this.plugin = plugin; + this.db = db; + } + + void grantRank(Player player, String rankId, String lpGroup, + String label, String defaultGroup, int days) { + boolean permanent = (days == 0); + + // dauerhaft: parent set | zeitbasiert: parent addtemp d + String cmd = permanent + ? "lp user " + player.getName() + " parent set " + lpGroup + : "lp user " + player.getName() + " parent addtemp " + lpGroup + " " + days + "d accumulate"; + + // LP-Befehle müssen auf dem Hauptthread laufen + Bukkit.getScheduler().runTask(plugin, () -> + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), cmd)); + + new BukkitRunnable() { + @Override public void run() { + db.saveRankSession(player.getName(), rankId, lpGroup, defaultGroup, days); + } + }.runTaskAsynchronously(plugin); + + player.sendMessage(""); + player.sendMessage(ChatColor.GOLD + "👑 ════════════════════════════"); + player.sendMessage(ChatColor.YELLOW + " Rang erhalten: " + + ChatColor.WHITE + label); + player.sendMessage(ChatColor.GRAY + " Dauer: " + + (permanent ? ChatColor.GREEN + "Dauerhaft" + : ChatColor.WHITE + "" + days + " Tag(e)")); + player.sendMessage(ChatColor.GOLD + " ════════════════════════════"); + player.sendMessage(""); + player.playSound(player.getLocation(), + Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 0.8F); + } + + void checkExpiredRanksForPlayer(Player player) { + List expired = db.getExpiredRanks(player.getName()); + for (RankEntry r : expired) { + db.deleteRankSession(r.id); + Bukkit.getScheduler().runTask(plugin, () -> { + // Rang-Gruppe entfernen statt alles überschreiben + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), + "lp user " + player.getName() + " parent remove " + r.group); + player.sendMessage(ChatColor.RED + "👑 Dein Rang " + + ChatColor.WHITE + r.rankId + + ChatColor.RED + " ist abgelaufen."); + }); + } + } + + void startExpiryChecker() { + new BukkitRunnable() { + @Override public void run() { + for (Player p : Bukkit.getOnlinePlayers()) { + new BukkitRunnable() { + @Override public void run() { + checkExpiredRanksForPlayer(p); + } + }.runTaskAsynchronously(plugin); + } + } + }.runTaskTimer(plugin, 20L * 300, 20L * 300); + } + + List getActiveRanks(String playerName) { + return db.getActiveRanks(playerName); + } + + static class RankEntry { + final int id; + final String rankId, group, defaultGroup, expiresAt; + final boolean permanent; + RankEntry(int id, String rankId, String group, String defaultGroup, + String expiresAt, boolean permanent) { + this.id = id; + this.rankId = rankId; + this.group = group; + this.defaultGroup = defaultGroup; + this.expiresAt = expiresAt; + this.permanent = permanent; + } + } + } + // =========================================================== // DATA CLASS // =========================================================== private static class OrderData { - int id; - String itemId; - String itemTitle; - double price; - int quantity; - String jsonPayload; + final int id, quantity; + final String itemId, itemTitle, jsonPayload; + final double price; - OrderData(int id, String itemId, String itemTitle, double price, int quantity, String jsonPayload) { + OrderData(int id, String itemId, String itemTitle, + double price, int quantity, String jsonPayload) { this.id = id; this.itemId = itemId; this.itemTitle = itemTitle; diff --git a/IngameShopSpigot/src/main/resources/config.yml b/IngameShopSpigot/src/main/resources/config.yml index 518b6b8..aecfc91 100644 --- a/IngameShopSpigot/src/main/resources/config.yml +++ b/IngameShopSpigot/src/main/resources/config.yml @@ -1,4 +1,4 @@ -# IngameShopSpigot v6.3 Konfiguration +# IngameShopSpigot v6.4 Konfiguration # ========================================== # Deine WordPress-URL (kein abschließendes /) @@ -12,10 +12,27 @@ api-key: "HIER_DEN_KEY_AUS_WORDPRESS_EINTRAGEN" server-name: "survival" # Währungsname (muss mit WordPress-Einstellung übereinstimmen) -currency-name: "Coins" +currency-name: "$" # Wie oft (in Sekunden) nach ausstehenden Bestellungen gesucht wird check-interval: 10 # Debug-Modus (ausführliche Logs in der Konsole) -debug-mode: false \ No newline at end of file +debug-mode: false + +# Fly-Code-Einlösung auf diesem Server deaktivieren +# true = Codes können hier NICHT eingelöst werden (z.B. Lobby, Hub, Event-Server) +# false = Codes können normal eingelöst werden (Standard) +fly-redeem-disabled: false + +# Spieler der die Einnahmen aus dem Shop erhält (Vault-Konto) +# Leer lassen ("") wenn niemand die Einnahmen erhalten soll +income-receiver: "" + +# MySQL-Verbindung (für Fly-Code-System und Rang-Sessions) +mysql: + host: "localhost" + port: "3306" + database: "minecraft" + username: "root" + password: "DEIN_PASSWORT" \ No newline at end of file diff --git a/IngameShopSpigot/src/main/resources/plugin.yml b/IngameShopSpigot/src/main/resources/plugin.yml index 3642e8d..95346b5 100644 --- a/IngameShopSpigot/src/main/resources/plugin.yml +++ b/IngameShopSpigot/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: IngameShopSpigot -version: 6.3 +version: 1.1 main: de.mviper.spigot.IngameShopSpigot api-version: 1.19 depend: [Vault] @@ -11,8 +11,50 @@ commands: description: Zeigt deine letzten Bestellungen im Shop an usage: /orders permission: ingameshop.orders + flytime: + description: Zeigt deine aktive verbleibende Fly-Zeit an + usage: /flytime + permission: ingameshop.flytime + flyredeem: + description: Löst einen Fly-Gutschein-Code ein + usage: /flyredeem + permission: ingameshop.flyredeem + flycodes: + description: Öffnet ein GUI mit deinen ungenutzten Fly-Gutscheinen (einlösen oder weitergeben) + usage: /flycodes + permission: ingameshop.flycodes + flygive: + description: Gibt einem Spieler einen Fly-Gutschein-Code + usage: /flygive [Label] + permission: ingameshop.flygive + flypause: + description: Pausiert oder setzt die aktive Fly-Zeit fort + usage: /flypause + permission: ingameshop.flypause + rankinfo: + description: Zeigt deine aktiven (zeitbasierten) Ränge an + usage: /rankinfo + permission: ingameshop.rankinfo permissions: ingameshop.orders: description: Kann eigene Bestellhistorie einsehen + default: true + ingameshop.flytime: + description: Kann aktive Fly-Zeit abfragen + default: true + ingameshop.flyredeem: + description: Kann Fly-Codes einlösen + default: true + ingameshop.flycodes: + description: Kann eigene ungenutzte Fly-Codes auflisten + default: true + ingameshop.flygive: + description: Kann Spielern Fly-Gutscheine geben (Admin/OP/Mod) + default: op + ingameshop.flypause: + description: Kann die eigene Fly-Zeit pausieren + default: true + ingameshop.rankinfo: + description: Kann eigene aktive Ränge einsehen default: true \ No newline at end of file