diff --git a/src/main/java/net/viper/status/StatusAPI.java b/src/main/java/net/viper/status/StatusAPI.java index 5ed899d..c745034 100644 --- a/src/main/java/net/viper/status/StatusAPI.java +++ b/src/main/java/net/viper/status/StatusAPI.java @@ -5,6 +5,7 @@ import net.md_5.bungee.api.config.ListenerInfo; import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.plugin.Plugin; import net.viper.status.module.ModuleManager; +import net.viper.status.modules.economy.EconomyModule; import net.viper.status.modules.antibot.AntiBotModule; import net.viper.status.modules.network.NetworkInfoModule; import net.viper.status.modules.AutoMessage.AutoMessageModule; @@ -89,6 +90,7 @@ public class StatusAPI extends Plugin implements Runnable { moduleManager.registerModule(new AutoMessageModule()); moduleManager.registerModule(new CustomCommandModule()); moduleManager.registerModule(new ServerSwitcherModule()); + moduleManager.registerModule(new EconomyModule()); try { Class forumBridge = Class.forName("net.viper.status.modules.forum.ForumBridgeModule"); @@ -506,8 +508,13 @@ public class StatusAPI extends Plugin implements Runnable { playerMap.put("playtime", ps.getPlaytimeWithCurrentSession()); playerMap.put("joins", ps.joins); playerMap.put("online", ProxyServer.getInstance().getPlayer(ps.uuid) != null); + // Balance direkt aus MySQL (serverübergreifend) + EconomyModule ecoModPlayer = (EconomyModule) moduleManager.getModule("EconomyModule"); + double playerBalance = (ecoModPlayer != null && ecoModPlayer.getManager() != null) + ? ecoModPlayer.getManager().getBalance(ps.uuid) + : ps.balance; Map economy = new LinkedHashMap<>(); - economy.put("balance", ps.balance); + economy.put("balance", playerBalance); economy.put("total_earned", ps.totalEarned); economy.put("total_spent", ps.totalSpent); economy.put("transactions_count", ps.transactionsCount); @@ -526,49 +533,92 @@ public class StatusAPI extends Plugin implements Runnable { } // GET /economy/player?uuid=... oder ?name=... + // Kein Cache – UUID und Balance kommen direkt aus der DB if ("GET".equalsIgnoreCase(method) && "/economy/player".equalsIgnoreCase(pathOnly)) { Map qp = parseQueryParams(path); - StatsModule statsMod = (StatsModule) moduleManager.getModule("StatsModule"); - PlayerStats ps = resolvePlayer(qp.get("uuid"), qp.get("name"), statsMod); - if (ps == null) { + EconomyModule ecoModGet = (EconomyModule) moduleManager.getModule("EconomyModule"); + if (ecoModGet == null || ecoModGet.getManager() == null) { + sendHttpResponse(out, "{\"success\":false,\"error\":\"economy_module_unavailable\"}", 503); + return; + } + // UUID auflösen: erst Query-Param, dann Name über EconomyManager (DB-Lookup) + UUID ecoUuid = null; + String ecoName = null; + String uuidParam = qp.get("uuid"); + String nameParam = qp.get("name"); + if (uuidParam != null && !uuidParam.isEmpty()) { + try { ecoUuid = UUID.fromString(uuidParam.trim()); } catch (IllegalArgumentException ignored) {} + } + if (ecoUuid == null && nameParam != null && !nameParam.isEmpty()) { + ecoUuid = ecoModGet.getManager().resolveUUID(nameParam.trim()); + ecoName = nameParam.trim(); + } + if (ecoUuid == null) { sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404); return; } + if (ecoName == null) ecoName = uuidParam; + double directBalance = ecoModGet.getManager().getBalance(ecoUuid); Map payload = new LinkedHashMap<>(); payload.put("success", true); - payload.put("uuid", ps.uuid.toString()); - payload.put("name", ps.name); + payload.put("uuid", ecoUuid.toString()); + payload.put("name", ecoName); Map economy = new LinkedHashMap<>(); - economy.put("balance", ps.balance); - economy.put("total_earned", ps.totalEarned); - economy.put("total_spent", ps.totalSpent); - economy.put("transactions_count", ps.transactionsCount); + economy.put("balance", directBalance); payload.put("economy", economy); sendHttpResponse(out, buildJsonString(payload), 200); return; } // POST /economy/update + // Kein Cache – Balance wird direkt in die DB geschrieben if ("POST".equalsIgnoreCase(method) && "/economy/update".equalsIgnoreCase(pathOnly)) { String body = readBody(in, headers); - StatsModule statsModEco = (StatsModule) moduleManager.getModule("StatsModule"); - PlayerStats psEco = resolvePlayer(extractJsonString(body, "uuid"), extractJsonString(body, "name"), statsModEco); - if (psEco == null) { + EconomyModule ecoModUpd = (EconomyModule) moduleManager.getModule("EconomyModule"); + if (ecoModUpd == null || ecoModUpd.getManager() == null) { + sendHttpResponse(out, "{\"success\":false,\"error\":\"economy_module_unavailable\"}", 503); + return; + } + // UUID auflösen + UUID ecoUpdUuid = null; + String uuidBody = extractJsonString(body, "uuid"); + String nameBody = extractJsonString(body, "name"); + if (uuidBody != null && !uuidBody.isEmpty()) { + try { ecoUpdUuid = UUID.fromString(uuidBody.trim()); } catch (IllegalArgumentException ignored) {} + } + if (ecoUpdUuid == null && nameBody != null && !nameBody.isEmpty()) { + ecoUpdUuid = ecoModUpd.getManager().resolveUUID(nameBody.trim()); + } + if (ecoUpdUuid == null) { sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404); return; } - String balStr = extractJsonString(body, "balance"); - String earnStr = extractJsonString(body, "total_earned"); - if (earnStr == null) earnStr = extractJsonString(body, "totalEarned"); - String spentStr = extractJsonString(body, "total_spent"); - if (spentStr == null) spentStr = extractJsonString(body, "totalSpent"); - String txStr = extractJsonString(body, "transactions_count"); - if (txStr == null) txStr = extractJsonString(body, "transactionsCount"); - synchronized (psEco) { - try { if (balStr != null && !balStr.isEmpty()) psEco.balance = Double.parseDouble(balStr); } catch (Exception ignored) {} - try { if (earnStr != null && !earnStr.isEmpty()) psEco.totalEarned = Double.parseDouble(earnStr); } catch (Exception ignored) {} - try { if (spentStr != null && !spentStr.isEmpty()) psEco.totalSpent = Double.parseDouble(spentStr); } catch (Exception ignored) {} - try { if (txStr != null && !txStr.isEmpty()) psEco.transactionsCount = Integer.parseInt(txStr); } catch (Exception ignored) {} + // Balance direkt in DB schreiben + String balStr = extractJsonString(body, "balance"); + if (balStr != null && !balStr.isEmpty()) { + try { + double newBal = Double.parseDouble(balStr); + ecoModUpd.getManager().setBalance(ecoUpdUuid, newBal); + } catch (NumberFormatException ignored) {} + } + // Stats-Felder (total_earned etc.) im Cache aktualisieren, falls vorhanden + StatsModule statsModEco = (StatsModule) moduleManager.getModule("StatsModule"); + if (statsModEco != null) { + PlayerStats psEco = statsModEco.getManager().getIfPresent(ecoUpdUuid); + if (psEco != null) { + String earnStr = extractJsonString(body, "total_earned"); + if (earnStr == null) earnStr = extractJsonString(body, "totalEarned"); + String spentStr = extractJsonString(body, "total_spent"); + if (spentStr == null) spentStr = extractJsonString(body, "totalSpent"); + String txStr = extractJsonString(body, "transactions_count"); + if (txStr == null) txStr = extractJsonString(body, "transactionsCount"); + synchronized (psEco) { + try { if (balStr != null && !balStr.isEmpty()) psEco.balance = Double.parseDouble(balStr); } catch (Exception ignored) {} + try { if (earnStr != null && !earnStr.isEmpty()) psEco.totalEarned = Double.parseDouble(earnStr); } catch (Exception ignored) {} + try { if (spentStr != null && !spentStr.isEmpty()) psEco.totalSpent = Double.parseDouble(spentStr); } catch (Exception ignored) {} + try { if (txStr != null && !txStr.isEmpty()) psEco.transactionsCount = Integer.parseInt(txStr); } catch (Exception ignored) {} + } + } } sendHttpResponse(out, "{\"success\":true}", 200); return; @@ -690,8 +740,13 @@ public class StatusAPI extends Plugin implements Runnable { playerInfo.put("joins", ps.joins); playerInfo.put("first_seen", ps.firstSeen); playerInfo.put("last_seen", ps.lastSeen); + // Balance direkt aus MySQL (serverübergreifend) + EconomyModule ecoModStatus = (EconomyModule) moduleManager.getModule("EconomyModule"); + double statusBalance = (ecoModStatus != null && ecoModStatus.getManager() != null) + ? ecoModStatus.getManager().getBalance(p.getUniqueId()) + : ps.balance; Map eco = new LinkedHashMap<>(); - eco.put("balance", ps.balance); + eco.put("balance", statusBalance); eco.put("total_earned", ps.totalEarned); eco.put("total_spent", ps.totalSpent); eco.put("transactions_count", ps.transactionsCount); diff --git a/src/main/java/net/viper/status/modules/economy/EcoAdminCommand.java b/src/main/java/net/viper/status/modules/economy/EcoAdminCommand.java new file mode 100644 index 0000000..82c8796 --- /dev/null +++ b/src/main/java/net/viper/status/modules/economy/EcoAdminCommand.java @@ -0,0 +1,134 @@ +package net.viper.status.modules.economy; + +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.api.plugin.Plugin; + +import java.util.UUID; + +/** + * /ecoadmin [Betrag] + * Admin-Verwaltung des Economy-Systems (auch für Offline-Spieler). + * + * Berechtigung: economy.admin + */ +public class EcoAdminCommand extends Command { + + private final Plugin plugin; + private final EconomyManager manager; + + public EcoAdminCommand(Plugin plugin, EconomyManager manager) { + super("ecoadmin", "economy.admin", "ecomod", "moneyadmin"); + this.plugin = plugin; + this.manager = manager; + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (!sender.hasPermission("economy.admin")) { + sender.sendMessage(new TextComponent(c("&cKeine Berechtigung."))); + return; + } + + if (args.length < 2) { + sendHelp(sender); + return; + } + + String action = args[0].toLowerCase(); + String targetName = args[1]; + + plugin.getProxy().getScheduler().runAsync(plugin, () -> { + UUID targetUUID = manager.resolveUUID(targetName); + + if (targetUUID == null) { + sender.sendMessage(new TextComponent(c("&cSpieler &e" + targetName + " &cnicht gefunden."))); + return; + } + + switch (action) { + + case "check": { + double bal = manager.getBalance(targetUUID); + sender.sendMessage(new TextComponent(c("&7Kontostand von &e" + targetName + + "&7: &a" + formatAmount(bal) + " $"))); + break; + } + + case "give": { + if (args.length < 3) { sendHelp(sender); return; } + double amount = parseAmount(args[2]); + if (amount <= 0) { sender.sendMessage(new TextComponent(c("&cUngültiger Betrag."))); return; } + manager.deposit(targetUUID, amount); + double newBal = manager.getBalance(targetUUID); + sender.sendMessage(new TextComponent(c("&a+ " + formatAmount(amount) + + " $ &7an &e" + targetName + " &7→ Neues Guthaben: &a" + formatAmount(newBal) + " $"))); + notifyPlayer(targetUUID, c("&7Admin hat dir &a+" + formatAmount(amount) + + " $ &7gegeben. Guthaben: &a" + formatAmount(newBal) + " $")); + break; + } + + case "take": { + if (args.length < 3) { sendHelp(sender); return; } + double amount = parseAmount(args[2]); + if (amount <= 0) { sender.sendMessage(new TextComponent(c("&cUngültiger Betrag."))); return; } + boolean ok = manager.withdraw(targetUUID, amount); + if (!ok) { + sender.sendMessage(new TextComponent(c("&cNicht genug Guthaben bei &e" + targetName + "&c."))); + return; + } + double newBal = manager.getBalance(targetUUID); + sender.sendMessage(new TextComponent(c("&c- " + formatAmount(amount) + + " $ &7von &e" + targetName + " &7→ Neues Guthaben: &a" + formatAmount(newBal) + " $"))); + notifyPlayer(targetUUID, c("&7Admin hat dir &c-" + formatAmount(amount) + + " $ &7abgezogen. Guthaben: &a" + formatAmount(newBal) + " $")); + break; + } + + case "set": { + if (args.length < 3) { sendHelp(sender); return; } + double amount = parseAmount(args[2]); + if (amount < 0) { sender.sendMessage(new TextComponent(c("&cBetrag darf nicht negativ sein."))); return; } + manager.setBalance(targetUUID, amount); + sender.sendMessage(new TextComponent(c("&7Guthaben von &e" + targetName + + " &7gesetzt auf &a" + formatAmount(amount) + " $"))); + notifyPlayer(targetUUID, c("&7Dein Guthaben wurde auf &a" + formatAmount(amount) + " $ &7gesetzt.")); + break; + } + + default: + sendHelp(sender); + } + }); + } + + private void notifyPlayer(UUID uuid, String message) { + ProxiedPlayer target = plugin.getProxy().getPlayer(uuid); + if (target != null) { + target.sendMessage(new TextComponent(message)); + } + } + + private void sendHelp(CommandSender sender) { + sender.sendMessage(new TextComponent(c("&e/ecoadmin check "))); + sender.sendMessage(new TextComponent(c("&e/ecoadmin give "))); + sender.sendMessage(new TextComponent(c("&e/ecoadmin take "))); + sender.sendMessage(new TextComponent(c("&e/ecoadmin set "))); + } + + private static double parseAmount(String s) { + try { return Double.parseDouble(s.replace(",", ".")); } + catch (NumberFormatException e) { return -1; } + } + + private static String formatAmount(double amount) { + return String.format("%,.2f", amount); + } + + private static String c(String msg) { + return ChatColor.translateAlternateColorCodes('&', msg); + } +} diff --git a/src/main/java/net/viper/status/modules/economy/EconomyDatabase.java b/src/main/java/net/viper/status/modules/economy/EconomyDatabase.java new file mode 100644 index 0000000..69dcec2 --- /dev/null +++ b/src/main/java/net/viper/status/modules/economy/EconomyDatabase.java @@ -0,0 +1,183 @@ +package net.viper.status.modules.economy; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import net.md_5.bungee.api.plugin.Plugin; + +import java.sql.*; +import java.util.UUID; +import java.util.logging.Logger; + +/** + * Verwaltet die MySQL-Verbindung (HikariCP) und die Tabelle bc_accounts. + * Wird vom EconomyModule gehalten und an den EconomyManager weitergegeben. + */ +public class EconomyDatabase { + + private static final String TABLE = "bc_accounts"; + + private final Logger log; + private HikariDataSource dataSource; + + public EconomyDatabase(Plugin plugin, String host, int port, String database, String user, String password) { + this.log = plugin.getLogger(); + + HikariConfig cfg = new HikariConfig(); + cfg.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + database + + "?useSSL=false&autoReconnect=true&characterEncoding=UTF-8&useUnicode=true"); + cfg.setUsername(user); + cfg.setPassword(password); + cfg.setMaximumPoolSize(5); + cfg.setMinimumIdle(1); + cfg.setConnectionTimeout(10_000); + cfg.setIdleTimeout(600_000); + cfg.setMaxLifetime(1_800_000); + cfg.setPoolName("StatusAPI-Economy"); + cfg.addDataSourceProperty("cachePrepStmts", "true"); + cfg.addDataSourceProperty("prepStmtCacheSize", "250"); + cfg.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + + try { + dataSource = new HikariDataSource(cfg); + } catch (Exception e) { + log.severe("[Economy] MySQL-Verbindung fehlgeschlagen: " + e.getMessage()); + log.severe("[Economy] Bitte Zugangsdaten in verify.properties prüfen!"); + return; + } + + // Tabelle anlegen falls nicht vorhanden (kompatibel mit SurvivalPlus) + try (Connection con = dataSource.getConnection(); + PreparedStatement ps = con.prepareStatement( + "CREATE TABLE IF NOT EXISTS `" + TABLE + "` (" + + " `player_name` VARCHAR(36) NOT NULL," + + " `balance` VARCHAR(255) NOT NULL DEFAULT '0'," + + " PRIMARY KEY (`player_name`)" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;")) { + ps.executeUpdate(); + } catch (SQLException e) { + log.severe("[Economy] Tabellen-Setup (bc_accounts) fehlgeschlagen: " + e.getMessage()); + } + + // bc_player_names Tabelle anlegen + try (Connection con = dataSource.getConnection(); + PreparedStatement ps = con.prepareStatement( + "CREATE TABLE IF NOT EXISTS `bc_player_names` (" + + " `uuid` VARCHAR(36) NOT NULL PRIMARY KEY," + + " `name` VARCHAR(16) NOT NULL," + + " `updated` BIGINT NOT NULL" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;")) { + ps.executeUpdate(); + log.info("[Economy] MySQL verbunden – Tabellen bereit."); + } catch (SQLException e) { + log.severe("[Economy] Tabellen-Setup (bc_player_names) fehlgeschlagen: " + e.getMessage()); + } + } + + public boolean isConnected() { + return dataSource != null && !dataSource.isClosed(); + } + + public void close() { + if (dataSource != null && !dataSource.isClosed()) { + dataSource.close(); + } + } + + /** + * Sucht UUID in CMI_users – nutzt eine separate DB-Verbindung falls + * economy.cmi.database konfiguriert ist, sonst dieselbe DB. + */ + public java.util.UUID findUUIDByName(String name) { + if (!isConnected()) return null; + try (Connection con = dataSource.getConnection(); + PreparedStatement ps = con.prepareStatement( + "SELECT `player_uuid` FROM `CMI_users` WHERE `username` = ? LIMIT 1")) { + ps.setString(1, name); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + String uuidStr = rs.getString("player_uuid"); + if (uuidStr != null && !uuidStr.isEmpty()) { + return java.util.UUID.fromString(uuidStr); + } + } + } + } catch (SQLException | IllegalArgumentException e) { + // CMI_users nicht in dieser DB – kein Problem, bc_player_names ist der Primär-Lookup + } + return null; + } + + /** Speichert den Spielernamen in einer eigenen Lookup-Tabelle. */ + public void saveNameMapping(java.util.UUID uuid, String name) { + if (!isConnected()) return; + try (Connection con = dataSource.getConnection(); + PreparedStatement ps = con.prepareStatement( + "INSERT INTO `bc_player_names` (`uuid`, `name`, `updated`) VALUES (?, ?, ?) " + + "ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `updated` = VALUES(`updated`)")) { + ps.setString(1, uuid.toString()); + ps.setString(2, name); + ps.setLong(3, System.currentTimeMillis()); + ps.executeUpdate(); + } catch (SQLException e) { + log.warning("[Economy] Name-Mapping fehlgeschlagen: " + e.getMessage()); + } + } + + /** UUID-Lookup über bc_player_names (eigene Tabelle). */ + public java.util.UUID findUUIDByNameOwn(String name) { + if (!isConnected()) return null; + try (Connection con = dataSource.getConnection(); + PreparedStatement ps = con.prepareStatement( + "SELECT `uuid` FROM `bc_player_names` WHERE `name` = ? LIMIT 1")) { + ps.setString(1, name); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return java.util.UUID.fromString(rs.getString("uuid")); + } + } + } catch (SQLException | IllegalArgumentException e) { + // Tabelle noch nicht vorhanden + } + return null; + } + + /** Kombinierter UUID-Lookup: erst eigene Tabelle, dann CMI_users. */ + public java.util.UUID resolveUUID(String name) { + java.util.UUID uuid = findUUIDByNameOwn(name); + if (uuid != null) return uuid; + return findUUIDByName(name); + } + + /** Lädt den Kontostand direkt aus der DB. Gibt -1 zurück wenn kein Eintrag. */ + public double load(UUID uuid) { + if (!isConnected()) return -1; + try (Connection con = dataSource.getConnection(); + PreparedStatement ps = con.prepareStatement( + "SELECT `balance` FROM `" + TABLE + "` WHERE `player_name` = ?")) { + ps.setString(1, uuid.toString()); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return Double.parseDouble(rs.getString("balance")); + } + } + } catch (SQLException | NumberFormatException e) { + log.warning("[Economy] Load fehlgeschlagen für " + uuid + ": " + e.getMessage()); + } + return -1; + } + + /** Schreibt einen Kontostand in die DB (INSERT oder UPDATE). */ + public void save(UUID uuid, double balance) { + if (!isConnected()) return; + try (Connection con = dataSource.getConnection(); + PreparedStatement ps = con.prepareStatement( + "INSERT INTO `" + TABLE + "` (`player_name`, `balance`) VALUES (?, ?) " + + "ON DUPLICATE KEY UPDATE `balance` = VALUES(`balance`)")) { + ps.setString(1, uuid.toString()); + ps.setString(2, String.valueOf(balance)); + ps.executeUpdate(); + } catch (SQLException e) { + log.warning("[Economy] Save fehlgeschlagen für " + uuid + ": " + e.getMessage()); + } + } +} diff --git a/src/main/java/net/viper/status/modules/economy/EconomyListener.java b/src/main/java/net/viper/status/modules/economy/EconomyListener.java new file mode 100644 index 0000000..8b7099a --- /dev/null +++ b/src/main/java/net/viper/status/modules/economy/EconomyListener.java @@ -0,0 +1,33 @@ +package net.viper.status.modules.economy; + +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.PostLoginEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.event.EventHandler; + +public class EconomyListener implements Listener { + + private final Plugin plugin; + private final EconomyManager manager; + + public EconomyListener(Plugin plugin, EconomyManager manager) { + this.plugin = plugin; + this.manager = manager; + } + + /** + * Beim ersten Login: Konto anlegen falls noch nicht vorhanden. + * Kein Caching – alle weiteren Zugriffe gehen direkt in die DB. + */ + @EventHandler + public void onLogin(PostLoginEvent event) { + ProxiedPlayer player = event.getPlayer(); + plugin.getProxy().getScheduler().runAsync(plugin, () -> { + // Namen für Offline-Lookup speichern + manager.saveNameMapping(player.getUniqueId(), player.getName()); + // Konto anlegen falls neu + manager.getBalance(player.getUniqueId()); + }); + } +} diff --git a/src/main/java/net/viper/status/modules/economy/EconomyManager.java b/src/main/java/net/viper/status/modules/economy/EconomyManager.java new file mode 100644 index 0000000..a57e03a --- /dev/null +++ b/src/main/java/net/viper/status/modules/economy/EconomyManager.java @@ -0,0 +1,109 @@ +package net.viper.status.modules.economy; + +import net.md_5.bungee.api.plugin.Plugin; + +import java.util.UUID; + +/** + * Kein Cache – jeder Zugriff geht direkt in die Datenbank. + * Damit ist der Kontostand immer aktuell, egal von welchem Server + * er zuletzt geändert wurde (SurvivalPlus, CMI, etc.). + */ +public class EconomyManager { + + private final Plugin plugin; + private final EconomyDatabase db; + private final double startBalance; + + public EconomyManager(Plugin plugin, EconomyDatabase db, double startBalance) { + this.plugin = plugin; + this.db = db; + this.startBalance = startBalance; + } + + public void saveNameMapping(UUID uuid, String name) { + db.saveNameMapping(uuid, name); + } + + public UUID resolveUUID(String name) { + // 1. Online-Spieler auf dem Proxy (case-insensitive) + for (net.md_5.bungee.api.connection.ProxiedPlayer p : plugin.getProxy().getPlayers()) { + if (p.getName().equalsIgnoreCase(name)) return p.getUniqueId(); + } + // 2. Eigene bc_player_names Tabelle + UUID uuid = db.findUUIDByNameOwn(name); + if (uuid != null) return uuid; + // 3. CMI_users Fallback + uuid = db.findUUIDByName(name); + if (uuid != null) return uuid; + // 4. Mojang API als letzter Ausweg + return lookupMojang(name); + } + + /** UUID via Mojang API holen (nur wenn alle lokalen Lookups fehlschlagen). */ + private UUID lookupMojang(String name) { + try { + java.net.URL url = new java.net.URL("https://api.mojang.com/users/profiles/minecraft/" + name); + java.net.HttpURLConnection con = (java.net.HttpURLConnection) url.openConnection(); + con.setConnectTimeout(3000); + con.setReadTimeout(3000); + con.setRequestMethod("GET"); + if (con.getResponseCode() != 200) return null; + java.io.BufferedReader br = new java.io.BufferedReader( + new java.io.InputStreamReader(con.getInputStream())); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) sb.append(line); + br.close(); + String json = sb.toString(); + int idIdx = json.indexOf("\"id\":\""); + if (idIdx < 0) return null; + String raw = json.substring(idIdx + 6, idIdx + 38); + String formatted = raw.replaceFirst( + "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5"); + UUID uuid = UUID.fromString(formatted); + // Für künftige Lookups speichern + db.saveNameMapping(uuid, name); + plugin.getLogger().info("[Economy] Mojang-Lookup: " + name + " → " + uuid); + return uuid; + } catch (Exception e) { + plugin.getLogger().warning("[Economy] Mojang-Lookup fehlgeschlagen für " + name + ": " + e.getMessage()); + return null; + } + } + + public double getBalance(UUID uuid) { + double bal = db.load(uuid); + if (bal < 0) { + // Neuer Spieler – Startkonto anlegen + db.save(uuid, startBalance); + return startBalance; + } + return bal; + } + + public void setBalance(UUID uuid, double amount) { + db.save(uuid, Math.max(0.0, amount)); + } + + public boolean deposit(UUID uuid, double amount) { + if (amount <= 0) return false; + double current = db.load(uuid); + if (current < 0) current = startBalance; + db.save(uuid, current + amount); + return true; + } + + public boolean withdraw(UUID uuid, double amount) { + if (amount <= 0) return false; + double current = db.load(uuid); + if (current < 0) current = 0; + if (current < amount) return false; + db.save(uuid, current - amount); + return true; + } + + public boolean hasAccount(UUID uuid) { + return db.load(uuid) >= 0; + } +} diff --git a/src/main/java/net/viper/status/modules/economy/EconomyModule.java b/src/main/java/net/viper/status/modules/economy/EconomyModule.java new file mode 100644 index 0000000..26498a3 --- /dev/null +++ b/src/main/java/net/viper/status/modules/economy/EconomyModule.java @@ -0,0 +1,95 @@ +package net.viper.status.modules.economy; + +import net.md_5.bungee.api.plugin.Plugin; +import net.viper.status.StatusAPI; +import net.viper.status.module.Module; + +import java.util.Properties; + +/** + * EconomyModule – serverübergreifendes Geldkonto über MySQL (bc_accounts). + * + * Konfiguration in verify.properties: + * economy.mysql.host=localhost + * economy.mysql.port=3306 + * economy.mysql.database=survivalplus + * economy.mysql.username=root + * economy.mysql.password= + * economy.start-balance=500.0 + * + * Das Modul registriert sich im Modul-System der StatusAPI und hält + * EconomyDatabase + EconomyManager, die von SurvivalPlus und allen + * anderen Servern über die gemeinsame bc_accounts-Tabelle genutzt werden. + */ +public class EconomyModule implements Module { + + private EconomyDatabase database; + private EconomyManager manager; + + @Override + public String getName() { + return "EconomyModule"; + } + + @Override + public void onEnable(Plugin plugin) { + // Konfiguration aus verify.properties laden + Properties props = ((StatusAPI) plugin).getVerifyProperties(); + + String host = getProp(props, "economy.mysql.host", "localhost"); + int port = getInt (props, "economy.mysql.port", 3306); + String database = getProp(props, "economy.mysql.database", "survivalplus"); + String user = getProp(props, "economy.mysql.username", "root"); + String password = getProp(props, "economy.mysql.password", ""); + double startBal = getDbl (props, "economy.start-balance", 500.0); + + this.database = new EconomyDatabase(plugin, host, port, database, user, password); + + if (!this.database.isConnected()) { + plugin.getLogger().severe("[Economy] Modul wird NICHT aktiviert – keine DB-Verbindung."); + return; + } + + this.manager = new EconomyManager(plugin, this.database, startBal); + + // Listener registrieren (Login → load, Disconnect → save) + plugin.getProxy().getPluginManager().registerListener(plugin, new EconomyListener(plugin, manager)); + plugin.getProxy().getPluginManager().registerCommand(plugin, new PayCommand(plugin, manager)); + plugin.getProxy().getPluginManager().registerCommand(plugin, new EcoAdminCommand(plugin, manager)); + + plugin.getLogger().info("[Economy] EconomyModule aktiviert (start-balance: " + startBal + ")."); + } + + @Override + public void onDisable(Plugin plugin) { + if (database != null) { + database.close(); + plugin.getLogger().info("[Economy] MySQL-Verbindung geschlossen."); + } + } + + /** Gibt den EconomyManager zurück (für andere Module oder HTTP-Endpoints). */ + public EconomyManager getManager() { + return manager; + } + + // ---------------------------------------------------------------- + // Hilfsmethoden + // ---------------------------------------------------------------- + + private static String getProp(Properties p, String key, String def) { + if (p == null) return def; + String v = p.getProperty(key, def); + return (v == null || v.trim().isEmpty()) ? def : v.trim(); + } + + private static int getInt(Properties p, String key, int def) { + try { return Integer.parseInt(getProp(p, key, String.valueOf(def))); } + catch (NumberFormatException e) { return def; } + } + + private static double getDbl(Properties p, String key, double def) { + try { return Double.parseDouble(getProp(p, key, String.valueOf(def))); } + catch (NumberFormatException e) { return def; } + } +} diff --git a/src/main/java/net/viper/status/modules/economy/PayCommand.java b/src/main/java/net/viper/status/modules/economy/PayCommand.java new file mode 100644 index 0000000..eb87e4d --- /dev/null +++ b/src/main/java/net/viper/status/modules/economy/PayCommand.java @@ -0,0 +1,102 @@ +package net.viper.status.modules.economy; + +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.api.plugin.Plugin; + +import java.util.UUID; + +/** + * /pay + * Überweist Geld an einen anderen Spieler (auch offline). + */ +public class PayCommand extends Command { + + private final Plugin plugin; + private final EconomyManager manager; + + public PayCommand(Plugin plugin, EconomyManager manager) { + super("pay", null); + this.plugin = plugin; + this.manager = manager; + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof ProxiedPlayer)) { + sender.sendMessage(new TextComponent(c("&cNur Spieler können diesen Befehl nutzen."))); + return; + } + + if (args.length < 2) { + sender.sendMessage(new TextComponent(c("&eVerwendung: &f/pay "))); + return; + } + + ProxiedPlayer payer = (ProxiedPlayer) sender; + String targetName = args[0]; + double amount; + + try { + amount = Double.parseDouble(args[1].replace(",", ".")); + } catch (NumberFormatException e) { + sender.sendMessage(new TextComponent(c("&cUngültiger Betrag."))); + return; + } + + if (amount <= 0) { + sender.sendMessage(new TextComponent(c("&cDer Betrag muss größer als 0 sein."))); + return; + } + + if (targetName.equalsIgnoreCase(payer.getName())) { + sender.sendMessage(new TextComponent(c("&cDu kannst dir nicht selbst Geld überweisen."))); + return; + } + + plugin.getProxy().getScheduler().runAsync(plugin, () -> { + UUID targetUUID = manager.resolveUUID(targetName); + + if (targetUUID == null) { + payer.sendMessage(new TextComponent(c("&cSpieler &e" + targetName + " &cnicht gefunden."))); + return; + } + + double payerBalance = manager.getBalance(payer.getUniqueId()); + + if (payerBalance < amount) { + payer.sendMessage(new TextComponent(c("&cNicht genug Guthaben. Dein Kontostand: &e" + + formatAmount(payerBalance) + " $"))); + return; + } + + manager.withdraw(payer.getUniqueId(), amount); + manager.deposit(targetUUID, amount); + + payer.sendMessage(new TextComponent(c("&aErfolgreich &e" + formatAmount(amount) + + " $ &aan &e" + targetName + " &aüberwiesen."))); + payer.sendMessage(new TextComponent(c("&7Dein neues Guthaben: &e" + + formatAmount(manager.getBalance(payer.getUniqueId())) + " $"))); + + // Online-Benachrichtigung an Empfänger + ProxiedPlayer target = plugin.getProxy().getPlayer(targetUUID); + if (target != null) { + target.sendMessage(new TextComponent(c("&a" + payer.getName() + + " hat dir &e" + formatAmount(amount) + " $ &aüberwiesen."))); + target.sendMessage(new TextComponent(c("&7Dein neues Guthaben: &e" + + formatAmount(manager.getBalance(targetUUID)) + " $"))); + } + }); + } + + private static String formatAmount(double amount) { + return String.format("%,.2f", amount); + } + + private static String c(String msg) { + return ChatColor.translateAlternateColorCodes('&', msg); + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 0724c89..667b0fa 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -10,6 +10,17 @@ softdepend: - Geyser-BungeeCord commands: + # ── Economy ─────────────────────────────────────────────── + pay: + description: Überweise Geld an einen Spieler (auch offline) + usage: /pay + + ecoadmin: + description: Admin-Verwaltung des Economy-Systems + usage: /ecoadmin [Betrag] + permission: economy.admin + aliases: [ecomod, moneyadmin] + # ── VanishModule ────────────────────────────────────────── vanish: description: Vanish ein-/ausschalten diff --git a/src/main/resources/verify.properties b/src/main/resources/verify.properties index 434cac8..9f79df2 100644 --- a/src/main/resources/verify.properties +++ b/src/main/resources/verify.properties @@ -80,3 +80,16 @@ automessage.prefix= # Der Name der Datei, in der die Nachrichten stehen (liegt im Plugin-Ordner) automessage.file=messages.txt + + +# =========================== +# ECONOMY (Serverübergreifendes Geld) +# =========================== +# Alle Server (SurvivalPlus + StatusAPI/BungeeCord) müssen dieselbe Datenbank nutzen. +# Die Tabelle bc_accounts wird automatisch erstellt. +economy.mysql.host=localhost +economy.mysql.port=3306 +economy.mysql.database=survivalplus +economy.mysql.username=root +economy.mysql.password= +economy.start-balance=500.0