Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 <repo-url>
cd CityBuildV2
mvn clean package
```

Das fertige Plugin liegt danach unter `target/CityBuildV2-<version>.jar` (Shaded-Jar via `maven-shade-plugin`, `org.bstats` wird nach `com.jubyte` relokiert, um Konflikte mit anderen Plugins zu vermeiden).

## Installation

1. `CityBuildV2-<version>.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.<befehl>.<aktion>`).
- **`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
4 changes: 4 additions & 0 deletions src/main/java/com/jubyte/citybuild/CityBuildV2.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
35 changes: 20 additions & 15 deletions src/main/java/com/jubyte/citybuild/commands/TeleportCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
94 changes: 69 additions & 25 deletions src/main/java/com/jubyte/citybuild/commands/VanishCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<UUID> 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<UUID> 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);
}
}

return false;
}
}
8 changes: 8 additions & 0 deletions src/main/java/com/jubyte/citybuild/data/MessagesData.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
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;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;

import java.util.List;
import java.util.Map;

public class PlayerQuitListener implements Listener {

Expand All @@ -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<Player, Player> 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
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,8 @@ Settings:
Back:
Name: 'back'
Aliases: [ 'zurück', 'lastdeathlocation' ]
Active: true
Vanish:
Name: 'vanish'
Aliases: [ 'v' ]
Active: true
Loading