diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4dfce9 --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# CityBuildV2 + +CityBuildV2 ist ein **Spigot/Paper-Plugin** (Minecraft `1.19`) für den Betrieb eines CityBuild-/PlotSquared-Servernetzwerks. Es bündelt Moderations-Tools, Komfort-Commands für Spieler sowie mehrere Spielmechaniken (Booster, Food-Punkte, Farmwelt, Nether, Statusnachrichten, u. v. m.) in einem Plugin und speichert seine Daten über eine SQL-Datenbank. + +> Dieses Repository ist ein internes/privates Plugin-Projekt von JuByte und hängt von mehreren privat gehosteten Bibliotheken ab (siehe [Abhängigkeiten](#abhängigkeiten)). Ohne Zugriff auf die entsprechenden Repositories lässt sich das Projekt **nicht** bauen. + +## Features + +Das Plugin stellt u. a. folgende Befehle bereit (je nach `config.yml` einzeln aktivierbar): + +| Kategorie | Befehle | +|---|---| +| Moderation | `startkick`, `unstartkick`, `mutep`, `unmutep`, `commandspy`, `sudo`, `invsee`, `clearchat` | +| Spieler-Komfort | `back`, `teleport`/`tp`, `tphere`, `spawn`, `fly`, `speed`, `heal`, `feed`, `god`, `gamemode`, `glow`, `vanish`, `head` | +| Welt/Plot | `breakblock`, `farmworld`, `nether`, `signedit`, `checkplot` | +| Sonstiges | `status`, `giftrank`, `booster`, `food`, `slowchat`, `allornothing` (`ja`/`nein`) | + +Alle Namen, Aliase und Aktivierungs-Flags lassen sich in `config.yml` anpassen; alle Nachrichten und Berechtigungsnodes in `messages.yml`. + +## Voraussetzungen + +- **Java 16** (siehe `pom.xml`, `maven.compiler.source`/`target`) +- **Maven 3.6+** +- Ein Spigot/Paper-Server `1.19-R0.1-SNAPSHOT` +- Zugriff auf die privaten Maven-Repositories (siehe unten), da `DeveloperAPI`, `pretronicdatabasequery` und `pretroniclibraries` nicht öffentlich verfügbar sind +- Eine erreichbare **MySQL-Datenbank** (oder kompatibler Dialekt) für den produktiven Betrieb +- Optionale Softdependencies für vollen Funktionsumfang: `PlotSquared`, `ProtocolLib`, `AutoUpdaterAPI`, `JuByteLibraries` + +## Abhängigkeiten + +| Bibliothek | Zweck | +|---|---| +| `spigot` (1.19) | Server-API | +| `com.jubyte.developerapi:DeveloperAPI` | Command-Framework, Config-/Message-Handling | +| `net.pretronic.databasequery:pretronicdatabasequery-sql` | SQL-Query-Builder/ORM (verhindert SQL-Injection, siehe unten) | +| `net.pretronic.libraries:pretroniclibraries-caching` | In-Memory-Caches für Spielerdaten | +| `com.intellectualsites.plotsquared:plotsquared-core` | Plot-Integration | +| `net.dmulloy2:ProtocolLib` | Packet-basiertes Sign-Editing | +| `org.projectlombok:lombok` | Getter-Generierung | +| `org.bstats:bstats-bukkit` | anonyme Nutzungsstatistik | + +Diese werden über die in `pom.xml` hinterlegten Repositories (`repo.jubyte.com`, `repo.papermc.io`, `repo.codemc.org`, `maven.enginehub.org`, `repo.dmulloy2.net`) bezogen. + +## Build + +```bash +git clone +cd CityBuildV2 +mvn clean package +``` + +Das fertige Plugin liegt danach unter `target/CityBuildV2-.jar` (Shaded-Jar via `maven-shade-plugin`, `org.bstats` wird nach `com.jubyte` relokiert, um Konflikte mit anderen Plugins zu vermeiden). + +## Installation + +1. `CityBuildV2-.jar` in den `plugins`-Ordner des Servers kopieren. +2. Server einmal starten, damit `config.yml`, `messages.yml` und `mysql.yml` im Plugin-Datenordner (`plugins/CityBuildV2/`) erzeugt werden. +3. `mysql.yml` mit den echten Zugangsdaten der Zieldatenbank befüllen (siehe [Konfiguration](#konfiguration) und [Sicherheitshinweise](#sicherheitshinweise)). +4. Server neu starten oder `/reload` verwenden. + +## Konfiguration + +- **`config.yml`** – Name/Aliase/Aktivierung jedes einzelnen Befehls. +- **`messages.yml`** – alle Spielernachrichten, Präfix sowie Permission-Nodes pro Befehl (Format `citybuild..`). +- **`mysql.yml`** – Datenbankverbindung: + + ```yaml + MySQL: + dialectName: 'H2Portable' # oder 'MySQL', je nach genutztem Dialekt + Connection: 'CityBuildV2' + Host: 'localhost' + Port: 3306 + Database: 'CityBuildV2' + User: 'root' + Password: 'CHANGE_ME' + useSSL: false + ``` + + > Die im Repository ausgelieferte `mysql.yml` enthält lediglich **Platzhalter-Zugangsdaten** (`root` / `Test`) als Vorlage. Diese Datei niemals mit echten Produktivzugangsdaten committen – die tatsächliche Konfiguration entsteht erst im Plugin-Datenordner auf dem Server und gehört **nicht** ins Git-Repository. + +Fehlende Schlüssel werden beim Start automatisch aus den mitgelieferten Default-Dateien nachgetragen (`setConfigKeys()` / `setMessagesConfigKeys()` / `setMySQLConfigKeys()`), bestehende Werte bleiben dabei erhalten. + +## Sicherheitshinweise + +- Alle Datenbankzugriffe laufen über den `pretronicdatabasequery`-Query-Builder (`.where(...)`, `.set(...)`) statt über String-Verkettung – dadurch ist das Plugin durchgängig gegen klassische SQL-Injection abgesichert. +- Berechtigungen (`hasPermission(...)`) werden nicht über `plugin.yml` mit Default-Werten deklariert, sondern ausschließlich über ein externes Permission-Plugin (z. B. LuckPerms) vergeben. Ohne ein solches Plugin greifen serverseitig die Bukkit-Standards (i. d. R. nur OP). +- Sudo-, Invsee-, God- und CommandSpy-Befehle sind mächtige Admin-Tools – die zugehörigen Permission-Nodes sollten ausschließlich an vertrauenswürdiges Team-Personal vergeben werden. +- `mysql.yml` **niemals** mit echten Zugangsdaten versionieren (siehe oben). + +## Bekannte gefundene und behobene Probleme + +Im Rahmen einer Code-Durchsicht wurden folgende Fehler identifiziert und in diesem Repository bereits behoben: + +1. **`CommandSpyCommand`** – Wurde der Befehl von der Konsole statt einem Spieler ausgeführt, fehlte ein `return` nach der Fehlermeldung, wodurch anschließend eine `ClassCastException` beim Cast auf `Player` auftrat. +2. **`TeleportCommand`** – Die Prüfung der Koordinaten-Grenzen (`-3.0E7`/`3.0E7`, Weltgrenze) nutzte `||` statt `&&` und war dadurch praktisch immer `true`; Teleports weit außerhalb der Weltgrenze waren möglich. Zusätzlich fehlte eine Behandlung für ungültige (nicht-numerische) Koordinateneingaben (`NumberFormatException`). +3. **`AsyncPlayerChatListener`** – Eine Debug-Ausgabe (`System.out.println("Keine Rechte")`) war im Produktivcode verblieben; außerdem konnte ein `assert`, das standardmäßig zur Laufzeit deaktiviert ist, eine `NullPointerException` bei unbekannten Offline-Spielern nicht verhindern. +4. **Speicherleck bei `GodCommand` / `CommandSpyCommand`** – Ausgeloggte Spieler wurden nie aus der `playerGodList` bzw. der CommandSpy-`Map` entfernt; die Referenzen blieben dauerhaft im Speicher und führten bei erneutem Login zu inkonsistentem Zustand. Wird jetzt in `PlayerQuitListener` bereinigt. +5. **`VanishCommand` war komplett funktionslos** – der Befehl war nirgends registriert, referenzierte nicht existierende Config-/Message-Schlüssel und enthielt keinerlei Vanish-Logik. Der Befehl wurde vollständig neu implementiert (Sichtbarkeits-Toggle inkl. Bereinigung beim Logout) und in `CityBuildV2#loadCommands()` registriert; die fehlenden Einträge in `config.yml`/`messages.yml` wurden ergänzt. + +### Bekannte, noch offene Punkte + +- **`BreakblockCommand`**: Die verschachtelte Logik prüft nach einem bereits als „PlotSquared vorhanden“ bestätigten Zweig erneut `getPlugin("PlotSquared") == null` – dieser Zweig ist dadurch unerreichbar (toter Code). Zusätzlich wird eine `PlotArea` über `String.valueOf(y)` mit dem Weltnamen verglichen, was mit hoher Wahrscheinlichkeit nie zutrifft. Eine Korrektur erfordert Kenntnis der exakt genutzten PlotSquared-API-Version und wurde daher nicht blind verändert – vor einer Anpassung sollte die gewünschte Fachlogik (welche Welten/Areas ein Blockabbau ohne Plot-Prüfung erlauben) geklärt werden. +- Es existiert kein `permissions:`-Block in `plugin.yml`; alle Rechte werden ausschließlich über ein externes Permission-Plugin vergeben. Für Server ohne ein solches Plugin sind viele Befehle faktisch nur für Operatoren nutzbar. + +## Projektstruktur + +``` +src/main/java/com/jubyte/citybuild/ +├── CityBuildV2.java # Plugin-Einstiegspunkt (onEnable/onDisable, Registrierung) +├── commands/ # Ein Command pro Datei (+ Subcommands für farmworld/nether) +├── listener/ # Bukkit-Event-Listener (block, entity, inventory, player) +├── manager/ # In-Memory-Caches je Spieler-Datentyp (Cooldown, Glow, Status, …) +├── storage/ # SQL-Zugriffsschicht (Query-Builder, keine rohen SQL-Strings) +├── data/ # Zentraler Zugriff auf Config-/Message-Werte als statische Felder +└── utils/ # Hilfsklassen (u. a. PlotSquared-Utils, Sign-Editing) +``` + +## Mitwirkende + +- JuByte +- Justin_SGD +- KeinByte +- Forumat diff --git a/src/main/java/com/jubyte/citybuild/CityBuildV2.java b/src/main/java/com/jubyte/citybuild/CityBuildV2.java index 9aa7104..8ee3e87 100644 --- a/src/main/java/com/jubyte/citybuild/CityBuildV2.java +++ b/src/main/java/com/jubyte/citybuild/CityBuildV2.java @@ -265,6 +265,10 @@ private void loadCommands() { AbstractCommand backCommand = new BackCommand(); backCommand.register(); } + if (ConfigData.CONFIG_COMMAND_VANISH_ACTIVE) { + AbstractCommand vanishCommand = new VanishCommand(); + vanishCommand.register(); + } } private void loadListener() { diff --git a/src/main/java/com/jubyte/citybuild/commands/CommandSpyCommand.java b/src/main/java/com/jubyte/citybuild/commands/CommandSpyCommand.java index c812814..424d9a2 100644 --- a/src/main/java/com/jubyte/citybuild/commands/CommandSpyCommand.java +++ b/src/main/java/com/jubyte/citybuild/commands/CommandSpyCommand.java @@ -30,6 +30,7 @@ public boolean onCommand( CommandSender commandSender, Command command, String s, String[] strings) { if (!(commandSender instanceof Player)) { commandSender.sendMessage(MessagesData.ISNOT_PLAYER); + return false; } Player player = (Player) commandSender; if (player.hasPermission(MessagesData.COMMANDSPY_COMMAND_PERMISSION_USE)) { diff --git a/src/main/java/com/jubyte/citybuild/commands/TeleportCommand.java b/src/main/java/com/jubyte/citybuild/commands/TeleportCommand.java index 5c0c06d..31cd0ac 100644 --- a/src/main/java/com/jubyte/citybuild/commands/TeleportCommand.java +++ b/src/main/java/com/jubyte/citybuild/commands/TeleportCommand.java @@ -43,21 +43,26 @@ public boolean onCommand( "[targetPlayer]", strings[0])); } } else if (strings.length == 3) { - double x = Double.parseDouble(strings[0]); - double y = Double.parseDouble(strings[1]); - double z = Double.parseDouble(strings[2]); - if ((x < 3.0E7D) - || (y < 3.0E7D) - || (z < 3.0E7D) - || (x > -3.0E7D) - || (y > -3.0E7D) - || (z > -3.0E7D)) { - Location location = new Location(player.getWorld(), x, y, z); - player.teleport(location); - player.sendMessage( - MessagesData.TELEPORT_COMMAND_MESSAGE_TPTOLOCATION.replace( - "[location]", location.toString())); - } else { + try { + double x = Double.parseDouble(strings[0]); + double y = Double.parseDouble(strings[1]); + double z = Double.parseDouble(strings[2]); + // Bugfix: previously used OR instead of AND, which made this check almost always + // true and allowed teleports far outside the Minecraft world border. + if ((x > -3.0E7D && x < 3.0E7D) + && (y > -3.0E7D && y < 3.0E7D) + && (z > -3.0E7D && z < 3.0E7D)) { + Location location = new Location(player.getWorld(), x, y, z); + player.teleport(location); + player.sendMessage( + MessagesData.TELEPORT_COMMAND_MESSAGE_TPTOLOCATION.replace( + "[location]", location.toString())); + } else { + player.sendMessage(MessagesData.TELEPORT_COMMAND_MESSAGE_FALSELOCATION); + } + } catch (NumberFormatException exception) { + // Bugfix: invalid (non-numeric) coordinates previously threw an uncaught + // NumberFormatException that aborted command handling without feedback. player.sendMessage(MessagesData.TELEPORT_COMMAND_MESSAGE_FALSELOCATION); } } diff --git a/src/main/java/com/jubyte/citybuild/commands/VanishCommand.java b/src/main/java/com/jubyte/citybuild/commands/VanishCommand.java index 8b73608..43fe2a6 100644 --- a/src/main/java/com/jubyte/citybuild/commands/VanishCommand.java +++ b/src/main/java/com/jubyte/citybuild/commands/VanishCommand.java @@ -2,44 +2,88 @@ import com.jubyte.citybuild.CityBuildV2; import com.jubyte.citybuild.data.ConfigData; +import com.jubyte.citybuild.data.MessagesData; import com.jubyte.developerapi.commands.AbstractCommand; +import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + /** * @author Justin_SGD * @since 19.04.2023 + * + * Bugfix: this command previously did nothing at all - it wasn't registered in + * CityBuildV2#loadCommands(), had a constructor signature that didn't match how every other + * command is instantiated (new XCommand() with no args), referenced permission/message keys + * that didn't exist in messages.yml, and its onCommand() body never actually vanished anyone. + * It has been reimplemented to match the rest of the codebase and to actually toggle + * visibility for the executing player. */ - public class VanishCommand extends AbstractCommand { - private final CityBuildV2 cityBuildV2; + private static final Set VANISHED_PLAYERS = new HashSet<>(); + + public VanishCommand() { + super( + ConfigData.CONFIG_COMMAND_VANISH_NAME, + null, + "Go into vanish mode.", + ConfigData.CONFIG_COMMAND_VANISH_ALIASES); + } + + public static Set getVanishedPlayers() { + return Collections.unmodifiableSet(VANISHED_PLAYERS); + } + + public static boolean isVanished(UUID uuid) { + return VANISHED_PLAYERS.contains(uuid); + } + + /** Removes a player from the vanish list, e.g. when they log out, to avoid stale entries. */ + public static void clearVanish(UUID uuid) { + VANISHED_PLAYERS.remove(uuid); + } + + @Override + public boolean onCommand( + CommandSender commandSender, Command command, String s, String[] strings) { - public VanishCommand(CityBuildV2 cityBuildV2) { - super( - ConfigData.CONFIG_COMMAND_VANISH_NAME, - null, - "Go into vanish mode.", - ConfigData.CONFIG_COMMAND_VANISH_ALIASES); - this.cityBuildV2 = cityBuildV2; + if (!(commandSender instanceof Player)) { + commandSender.sendMessage(MessagesData.ISNOT_PLAYER); + return false; } - @Override - public boolean onCommand(CommandSender commandSender, Command command, String s, String[] strings) { - if(!commandSender.hasPermission(this.cityBuildV2.getMessageHandler() - .getString("Commands.Vanish.Permissions.Use"))) { - commandSender.sendMessage(this.cityBuildV2.getMessageHandler().getPrefixString("NoPerms")); - return false; - } - if (strings.length == 0) { - if(!(commandSender instanceof Player)) { - commandSender.sendMessage(this.cityBuildV2.getMessageHandler().getPrefixString("IsntPlayer")); - return false; - } - Player player = (Player) commandSender; - + Player player = (Player) commandSender; + + if (!player.hasPermission(MessagesData.VANISH_COMMAND_PERMISSION_USE)) { + player.sendMessage(MessagesData.NOPERMS); + return false; + } + + UUID uuid = player.getUniqueId(); + + if (VANISHED_PLAYERS.contains(uuid)) { + VANISHED_PLAYERS.remove(uuid); + for (Player online : Bukkit.getOnlinePlayers()) { + online.showPlayer(CityBuildV2.getPlugin(), player); + } + player.sendMessage(MessagesData.VANISH_COMMAND_MESSAGE_DEACTIVATED); + } else { + VANISHED_PLAYERS.add(uuid); + for (Player online : Bukkit.getOnlinePlayers()) { + if (!online.hasPermission(MessagesData.VANISH_COMMAND_PERMISSION_USE)) { + online.hidePlayer(CityBuildV2.getPlugin(), player); } - return false; + } + player.sendMessage(MessagesData.VANISH_COMMAND_MESSAGE_ACTIVATED); } -} \ No newline at end of file + + return false; + } +} diff --git a/src/main/java/com/jubyte/citybuild/data/MessagesData.java b/src/main/java/com/jubyte/citybuild/data/MessagesData.java index f497df8..841046d 100644 --- a/src/main/java/com/jubyte/citybuild/data/MessagesData.java +++ b/src/main/java/com/jubyte/citybuild/data/MessagesData.java @@ -635,4 +635,12 @@ public class MessagesData { messageHandler.getPrefixString("Commands.Back.Messages.NoBackPoint"); public static boolean BACK_COMMAND_SAVE_BACK_LOCATION_BY_DEATH = plugin.getMessagesConfig().getBoolean("Commands.Back.SaveBackLocationByDeath.Enabled"); + + /** Vanish Command */ + public static String VANISH_COMMAND_PERMISSION_USE = + messageHandler.getString("Commands.Vanish.Permissions.Use"); + public static String VANISH_COMMAND_MESSAGE_ACTIVATED = + messageHandler.getPrefixString("Commands.Vanish.Messages.Activated"); + public static String VANISH_COMMAND_MESSAGE_DEACTIVATED = + messageHandler.getPrefixString("Commands.Vanish.Messages.Deactivated"); } diff --git a/src/main/java/com/jubyte/citybuild/listener/player/AsyncPlayerChatListener.java b/src/main/java/com/jubyte/citybuild/listener/player/AsyncPlayerChatListener.java index d541055..33314a1 100644 --- a/src/main/java/com/jubyte/citybuild/listener/player/AsyncPlayerChatListener.java +++ b/src/main/java/com/jubyte/citybuild/listener/player/AsyncPlayerChatListener.java @@ -34,7 +34,6 @@ public void handleAsyncPlayerChat(AsyncPlayerChatEvent event) { } if (SlowChatCommand.SLOWCHAT_STATUS) { if (!player.hasPermission(MessagesData.SLOWCHAT_COMMAND_PERMISSION_BYPASS)) { - System.out.println("Keine Rechte"); if (!SLOW_CHAT_PLAYER.contains(player)) { SLOW_CHAT_PLAYER.add(player); Bukkit.getScheduler() @@ -56,7 +55,9 @@ public void handleAsyncPlayerChat(AsyncPlayerChatEvent event) { .replace("[reason]", mutepPlayer.getReason())); } else { String offlineCreator = Bukkit.getOfflinePlayer(mutepPlayer.getCreator()).getName(); - assert offlineCreator != null; + if (offlineCreator == null) { + offlineCreator = "Unknown"; + } player.sendMessage( MessagesData.MUTEP_COMMAND_MESSAGE_MUTE_SCREEN .replace("[player]", offlineCreator) diff --git a/src/main/java/com/jubyte/citybuild/listener/player/PlayerJoinListener.java b/src/main/java/com/jubyte/citybuild/listener/player/PlayerJoinListener.java index b3861df..98073e6 100644 --- a/src/main/java/com/jubyte/citybuild/listener/player/PlayerJoinListener.java +++ b/src/main/java/com/jubyte/citybuild/listener/player/PlayerJoinListener.java @@ -1,6 +1,7 @@ package com.jubyte.citybuild.listener.player; import com.jubyte.citybuild.CityBuildV2; +import com.jubyte.citybuild.commands.VanishCommand; import com.jubyte.citybuild.data.ConfigData; import com.jubyte.citybuild.data.MessagesData; import com.jubyte.citybuild.manager.checkplot.CheckPlotPlayer; @@ -75,5 +76,13 @@ public void handleJoin(PlayerJoinEvent event) { CheckPlotPlayer checkPlotPlayer = CityBuildV2.getPlugin().getCheckPlotCache().getPlayerByUUID(player.getUniqueId()); checkPlotPlayer.setLastJoin(); + + if (!player.hasPermission(MessagesData.VANISH_COMMAND_PERMISSION_USE)) { + for (Player online : Bukkit.getOnlinePlayers()) { + if (VanishCommand.isVanished(online.getUniqueId())) { + player.hidePlayer(CityBuildV2.getPlugin(), online); + } + } + } } } diff --git a/src/main/java/com/jubyte/citybuild/listener/player/PlayerQuitListener.java b/src/main/java/com/jubyte/citybuild/listener/player/PlayerQuitListener.java index 5f4e1e4..bc36588 100644 --- a/src/main/java/com/jubyte/citybuild/listener/player/PlayerQuitListener.java +++ b/src/main/java/com/jubyte/citybuild/listener/player/PlayerQuitListener.java @@ -1,6 +1,8 @@ package com.jubyte.citybuild.listener.player; import com.jubyte.citybuild.CityBuildV2; +import com.jubyte.citybuild.commands.GodCommand; +import com.jubyte.citybuild.commands.VanishCommand; import com.jubyte.developerapi.utils.config.MessageHandler; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -8,6 +10,7 @@ import org.bukkit.event.player.PlayerQuitEvent; import java.util.List; +import java.util.Map; public class PlayerQuitListener implements Listener { @@ -19,6 +22,15 @@ public void handlePlayerQuit(PlayerQuitEvent event) { Player player = event.getPlayer(); slowchatPlayer.remove(player); + // Bugfix: these collections stored raw Player references and were never cleaned up on + // logout, causing a memory leak (stale Player objects piling up for the lifetime of the + // server) and stale god-mode/commandspy state if the same player rejoined. + GodCommand.playerGodList.remove(player); + Map commandSpyMap = CityBuildV2.getPlugin().getCOMMANDSPY_MAP(); + commandSpyMap.remove(player); + commandSpyMap.values().removeIf(target -> target != null && target.equals(player)); + VanishCommand.clearVanish(player.getUniqueId()); + if (CityBuildV2.getPlugin().getMessagesConfig().getBoolean("Settings.PlayerQuit.Enabled")) { event.setQuitMessage( messageHandler diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 2d8b1e3..7812ff6 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -134,4 +134,8 @@ Settings: Back: Name: 'back' Aliases: [ 'zurück', 'lastdeathlocation' ] + Active: true + Vanish: + Name: 'vanish' + Aliases: [ 'v' ] Active: true \ No newline at end of file diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index d27d810..f3f6db0 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -442,6 +442,12 @@ Commands: NoBackPoint: '[prefix] &7Es konnte kein Todespunkt gefunden werden!' SaveBackLocationByDeath: Enabled: true + Vanish: + Permissions: + Use: 'citybuild.vanish.use' + Messages: + Activated: '[prefix] &7Du bist jetzt &eunsichtbar&7.' + Deactivated: '[prefix] &7Du bist jetzt wieder &esichtbar&7.' Settings: ColoredChat: