From f5b14ffd6f68270a172dfaaf95cafd9c8c19e4e4 Mon Sep 17 00:00:00 2001 From: Git Manager GUI Date: Wed, 29 Apr 2026 16:40:53 +0200 Subject: [PATCH] Upload folder via GUI - src --- .../de/mviper/spigot/IngameShopSpigot.java | 745 +++++++++++++++++- .../src/main/resources/config.yml | 28 +- .../src/main/resources/plugin.yml | 53 +- 3 files changed, 804 insertions(+), 22 deletions(-) diff --git a/IngameShopSpigot/src/main/java/de/mviper/spigot/IngameShopSpigot.java b/IngameShopSpigot/src/main/java/de/mviper/spigot/IngameShopSpigot.java index 2d67aad..7a647ec 100644 --- a/IngameShopSpigot/src/main/java/de/mviper/spigot/IngameShopSpigot.java +++ b/IngameShopSpigot/src/main/java/de/mviper/spigot/IngameShopSpigot.java @@ -92,10 +92,15 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { private Map orderCache = new HashMap<>(); private Map activeOrderIds = new ConcurrentHashMap<>(); - private FlyManager flyManager; - private FlyCodeManager flyCodeManager; - private RankManager rankManager; - private FlyAboManager flyAboManager; + private FlyManager flyManager; + private FlyCodeManager flyCodeManager; + private RankManager rankManager; + private FlyAboManager flyAboManager; + private PlotSlotManager plotSlotManager; + + // Plot-Slot Konfiguration + private String plotServer = "citybuild"; + private Map plotRankDefaults = new HashMap<>(); private static final String GUI_FLYCODES = ChatColor.GOLD + "✈ Deine Fly-Gutscheine"; private final Map flyCodesPage = new HashMap<>(); @@ -140,6 +145,22 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { sellPriceOffset = getConfig().getDouble("sell.price-offset", 0.0); flyAboMaxDailySec = getConfig().getInt("fly-abo.max-daily-hours", 6) * 3600; + // Plot-Slot Konfiguration laden + plotServer = getConfig().getString("plot-slots.server", "citybuild").toLowerCase(); + plotRankDefaults.clear(); + org.bukkit.configuration.ConfigurationSection rankSec = + getConfig().getConfigurationSection("plot-slots.rank-defaults"); + if (rankSec != null) { + for (String key : rankSec.getKeys(false)) + plotRankDefaults.put(key.toLowerCase(), rankSec.getInt(key, 2)); + } else { + // Viper Network Defaults + plotRankDefaults.put("member", 2); + plotRankDefaults.put("vip", 3); + plotRankDefaults.put("scout", 4); + plotRankDefaults.put("primo", 5); + } + if (apiKey.isEmpty()) { getLogger().warning("⚠️ Kein api-key in config.yml gesetzt!"); } @@ -156,6 +177,8 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { rankManager.startExpiryChecker(); flyAboManager = new FlyAboManager(this, flyCodeManager); flyAboManager.startDailyResetChecker(); + plotSlotManager = new PlotSlotManager(this, flyCodeManager); + plotSlotManager.startBillingChecker(); int intervalSeconds = getConfig().getInt("check-interval", 10); long pollInterval = intervalSeconds * 20L; @@ -171,6 +194,11 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { getCommand("flyabo").setExecutor(new FlyAboCommand()); getCommand("flyabocancel").setExecutor(new FlyAboCancelCommand()); getCommand("flyabogive").setExecutor(new FlyAboGiveCommand()); + getCommand("plotslots").setExecutor(new PlotSlotsCommand()); + getCommand("plotabo").setExecutor(new PlotAboCommand()); + getCommand("plotabocancel").setExecutor(new PlotAboCancelCommand()); + getCommand("plotabogive").setExecutor(new PlotAboGiveCommand()); + getCommand("plotslotsgive").setExecutor(new PlotSlotsGiveCommand()); getCommand("sell").setExecutor(new SellCommand()); getCommand("wpis").setExecutor(new ReloadCommand()); @@ -358,6 +386,13 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { @Override public void run() { flyAboManager.onPlayerJoin(p); } }.runTaskAsynchronously(this); + // Plot-Slot Limit anwenden (nur auf Plot-Server) + if (targetServer.equalsIgnoreCase(plotServer)) { + new BukkitRunnable() { + @Override public void run() { plotSlotManager.onPlayerJoin(p); } + }.runTaskAsynchronously(this); + } + // Ausstehende Fly-Codes anzeigen new BukkitRunnable() { @Override public void run() { @@ -928,6 +963,25 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { flyAboManager.grantAbo(player, label, monthlyPrice); anyCode = true; + } else if ("plot_slots".equals(type)) { + // Einmalige Plot-Slot Erweiterung (permanent) + int slots = cmdObj.has("slots") + ? cmdObj.get("slots").getAsInt() : 1; + String label = cmdObj.has("label") + ? cmdObj.get("label").getAsString() : slots + " Extra-Plot-Slot(s)"; + plotSlotManager.grantExtraSlots(player, slots, label); + anyCode = true; + + } else if ("plot_abo".equals(type)) { + // Monatliches Plot-Abo + int slots = cmdObj.has("slots") + ? cmdObj.get("slots").getAsInt() : 1; + String label = cmdObj.has("label") + ? cmdObj.get("label").getAsString() : "Plot-Abo +" + slots; + int monthlyPrice = (int) data.price; + plotSlotManager.grantAbo(player, slots, label, monthlyPrice); + anyCode = true; + } else if ("generic".equals(type) && cmdObj.has("cmd")) { String cmd = cmdObj.get("cmd").getAsString() .replace("{player}", player.getName()); @@ -1453,6 +1507,208 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { } } + // =========================================================== + // PLOT-SLOT COMMANDS + // =========================================================== + + /** /plotslots – Gesamtstatus: Basis + extra + Abo */ + private class PlotSlotsCommand 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() { + int base = plotSlotManager.getRankBase(p); + int extra = plotSlotManager.getExtraSlots(p.getName()); + int abo = plotSlotManager.getAboSlots(p.getName()); + int total = base + extra + abo; + Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> { + p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + p.sendMessage(ChatColor.YELLOW + "📦 Deine Plot-Slots:"); + p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + p.sendMessage(ChatColor.AQUA + " Rang-Basis: " + ChatColor.WHITE + base); + p.sendMessage(ChatColor.AQUA + " Einmalig: " + ChatColor.WHITE + extra); + p.sendMessage(ChatColor.AQUA + " Abo: " + ChatColor.WHITE + abo); + p.sendMessage(ChatColor.AQUA + " Gesamt: " + ChatColor.GREEN + total); + p.sendMessage(ChatColor.GRAY + " Mehr Slots: " + ChatColor.AQUA + "viper-network.de"); + p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + }); + } + }.runTaskAsynchronously(IngameShopSpigot.this); + return true; + } + } + + /** /plotabo – Abo-Status anzeigen */ + private class PlotAboCommand 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() { + PlotSlotManager.PlotAboEntry abo = plotSlotManager.getActiveAbo(p.getName()); + Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> { + p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + p.sendMessage(ChatColor.YELLOW + "📦 Dein Plot-Abo:"); + p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + if (abo == null) { + p.sendMessage(ChatColor.GRAY + " Kein aktives Plot-Abo."); + p.sendMessage(ChatColor.GRAY + " Im Shop erhältlich: " + ChatColor.AQUA + "viper-network.de"); + } else { + p.sendMessage(ChatColor.AQUA + " Paket: " + ChatColor.WHITE + abo.label); + p.sendMessage(ChatColor.AQUA + " Slots: " + ChatColor.WHITE + "+" + abo.aboSlots); + p.sendMessage(ChatColor.AQUA + " Preis: " + ChatColor.WHITE + abo.monthlyPrice + " " + currency + " / Monat"); + if (abo.cancelled) { + String reason = "payment_failed".equals(abo.cancellationReason) + ? " (Zahlung fehlgeschlagen)" : " (durch dich)"; + p.sendMessage(ChatColor.RED + " Status: ⚠ Gekündigt" + reason); + p.sendMessage(ChatColor.AQUA + " Aktiv bis: " + ChatColor.YELLOW + abo.periodEnd); + } else { + p.sendMessage(ChatColor.AQUA + " Status: " + ChatColor.GREEN + "✔ Aktiv"); + p.sendMessage(ChatColor.AQUA + " Aktiv bis: " + ChatColor.YELLOW + abo.periodEnd); + p.sendMessage(ChatColor.AQUA + " Nächste Abbuchung: " + ChatColor.WHITE + abo.nextBillingDate); + p.sendMessage(ChatColor.GRAY + " Kündigen: " + ChatColor.WHITE + "/plotabocancel"); + } + } + p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + }); + } + }.runTaskAsynchronously(IngameShopSpigot.this); + return true; + } + } + + /** /plotabocancel [confirm] – Abo zum Monatsende kündigen */ + private class PlotAboCancelCommand 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 (args.length == 0 || !args[0].equalsIgnoreCase("confirm")) { + p.sendMessage(ChatColor.YELLOW + "⚠ Möchtest du dein Plot-Abo wirklich kündigen?"); + p.sendMessage(ChatColor.GRAY + " Die Abo-Slots bleiben bis Monatsende erhalten."); + p.sendMessage(ChatColor.GRAY + " Plots mit überzähligen Slots werden eingefroren."); + p.sendMessage(ChatColor.WHITE + " Bestätigen: " + ChatColor.RED + "/plotabocancel confirm"); + return true; + } + new BukkitRunnable() { + @Override public void run() { + boolean ok = plotSlotManager.cancelAbo(p.getName()); + Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> { + if (ok) { + p.sendMessage(ChatColor.YELLOW + "📦 Plot-Abo zur Kündigung vorgemerkt."); + p.sendMessage(ChatColor.GRAY + " Deine Abo-Slots bleiben bis Ende des Monats aktiv."); + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1.0F, 0.8F); + } else { + p.sendMessage(ChatColor.RED + "✗ Kein aktives Plot-Abo gefunden."); + } + }); + } + }.runTaskAsynchronously(IngameShopSpigot.this); + return true; + } + } + + /** /plotabogive [Label] – Admin */ + private class PlotAboGiveCommand implements CommandExecutor { + @Override + public boolean onCommand(CommandSender sender, Command command, + String label, String[] args) { + if (!sender.hasPermission("ingameshop.plotabogive")) { + sender.sendMessage(ChatColor.RED + "✗ Keine Berechtigung."); + return true; + } + if (args.length < 3) { + sender.sendMessage(ChatColor.YELLOW + "Verwendung: /plotabogive [Label]"); + return true; + } + String targetName = args[0]; + int slots, price; + try { slots = Integer.parseInt(args[1]); price = Integer.parseInt(args[2]); } + catch (NumberFormatException e) { + sender.sendMessage(ChatColor.RED + "✗ Slots und Preis müssen Zahlen sein."); + return true; + } + String aboLabel = args.length >= 4 + ? String.join(" ", Arrays.copyOfRange(args, 3, args.length)) + : "Plot-Abo +" + slots; + Player target = Bukkit.getPlayer(targetName); + new BukkitRunnable() { + @Override public void run() { + plotSlotManager.grantAboByName(targetName, slots, aboLabel, price); + Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> { + sender.sendMessage(ChatColor.GREEN + "✔ Plot-Abo (+" + slots + " Slots) an " + + ChatColor.YELLOW + targetName + ChatColor.GREEN + " vergeben."); + if (target != null && target.isOnline()) { + target.sendMessage(""); + target.sendMessage(ChatColor.GOLD + "📦 ════════════════════════════"); + target.sendMessage(ChatColor.YELLOW + " Plot-Abo aktiviert!"); + target.sendMessage(ChatColor.GRAY + " Paket: " + ChatColor.WHITE + aboLabel); + target.sendMessage(ChatColor.GRAY + " Slots: " + ChatColor.WHITE + "+" + slots); + target.sendMessage(ChatColor.GRAY + " Monatspreis: " + ChatColor.WHITE + price + " " + currency); + target.sendMessage(ChatColor.GRAY + " Status: /plotabo"); + target.sendMessage(ChatColor.GOLD + " ════════════════════════════"); + target.playSound(target.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.2F); + } + }); + } + }.runTaskAsynchronously(IngameShopSpigot.this); + return true; + } + } + + /** /plotslotsgive [Label] – Admin gibt permanente Slots */ + private class PlotSlotsGiveCommand implements CommandExecutor { + @Override + public boolean onCommand(CommandSender sender, Command command, + String label, String[] args) { + if (!sender.hasPermission("ingameshop.plotslotsgive")) { + sender.sendMessage(ChatColor.RED + "✗ Keine Berechtigung."); + return true; + } + if (args.length < 2) { + sender.sendMessage(ChatColor.YELLOW + "Verwendung: /plotslotsgive [Label]"); + return true; + } + String targetName = args[0]; + int slots; + try { slots = Integer.parseInt(args[1]); } + catch (NumberFormatException e) { + sender.sendMessage(ChatColor.RED + "✗ Slots muss eine Zahl sein."); + return true; + } + String slotLabel = args.length >= 3 + ? String.join(" ", Arrays.copyOfRange(args, 2, args.length)) + : "+" + slots + " Plot-Slots"; + Player target = Bukkit.getPlayer(targetName); + new BukkitRunnable() { + @Override public void run() { + plotSlotManager.grantExtraSlotsByName(targetName, slots, slotLabel); + Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> { + sender.sendMessage(ChatColor.GREEN + "✔ " + slots + " permanente Plot-Slots an " + + ChatColor.YELLOW + targetName + ChatColor.GREEN + " vergeben."); + if (target != null && target.isOnline()) { + target.sendMessage(""); + target.sendMessage(ChatColor.GOLD + "📦 ════════════════════════════"); + target.sendMessage(ChatColor.YELLOW + " Plot-Slots erweitert!"); + target.sendMessage(ChatColor.GRAY + " Paket: " + ChatColor.WHITE + slotLabel); + target.sendMessage(ChatColor.GRAY + " Dauerhaft: +" + slots + " Slots"); + target.sendMessage(ChatColor.GRAY + " Status: /plotslots"); + target.sendMessage(ChatColor.GOLD + " ════════════════════════════"); + target.playSound(target.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.0F); + } + }); + } + }.runTaskAsynchronously(IngameShopSpigot.this); + return true; + } + } + // =========================================================== // HTTP HILFSMETHODEN // =========================================================== @@ -2362,7 +2618,7 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { FlyAboManager.AboEntry getActiveAbo(String playerName) { String sql = - "SELECT label, monthly_price, cancelled, cancellation_reason," + "SELECT player_name, label, monthly_price, cancelled, cancellation_reason," + " DATE_FORMAT(next_billing_date, '%d.%m.%Y') AS billing_fmt," + " DATE_FORMAT(period_end, '%d.%m.%Y') AS period_fmt" + " FROM wis_fly_abos" @@ -2426,6 +2682,235 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { } } + // ── Plot-Slot CRUD ──────────────────────────────────────────────── + + void createPlotSlotTables() { + String sqlExtra = + "CREATE TABLE IF NOT EXISTS wis_plot_extra_slots (" + + " id INT AUTO_INCREMENT PRIMARY KEY," + + " player_name VARCHAR(64) NOT NULL COLLATE utf8mb4_general_ci," + + " extra_slots INT NOT NULL DEFAULT 0," + + " last_perm INT NOT NULL DEFAULT 0," + + " granted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," + + " UNIQUE KEY uq_player (player_name)" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; + String sqlAbo = + "CREATE TABLE IF NOT EXISTS wis_plot_abos (" + + " id INT AUTO_INCREMENT PRIMARY KEY," + + " player_name VARCHAR(64) NOT NULL COLLATE utf8mb4_general_ci," + + " label VARCHAR(128) NOT NULL," + + " abo_slots INT NOT NULL DEFAULT 1," + + " monthly_price INT NOT NULL DEFAULT 0," + + " cancelled TINYINT(1) NOT NULL DEFAULT 0," + + " cancellation_reason VARCHAR(32) DEFAULT NULL," + + " next_billing_date DATE NOT NULL," + + " period_end DATE NOT NULL," + + " granted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," + + " UNIQUE KEY uq_player (player_name)" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; + try (Statement stmt = connection.createStatement()) { + stmt.execute(sqlExtra); + stmt.execute(sqlAbo); + // Migration: last_perm Spalte ergänzen falls Tabelle schon existiert + try { stmt.execute("ALTER TABLE wis_plot_extra_slots ADD COLUMN IF NOT EXISTS last_perm INT NOT NULL DEFAULT 0 AFTER extra_slots"); } + catch (SQLException ignored) {} + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Plot-Slot-Tabellen Erstellung fehlgeschlagen", e); + } + } + + // Letzten Permission-Wert lesen/schreiben (für sauberes Entfernen) + int getLastPerm(String playerName) { + String sql = "SELECT last_perm FROM wis_plot_extra_slots WHERE player_name = ?"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, playerName); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) return rs.getInt("last_perm"); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "getLastPerm SQL Fehler", e); + } + return 0; + } + + void saveLastPerm(String playerName, int perm) { + String sql = + "INSERT INTO wis_plot_extra_slots (player_name, extra_slots, last_perm)" + + " VALUES (?, 0, ?)" + + " ON DUPLICATE KEY UPDATE last_perm = ?"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, playerName); + ps.setInt(2, perm); + ps.setInt(3, perm); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "saveLastPerm SQL Fehler", e); + } + } + + // Extra-Slots (permanent) + void addExtraSlots(String playerName, int slots) { + String sql = + "INSERT INTO wis_plot_extra_slots (player_name, extra_slots)" + + " VALUES (?, ?)" + + " ON DUPLICATE KEY UPDATE extra_slots = extra_slots + ?"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, playerName); + ps.setInt(2, slots); + ps.setInt(3, slots); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "addExtraSlots SQL Fehler", e); + } + } + + int getExtraSlots(String playerName) { + String sql = "SELECT extra_slots FROM wis_plot_extra_slots WHERE player_name = ?"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, playerName); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) return rs.getInt("extra_slots"); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "getExtraSlots SQL Fehler", e); + } + return 0; + } + + // Plot-Abo + void savePlotAbo(String playerName, int slots, String label, int monthlyPrice) { + String sql = + "INSERT INTO wis_plot_abos" + + " (player_name, label, abo_slots, monthly_price, cancelled, cancellation_reason," + + " next_billing_date, period_end)" + + " VALUES (?, ?, ?, ?, 0, NULL," + + " DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01')," + + " LAST_DAY(CURDATE()))" + + " ON DUPLICATE KEY UPDATE" + + " label = ?, abo_slots = ?, monthly_price = ?," + + " cancelled = 0, cancellation_reason = NULL," + + " next_billing_date = DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01')," + + " period_end = LAST_DAY(CURDATE()), granted_at = NOW()"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, playerName); ps.setString(2, label); + ps.setInt(3, slots); ps.setInt(4, monthlyPrice); + ps.setString(5, label); ps.setInt(6, slots); + ps.setInt(7, monthlyPrice); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "savePlotAbo SQL Fehler", e); + } + } + + boolean cancelPlotAbo(String playerName) { + String sql = + "UPDATE wis_plot_abos SET cancelled = 1, cancellation_reason = 'user'," + + " period_end = LAST_DAY(CURDATE())" + + " WHERE player_name = ? AND period_end >= CURDATE() AND cancelled = 0"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, playerName); + return ps.executeUpdate() == 1; + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "cancelPlotAbo SQL Fehler", e); + return false; + } + } + + void cancelPlotAboPaymentFailed(String playerName) { + String sql = + "UPDATE wis_plot_abos SET cancelled = 1, cancellation_reason = 'payment_failed'," + + " period_end = LAST_DAY(CURDATE())" + + " WHERE player_name = ? AND cancelled = 0"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, playerName); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "cancelPlotAboPaymentFailed SQL Fehler", e); + } + } + + void renewPlotAbo(String playerName) { + String sql = + "UPDATE wis_plot_abos" + + " SET next_billing_date = DATE_FORMAT(DATE_ADD(next_billing_date, INTERVAL 1 MONTH), '%Y-%m-01')," + + " period_end = LAST_DAY(next_billing_date)" + + " WHERE player_name = ? AND cancelled = 0"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, playerName); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "renewPlotAbo SQL Fehler", e); + } + } + + int getPlotAboSlots(String playerName) { + String sql = + "SELECT abo_slots FROM wis_plot_abos" + + " WHERE player_name = ? AND period_end >= CURDATE() AND cancelled = 0"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, playerName); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) return rs.getInt("abo_slots"); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "getPlotAboSlots SQL Fehler", e); + } + return 0; + } + + PlotSlotManager.PlotAboEntry getActivePlotAbo(String playerName) { + String sql = + "SELECT label, abo_slots, monthly_price, cancelled, cancellation_reason," + + " DATE_FORMAT(next_billing_date, '%d.%m.%Y') AS billing_fmt," + + " DATE_FORMAT(period_end, '%d.%m.%Y') AS period_fmt" + + " FROM wis_plot_abos" + + " WHERE player_name = ? AND period_end >= CURDATE()"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, playerName); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) + return new PlotSlotManager.PlotAboEntry( + playerName, + rs.getString("label"), + rs.getInt("abo_slots"), + rs.getInt("monthly_price"), + rs.getInt("cancelled") == 1, + rs.getString("cancellation_reason"), + rs.getString("billing_fmt"), + rs.getString("period_fmt")); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "getActivePlotAbo SQL Fehler", e); + } + return null; + } + + List getDuePlotBillings() { + List list = new ArrayList<>(); + String sql = + "SELECT player_name, label, abo_slots, monthly_price," + + " DATE_FORMAT(next_billing_date, '%d.%m.%Y') AS billing_fmt," + + " DATE_FORMAT(period_end, '%d.%m.%Y') AS period_fmt" + + " FROM wis_plot_abos" + + " WHERE next_billing_date <= CURDATE() AND cancelled = 0"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) + list.add(new PlotSlotManager.PlotAboEntry( + rs.getString("player_name"), + rs.getString("label"), + rs.getInt("abo_slots"), + rs.getInt("monthly_price"), + false, null, + rs.getString("billing_fmt"), + rs.getString("period_fmt"))); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "getDuePlotBillings SQL Fehler", e); + } + return list; + } + // ── Fly-Code CRUD ──────────────────────────────────────────────── String generateCode(String playerName, int durationSec, String label) { @@ -3298,6 +3783,256 @@ public class IngameShopSpigot extends JavaPlugin implements Listener { } } + // =========================================================== + // PLOT SLOT MANAGER + // =========================================================== + + private class PlotSlotManager { + + private final IngameShopSpigot plugin; + private final FlyCodeManager db; + + PlotSlotManager(IngameShopSpigot plugin, FlyCodeManager db) { + this.plugin = plugin; + this.db = db; + db.createPlotSlotTables(); + } + + // ── Rang-Basis aus Config ────────────────────────────────────── + + int getRankBase(Player player) { + int highest = plotRankDefaults.getOrDefault("member", 2); + for (Map.Entry entry : plotRankDefaults.entrySet()) { + if (player.hasPermission("group." + entry.getKey())) { + if (entry.getValue() > highest) highest = entry.getValue(); + } + } + return highest; + } + + int getRankBaseByName(String playerName) { + Player online = Bukkit.getPlayer(playerName); + if (online != null) return getRankBase(online); + return plotRankDefaults.getOrDefault("member", 2); + } + + // ── Gesamtlimit berechnen und LuckPerms setzen ───────────────── + + void applyLimit(String playerName) { + int extra = db.getExtraSlots(playerName); + int abo = db.getPlotAboSlots(playerName); + int base = getRankBaseByName(playerName); + int total = base + extra + abo; + int last = db.getLastPerm(playerName); + Bukkit.getScheduler().runTask(plugin, () -> { + // Nur die tatsächlich gesetzte alte Permission entfernen + if (last > 0 && last != total) { + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), + "lp user " + playerName + " permission unset plots.plot." + last); + } + // Neue Permission setzen + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), + "lp user " + playerName + " permission set plots.plot." + total + " true"); + }); + // last_perm async speichern + new BukkitRunnable() { + @Override public void run() { db.saveLastPerm(playerName, total); } + }.runTaskAsynchronously(plugin); + if (debug) plugin.getLogger().info("[PlotSlots] " + playerName + + " → Basis=" + base + " Extra=" + extra + " Abo=" + abo + + " Gesamt=" + total + " (vorher: " + last + ") → plots.plot." + total); + } + + // ── Einmalige Extra-Slots ───────────────────────────────────── + + void grantExtraSlots(Player player, int slots, String label) { + db.addExtraSlots(player.getName(), slots); + applyLimit(player.getName()); + Bukkit.getScheduler().runTask(plugin, () -> { + int total = getRankBase(player) + + db.getExtraSlots(player.getName()) + + db.getPlotAboSlots(player.getName()); + player.sendMessage(""); + player.sendMessage(ChatColor.GOLD + "📦 ════════════════════════════"); + player.sendMessage(ChatColor.YELLOW + " Plot-Slots erweitert!"); + player.sendMessage(ChatColor.GRAY + " Paket: " + ChatColor.WHITE + label); + player.sendMessage(ChatColor.GRAY + " Neu: " + ChatColor.WHITE + "+" + slots + " permanente Slots"); + player.sendMessage(ChatColor.GRAY + " Gesamt: " + ChatColor.GREEN + total + " Plot-Slots"); + player.sendMessage(ChatColor.GOLD + " ════════════════════════════"); + player.sendMessage(""); + player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.0F); + }); + } + + int getExtraSlots(String playerName) { return db.getExtraSlots(playerName); } + + void grantExtraSlotsByName(String playerName, int slots, String label) { + db.addExtraSlots(playerName, slots); + applyLimit(playerName); + } + + // ── Monatliches Abo ─────────────────────────────────────────── + + void grantAbo(Player player, int slots, String label, int monthlyPrice) { + db.savePlotAbo(player.getName(), slots, label, monthlyPrice); + applyLimit(player.getName()); + Bukkit.getScheduler().runTask(plugin, () -> { + player.sendMessage(""); + player.sendMessage(ChatColor.GOLD + "📦 ════════════════════════════"); + player.sendMessage(ChatColor.YELLOW + " Plot-Abo aktiviert!"); + player.sendMessage(ChatColor.GRAY + " Paket: " + ChatColor.WHITE + label); + player.sendMessage(ChatColor.GRAY + " Slots: " + ChatColor.WHITE + "+" + slots); + player.sendMessage(ChatColor.GRAY + " Monatspreis: " + ChatColor.WHITE + monthlyPrice + " " + currency); + player.sendMessage(ChatColor.GRAY + " Nächste Abbuchung: " + ChatColor.YELLOW + "1. des Folgemonats"); + player.sendMessage(ChatColor.GRAY + " Status: /plotabo"); + player.sendMessage(ChatColor.GOLD + " ════════════════════════════"); + player.sendMessage(""); + player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.2F); + }); + } + + void grantAboByName(String playerName, int slots, String label, int monthlyPrice) { + db.savePlotAbo(playerName, slots, label, monthlyPrice); + applyLimit(playerName); + } + + boolean cancelAbo(String playerName) { + boolean ok = db.cancelPlotAbo(playerName); + if (ok) applyLimit(playerName); + return ok; + } + + int getAboSlots(String playerName) { return db.getPlotAboSlots(playerName); } + + PlotAboEntry getActiveAbo(String playerName) { return db.getActivePlotAbo(playerName); } + + // ── Einfrieren überzähliger Plots ───────────────────────────── + + void freezeExcessPlots(String playerName, int newLimit) { + Bukkit.getScheduler().runTask(plugin, () -> { + if (Bukkit.getPluginManager().getPlugin("PlotSquared") == null) return; + Player online = Bukkit.getPlayer(playerName); + if (online != null) { + online.sendMessage(""); + online.sendMessage(ChatColor.RED + "📦 ════════════════════════════"); + online.sendMessage(ChatColor.RED + " Plot-Abo abgelaufen!"); + online.sendMessage(ChatColor.GRAY + " Limit reduziert auf: " + + ChatColor.WHITE + newLimit); + online.sendMessage(ChatColor.YELLOW + " Überzählige Plots sind eingefroren."); + online.sendMessage(ChatColor.GRAY + " Abo erneuern: " + ChatColor.AQUA + "viper-network.de"); + online.sendMessage(ChatColor.RED + " ════════════════════════════"); + online.sendMessage(""); + online.playSound(online.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1.0F, 0.5F); + } + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), + "lp user " + playerName + " meta set frozen_excess_plots true"); + }); + } + + void unfreezeExcessPlots(String playerName) { + Bukkit.getScheduler().runTask(plugin, () -> + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), + "lp user " + playerName + " meta unset frozen_excess_plots")); + } + + // ── Monatlicher Billing-Check ───────────────────────────────── + + void startBillingChecker() { + new BukkitRunnable() { + @Override public void run() { + if (java.time.LocalDate.now().getDayOfMonth() != 1) return; + new BukkitRunnable() { + @Override public void run() { + List due = db.getDuePlotBillings(); + if (due.isEmpty()) return; + plugin.getLogger().info("[PlotSlots] Billing: " + due.size() + " Abo(s)."); + for (PlotAboEntry entry : due) { + final String pName = entry.playerName; + final int price = entry.monthlyPrice; + final int slots = entry.aboSlots; + Bukkit.getScheduler().runTask(plugin, () -> { + @SuppressWarnings("deprecation") + org.bukkit.OfflinePlayer op = Bukkit.getOfflinePlayer(pName); + if (op == null || !op.hasPlayedBefore()) return; + if ((price == 0) || econ.getBalance(op) >= price) { + if (price > 0) { + econ.withdrawPlayer(op, price); + if (!incomeReceiver.isEmpty()) { + @SuppressWarnings("deprecation") + org.bukkit.OfflinePlayer recv = Bukkit.getOfflinePlayer(incomeReceiver); + if (recv != null && recv.hasPlayedBefore()) + econ.depositPlayer(recv, price); + } + } + new BukkitRunnable() { + @Override public void run() { + db.renewPlotAbo(pName); + } + }.runTaskAsynchronously(plugin); + unfreezeExcessPlots(pName); + applyLimit(pName); + Player online = Bukkit.getPlayer(pName); + if (online != null) { + online.sendMessage(ChatColor.GOLD + "📦 Plot-Abo verlängert! +" + + slots + " Slots | " + price + " " + currency + " abgebucht."); + online.playSound(online.getLocation(), + Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.0F); + } + } else { + new BukkitRunnable() { + @Override public void run() { + db.cancelPlotAboPaymentFailed(pName); + } + }.runTaskAsynchronously(plugin); + new BukkitRunnable() { + @Override public void run() { + int newLimit = getRankBaseByName(pName) + db.getExtraSlots(pName); + freezeExcessPlots(pName, newLimit); + // applyLimit kümmert sich um die Permission + applyLimit(pName); + } + }.runTaskAsynchronously(plugin); + } + }); + } + } + }.runTaskAsynchronously(plugin); + } + }.runTaskTimer(plugin, 20L * 60 * 5, 20L * 60 * 60 * 24); + } + + // ── PlayerJoin ──────────────────────────────────────────────── + + void onPlayerJoin(Player player) { + applyLimit(player.getName()); + if (player.hasPermission("meta.frozen_excess_plots.true")) { + player.sendMessage(ChatColor.RED + "📦 Einige deiner Plots sind eingefroren!"); + player.sendMessage(ChatColor.GRAY + " Grund: Plot-Abo abgelaufen."); + player.sendMessage(ChatColor.WHITE + " Erneuern: " + ChatColor.AQUA + "viper-network.de"); + } + } + + // ── Data class ──────────────────────────────────────────────── + + static class PlotAboEntry { + final String playerName, label, cancellationReason, nextBillingDate, periodEnd; + final int aboSlots, monthlyPrice; + final boolean cancelled; + PlotAboEntry(String playerName, String label, int aboSlots, int monthlyPrice, + boolean cancelled, String cancellationReason, + String nextBillingDate, String periodEnd) { + this.playerName = playerName; + this.label = label; + this.aboSlots = aboSlots; + this.monthlyPrice = monthlyPrice; + this.cancelled = cancelled; + this.cancellationReason = cancellationReason; + this.nextBillingDate = nextBillingDate; + this.periodEnd = periodEnd; + } + } + } + // =========================================================== // SELL MANAGER // =========================================================== diff --git a/IngameShopSpigot/src/main/resources/config.yml b/IngameShopSpigot/src/main/resources/config.yml index 208bb7e..bdc7acd 100644 --- a/IngameShopSpigot/src/main/resources/config.yml +++ b/IngameShopSpigot/src/main/resources/config.yml @@ -29,7 +29,7 @@ fly-redeem-disabled: false # Leer lassen ("") wenn niemand die Einnahmen erhalten soll income-receiver: "" -# MySQL-Verbindung (für Fly-Code-System und Rang-Sessions) +# MySQL-Verbindung (für Fly-Code-System, Rang-Sessions, Fly-Abo und Plot-Slots) mysql: host: "localhost" port: "3306" @@ -43,20 +43,32 @@ mysql: sell: # Ankauf auf diesem Server aktivieren enabled: true - # Preiskorrektur relativ zum WP-Ankaufspreis (in Prozent) # Beispiele: # 0.0 → exakt den WP-Ankaufspreis zahlen # -10.0 → 10 % weniger als der WP-Ankaufspreis # +5.0 → 5 % mehr als der WP-Ankaufspreis - # Hinweis: Den Basis-Ankaufspreis konfigurierst du im WP-Admin - # unter Items → → "Ankauf aktivieren" price-offset: -10.0 - + # ────────────────────────────────────────────── # Fly-Abo Einstellungen # ────────────────────────────────────────────── fly-abo: - # Maximale Fly-Zeit pro Tag in Stunden - # Standard: 6 (= 6 Stunden = 21.600 Sekunden) - max-daily-hours: 6 \ No newline at end of file + # Maximale Fly-Zeit pro Tag in Stunden (Standard: 6) + max-daily-hours: 6 + +# ────────────────────────────────────────────── +# Plot-Slot Einstellungen (nur Citybuild) +# ────────────────────────────────────────────── +plot-slots: + # Auf welchem Server greift das Plot-Slot-System? + # Muss mit server-name des Citybuild-Servers übereinstimmen + server: "citybuild" + # Standard-Plot-Slots pro LuckPerms-Gruppe + # LuckPerms Meta "plotlimit" wird automatisch gesetzt und von + # PlotSquared über die Option "max-plots-per-player: -1" + Meta gelesen + rank-defaults: + member: 2 + vip: 3 + scout: 4 + primo: 5 \ No newline at end of file diff --git a/IngameShopSpigot/src/main/resources/plugin.yml b/IngameShopSpigot/src/main/resources/plugin.yml index 2f38ec5..ce0bae3 100644 --- a/IngameShopSpigot/src/main/resources/plugin.yml +++ b/IngameShopSpigot/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: IngameShopSpigot -version: 1.1 +version: 1.2 main: de.mviper.spigot.IngameShopSpigot api-version: 1.19 depend: [Vault] @@ -20,11 +20,11 @@ commands: usage: /flyredeem permission: ingameshop.flyredeem flycodes: - description: Öffnet ein GUI mit deinen ungenutzten Fly-Gutscheinen (einlösen oder weitergeben) + description: Öffnet ein GUI mit deinen ungenutzten Fly-Gutscheinen usage: /flycodes permission: ingameshop.flycodes flygive: - description: Gibt einem Spieler einen Fly-Gutschein-Code + description: Gibt einem Spieler einen Fly-Gutschein-Code (Admin) usage: /flygive [Label] permission: ingameshop.flygive flypause: @@ -32,7 +32,7 @@ commands: usage: /flypause permission: ingameshop.flypause rankinfo: - description: Zeigt deine aktiven (zeitbasierten) Ränge an + description: Zeigt deine aktiven zeitbasierten Ränge an usage: /rankinfo permission: ingameshop.rankinfo sell: @@ -44,17 +44,37 @@ commands: usage: /wpis permission: ingameshop.reload flyabo: - description: Zeigt deinen Fly-Abo Status und Tageslimit an + description: Zeigt deinen Fly-Abo Status und heutiges Tageslimit usage: /flyabo permission: ingameshop.flyabo flyabocancel: - description: Kündigt dein Fly-Abo zum Ablaufdatum + description: Kündigt dein Fly-Abo zum Monatsende usage: /flyabocancel [confirm] permission: ingameshop.flyabocancel flyabogive: - description: Gibt einem Spieler ein Fly-Abo (Admin) - usage: /flyabogive [Label] + description: Gibt einem Spieler ein Fly-Abo manuell (Admin) + usage: /flyabogive [Label] permission: ingameshop.flyabogive + plotslots: + description: Zeigt deine aktuellen Plot-Slots (Basis + Extra + Abo) + usage: /plotslots + permission: ingameshop.plotslots + plotabo: + description: Zeigt deinen Plot-Abo Status + usage: /plotabo + permission: ingameshop.plotabo + plotabocancel: + description: Kündigt dein Plot-Abo zum Monatsende + usage: /plotabocancel [confirm] + permission: ingameshop.plotabocancel + plotabogive: + description: Gibt einem Spieler ein Plot-Abo manuell (Admin) + usage: /plotabogive [Label] + permission: ingameshop.plotabogive + plotslotsgive: + description: Gibt einem Spieler permanente Plot-Slots (Admin) + usage: /plotslotsgive [Label] + permission: ingameshop.plotslotsgive permissions: ingameshop.orders: @@ -70,7 +90,7 @@ permissions: description: Kann eigene ungenutzte Fly-Codes auflisten default: true ingameshop.flygive: - description: Kann Spielern Fly-Gutscheine geben (Admin/OP/Mod) + description: Kann Spielern Fly-Gutscheine geben (Admin) default: op ingameshop.flypause: description: Kann die eigene Fly-Zeit pausieren @@ -92,4 +112,19 @@ permissions: default: true ingameshop.flyabogive: description: Kann Spielern Fly-Abos manuell vergeben (Admin) + default: op + ingameshop.plotslots: + description: Kann eigene Plot-Slot Übersicht einsehen + default: true + ingameshop.plotabo: + description: Kann eigenen Plot-Abo-Status einsehen + default: true + ingameshop.plotabocancel: + description: Kann eigenes Plot-Abo kündigen + default: true + ingameshop.plotabogive: + description: Kann Spielern Plot-Abos manuell vergeben (Admin) + default: op + ingameshop.plotslotsgive: + description: Kann Spielern permanente Plot-Slots vergeben (Admin) default: op \ No newline at end of file