From f29bb25435df64a5d12dabc14a434f0a5521c7dd Mon Sep 17 00:00:00 2001 From: Git Manager GUI Date: Thu, 7 May 2026 22:02:37 +0200 Subject: [PATCH] Upload folder via GUI - BCEconomy_pl --- BCEconomy_pl/pom.xml | 92 +++++++ .../java/net/viper/bceconomy/BCDatabase.java | 204 +++++++++++++++ .../net/viper/bceconomy/BCEconomyPlugin.java | 97 +++++++ .../viper/bceconomy/BCEconomyProvider.java | 245 ++++++++++++++++++ BCEconomy_pl/src/main/resources/config.yml | 17 ++ BCEconomy_pl/src/main/resources/plugin.yml | 12 + 6 files changed, 667 insertions(+) create mode 100644 BCEconomy_pl/pom.xml create mode 100644 BCEconomy_pl/src/main/java/net/viper/bceconomy/BCDatabase.java create mode 100644 BCEconomy_pl/src/main/java/net/viper/bceconomy/BCEconomyPlugin.java create mode 100644 BCEconomy_pl/src/main/java/net/viper/bceconomy/BCEconomyProvider.java create mode 100644 BCEconomy_pl/src/main/resources/config.yml create mode 100644 BCEconomy_pl/src/main/resources/plugin.yml diff --git a/BCEconomy_pl/pom.xml b/BCEconomy_pl/pom.xml new file mode 100644 index 0000000..4185805 --- /dev/null +++ b/BCEconomy_pl/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + net.viper + BCEconomy + 1.0.0 + jar + + + 11 + 11 + UTF-8 + + + + + spigot-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + vault-repo + https://jitpack.io + + + + + + + org.spigotmc + spigot-api + 1.20.4-R0.1-SNAPSHOT + provided + + + + + com.github.MilkBowl + VaultAPI + 1.7 + provided + + + + + com.zaxxer + HikariCP + 5.1.0 + compile + + + + + com.mysql + mysql-connector-j + 8.3.0 + compile + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.2 + + + package + shade + + false + + + com.zaxxer.hikari + net.viper.bceconomy.libs.hikari + + + com.mysql + net.viper.bceconomy.libs.mysql + + + + + + + + + diff --git a/BCEconomy_pl/src/main/java/net/viper/bceconomy/BCDatabase.java b/BCEconomy_pl/src/main/java/net/viper/bceconomy/BCDatabase.java new file mode 100644 index 0000000..a308680 --- /dev/null +++ b/BCEconomy_pl/src/main/java/net/viper/bceconomy/BCDatabase.java @@ -0,0 +1,204 @@ +package net.viper.bceconomy; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import java.sql.*; +import java.util.UUID; +import java.util.logging.Logger; + +/** + * Verwaltet die MySQL-Verbindung zur bc_accounts Tabelle. + * Kein Cache – jeder Zugriff ist ein direkter DB-Query. + * Kompatibel mit der Tabelle die StatusAPI / SurvivalPlus anlegt. + */ +public class BCDatabase { + + private static final String TABLE = "bc_accounts"; + private static final String TABLE_NAMES = "bc_player_names"; + + private final Logger log; + private HikariDataSource dataSource; + + public BCDatabase(Logger log, String host, int port, String database, + String user, String password) { + this.log = log; + + HikariConfig cfg = new HikariConfig(); + cfg.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + database + + "?useSSL=false&autoReconnect=true" + + "&characterEncoding=UTF-8&useUnicode=true" + + "&allowPublicKeyRetrieval=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("BCEconomy"); + cfg.addDataSourceProperty("cachePrepStmts", "true"); + cfg.addDataSourceProperty("prepStmtCacheSize", "250"); + cfg.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + + try { + dataSource = new HikariDataSource(cfg); + } catch (Exception e) { + log.severe("[BCEconomy] MySQL-Verbindung fehlgeschlagen: " + e.getMessage()); + return; + } + + createTables(); + log.info("[BCEconomy] MySQL verbunden – Tabellen bereit."); + } + + private void createTables() { + // bc_accounts – kompatibel mit SurvivalPlus-Struktur (id + player_name + balance) + // Wir legen sie nur an wenn sie noch nicht existieren. + String createAccounts = + "CREATE TABLE IF NOT EXISTS `" + TABLE + "` (" + + " `id` INT(10) NOT NULL AUTO_INCREMENT," + + " `player_name` VARCHAR(50) NOT NULL," + + " `balance` DOUBLE(30,2) NOT NULL DEFAULT 0.00," + + " PRIMARY KEY (`id`)," + + " UNIQUE KEY `player_name` (`player_name`)" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; + + String createNames = + "CREATE TABLE IF NOT EXISTS `" + TABLE_NAMES + "` (" + + " `uuid` VARCHAR(36) NOT NULL PRIMARY KEY," + + " `name` VARCHAR(16) NOT NULL," + + " `updated` BIGINT NOT NULL" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; + + try (Connection con = dataSource.getConnection()) { + try (PreparedStatement ps = con.prepareStatement(createAccounts)) { + ps.executeUpdate(); + } + try (PreparedStatement ps = con.prepareStatement(createNames)) { + ps.executeUpdate(); + } + } catch (SQLException e) { + log.severe("[BCEconomy] Tabellen-Setup fehlgeschlagen: " + e.getMessage()); + } + } + + public boolean isConnected() { + return dataSource != null && !dataSource.isClosed(); + } + + public void close() { + if (isConnected()) dataSource.close(); + } + + // ──────────────────────────────────────────────── + // Balance – direkt aus DB (kein Cache) + // ──────────────────────────────────────────────── + + /** + * Liest Balance direkt aus der DB. + * @return Balance oder -1 wenn kein Konto vorhanden. + */ + public double loadBalance(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 rs.getDouble("balance"); + } + } catch (SQLException e) { + log.warning("[BCEconomy] loadBalance fehlgeschlagen für " + uuid + ": " + e.getMessage()); + } + return -1; + } + + /** + * Schreibt Balance direkt in die DB (INSERT oder UPDATE). + */ + public void saveBalance(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.setDouble(2, balance); + ps.executeUpdate(); + } catch (SQLException e) { + log.warning("[BCEconomy] saveBalance fehlgeschlagen für " + uuid + ": " + e.getMessage()); + } + } + + /** + * Legt ein neues Konto mit Startguthaben an, falls noch keines existiert. + */ + public void createAccountIfAbsent(UUID uuid, double startBalance) { + if (!isConnected()) return; + try (Connection con = dataSource.getConnection(); + PreparedStatement ps = con.prepareStatement( + "INSERT IGNORE INTO `" + TABLE + "` (`player_name`, `balance`) VALUES (?, ?)")) { + ps.setString(1, uuid.toString()); + ps.setDouble(2, startBalance); + ps.executeUpdate(); + } catch (SQLException e) { + log.warning("[BCEconomy] createAccount fehlgeschlagen für " + uuid + ": " + e.getMessage()); + } + } + + public boolean hasAccount(UUID uuid) { + return loadBalance(uuid) >= 0; + } + + // ──────────────────────────────────────────────── + // Name-Mapping (für Offline-Spieler Lookup) + // ──────────────────────────────────────────────── + + public void saveNameMapping(UUID uuid, String name) { + if (!isConnected()) return; + try (Connection con = dataSource.getConnection(); + PreparedStatement ps = con.prepareStatement( + "INSERT INTO `" + TABLE_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("[BCEconomy] saveNameMapping fehlgeschlagen: " + e.getMessage()); + } + } + + public UUID resolveUUIDByName(String name) { + if (!isConnected()) return null; + // 1. Eigene bc_player_names Tabelle + try (Connection con = dataSource.getConnection(); + PreparedStatement ps = con.prepareStatement( + "SELECT `uuid` FROM `" + TABLE_NAMES + "` WHERE `name` = ? LIMIT 1")) { + ps.setString(1, name); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return UUID.fromString(rs.getString("uuid")); + } + } + } catch (SQLException | IllegalArgumentException e) { + // ignorieren + } + // 2. CMI_users Fallback + 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 raw = rs.getString("player_uuid"); + if (raw != null && !raw.isEmpty()) return UUID.fromString(raw); + } + } + } catch (SQLException | IllegalArgumentException e) { + // CMI nicht installiert – kein Problem + } + return null; + } +} diff --git a/BCEconomy_pl/src/main/java/net/viper/bceconomy/BCEconomyPlugin.java b/BCEconomy_pl/src/main/java/net/viper/bceconomy/BCEconomyPlugin.java new file mode 100644 index 0000000..9f82d96 --- /dev/null +++ b/BCEconomy_pl/src/main/java/net/viper/bceconomy/BCEconomyPlugin.java @@ -0,0 +1,97 @@ +package net.viper.bceconomy; + +import net.milkbowl.vault.economy.Economy; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerLoginEvent; +import org.bukkit.plugin.ServicePriority; +import org.bukkit.plugin.java.JavaPlugin; + +/** + * BCEconomy – Spigot-Plugin für Lobby, Citybuild usw. + * + * Registriert sich als Vault-Economy-Provider und liest/schreibt + * den Kontostand direkt aus der gemeinsamen MySQL-Tabelle bc_accounts. + * Kein Cache – immer aktuell, serverübergreifend korrekt. + * + * Voraussetzung: Vault muss auf dem Server installiert sein. + * SurvivalPlus ist NICHT nötig – dieses Plugin ersetzt es für Lobby/Citybuild. + */ +public class BCEconomyPlugin extends JavaPlugin implements Listener { + + private BCDatabase database; + private BCEconomyProvider provider; + + @Override + public void onEnable() { + // Config laden / erzeugen + saveDefaultConfig(); + + String host = getConfig().getString ("mysql.host", "localhost"); + int port = getConfig().getInt ("mysql.port", 3306); + String database = getConfig().getString ("mysql.database", "Survival"); + String user = getConfig().getString ("mysql.username", "root"); + String password = getConfig().getString ("mysql.password", ""); + + String currName = getConfig().getString("economy.currency-name", "Dollar"); + String currPlural= getConfig().getString("economy.currency-name-plural", "Dollar"); + double startBal = getConfig().getDouble("economy.start-balance", 500.0); + int decimals = getConfig().getInt ("economy.decimals", 2); + + // DB verbinden + this.database = new BCDatabase(getLogger(), host, port, database, user, password); + + if (!this.database.isConnected()) { + getLogger().severe("[BCEconomy] Keine DB-Verbindung – Plugin wird deaktiviert."); + Bukkit.getPluginManager().disablePlugin(this); + return; + } + + // Vault prüfen + if (Bukkit.getPluginManager().getPlugin("Vault") == null) { + getLogger().severe("[BCEconomy] Vault nicht gefunden – Plugin wird deaktiviert."); + Bukkit.getPluginManager().disablePlugin(this); + return; + } + + // Economy-Provider registrieren + this.provider = new BCEconomyProvider(this.database, currName, currPlural, startBal, decimals); + getServer().getServicesManager().register( + Economy.class, this.provider, this, ServicePriority.Highest); + + // Login-Listener: Konto anlegen + Name-Mapping speichern + Bukkit.getPluginManager().registerEvents(this, this); + + getLogger().info("[BCEconomy] Aktiviert – Vault-Economy verbunden mit " + + database + "@" + host + ":" + port); + } + + @Override + public void onDisable() { + if (database != null) { + database.close(); + getLogger().info("[BCEconomy] MySQL-Verbindung geschlossen."); + } + } + + /** + * Bei jedem Login: Konto anlegen (falls neu) und Name speichern. + * Alles async – kein Blockieren des Main-Threads. + */ + @EventHandler(priority = EventPriority.MONITOR) + public void onLogin(PlayerLoginEvent event) { + if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) return; + Player player = event.getPlayer(); + Bukkit.getScheduler().runTaskAsynchronously(this, () -> { + database.saveNameMapping(player.getUniqueId(), player.getName()); + database.createAccountIfAbsent(player.getUniqueId(), + getConfig().getDouble("economy.start-balance", 500.0)); + }); + } + + public BCDatabase getDatabase() { return database; } + public BCEconomyProvider getProvider() { return provider; } +} diff --git a/BCEconomy_pl/src/main/java/net/viper/bceconomy/BCEconomyProvider.java b/BCEconomy_pl/src/main/java/net/viper/bceconomy/BCEconomyProvider.java new file mode 100644 index 0000000..b8b12d0 --- /dev/null +++ b/BCEconomy_pl/src/main/java/net/viper/bceconomy/BCEconomyProvider.java @@ -0,0 +1,245 @@ +package net.viper.bceconomy; + +import net.milkbowl.vault.economy.Economy; +import net.milkbowl.vault.economy.EconomyResponse; +import org.bukkit.OfflinePlayer; + +import java.util.List; +import java.util.UUID; + +/** + * Vault Economy-Implementierung. + * Jeder Aufruf geht direkt in die DB – kein Cache. + */ +public class BCEconomyProvider implements Economy { + + private final BCDatabase db; + private final String currencyName; + private final String currencyNamePlural; + private final double startBalance; + private final int decimals; + + public BCEconomyProvider(BCDatabase db, String currencyName, + String currencyNamePlural, + double startBalance, int decimals) { + this.db = db; + this.currencyName = currencyName; + this.currencyNamePlural = currencyNamePlural; + this.startBalance = startBalance; + this.decimals = decimals; + } + + // ──────────────────────────────────────────────── + // Meta + // ──────────────────────────────────────────────── + + @Override public boolean isEnabled() { return db.isConnected(); } + @Override public String getName() { return "BCEconomy"; } + @Override public boolean hasBankSupport() { return false; } + @Override public int fractionalDigits() { return decimals; } + @Override public String format(double amount) { + return String.format("%,." + decimals + "f " + currencyName, amount); + } + @Override public String currencyNamePlural() { return currencyNamePlural; } + @Override public String currencyNameSingular() { return currencyName; } + + // ──────────────────────────────────────────────── + // Account + // ──────────────────────────────────────────────── + + @Override + public boolean hasAccount(OfflinePlayer player) { + return db.hasAccount(player.getUniqueId()); + } + + @Override + public boolean hasAccount(OfflinePlayer player, String world) { + return hasAccount(player); + } + + @Override + public boolean createPlayerAccount(OfflinePlayer player) { + db.createAccountIfAbsent(player.getUniqueId(), startBalance); + if (player.getName() != null) + db.saveNameMapping(player.getUniqueId(), player.getName()); + return true; + } + + @Override + public boolean createPlayerAccount(OfflinePlayer player, String world) { + return createPlayerAccount(player); + } + + // ──────────────────────────────────────────────── + // Balance – direkt aus DB + // ──────────────────────────────────────────────── + + @Override + public double getBalance(OfflinePlayer player) { + double bal = db.loadBalance(player.getUniqueId()); + if (bal < 0) { + // Neuer Spieler – Konto anlegen + db.createAccountIfAbsent(player.getUniqueId(), startBalance); + if (player.getName() != null) + db.saveNameMapping(player.getUniqueId(), player.getName()); + return startBalance; + } + return bal; + } + + @Override + public double getBalance(OfflinePlayer player, String world) { + return getBalance(player); + } + + @Override + public boolean has(OfflinePlayer player, double amount) { + return getBalance(player) >= amount; + } + + @Override + public boolean has(OfflinePlayer player, String world, double amount) { + return has(player, amount); + } + + // ──────────────────────────────────────────────── + // Deposit / Withdraw – direkt in DB + // ──────────────────────────────────────────────── + + @Override + public EconomyResponse depositPlayer(OfflinePlayer player, double amount) { + if (amount < 0) + return new EconomyResponse(0, 0, EconomyResponse.ResponseType.FAILURE, + "Betrag darf nicht negativ sein."); + + double current = db.loadBalance(player.getUniqueId()); + if (current < 0) { + db.createAccountIfAbsent(player.getUniqueId(), startBalance); + current = startBalance; + } + double newBal = current + amount; + db.saveBalance(player.getUniqueId(), newBal); + return new EconomyResponse(amount, newBal, EconomyResponse.ResponseType.SUCCESS, null); + } + + @Override + public EconomyResponse depositPlayer(OfflinePlayer player, String world, double amount) { + return depositPlayer(player, amount); + } + + @Override + public EconomyResponse withdrawPlayer(OfflinePlayer player, double amount) { + if (amount < 0) + return new EconomyResponse(0, 0, EconomyResponse.ResponseType.FAILURE, + "Betrag darf nicht negativ sein."); + + double current = db.loadBalance(player.getUniqueId()); + if (current < 0) current = 0; + if (current < amount) + return new EconomyResponse(amount, current, EconomyResponse.ResponseType.FAILURE, + "Nicht genug Guthaben."); + + double newBal = current - amount; + db.saveBalance(player.getUniqueId(), newBal); + return new EconomyResponse(amount, newBal, EconomyResponse.ResponseType.SUCCESS, null); + } + + @Override + public EconomyResponse withdrawPlayer(OfflinePlayer player, String world, double amount) { + return withdrawPlayer(player, amount); + } + + // ──────────────────────────────────────────────── + // Deprecated String-Methoden (Vault-Kompatibilität) + // ──────────────────────────────────────────────── + + @Override @Deprecated + public boolean hasAccount(String playerName) { + UUID uuid = db.resolveUUIDByName(playerName); + return uuid != null && db.hasAccount(uuid); + } + + @Override @Deprecated + public boolean hasAccount(String playerName, String world) { return hasAccount(playerName); } + + @Override @Deprecated + public double getBalance(String playerName) { + UUID uuid = db.resolveUUIDByName(playerName); + if (uuid == null) return 0; + double bal = db.loadBalance(uuid); + return bal < 0 ? startBalance : bal; + } + + @Override @Deprecated + public double getBalance(String playerName, String world) { return getBalance(playerName); } + + @Override @Deprecated + public boolean has(String playerName, double amount) { return getBalance(playerName) >= amount; } + + @Override @Deprecated + public boolean has(String playerName, String world, double amount) { return has(playerName, amount); } + + @Override @Deprecated + public EconomyResponse depositPlayer(String playerName, double amount) { + UUID uuid = db.resolveUUIDByName(playerName); + if (uuid == null) + return new EconomyResponse(0, 0, EconomyResponse.ResponseType.FAILURE, "Spieler nicht gefunden."); + double current = db.loadBalance(uuid); + if (current < 0) current = 0; + double newBal = current + amount; + db.saveBalance(uuid, newBal); + return new EconomyResponse(amount, newBal, EconomyResponse.ResponseType.SUCCESS, null); + } + + @Override @Deprecated + public EconomyResponse depositPlayer(String playerName, String world, double amount) { + return depositPlayer(playerName, amount); + } + + @Override @Deprecated + public EconomyResponse withdrawPlayer(String playerName, double amount) { + UUID uuid = db.resolveUUIDByName(playerName); + if (uuid == null) + return new EconomyResponse(0, 0, EconomyResponse.ResponseType.FAILURE, "Spieler nicht gefunden."); + double current = db.loadBalance(uuid); + if (current < 0) current = 0; + if (current < amount) + return new EconomyResponse(amount, current, EconomyResponse.ResponseType.FAILURE, "Nicht genug Guthaben."); + double newBal = current - amount; + db.saveBalance(uuid, newBal); + return new EconomyResponse(amount, newBal, EconomyResponse.ResponseType.SUCCESS, null); + } + + @Override @Deprecated + public EconomyResponse withdrawPlayer(String playerName, String world, double amount) { + return withdrawPlayer(playerName, amount); + } + + @Override @Deprecated + public boolean createPlayerAccount(String playerName) { return false; } + + @Override @Deprecated + public boolean createPlayerAccount(String playerName, String world) { return false; } + + // ──────────────────────────────────────────────── + // Bank (nicht unterstützt) + // ──────────────────────────────────────────────── + + @Override public EconomyResponse createBank(String name, String player) { return notSupported(); } + @Override public EconomyResponse createBank(String name, OfflinePlayer p) { return notSupported(); } + @Override public EconomyResponse deleteBank(String name) { return notSupported(); } + @Override public EconomyResponse bankBalance(String name) { return notSupported(); } + @Override public EconomyResponse bankHas(String name, double amount) { return notSupported(); } + @Override public EconomyResponse bankWithdraw(String name, double amount) { return notSupported(); } + @Override public EconomyResponse bankDeposit(String name, double amount) { return notSupported(); } + @Override public EconomyResponse isBankOwner(String name, String p) { return notSupported(); } + @Override public EconomyResponse isBankOwner(String name, OfflinePlayer p) { return notSupported(); } + @Override public EconomyResponse isBankMember(String name, String p) { return notSupported(); } + @Override public EconomyResponse isBankMember(String name, OfflinePlayer p) { return notSupported(); } + @Override public List getBanks() { return List.of(); } + + private EconomyResponse notSupported() { + return new EconomyResponse(0, 0, EconomyResponse.ResponseType.NOT_IMPLEMENTED, + "Banken werden nicht unterstützt."); + } +} diff --git a/BCEconomy_pl/src/main/resources/config.yml b/BCEconomy_pl/src/main/resources/config.yml new file mode 100644 index 0000000..6b4ac54 --- /dev/null +++ b/BCEconomy_pl/src/main/resources/config.yml @@ -0,0 +1,17 @@ +# BCEconomy – Konfiguration +# Dieselben Zugangsdaten wie in StatusAPI (verify.properties: economy.mysql.*) + +mysql: + host: localhost + port: 3306 + database: Survival + username: root + password: "" + +economy: + currency-name: Dollar + currency-name-plural: Dollar + currency-symbol: "$" + start-balance: 500.0 + # Wie viele Dezimalstellen angezeigt werden (2 = xx.xx) + decimals: 2 diff --git a/BCEconomy_pl/src/main/resources/plugin.yml b/BCEconomy_pl/src/main/resources/plugin.yml new file mode 100644 index 0000000..7147ca7 --- /dev/null +++ b/BCEconomy_pl/src/main/resources/plugin.yml @@ -0,0 +1,12 @@ +name: BCEconomy +main: net.viper.bceconomy.BCEconomyPlugin +version: 1.0.0 +api-version: 1.20 +author: M_Viper +description: Serverübergreifendes Economy-System via MySQL (bc_accounts) + +depend: + - Vault + +softdepend: + - CMI