Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5b8717ad4 | ||
|
|
23e77d7204 | ||
|
|
27b5ca3c13 | ||
| 37a82ebd47 | |||
| c52ef617e9 | |||
| b7ec18cf21 | |||
| 15f09af83a | |||
| 326ddaa9af | |||
| ee1d705447 | |||
| 87fd77666a | |||
| c1154f93ae | |||
| 342fa394ca | |||
| b73a5950c7 | |||
| 918ad4e6bf | |||
| 0e5bc86455 | |||
| 57d0ad1a6c | |||
| d2ce86a5d5 | |||
| 7bb29b396b | |||
| dd1bb37886 | |||
| 32df7f142a | |||
| e48e687520 | |||
| 1d7c9e854b | |||
| 093d31725a | |||
| 8edc4885db | |||
| 4edbcc2386 | |||
| 51db120a1e | |||
| 95763ea9a6 | |||
| 2dc8511844 | |||
| 048c3eda30 | |||
| 2394ebe031 | |||
| 7808ecef97 | |||
| a4e884420b | |||
| 4402439ad4 | |||
| 11a49e9c58 | |||
| 5afde181d8 | |||
| 6f50a03c70 | |||
| e690f23c4e | |||
| bf231a39ad | |||
| 3864f40bac | |||
| d169f008f5 | |||
| 06414edeb4 | |||
| 76f143d44e | |||
| deea73b19d | |||
| 7c9e599150 | |||
| c951969812 | |||
| b680355fe9 | |||
| 46c8a6b3e1 | |||
| b9f1e224e3 | |||
| 2584abbd95 | |||
| f7979b5359 | |||
| d272e709e1 | |||
| 2609f783b2 | |||
| a534e6445b | |||
| b7dd32493b | |||
|
|
71534e7e06 | ||
|
|
d703187fae | ||
|
|
90cca7c559 | ||
| 7c8bf27628 |
275
README.md
275
README.md
@@ -2,41 +2,127 @@
|
|||||||
|
|
||||||
TeleportSuite ist ein umfassendes Teleport-Plugin für Paper/Spigot mit Homes, Warps, Portalen, Spawn-System, Savepoints, Back/Deathback, Teleportanfragen und BungeeCord-Unterstützung.
|
TeleportSuite ist ein umfassendes Teleport-Plugin für Paper/Spigot mit Homes, Warps, Portalen, Spawn-System, Savepoints, Back/Deathback, Teleportanfragen und BungeeCord-Unterstützung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Homes mit konfigurierbaren Limits
|
- Homes mit konfigurierbaren Limits pro Rang
|
||||||
- Warps mit optionalen Berechtigungen
|
- Warps mit optionalen Berechtigungen
|
||||||
- Portale für Server- oder Weltwechsel
|
- Portale für Server- oder Weltwechsel
|
||||||
- Spawn, FirstSpawn und Deathback
|
- Spawn, FirstSpawn und Deathback
|
||||||
- Teleportanfragen mit Accept/Deny
|
- Teleportanfragen (TPA) mit Accept/Deny
|
||||||
- Back- und Cooldown-/Warmup-System
|
- Back- und Cooldown-/Warmup-System
|
||||||
- Teleport zu Koordinaten, Welten und Spielern
|
- Teleport zu Koordinaten, Welten und Spielern
|
||||||
- Entity-Transport
|
- Entity-Transport
|
||||||
- SQLite- und MySQL-Unterstützung
|
- SQLite- und MySQL-Unterstützung
|
||||||
- BungeeCord-Unterstützung für verteilte Servernetzwerke
|
- BungeeCord-Unterstützung für verteilte Servernetzwerke
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
- Paper oder Spigot ab Minecraft 1.20
|
- Paper oder Spigot ab Minecraft 1.20
|
||||||
- Java 21 oder neuer
|
- Java 21 oder neuer
|
||||||
- Optional: MySQL, falls du nicht SQLite verwenden möchtest
|
- Optional: MySQL, falls mehrere Server auf dieselbe Datenbank zugreifen sollen
|
||||||
|
- Optional: BungeeCord-Proxy, falls du mehrere Server vernetzt hast
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Die fertige JAR-Datei in den Ordner plugins deines Servers kopieren.
|
### Einzelner Server (kein BungeeCord)
|
||||||
2. Den Server einmal starten, damit die Konfigurationsdatei und die Datenbank angelegt werden.
|
|
||||||
3. Den Server stoppen und die Datei config.yml nach Bedarf anpassen.
|
|
||||||
4. Den Server neu starten.
|
|
||||||
|
|
||||||
## Konfiguration
|
1. Die JAR-Datei in den `plugins`-Ordner deines Servers kopieren.
|
||||||
|
2. Server starten → Plugin legt `config.yml` und Datenbank automatisch an.
|
||||||
|
3. Server stoppen und `config.yml` nach Bedarf anpassen.
|
||||||
|
4. Server neu starten.
|
||||||
|
|
||||||
Die Datei config.yml wird beim ersten Start im Plugin-Ordner angelegt.
|
---
|
||||||
|
|
||||||
### Datenbank
|
### BungeeCord-Netzwerk ⚠️
|
||||||
|
|
||||||
|
> Wenn du mehrere Server mit BungeeCord verbunden hast und Spieler **serverübergreifend** teleportieren sollen, musst du das Plugin auf **jedem Spigot-Server UND auf dem BungeeCord-Proxy** installieren.
|
||||||
|
|
||||||
|
**Schritt-für-Schritt:**
|
||||||
|
|
||||||
|
**1. JAR auf jeden Spigot-Server kopieren**
|
||||||
|
|
||||||
|
Kopiere die `TeleportSuite.jar` in den `plugins`-Ordner von **jedem einzelnen** Spigot-Server in deinem Netzwerk (z.B. Lobby, Survival, Creative, ...).
|
||||||
|
|
||||||
|
**2. JAR auf den BungeeCord-Proxy kopieren**
|
||||||
|
|
||||||
|
Kopiere dieselbe `TeleportSuite.jar` auch in den `plugins`-Ordner deines **BungeeCord-Proxys**. Das Plugin erkennt automatisch ob es auf Spigot oder BungeeCord läuft.
|
||||||
|
|
||||||
|
**3. Jeden Server einmal starten**
|
||||||
|
|
||||||
|
Starte jeden Server einmal kurz, damit die `config.yml` angelegt wird. Dann stoppe ihn wieder.
|
||||||
|
|
||||||
|
**4. config.yml auf jedem Spigot-Server anpassen**
|
||||||
|
|
||||||
|
Öffne die `plugins/TeleportSuite/config.yml` auf **jedem Spigot-Server** und passe folgende Einstellungen an:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
bungee:
|
||||||
|
enabled: true # ← MUSS auf true stehen, sonst kein Cross-Server-Teleport!
|
||||||
|
server-name: "survival" # ← Den Namen dieses Servers eintragen (siehe unten)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Woher bekomme ich den `server-name`?**
|
||||||
|
> Öffne die `config.yml` deines **BungeeCord-Proxys** (nicht des Spigot-Servers!).
|
||||||
|
> Dort findest du einen Abschnitt wie diesen:
|
||||||
|
> ```yaml
|
||||||
|
> servers:
|
||||||
|
> lobby:
|
||||||
|
> address: localhost:25566
|
||||||
|
> survival:
|
||||||
|
> address: localhost:25567
|
||||||
|
> creative:
|
||||||
|
> address: localhost:25568
|
||||||
|
> ```
|
||||||
|
> Der `server-name` in der TeleportSuite-Config muss **exakt** dem Namen aus dieser Liste entsprechen.
|
||||||
|
> Beispiel: Auf dem Survival-Server trägst du `server-name: "survival"` ein.
|
||||||
|
> Auf dem Lobby-Server trägst du `server-name: "lobby"` ein. Und so weiter.
|
||||||
|
|
||||||
|
**5. Datenbank einrichten (empfohlen: MySQL)**
|
||||||
|
|
||||||
|
Damit alle Server auf dieselben Homes, Warps usw. zugreifen, solltest du MySQL verwenden und auf jedem Spigot-Server dieselben Zugangsdaten eintragen:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
database:
|
database:
|
||||||
type: sqlite
|
type: mysql
|
||||||
|
mysql:
|
||||||
|
host: localhost # Adresse deines MySQL-Servers
|
||||||
|
port: 3306
|
||||||
|
database: teleportsuite
|
||||||
|
username: root
|
||||||
|
password: deinPasswort
|
||||||
|
```
|
||||||
|
|
||||||
|
**6. Alle Server neu starten**
|
||||||
|
|
||||||
|
Starte jetzt alle Spigot-Server und den BungeeCord-Proxy neu. Wenn alles korrekt konfiguriert ist, siehst du beim Start des jeweiligen Spigot-Servers in der Konsole:
|
||||||
|
|
||||||
|
```
|
||||||
|
[TeleportSuite] BungeeCord-Unterstuetzung aktiviert. Server: survival
|
||||||
|
```
|
||||||
|
|
||||||
|
Falls du stattdessen diese Warnung siehst:
|
||||||
|
|
||||||
|
```
|
||||||
|
[TeleportSuite] BungeeCord deaktiviert (bungee.enabled=false).
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Dann hast du in der `config.yml` dieses Servers `enabled: false` stehen gelassen. Auf `true` ändern und neu starten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
Die vollständige `config.yml` mit allen Optionen:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
database:
|
||||||
|
type: sqlite # sqlite oder mysql
|
||||||
mysql:
|
mysql:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 3306
|
port: 3306
|
||||||
@@ -46,144 +132,101 @@ database:
|
|||||||
pool-size: 10
|
pool-size: 10
|
||||||
sqlite:
|
sqlite:
|
||||||
file: teleportsuite.db
|
file: teleportsuite.db
|
||||||
```
|
|
||||||
|
|
||||||
- `sqlite` ist die Standard-Einstellung und benötigt keine zusätzliche Serverdatenbank.
|
|
||||||
- `mysql` ist sinnvoll, wenn mehrere Server auf dieselben Teleport-Daten zugreifen sollen.
|
|
||||||
|
|
||||||
### BungeeCord
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
bungee:
|
bungee:
|
||||||
enabled: false
|
enabled: true
|
||||||
server-name: "survival"
|
server-name: "survival" # Name dieses Servers in der BungeeCord config.yml
|
||||||
```
|
|
||||||
|
|
||||||
- Aktiviere `enabled: true`, wenn das Plugin im BungeeCord-Setup genutzt werden soll.
|
|
||||||
- `server-name` muss dem Namen des Servers im Netzwerk entsprechen.
|
|
||||||
|
|
||||||
### Teleport
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
teleport:
|
teleport:
|
||||||
delay: 3
|
delay: 3 # Wartezeit in Sekunden vor dem Teleport (0 = sofort)
|
||||||
cooldown: 5
|
cooldown: 5 # Cooldown in Sekunden zwischen Teleports
|
||||||
warmup-cancel-on-move: true
|
warmup-cancel-on-move: true # Teleport abbrechen wenn Spieler sich bewegt
|
||||||
request-timeout: 60
|
request-timeout: 60 # Sekunden bis eine TPA-Anfrage automatisch verfällt
|
||||||
```
|
|
||||||
|
|
||||||
- `delay` ist die Wartezeit vor einem Teleport.
|
|
||||||
- `cooldown` verhindert zu häufige Teleports.
|
|
||||||
- `warmup-cancel-on-move` bricht den Teleport ab, wenn sich der Spieler bewegt.
|
|
||||||
- `request-timeout` legt fest, wie lange eine TPA-Anfrage gültig bleibt.
|
|
||||||
|
|
||||||
### Homes
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
homes:
|
homes:
|
||||||
max-homes-default: 3
|
max-homes-default: 3
|
||||||
max-homes-vip: 10
|
max-homes-vip: 10
|
||||||
max-homes-premium: 25
|
max-homes-premium: 25
|
||||||
```
|
|
||||||
|
|
||||||
### Warps
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
warps:
|
warps:
|
||||||
allow-player-warps: false
|
allow-player-warps: false
|
||||||
warp-permission-prefix: "teleportsuite.warp."
|
warp-permission-prefix: "teleportsuite.warp."
|
||||||
```
|
|
||||||
|
|
||||||
### Portale
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
portals:
|
portals:
|
||||||
check-interval: 5
|
check-interval: 5
|
||||||
particle-effect: true
|
particle-effect: true
|
||||||
```
|
|
||||||
|
|
||||||
### Spawn
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
spawn:
|
spawn:
|
||||||
first-join-teleport: true
|
first-join-teleport: true
|
||||||
death-respawn-to-spawn: false
|
death-respawn-to-spawn: false
|
||||||
```
|
```
|
||||||
|
|
||||||
### Nachrichten
|
---
|
||||||
|
|
||||||
Alle Nachrichten unterstützen `&`-Farbcodes und Platzhalter wie `{player}`, `{name}`, `{seconds}` und `{max}`.
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
| Command | Beschreibung | Permission |
|
| Command | Beschreibung | Permission |
|
||||||
| --- | --- | --- |
|
|---|---|---|
|
||||||
| /tp <player> | Teleportiere zu einem Spieler | teleportsuite.tp |
|
| `/tp <spieler>` | Teleportiere zu einem Spieler | `teleportsuite.tp` |
|
||||||
| /tphere <player> | Teleportiere einen Spieler zu dir | teleportsuite.tphere |
|
| `/tphere <spieler>` | Teleportiere einen Spieler zu dir | `teleportsuite.tphere` |
|
||||||
| /tpa <player> | Sende eine Teleportanfrage | teleportsuite.tpa |
|
| `/tpa <spieler>` | Sende eine Teleportanfrage | `teleportsuite.tpa` |
|
||||||
| /tpaccept | Akzeptiere eine Teleportanfrage | teleportsuite.tpaccept |
|
| `/tpaccept` | Akzeptiere eine Teleportanfrage | `teleportsuite.tpaccept` |
|
||||||
| /tpdeny | Lehne eine Teleportanfrage ab | teleportsuite.tpdeny |
|
| `/tpdeny` | Lehne eine Teleportanfrage ab | `teleportsuite.tpdeny` |
|
||||||
| /back | Teleportiere zur letzten Position | teleportsuite.back |
|
| `/back` | Teleportiere zur letzten Position | `teleportsuite.back` |
|
||||||
| /deathback | Teleportiere zum letzten Todesort | teleportsuite.deathback |
|
| `/deathback` | Teleportiere zum letzten Todesort | `teleportsuite.deathback` |
|
||||||
| /sethome [name] | Setze ein Home | teleportsuite.sethome |
|
| `/sethome [name]` | Setze ein Home | `teleportsuite.sethome` |
|
||||||
| /home [name] | Teleportiere zu einem Home | teleportsuite.home |
|
| `/home [name]` | Teleportiere zu einem Home | `teleportsuite.home` |
|
||||||
| /delhome <name> | Lösche ein Home | teleportsuite.delhome |
|
| `/delhome <name>` | Lösche ein Home | `teleportsuite.delhome` |
|
||||||
| /homes | Liste alle Homes auf | teleportsuite.home |
|
| `/homes` | Liste alle Homes auf | `teleportsuite.home` |
|
||||||
| /setwarp <name> | Setze einen Warp | teleportsuite.setwarp |
|
| `/setwarp <name>` | Setze einen Warp | `teleportsuite.setwarp` |
|
||||||
| /warp <name> | Teleportiere zu einem Warp | teleportsuite.warp |
|
| `/warp <name>` | Teleportiere zu einem Warp | `teleportsuite.warp` |
|
||||||
| /delwarp <name> | Lösche einen Warp | teleportsuite.delwarp |
|
| `/delwarp <name>` | Lösche einen Warp | `teleportsuite.delwarp` |
|
||||||
| /warps | Liste alle Warps auf | teleportsuite.warp |
|
| `/warps` | Liste alle Warps auf | `teleportsuite.warp` |
|
||||||
| /setportal <name> <target-server/world> | Erstelle ein Portal | teleportsuite.setportal |
|
| `/setportal <name> <server/welt>` | Erstelle ein Portal | `teleportsuite.setportal` |
|
||||||
| /delportal <name> | Lösche ein Portal | teleportsuite.delportal |
|
| `/delportal <name>` | Lösche ein Portal | `teleportsuite.delportal` |
|
||||||
| /portals | Liste alle Portale auf | teleportsuite.portals |
|
| `/portals` | Liste alle Portale auf | `teleportsuite.portals` |
|
||||||
| /setsavepoint [name] | Setze einen Savepoint | teleportsuite.savepoint |
|
| `/setsavepoint [name]` | Setze einen Savepoint | `teleportsuite.savepoint` |
|
||||||
| /savepoint [name] | Teleportiere zu einem Savepoint | teleportsuite.savepoint |
|
| `/savepoint [name]` | Teleportiere zu einem Savepoint | `teleportsuite.savepoint` |
|
||||||
| /setspawn | Setze den Spawn | teleportsuite.setspawn |
|
| `/setspawn` | Setze den Spawn | `teleportsuite.setspawn` |
|
||||||
| /spawn | Teleportiere zum Spawn | teleportsuite.spawn |
|
| `/spawn` | Teleportiere zum Spawn | `teleportsuite.spawn` |
|
||||||
| /setfirstspawn | Setze den FirstSpawn | teleportsuite.setfirstspawn |
|
| `/setfirstspawn` | Setze den FirstSpawn (für neue Spieler) | `teleportsuite.setfirstspawn` |
|
||||||
| /tppos <x> <y> <z> [world] | Teleportiere zu Koordinaten | teleportsuite.tppos |
|
| `/tppos <x> <y> <z> [welt]` | Teleportiere zu Koordinaten | `teleportsuite.tppos` |
|
||||||
| /tpall | Teleportiere alle Spieler zu dir | teleportsuite.tpall |
|
| `/tpall` | Teleportiere alle Spieler zu dir | `teleportsuite.tpall` |
|
||||||
| /tpworld <world> | Teleportiere in eine Welt | teleportsuite.tpworld |
|
| `/tpworld <welt>` | Teleportiere in eine Welt | `teleportsuite.tpworld` |
|
||||||
| /entitytransport <entity-id> <player/world> | Transportiere ein Entity | teleportsuite.entitytransport |
|
| `/entitytransport <id> <spieler/welt>` | Transportiere ein Entity | `teleportsuite.entitytransport` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Permissions
|
## Permissions
|
||||||
|
|
||||||
### Sammelrechte
|
| Permission | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| `teleportsuite.*` | Alle Rechte des Plugins |
|
||||||
|
| `teleportsuite.home.unlimited` | Unbegrenzte Homes (Standard: nur OPs) |
|
||||||
|
| `teleportsuite.nodelay` | Kein Teleport-Warmup |
|
||||||
|
| `teleportsuite.nocooldown` | Kein Teleport-Cooldown |
|
||||||
|
| `teleportsuite.admin` | Andere Spieler teleportieren (`/tp <von> <zu>`) |
|
||||||
|
|
||||||
- `teleportsuite.*` - Alle Rechte des Plugins
|
Alle weiteren Einzelrechte entsprechen dem jeweiligen Command-Namen, z.B. `teleportsuite.tp`, `teleportsuite.home`, `teleportsuite.warp` usw.
|
||||||
- `teleportsuite.home.unlimited` - Unbegrenzte Homes, standardmäßig nur für OPs
|
|
||||||
|
|
||||||
### Wichtige Einzelrechte
|
---
|
||||||
|
|
||||||
- `teleportsuite.tp`
|
## Häufige Probleme
|
||||||
- `teleportsuite.tphere`
|
|
||||||
- `teleportsuite.tpa`
|
**„Spieler XY nicht gefunden" beim serverübergreifenden `/tp`**
|
||||||
- `teleportsuite.tpaccept`
|
→ `bungee.enabled` in der `config.yml` steht auf `false`. Auf `true` setzen und neu starten.
|
||||||
- `teleportsuite.tpdeny`
|
|
||||||
- `teleportsuite.back`
|
**Spieler landet am Spawn statt beim Zielspieler**
|
||||||
- `teleportsuite.deathback`
|
→ `server-name` in der `config.yml` stimmt nicht mit dem Namen in der BungeeCord `config.yml` überein.
|
||||||
- `teleportsuite.sethome`
|
|
||||||
- `teleportsuite.home`
|
**TPA-Nachrichten kommen nicht an**
|
||||||
- `teleportsuite.delhome`
|
→ Das Plugin läuft nicht auf dem BungeeCord-Proxy. Die JAR muss auch in den `plugins`-Ordner des Proxys.
|
||||||
- `teleportsuite.setwarp`
|
|
||||||
- `teleportsuite.warp`
|
**Homes/Warps sind auf anderen Servern nicht verfügbar**
|
||||||
- `teleportsuite.delwarp`
|
→ Alle Server müssen dieselbe MySQL-Datenbank verwenden. SQLite funktioniert nur lokal auf einem Server.
|
||||||
- `teleportsuite.setportal`
|
|
||||||
- `teleportsuite.delportal`
|
---
|
||||||
- `teleportsuite.portals`
|
|
||||||
- `teleportsuite.savepoint`
|
|
||||||
- `teleportsuite.setspawn`
|
|
||||||
- `teleportsuite.spawn`
|
|
||||||
- `teleportsuite.setfirstspawn`
|
|
||||||
- `teleportsuite.tppos`
|
|
||||||
- `teleportsuite.tpall`
|
|
||||||
- `teleportsuite.tpworld`
|
|
||||||
- `teleportsuite.entitytransport`
|
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
Wenn du Probleme, Wünsche oder Fehlerberichte hast, eröffne ein Issue auf GitHub und füge wenn möglich den vollständigen Server-Log sowie deine `config.yml` bei.
|
Probleme, Wünsche oder Fehlerberichte bitte als Issue auf GitHub melden. Füge wenn möglich den vollständigen Server-Log sowie deine `config.yml` bei.
|
||||||
|
|
||||||
## Lizenz
|
|
||||||
|
|
||||||
Falls du eine Lizenz verwenden möchtest, ergänze sie bitte in diesem Abschnitt oder als separate LICENSE-Datei im Repository.
|
|
||||||
2
pom.xml
2
pom.xml
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<groupId>de.teleportsuite</groupId>
|
<groupId>de.teleportsuite</groupId>
|
||||||
<artifactId>TeleportSuite</artifactId>
|
<artifactId>TeleportSuite</artifactId>
|
||||||
<version>1.0.0</version>
|
<version>1.0.2</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package de.teleportsuite.bungee;
|
||||||
|
|
||||||
|
import de.teleportsuite.bungee.listener.PlayerConnectionListener;
|
||||||
|
import de.teleportsuite.bungee.manager.TeleportMessageListener;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.md_5.bungee.api.plugin.PluginManager;
|
||||||
|
|
||||||
|
public class TeleportSuiteBungee extends Plugin {
|
||||||
|
|
||||||
|
public static final String CHANNEL_TO_BUNGEE = "teleportsuite:tobungee";
|
||||||
|
public static final String CHANNEL_TO_SPIGOT = "teleportsuite:tospigot";
|
||||||
|
|
||||||
|
private static TeleportSuiteBungee instance;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
instance = this;
|
||||||
|
PluginManager pm = getProxy().getPluginManager();
|
||||||
|
|
||||||
|
getProxy().registerChannel(CHANNEL_TO_BUNGEE);
|
||||||
|
getProxy().registerChannel(CHANNEL_TO_SPIGOT);
|
||||||
|
|
||||||
|
pm.registerListener(this, new TeleportMessageListener(this));
|
||||||
|
pm.registerListener(this, new PlayerConnectionListener(this));
|
||||||
|
|
||||||
|
getLogger().info("TeleportSuite-Bungee aktiviert.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
getProxy().getScheduler().cancel(this);
|
||||||
|
getLogger().info("TeleportSuite-Bungee deaktiviert.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TeleportSuiteBungee getInstance() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package de.teleportsuite.bungee.listener;
|
||||||
|
|
||||||
|
import de.teleportsuite.bungee.TeleportSuiteBungee;
|
||||||
|
import de.teleportsuite.bungee.manager.PendingTpa;
|
||||||
|
import de.teleportsuite.bungee.manager.TeleportMessageListener;
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
|
||||||
|
import net.md_5.bungee.api.plugin.Listener;
|
||||||
|
import net.md_5.bungee.event.EventHandler;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up pending TPA requests when a player disconnects.
|
||||||
|
*/
|
||||||
|
public class PlayerConnectionListener implements Listener {
|
||||||
|
|
||||||
|
private final TeleportSuiteBungee plugin;
|
||||||
|
|
||||||
|
public PlayerConnectionListener(TeleportSuiteBungee plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onDisconnect(PlayerDisconnectEvent event) {
|
||||||
|
String name = event.getPlayer().getName();
|
||||||
|
|
||||||
|
// Player was a requester
|
||||||
|
PendingTpa tpa = TeleportMessageListener.getPendingTpa().remove(name);
|
||||||
|
if (tpa != null) {
|
||||||
|
notifyOther(tpa.getToName(), name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player was a target — find the requester
|
||||||
|
TeleportMessageListener.getPendingTpa().entrySet().removeIf(entry -> {
|
||||||
|
if (entry.getValue().getToName().equalsIgnoreCase(name)) {
|
||||||
|
notifyOther(entry.getValue().getFromName(), name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyOther(String otherName, String disconnectedName) {
|
||||||
|
ProxiedPlayer other = plugin.getProxy().getPlayer(otherName);
|
||||||
|
if (other == null || other.getServer() == null) return;
|
||||||
|
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream out = new DataOutputStream(bos)) {
|
||||||
|
out.writeUTF("SEND_MSG");
|
||||||
|
out.writeUTF(otherName);
|
||||||
|
out.writeUTF("§cDeine TPA-Anfrage wurde abgebrochen, da §6" + disconnectedName + " §cdas Netzwerk verlassen hat.");
|
||||||
|
other.getServer().sendData(TeleportSuiteBungee.CHANNEL_TO_SPIGOT, bos.toByteArray());
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().warning("Disconnect-Benachrichtigung fehlgeschlagen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package de.teleportsuite.bungee.manager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a pending cross-server TPA request.
|
||||||
|
*/
|
||||||
|
public class PendingTpa {
|
||||||
|
|
||||||
|
public enum Type { TPTO, TPHERE }
|
||||||
|
|
||||||
|
private final String fromName;
|
||||||
|
private final String toName;
|
||||||
|
private final Type type;
|
||||||
|
|
||||||
|
public PendingTpa(String fromName, String toName, Type type) {
|
||||||
|
this.fromName = fromName;
|
||||||
|
this.toName = toName;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFromName() { return fromName; }
|
||||||
|
public String getToName() { return toName; }
|
||||||
|
public Type getType() { return type; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,413 @@
|
|||||||
|
package de.teleportsuite.bungee.manager;
|
||||||
|
|
||||||
|
import de.teleportsuite.bungee.TeleportSuiteBungee;
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.md_5.bungee.api.connection.Server;
|
||||||
|
import net.md_5.bungee.api.event.PluginMessageEvent;
|
||||||
|
import net.md_5.bungee.api.plugin.Listener;
|
||||||
|
import net.md_5.bungee.event.EventHandler;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives plugin messages from Spigot servers on CHANNEL_TO_BUNGEE,
|
||||||
|
* performs the cross-server operation (connect + forward), and sends
|
||||||
|
* results back on CHANNEL_TO_SPIGOT.
|
||||||
|
*
|
||||||
|
* Payload identifiers (first UTF written by Spigot):
|
||||||
|
*
|
||||||
|
* TP_PLAYER – teleport sender to a named target player
|
||||||
|
* TP_POS – teleport player to fixed coordinates on a server
|
||||||
|
* TP_HERE – pull a remote player to the sender's location
|
||||||
|
* TP_ALL – pull all network players to sender
|
||||||
|
* TPA_REQUEST – forward "you have a TPA request" to target
|
||||||
|
* TPA_ACCEPT – accept pending TPA; trigger the actual teleport
|
||||||
|
* TPA_DENY – deny pending TPA; notify requester
|
||||||
|
* TPA_CANCEL – requester cancels their own pending request
|
||||||
|
* TPA_EXPIRED – timeout: notify both parties
|
||||||
|
* SEND_MSG – route a chat message to a player on any server
|
||||||
|
*/
|
||||||
|
public class TeleportMessageListener implements Listener {
|
||||||
|
|
||||||
|
// fromName → pending request
|
||||||
|
private static final Map<String, PendingTpa> pendingTpa = new HashMap<>();
|
||||||
|
|
||||||
|
private final TeleportSuiteBungee plugin;
|
||||||
|
|
||||||
|
public TeleportMessageListener(TeleportSuiteBungee plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, PendingTpa> getPendingTpa() {
|
||||||
|
return pendingTpa;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Incoming messages from Spigot
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPluginMessage(PluginMessageEvent event) throws IOException {
|
||||||
|
if (event.isCancelled()) return;
|
||||||
|
if (!(event.getSender() instanceof Server)) return;
|
||||||
|
if (!event.getTag().equals(TeleportSuiteBungee.CHANNEL_TO_BUNGEE)) return;
|
||||||
|
|
||||||
|
DataInputStream in = new DataInputStream(new ByteArrayInputStream(event.getData()));
|
||||||
|
String task = in.readUTF();
|
||||||
|
|
||||||
|
switch (task) {
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// /tp <target> → teleport sender directly to target player
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
case "TP_PLAYER": {
|
||||||
|
String senderName = in.readUTF();
|
||||||
|
String targetName = in.readUTF();
|
||||||
|
|
||||||
|
ProxiedPlayer sender = plugin.getProxy().getPlayer(senderName);
|
||||||
|
ProxiedPlayer target = plugin.getProxy().getPlayer(targetName);
|
||||||
|
|
||||||
|
if (sender == null) return;
|
||||||
|
if (target == null) {
|
||||||
|
sendMsg(sender, "§cSpieler §6" + targetName + " §cnicht gefunden.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
teleportPlayerToPlayer(sender, target, 50);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// /tp <from> <to> → teleport another player to a target player
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
case "TP_PLAYER_TO_PLAYER": {
|
||||||
|
String requesterName = in.readUTF();
|
||||||
|
String fromName = in.readUTF();
|
||||||
|
String targetName = in.readUTF();
|
||||||
|
|
||||||
|
ProxiedPlayer requester = plugin.getProxy().getPlayer(requesterName);
|
||||||
|
ProxiedPlayer from = plugin.getProxy().getPlayer(fromName);
|
||||||
|
ProxiedPlayer target = plugin.getProxy().getPlayer(targetName);
|
||||||
|
|
||||||
|
if (requester == null) return;
|
||||||
|
if (from == null || target == null) {
|
||||||
|
sendMsg(requester, "§cEin Spieler wurde nicht gefunden.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
teleportPlayerToPlayer(from, target, 50);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// /tphere <target> → pull target to sender's location
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
case "TP_HERE": {
|
||||||
|
String senderName = in.readUTF();
|
||||||
|
String targetName = in.readUTF();
|
||||||
|
|
||||||
|
ProxiedPlayer sender = plugin.getProxy().getPlayer(senderName);
|
||||||
|
ProxiedPlayer target = plugin.getProxy().getPlayer(targetName);
|
||||||
|
|
||||||
|
if (sender == null) return;
|
||||||
|
if (target == null) {
|
||||||
|
sendMsg(sender, "§cSpieler §6" + targetName + " §cnicht gefunden.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
teleportPlayerToPlayer(target, sender, 50);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// /tpall → pull every player on the network to sender
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
case "TP_ALL": {
|
||||||
|
String senderName = in.readUTF();
|
||||||
|
ProxiedPlayer sender = plugin.getProxy().getPlayer(senderName);
|
||||||
|
if (sender == null) return;
|
||||||
|
|
||||||
|
for (ProxiedPlayer p : plugin.getProxy().getPlayers()) {
|
||||||
|
if (!p.getName().equals(senderName)) {
|
||||||
|
teleportPlayerToPlayer(p, sender, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// /home, /warp, /spawn, /back, /savepoint, /portal, /tppos …
|
||||||
|
// Spigot already knows the coordinates; we just switch the server
|
||||||
|
// and tell that server where to put the player.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
case "TP_POS": {
|
||||||
|
String playerName = in.readUTF();
|
||||||
|
String server = in.readUTF();
|
||||||
|
String world = in.readUTF();
|
||||||
|
double x = in.readDouble(), y = in.readDouble(), z = in.readDouble();
|
||||||
|
float yaw = in.readFloat(), pitch = in.readFloat();
|
||||||
|
|
||||||
|
ProxiedPlayer player = plugin.getProxy().getPlayer(playerName);
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
teleportPlayerToPosition(player, server, world, x, y, z, yaw, pitch, 100);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// TPA: requester asks target for a teleport
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
case "TPA_REQUEST": {
|
||||||
|
String fromName = in.readUTF();
|
||||||
|
String toName = in.readUTF();
|
||||||
|
String type = in.readUTF(); // TPTO or TPHERE
|
||||||
|
String msgToTarget = in.readUTF();
|
||||||
|
String msgToRequester = in.readUTF();
|
||||||
|
|
||||||
|
ProxiedPlayer from = plugin.getProxy().getPlayer(fromName);
|
||||||
|
ProxiedPlayer to = plugin.getProxy().getPlayer(toName);
|
||||||
|
|
||||||
|
if (from == null) return;
|
||||||
|
if (to == null) {
|
||||||
|
sendMsg(from, "§cSpieler §6" + toName + " §cnicht gefunden.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing pending request
|
||||||
|
if (pendingTpa.containsKey(fromName)) {
|
||||||
|
sendMsg(from, "§cDu hast bereits eine offene Anfrage.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingTpa.put(fromName, new PendingTpa(fromName, toName, PendingTpa.Type.valueOf(type)));
|
||||||
|
|
||||||
|
// Notify both players — via their respective servers
|
||||||
|
sendMsgToServer(from.getServer().getInfo(), fromName, msgToRequester);
|
||||||
|
sendMsgToServer(to.getServer().getInfo(), toName, msgToTarget);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// TPA: target accepts → trigger the actual teleport
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
case "TPA_ACCEPT": {
|
||||||
|
String toName = in.readUTF(); // player who accepted
|
||||||
|
String fromName = in.readUTF(); // original requester (may be "nu" = any)
|
||||||
|
String errMsg = in.readUTF();
|
||||||
|
|
||||||
|
PendingTpa tpa = resolvePending(fromName, toName);
|
||||||
|
if (tpa == null) {
|
||||||
|
ProxiedPlayer to = plugin.getProxy().getPlayer(toName);
|
||||||
|
if (to != null) sendMsg(to, errMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingTpa.remove(tpa.getFromName());
|
||||||
|
|
||||||
|
ProxiedPlayer from = plugin.getProxy().getPlayer(tpa.getFromName());
|
||||||
|
ProxiedPlayer to = plugin.getProxy().getPlayer(tpa.getToName());
|
||||||
|
|
||||||
|
if (from == null || to == null) return;
|
||||||
|
|
||||||
|
if (tpa.getType() == PendingTpa.Type.TPTO) {
|
||||||
|
teleportPlayerToPlayer(from, to, 50);
|
||||||
|
} else {
|
||||||
|
teleportPlayerToPlayer(to, from, 50);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// TPA: target denies → notify requester
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
case "TPA_DENY": {
|
||||||
|
String toName = in.readUTF();
|
||||||
|
String fromName = in.readUTF();
|
||||||
|
String msg = in.readUTF();
|
||||||
|
|
||||||
|
PendingTpa tpa = resolvePending(fromName, toName);
|
||||||
|
if (tpa == null) return;
|
||||||
|
|
||||||
|
pendingTpa.remove(tpa.getFromName());
|
||||||
|
|
||||||
|
ProxiedPlayer from = plugin.getProxy().getPlayer(tpa.getFromName());
|
||||||
|
ProxiedPlayer to = plugin.getProxy().getPlayer(tpa.getToName());
|
||||||
|
|
||||||
|
if (from != null) sendMsg(from, msg);
|
||||||
|
if (to != null) sendMsg(to, msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// TPA: requester cancels their own request
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
case "TPA_CANCEL": {
|
||||||
|
String fromName = in.readUTF();
|
||||||
|
String msgFrom = in.readUTF();
|
||||||
|
String msgTo = in.readUTF();
|
||||||
|
|
||||||
|
PendingTpa tpa = pendingTpa.remove(fromName);
|
||||||
|
if (tpa == null) return;
|
||||||
|
|
||||||
|
ProxiedPlayer from = plugin.getProxy().getPlayer(fromName);
|
||||||
|
ProxiedPlayer to = plugin.getProxy().getPlayer(tpa.getToName());
|
||||||
|
|
||||||
|
if (from != null) sendMsg(from, msgFrom);
|
||||||
|
if (to != null) sendMsg(to, msgTo);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// TPA: timeout — remove pending and notify both sides
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
case "TPA_EXPIRED": {
|
||||||
|
String fromName = in.readUTF();
|
||||||
|
String toName = in.readUTF();
|
||||||
|
String msgFrom = in.readUTF();
|
||||||
|
String msgTo = in.readUTF();
|
||||||
|
|
||||||
|
pendingTpa.remove(fromName);
|
||||||
|
|
||||||
|
ProxiedPlayer from = plugin.getProxy().getPlayer(fromName);
|
||||||
|
ProxiedPlayer to = plugin.getProxy().getPlayer(toName);
|
||||||
|
|
||||||
|
if (from != null) sendMsg(from, msgFrom);
|
||||||
|
if (to != null) sendMsg(to, msgTo);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Generic cross-server message routing
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
case "SEND_MSG": {
|
||||||
|
String targetName = in.readUTF();
|
||||||
|
String message = in.readUTF();
|
||||||
|
ProxiedPlayer target = plugin.getProxy().getPlayer(targetName);
|
||||||
|
if (target != null) sendMsg(target, message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
plugin.getLogger().warning("Unbekannter Task: " + task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Core teleport helpers — the BTM way
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves {@code sender} to {@code target}'s current server (if needed),
|
||||||
|
* then after a short delay tells the target server to do the final
|
||||||
|
* player.teleport(target) call via TP_PLAYERTOPLAYER.
|
||||||
|
*
|
||||||
|
* This is done entirely on the Bungee side:
|
||||||
|
* 1. sender.connect(target.server) — BungeeCord switches the server
|
||||||
|
* 2. delay ms later: server.sendData(TP_PLAYERTOPLAYER, ...)
|
||||||
|
* → Spigot polls until both players are on the same server and teleports
|
||||||
|
*/
|
||||||
|
private void teleportPlayerToPlayer(ProxiedPlayer sender, ProxiedPlayer target, int delayMs) {
|
||||||
|
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||||
|
if (sender == null || target == null) return;
|
||||||
|
if (sender.getServer() == null || target.getServer() == null) return;
|
||||||
|
|
||||||
|
// Switch server if needed
|
||||||
|
if (!sender.getServer().getInfo().getName()
|
||||||
|
.equals(target.getServer().getInfo().getName())) {
|
||||||
|
sender.connect(target.getServer().getInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell the target server to teleport sender → target
|
||||||
|
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream out = new DataOutputStream(bos)) {
|
||||||
|
out.writeUTF("TP_PLAYERTOPLAYER");
|
||||||
|
out.writeUTF(sender.getName());
|
||||||
|
out.writeUTF(target.getName());
|
||||||
|
target.getServer().sendData(TeleportSuiteBungee.CHANNEL_TO_SPIGOT, bos.toByteArray());
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().warning("TP_PLAYERTOPLAYER Fehler: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}, delayMs, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves {@code player} to {@code server}, then after a delay sends
|
||||||
|
* TP_PLAYERTOPOSITION so Spigot teleports them to exact coordinates.
|
||||||
|
*/
|
||||||
|
private void teleportPlayerToPosition(ProxiedPlayer player, String server,
|
||||||
|
String world, double x, double y, double z,
|
||||||
|
float yaw, float pitch, int delayMs) {
|
||||||
|
if (!plugin.getProxy().getServers().containsKey(server)) {
|
||||||
|
sendMsg(player, "§cServer §6" + server + " §cnicht gefunden.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch server if needed
|
||||||
|
if (!player.getServer().getInfo().getName().equals(server)) {
|
||||||
|
player.connect(plugin.getProxy().getServerInfo(server));
|
||||||
|
}
|
||||||
|
|
||||||
|
// After delay, tell the (now correct) server where to put the player
|
||||||
|
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||||
|
if (player.getServer() == null) return;
|
||||||
|
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream out = new DataOutputStream(bos)) {
|
||||||
|
out.writeUTF("TP_PLAYERTOPOSITION");
|
||||||
|
out.writeUTF(player.getName());
|
||||||
|
out.writeUTF(world);
|
||||||
|
out.writeDouble(x);
|
||||||
|
out.writeDouble(y);
|
||||||
|
out.writeDouble(z);
|
||||||
|
out.writeFloat(yaw);
|
||||||
|
out.writeFloat(pitch);
|
||||||
|
player.getServer().sendData(TeleportSuiteBungee.CHANNEL_TO_SPIGOT, bos.toByteArray());
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().warning("TP_PLAYERTOPOSITION Fehler: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}, delayMs, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Resolves which pending TPA entry matches an accept/deny. */
|
||||||
|
private PendingTpa resolvePending(String fromName, String toName) {
|
||||||
|
// Exact match
|
||||||
|
if (!fromName.equals("nu") && pendingTpa.containsKey(fromName)) {
|
||||||
|
return pendingTpa.get(fromName);
|
||||||
|
}
|
||||||
|
// Find by target name only
|
||||||
|
for (PendingTpa tpa : pendingTpa.values()) {
|
||||||
|
if (tpa.getToName().equalsIgnoreCase(toName)) {
|
||||||
|
return tpa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sends a formatted chat message directly to a ProxiedPlayer. */
|
||||||
|
private void sendMsg(ProxiedPlayer player, String message) {
|
||||||
|
player.sendMessage(net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routes a chat message to a named player via their Spigot server.
|
||||||
|
* The Spigot side handles the actual sendMessage() call.
|
||||||
|
*/
|
||||||
|
private void sendMsgToServer(net.md_5.bungee.api.config.ServerInfo server,
|
||||||
|
String playerName, String message) {
|
||||||
|
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream out = new DataOutputStream(bos)) {
|
||||||
|
out.writeUTF("SEND_MSG");
|
||||||
|
out.writeUTF(playerName);
|
||||||
|
out.writeUTF(message);
|
||||||
|
server.sendData(TeleportSuiteBungee.CHANNEL_TO_SPIGOT, bos.toByteArray());
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().warning("SEND_MSG Fehler: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,105 +1,277 @@
|
|||||||
package de.teleportsuite.bungeemessaging;
|
package de.teleportsuite.bungeemessaging;
|
||||||
|
|
||||||
import de.teleportsuite.TeleportSuite;
|
import de.teleportsuite.TeleportSuite;
|
||||||
|
import de.teleportsuite.models.TeleportLocation;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.plugin.messaging.PluginMessageListener;
|
import org.bukkit.plugin.messaging.PluginMessageListener;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles BungeeCord Plugin Messaging.
|
* Spigot-seitiger Kanal zum TeleportSuite-Bungee-Plugin.
|
||||||
* Sends players to other servers and transmits target coordinates
|
|
||||||
* so a receiving TeleportSuite instance can teleport them on arrival.
|
|
||||||
*
|
*
|
||||||
* Channel "teleportsuite:tp" payload format (DataOutputStream):
|
* Prinzip (identisch zu BTM):
|
||||||
* String targetPlayer
|
* Spigot schickt Anfragen auf CHANNEL_TO_BUNGEE.
|
||||||
* String world
|
* BungeeCord erledigt connect() + routing und schickt Befehle auf CHANNEL_TO_SPIGOT.
|
||||||
* Double x, y, z
|
* Spigot führt den finalen player.teleport() aus.
|
||||||
* Float yaw, pitch
|
*
|
||||||
|
* Payloads Spigot → Bungee (CHANNEL_TO_BUNGEE):
|
||||||
|
* TP_PLAYER senderName, targetName
|
||||||
|
* TP_PLAYER_TO_PLAYER requesterName, fromName, toName
|
||||||
|
* TP_HERE senderName, targetName
|
||||||
|
* TP_ALL senderName
|
||||||
|
* TP_POS playerName, server, world, x, y, z, yaw, pitch
|
||||||
|
* TPA_REQUEST fromName, toName, type, msgToTarget, msgToRequester
|
||||||
|
* TPA_ACCEPT toName, fromName, errMsg
|
||||||
|
* TPA_DENY toName, fromName, msg
|
||||||
|
* TPA_CANCEL fromName, msgFrom, msgTo
|
||||||
|
* TPA_EXPIRED fromName, toName, msgFrom, msgTo
|
||||||
|
* SEND_MSG targetName, message
|
||||||
|
*
|
||||||
|
* Payloads Bungee → Spigot (CHANNEL_TO_SPIGOT):
|
||||||
|
* TP_PLAYERTOPLAYER senderName, targetName
|
||||||
|
* TP_PLAYERTOPOSITION playerName, world, x, y, z, yaw, pitch
|
||||||
|
* SEND_MSG playerName, message
|
||||||
*/
|
*/
|
||||||
public class BungeeMessenger implements PluginMessageListener {
|
public class BungeeMessenger implements PluginMessageListener {
|
||||||
|
|
||||||
private static final String BUNGEE_CHANNEL = "BungeeCord";
|
public static final String CHANNEL_TO_BUNGEE = "teleportsuite:tobungee";
|
||||||
private static final String TS_CHANNEL = "teleportsuite:tp";
|
public static final String CHANNEL_TO_SPIGOT = "teleportsuite:tospigot";
|
||||||
|
|
||||||
private final TeleportSuite plugin;
|
private final TeleportSuite plugin;
|
||||||
|
|
||||||
public BungeeMessenger(TeleportSuite plugin) { this.plugin = plugin; }
|
public BungeeMessenger(TeleportSuite plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
public void register() {
|
public void register() {
|
||||||
plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, BUNGEE_CHANNEL);
|
plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, CHANNEL_TO_BUNGEE);
|
||||||
plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, TS_CHANNEL);
|
plugin.getServer().getMessenger().registerIncomingPluginChannel(plugin, CHANNEL_TO_SPIGOT, this);
|
||||||
plugin.getServer().getMessenger().registerIncomingPluginChannel(plugin, TS_CHANNEL, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void unregister() {
|
public void unregister() {
|
||||||
plugin.getServer().getMessenger().unregisterOutgoingPluginChannel(plugin);
|
plugin.getServer().getMessenger().unregisterOutgoingPluginChannel(plugin, CHANNEL_TO_BUNGEE);
|
||||||
plugin.getServer().getMessenger().unregisterIncomingPluginChannel(plugin);
|
plugin.getServer().getMessenger().unregisterIncomingPluginChannel(plugin, CHANNEL_TO_SPIGOT, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Outgoing helpers (Spigot → Bungee)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** /tp <target> – Absender teleportiert sich zum Zielspieler */
|
||||||
|
public void teleportToPlayer(Player sender, String targetName) {
|
||||||
|
send(sender, out -> {
|
||||||
|
out.writeUTF("TP_PLAYER");
|
||||||
|
out.writeUTF(sender.getName());
|
||||||
|
out.writeUTF(targetName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** /tp <from> <to> – Admin teleportiert einen Spieler zu einem anderen */
|
||||||
|
public void teleportPlayerToPlayer(Player requester, String fromName, String targetName) {
|
||||||
|
send(requester, out -> {
|
||||||
|
out.writeUTF("TP_PLAYER_TO_PLAYER");
|
||||||
|
out.writeUTF(requester.getName());
|
||||||
|
out.writeUTF(fromName);
|
||||||
|
out.writeUTF(targetName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** /tphere <target> – Zielspieler wird zu Absender geholt */
|
||||||
|
public void teleportHere(Player sender, String targetName) {
|
||||||
|
send(sender, out -> {
|
||||||
|
out.writeUTF("TP_HERE");
|
||||||
|
out.writeUTF(sender.getName());
|
||||||
|
out.writeUTF(targetName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** /tpall – Alle Netzwerkspieler zum Absender teleportieren */
|
||||||
|
public void teleportAll(Player sender) {
|
||||||
|
send(sender, out -> {
|
||||||
|
out.writeUTF("TP_ALL");
|
||||||
|
out.writeUTF(sender.getName());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect a player to another BungeeCord server.
|
* Home / Warp / Spawn / Back / SavePoint / Portal / TpPos:
|
||||||
* Also sends a plugin message so the target server knows where to teleport the player.
|
* Koordinaten sind bekannt → Bungee wechselt den Server und Spigot
|
||||||
|
* setzt den Spieler direkt an die exakten Koordinaten.
|
||||||
*/
|
*/
|
||||||
public void connectToServer(Player player, String server, String world, double x, double y, double z, float yaw, float pitch) {
|
public void teleportToPosition(Player player, String server, String world,
|
||||||
// 1) Notify the target server about the pending teleport
|
|
||||||
sendTeleportPayload(player, player.getName(), server, world, x, y, z, yaw, pitch);
|
|
||||||
|
|
||||||
// 2) Switch server via BungeeCord
|
|
||||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
|
||||||
DataOutputStream out = new DataOutputStream(bos)) {
|
|
||||||
out.writeUTF("Connect");
|
|
||||||
out.writeUTF(server);
|
|
||||||
player.sendPluginMessage(plugin, BUNGEE_CHANNEL, bos.toByteArray());
|
|
||||||
} catch (IOException e) {
|
|
||||||
plugin.getLogger().warning("BungeeCord Connect Fehler: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendTeleportPayload(Player sender, String targetPlayer, String server, String world,
|
|
||||||
double x, double y, double z, float yaw, float pitch) {
|
double x, double y, double z, float yaw, float pitch) {
|
||||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
send(player, out -> {
|
||||||
DataOutputStream out = new DataOutputStream(bos)) {
|
out.writeUTF("TP_POS");
|
||||||
out.writeUTF("Forward");
|
out.writeUTF(player.getName());
|
||||||
out.writeUTF(server);
|
out.writeUTF(server);
|
||||||
out.writeUTF(TS_CHANNEL);
|
out.writeUTF(world);
|
||||||
// Sub-payload
|
out.writeDouble(x);
|
||||||
ByteArrayOutputStream sub = new ByteArrayOutputStream();
|
out.writeDouble(y);
|
||||||
DataOutputStream subOut = new DataOutputStream(sub);
|
out.writeDouble(z);
|
||||||
subOut.writeUTF(targetPlayer);
|
out.writeFloat(yaw);
|
||||||
subOut.writeUTF(world);
|
out.writeFloat(pitch);
|
||||||
subOut.writeDouble(x);
|
});
|
||||||
subOut.writeDouble(y);
|
|
||||||
subOut.writeDouble(z);
|
|
||||||
subOut.writeFloat(yaw);
|
|
||||||
subOut.writeFloat(pitch);
|
|
||||||
byte[] subBytes = sub.toByteArray();
|
|
||||||
out.writeShort(subBytes.length);
|
|
||||||
out.write(subBytes);
|
|
||||||
sender.sendPluginMessage(plugin, BUNGEE_CHANNEL, bos.toByteArray());
|
|
||||||
} catch (IOException e) {
|
|
||||||
plugin.getLogger().warning("TS Forward Fehler: " + e.getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TPA
|
||||||
|
|
||||||
|
public void sendTpaRequest(Player from, String toName,
|
||||||
|
String type, String msgToTarget, String msgToRequester) {
|
||||||
|
send(from, out -> {
|
||||||
|
out.writeUTF("TPA_REQUEST");
|
||||||
|
out.writeUTF(from.getName());
|
||||||
|
out.writeUTF(toName);
|
||||||
|
out.writeUTF(type);
|
||||||
|
out.writeUTF(msgToTarget);
|
||||||
|
out.writeUTF(msgToRequester);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void sendTpaAccept(Player acceptor, String fromName, String errMsg) {
|
||||||
|
send(acceptor, out -> {
|
||||||
|
out.writeUTF("TPA_ACCEPT");
|
||||||
|
out.writeUTF(acceptor.getName());
|
||||||
|
out.writeUTF(fromName == null ? "nu" : fromName);
|
||||||
|
out.writeUTF(errMsg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendTpaDeny(Player denier, String fromName, String msg) {
|
||||||
|
send(denier, out -> {
|
||||||
|
out.writeUTF("TPA_DENY");
|
||||||
|
out.writeUTF(denier.getName());
|
||||||
|
out.writeUTF(fromName == null ? "nu" : fromName);
|
||||||
|
out.writeUTF(msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendTpaCancel(Player canceller, String msgFrom, String msgTo) {
|
||||||
|
send(canceller, out -> {
|
||||||
|
out.writeUTF("TPA_CANCEL");
|
||||||
|
out.writeUTF(canceller.getName());
|
||||||
|
out.writeUTF(msgFrom);
|
||||||
|
out.writeUTF(msgTo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendTpaExpired(Player from, String toName, String msgFrom, String msgTo) {
|
||||||
|
send(from, out -> {
|
||||||
|
out.writeUTF("TPA_EXPIRED");
|
||||||
|
out.writeUTF(from.getName());
|
||||||
|
out.writeUTF(toName);
|
||||||
|
out.writeUTF(msgFrom);
|
||||||
|
out.writeUTF(msgTo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Incoming handler (Bungee → Spigot)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPluginMessageReceived(String channel, Player player, byte[] message) {
|
public void onPluginMessageReceived(String channel, Player unused, byte[] bytes) {
|
||||||
if (!channel.equals(TS_CHANNEL)) return;
|
if (!channel.equals(CHANNEL_TO_SPIGOT)) return;
|
||||||
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(message))) {
|
|
||||||
|
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(bytes))) {
|
||||||
|
String task = in.readUTF();
|
||||||
|
|
||||||
|
switch (task) {
|
||||||
|
|
||||||
|
// Bungee hat sender bereits auf diesen Server gewechselt.
|
||||||
|
// Sobald beide Spieler lokal online sind, teleportieren.
|
||||||
|
case "TP_PLAYERTOPLAYER": {
|
||||||
|
String senderName = in.readUTF();
|
||||||
String targetName = in.readUTF();
|
String targetName = in.readUTF();
|
||||||
|
schedulePlayerToPlayerTeleport(senderName, targetName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bungee hat player bereits auf diesen Server gewechselt.
|
||||||
|
// Nach kurzem Delay an exakte Koordinaten teleportieren.
|
||||||
|
case "TP_PLAYERTOPOSITION": {
|
||||||
|
String playerName = in.readUTF();
|
||||||
String world = in.readUTF();
|
String world = in.readUTF();
|
||||||
double x = in.readDouble(), y = in.readDouble(), z = in.readDouble();
|
double x = in.readDouble(), y = in.readDouble(), z = in.readDouble();
|
||||||
float yaw = in.readFloat(), pitch = in.readFloat();
|
float yaw = in.readFloat(), pitch = in.readFloat();
|
||||||
|
|
||||||
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
|
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
|
||||||
Player target = plugin.getServer().getPlayer(targetName);
|
org.bukkit.entity.Player p = plugin.getServer().getPlayerExact(playerName);
|
||||||
if (target == null || !target.isOnline()) return;
|
if (p == null || !p.isOnline()) return;
|
||||||
de.teleportsuite.models.TeleportLocation loc =
|
TeleportLocation loc = new TeleportLocation(
|
||||||
new de.teleportsuite.models.TeleportLocation(world, x, y, z, yaw, pitch,
|
world, x, y, z, yaw, pitch,
|
||||||
plugin.getConfigManager().getServerName());
|
plugin.getConfigManager().getServerName());
|
||||||
plugin.getTeleportManager().teleport(target, loc, false);
|
plugin.getTeleportManager().teleport(p, loc, false);
|
||||||
}, 20L);
|
}, 10L);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direkte Chat-Nachricht an einen lokalen Spieler
|
||||||
|
case "SEND_MSG": {
|
||||||
|
String playerName = in.readUTF();
|
||||||
|
String message = in.readUTF();
|
||||||
|
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
|
||||||
|
org.bukkit.entity.Player p = plugin.getServer().getPlayerExact(playerName);
|
||||||
|
if (p != null && p.isOnline()) p.sendMessage(message);
|
||||||
|
}, 1L);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
plugin.getLogger().warning("Unbekannter Bungee-Task: " + task);
|
||||||
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
plugin.getLogger().warning("Fehler beim Lesen der TS-Nachricht: " + e.getMessage());
|
plugin.getLogger().warning("Fehler beim Lesen der Bungee-Nachricht: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Internal helpers
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wartet bis sender UND target lokal online sind (max. 100 × 10 Ticks = 50 s),
|
||||||
|
* dann führt player.teleport() aus – identisch zum BTM-Ansatz.
|
||||||
|
*/
|
||||||
|
private void schedulePlayerToPlayerTeleport(String senderName, String targetName) {
|
||||||
|
final int[] attempts = {0};
|
||||||
|
final int[] taskId = {-1};
|
||||||
|
|
||||||
|
taskId[0] = plugin.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, () -> {
|
||||||
|
attempts[0]++;
|
||||||
|
org.bukkit.entity.Player sender = plugin.getServer().getPlayerExact(senderName);
|
||||||
|
org.bukkit.entity.Player target = plugin.getServer().getPlayerExact(targetName);
|
||||||
|
|
||||||
|
if (sender != null && sender.isOnline() && target != null && target.isOnline()) {
|
||||||
|
plugin.getTeleportManager().teleport(
|
||||||
|
sender,
|
||||||
|
new TeleportLocation(target.getLocation(),
|
||||||
|
plugin.getConfigManager().getServerName()),
|
||||||
|
false);
|
||||||
|
plugin.getServer().getScheduler().cancelTask(taskId[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempts[0] >= 100) {
|
||||||
|
plugin.getServer().getScheduler().cancelTask(taskId[0]);
|
||||||
|
}
|
||||||
|
}, 5L, 10L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface IOWriter {
|
||||||
|
void write(DataOutputStream out) throws IOException;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Schreibt ein Payload und schickt es über den Bungee-Kanal. */
|
||||||
|
private void send(Player anchor, IOWriter writer) {
|
||||||
|
if (anchor == null || !anchor.isOnline()) return;
|
||||||
|
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream out = new DataOutputStream(bos)) {
|
||||||
|
writer.write(out);
|
||||||
|
anchor.sendPluginMessage(plugin, CHANNEL_TO_BUNGEE, bos.toByteArray());
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().warning("Bungee-Send Fehler: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
package de.teleportsuite.commands;
|
package de.teleportsuite.commands;
|
||||||
|
|
||||||
import de.teleportsuite.TeleportSuite;
|
import de.teleportsuite.TeleportSuite;
|
||||||
import de.teleportsuite.models.TeleportLocation;
|
import de.teleportsuite.models.TeleportLocation;
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.command.*;
|
import org.bukkit.command.*;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
public class TpAllCommand implements CommandExecutor {
|
public class TpAllCommand implements CommandExecutor {
|
||||||
private final TeleportSuite plugin;
|
private final TeleportSuite plugin;
|
||||||
public TpAllCommand(TeleportSuite p) { this.plugin = p; }
|
public TpAllCommand(TeleportSuite p) { this.plugin = p; }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
|
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
|
||||||
if (!(sender instanceof Player)) { sender.sendMessage("§cNur für Spieler!"); return true; }
|
if (!(sender instanceof Player)) { sender.sendMessage("§cNur für Spieler!"); return true; }
|
||||||
Player p = (Player) sender;
|
Player p = (Player) sender;
|
||||||
if (!p.hasPermission("teleportsuite.tpall")) { p.sendMessage(plugin.getConfigManager().getMessage("no-permission")); return true; }
|
if (!p.hasPermission("teleportsuite.tpall")) { p.sendMessage(plugin.getConfigManager().getMessage("no-permission")); return true; }
|
||||||
|
if (plugin.getConfigManager().isBungeeEnabled() && plugin.getBungeeMessenger() != null) {
|
||||||
|
plugin.getBungeeMessenger().teleportAll(p);
|
||||||
|
} else {
|
||||||
TeleportLocation dest = new TeleportLocation(p.getLocation(), plugin.getConfigManager().getServerName());
|
TeleportLocation dest = new TeleportLocation(p.getLocation(), plugin.getConfigManager().getServerName());
|
||||||
for (Player online : Bukkit.getOnlinePlayers()) {
|
for (Player online : plugin.getServer().getOnlinePlayers()) {
|
||||||
if (!online.equals(p)) plugin.getTeleportManager().teleport(online, dest);
|
if (!online.equals(p)) plugin.getTeleportManager().teleport(online, dest);
|
||||||
}
|
}
|
||||||
p.sendMessage("§aAlle Spieler wurden zu dir teleportiert.");
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,14 +19,24 @@ public class TpCommand implements CommandExecutor {
|
|||||||
|
|
||||||
if (args.length == 1) {
|
if (args.length == 1) {
|
||||||
Player target = Bukkit.getPlayer(args[0]);
|
Player target = Bukkit.getPlayer(args[0]);
|
||||||
if (target == null) { p.sendMessage(plugin.getConfigManager().getMessage("player-not-found","player",args[0])); return true; }
|
if (target != null) {
|
||||||
plugin.getTeleportManager().teleport(p, new TeleportLocation(target.getLocation(), plugin.getConfigManager().getServerName()));
|
plugin.getTeleportManager().teleport(p, new TeleportLocation(target.getLocation(), plugin.getConfigManager().getServerName()));
|
||||||
|
} else if (plugin.getConfigManager().isBungeeEnabled() && plugin.getBungeeMessenger() != null) {
|
||||||
|
plugin.getBungeeMessenger().teleportToPlayer(p, args[0]);
|
||||||
} else {
|
} else {
|
||||||
|
p.sendMessage(plugin.getConfigManager().getMessage("player-not-found", "player", args[0]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!p.hasPermission("teleportsuite.admin")) { p.sendMessage(plugin.getConfigManager().getMessage("no-permission")); return true; }
|
||||||
Player from = Bukkit.getPlayer(args[0]);
|
Player from = Bukkit.getPlayer(args[0]);
|
||||||
Player to = Bukkit.getPlayer(args[1]);
|
Player to = Bukkit.getPlayer(args[1]);
|
||||||
if (from == null || to == null) { p.sendMessage("§cEin Spieler nicht gefunden."); return true; }
|
if (from != null && to != null) {
|
||||||
if (!p.hasPermission("teleportsuite.admin")) { p.sendMessage(plugin.getConfigManager().getMessage("no-permission")); return true; }
|
|
||||||
plugin.getTeleportManager().teleport(from, new TeleportLocation(to.getLocation(), plugin.getConfigManager().getServerName()));
|
plugin.getTeleportManager().teleport(from, new TeleportLocation(to.getLocation(), plugin.getConfigManager().getServerName()));
|
||||||
|
} else if (plugin.getConfigManager().isBungeeEnabled() && plugin.getBungeeMessenger() != null) {
|
||||||
|
plugin.getBungeeMessenger().teleportPlayerToPlayer(p, args[0], args[1]);
|
||||||
|
} else {
|
||||||
|
p.sendMessage("§cEin Spieler nicht gefunden.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,13 @@ public class TpHereCommand implements CommandExecutor {
|
|||||||
if (!p.hasPermission("teleportsuite.tphere")) { p.sendMessage(plugin.getConfigManager().getMessage("no-permission")); return true; }
|
if (!p.hasPermission("teleportsuite.tphere")) { p.sendMessage(plugin.getConfigManager().getMessage("no-permission")); return true; }
|
||||||
if (args.length < 1) { p.sendMessage("§cVerwendung: /tphere <spieler>"); return true; }
|
if (args.length < 1) { p.sendMessage("§cVerwendung: /tphere <spieler>"); return true; }
|
||||||
Player target = Bukkit.getPlayer(args[0]);
|
Player target = Bukkit.getPlayer(args[0]);
|
||||||
if (target == null) { p.sendMessage(plugin.getConfigManager().getMessage("player-not-found","player",args[0])); return true; }
|
if (target != null) {
|
||||||
plugin.getTeleportManager().teleport(target, new TeleportLocation(p.getLocation(), plugin.getConfigManager().getServerName()));
|
plugin.getTeleportManager().teleport(target, new TeleportLocation(p.getLocation(), plugin.getConfigManager().getServerName()));
|
||||||
|
} else if (plugin.getConfigManager().isBungeeEnabled() && plugin.getBungeeMessenger() != null) {
|
||||||
|
plugin.getBungeeMessenger().teleportHere(p, args[0]);
|
||||||
|
} else {
|
||||||
|
p.sendMessage(plugin.getConfigManager().getMessage("player-not-found", "player", args[0]));
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,18 @@ public class TpaCommand implements CommandExecutor {
|
|||||||
Player p = (Player) sender;
|
Player p = (Player) sender;
|
||||||
if (!p.hasPermission("teleportsuite.tpa")) { p.sendMessage(plugin.getConfigManager().getMessage("no-permission")); return true; }
|
if (!p.hasPermission("teleportsuite.tpa")) { p.sendMessage(plugin.getConfigManager().getMessage("no-permission")); return true; }
|
||||||
if (args.length < 1) { p.sendMessage("§cVerwendung: /tpa <spieler>"); return true; }
|
if (args.length < 1) { p.sendMessage("§cVerwendung: /tpa <spieler>"); return true; }
|
||||||
|
if (p.getName().equalsIgnoreCase(args[0])) { p.sendMessage("§cDu kannst dir nicht selbst eine Anfrage senden."); return true; }
|
||||||
|
|
||||||
Player target = Bukkit.getPlayer(args[0]);
|
Player target = Bukkit.getPlayer(args[0]);
|
||||||
if (target == null) { p.sendMessage(plugin.getConfigManager().getMessage("player-not-found","player",args[0])); return true; }
|
if (target != null) {
|
||||||
if (target.equals(p)) { p.sendMessage("§cDu kannst dir nicht selbst eine Anfrage senden."); return true; }
|
// Same server
|
||||||
plugin.getTeleportManager().sendTpaRequest(p, target);
|
plugin.getTeleportManager().sendTpaRequest(p, target);
|
||||||
|
} else if (plugin.getConfigManager().isBungeeEnabled() && plugin.getBungeeMessenger() != null) {
|
||||||
|
// Cross-server — Bungee manages the pending state
|
||||||
|
plugin.getTeleportManager().sendCrossServerTpaRequest(p, args[0]);
|
||||||
|
} else {
|
||||||
|
p.sendMessage(plugin.getConfigManager().getMessage("player-not-found", "player", args[0]));
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.teleportsuite.managers;
|
package de.teleportsuite.managers;
|
||||||
|
|
||||||
import de.teleportsuite.TeleportSuite;
|
import de.teleportsuite.TeleportSuite;
|
||||||
|
import de.teleportsuite.bungeemessaging.BungeeMessenger;
|
||||||
import de.teleportsuite.models.TeleportLocation;
|
import de.teleportsuite.models.TeleportLocation;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
@@ -10,18 +11,22 @@ import java.util.*;
|
|||||||
|
|
||||||
public class TeleportManager {
|
public class TeleportManager {
|
||||||
private final TeleportSuite plugin;
|
private final TeleportSuite plugin;
|
||||||
// Pending TPA requests: requester -> target
|
// Same-server pending TPA: requesterUUID → targetUUID
|
||||||
private final Map<UUID, UUID> tpaRequests = new HashMap<>();
|
private final Map<UUID, UUID> tpaRequests = new HashMap<>();
|
||||||
private final Map<UUID, Long> requestTimestamps = new HashMap<>();
|
private final Map<UUID, Long> requestTimestamps = new HashMap<>();
|
||||||
// Cooldowns
|
// Cooldowns
|
||||||
private final Map<UUID, Long> cooldowns = new HashMap<>();
|
private final Map<UUID, Long> cooldowns = new HashMap<>();
|
||||||
// Warmup tasks
|
// Warmup tasks
|
||||||
private final Map<UUID, Integer> warmupTasks = new HashMap<>();
|
private final Map<UUID, Integer> warmupTasks = new HashMap<>();
|
||||||
// Saved "before teleport" locations (for /back)
|
// Back-locations (saved before every teleport)
|
||||||
private final Map<UUID, TeleportLocation> pendingBackLocations = new HashMap<>();
|
private final Map<UUID, TeleportLocation> pendingBackLocations = new HashMap<>();
|
||||||
|
|
||||||
public TeleportManager(TeleportSuite plugin) { this.plugin = plugin; }
|
public TeleportManager(TeleportSuite plugin) { this.plugin = plugin; }
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Main teleport entry point
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
public void teleport(Player player, TeleportLocation destination) {
|
public void teleport(Player player, TeleportLocation destination) {
|
||||||
teleport(player, destination, true);
|
teleport(player, destination, true);
|
||||||
}
|
}
|
||||||
@@ -30,31 +35,36 @@ public class TeleportManager {
|
|||||||
int delay = plugin.getConfigManager().getTeleportDelay();
|
int delay = plugin.getConfigManager().getTeleportDelay();
|
||||||
if (player.hasPermission("teleportsuite.nodelay")) delay = 0;
|
if (player.hasPermission("teleportsuite.nodelay")) delay = 0;
|
||||||
|
|
||||||
// Check cooldown
|
|
||||||
if (!player.hasPermission("teleportsuite.nocooldown")) {
|
if (!player.hasPermission("teleportsuite.nocooldown")) {
|
||||||
long cooldownMs = plugin.getConfigManager().getTeleportCooldown() * 1000L;
|
long cooldownMs = plugin.getConfigManager().getTeleportCooldown() * 1000L;
|
||||||
long lastTp = cooldowns.getOrDefault(player.getUniqueId(), 0L);
|
long lastTp = cooldowns.getOrDefault(player.getUniqueId(), 0L);
|
||||||
if (System.currentTimeMillis() - lastTp < cooldownMs) {
|
if (System.currentTimeMillis() - lastTp < cooldownMs) {
|
||||||
long remaining = (cooldownMs - (System.currentTimeMillis() - lastTp)) / 1000;
|
long remaining = (cooldownMs - (System.currentTimeMillis() - lastTp)) / 1000;
|
||||||
player.sendMessage(plugin.getConfigManager().getMessage("teleport-cooldown", "seconds", String.valueOf(remaining)));
|
player.sendMessage(plugin.getConfigManager().getMessage(
|
||||||
|
"teleport-cooldown", "seconds", String.valueOf(remaining)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saveBack) pendingBackLocations.put(player.getUniqueId(), new TeleportLocation(player.getLocation(), plugin.getConfigManager().getServerName()));
|
if (saveBack) {
|
||||||
|
pendingBackLocations.put(player.getUniqueId(),
|
||||||
|
new TeleportLocation(player.getLocation(), plugin.getConfigManager().getServerName()));
|
||||||
|
}
|
||||||
|
|
||||||
if (delay <= 0) {
|
if (delay <= 0) {
|
||||||
executeTeleport(player, destination);
|
executeTeleport(player, destination);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
player.sendMessage(plugin.getConfigManager().getMessage("teleport-warmup", "seconds", String.valueOf(delay)));
|
player.sendMessage(plugin.getConfigManager().getMessage(
|
||||||
|
"teleport-warmup", "seconds", String.valueOf(delay)));
|
||||||
Location startLoc = player.getLocation().clone();
|
Location startLoc = player.getLocation().clone();
|
||||||
|
|
||||||
int taskId = Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, () -> {
|
int taskId = Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, () -> {
|
||||||
if (plugin.getConfigManager().cancelOnMove()) {
|
if (plugin.getConfigManager().cancelOnMove()) {
|
||||||
Location now = player.getLocation();
|
Location now = player.getLocation();
|
||||||
if (Math.abs(now.getX()-startLoc.getX()) > 0.5 || Math.abs(now.getZ()-startLoc.getZ()) > 0.5) {
|
if (Math.abs(now.getX() - startLoc.getX()) > 0.5
|
||||||
|
|| Math.abs(now.getZ() - startLoc.getZ()) > 0.5) {
|
||||||
player.sendMessage(plugin.getConfigManager().getMessage("teleport-cancelled"));
|
player.sendMessage(plugin.getConfigManager().getMessage("teleport-cancelled"));
|
||||||
warmupTasks.remove(player.getUniqueId());
|
warmupTasks.remove(player.getUniqueId());
|
||||||
return;
|
return;
|
||||||
@@ -70,10 +80,17 @@ public class TeleportManager {
|
|||||||
private void executeTeleport(Player player, TeleportLocation dest) {
|
private void executeTeleport(Player player, TeleportLocation dest) {
|
||||||
String localServer = plugin.getConfigManager().getServerName();
|
String localServer = plugin.getConfigManager().getServerName();
|
||||||
|
|
||||||
|
// Save last location + apply cooldown always (also for cross-server)
|
||||||
|
plugin.getDatabaseManager().saveLastLocation(player.getUniqueId(),
|
||||||
|
new TeleportLocation(player.getLocation(), localServer));
|
||||||
|
cooldowns.put(player.getUniqueId(), System.currentTimeMillis());
|
||||||
|
|
||||||
if (!dest.isLocalServer(localServer)) {
|
if (!dest.isLocalServer(localServer)) {
|
||||||
// BungeeCord-Teleport
|
// Cross-server: delegate completely to the Bungee plugin.
|
||||||
if (plugin.getBungeeMessenger() != null) {
|
// It will call sender.connect() and then send TP_PLAYERTOPOSITION.
|
||||||
plugin.getBungeeMessenger().connectToServer(player, dest.getServer(), dest.getWorld(),
|
BungeeMessenger bm = plugin.getBungeeMessenger();
|
||||||
|
if (bm != null) {
|
||||||
|
bm.teleportToPosition(player, dest.getServer(), dest.getWorld(),
|
||||||
dest.getX(), dest.getY(), dest.getZ(), dest.getYaw(), dest.getPitch());
|
dest.getX(), dest.getY(), dest.getZ(), dest.getYaw(), dest.getPitch());
|
||||||
player.sendMessage(plugin.getConfigManager().getMessage("teleport-success"));
|
player.sendMessage(plugin.getConfigManager().getMessage("teleport-success"));
|
||||||
}
|
}
|
||||||
@@ -86,12 +103,7 @@ public class TeleportManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save last location to database
|
|
||||||
plugin.getDatabaseManager().saveLastLocation(player.getUniqueId(),
|
|
||||||
new TeleportLocation(player.getLocation(), localServer));
|
|
||||||
|
|
||||||
player.teleport(loc);
|
player.teleport(loc);
|
||||||
cooldowns.put(player.getUniqueId(), System.currentTimeMillis());
|
|
||||||
player.sendMessage(plugin.getConfigManager().getMessage("teleport-success"));
|
player.sendMessage(plugin.getConfigManager().getMessage("teleport-success"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +116,10 @@ public class TeleportManager {
|
|||||||
return pendingBackLocations.get(uuid);
|
return pendingBackLocations.get(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TPA
|
// -------------------------------------------------------------------------
|
||||||
|
// Same-server TPA
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
public void sendTpaRequest(Player from, Player to) {
|
public void sendTpaRequest(Player from, Player to) {
|
||||||
tpaRequests.put(from.getUniqueId(), to.getUniqueId());
|
tpaRequests.put(from.getUniqueId(), to.getUniqueId());
|
||||||
requestTimestamps.put(from.getUniqueId(), System.currentTimeMillis());
|
requestTimestamps.put(from.getUniqueId(), System.currentTimeMillis());
|
||||||
@@ -117,48 +132,89 @@ public class TeleportManager {
|
|||||||
if (tpaRequests.containsKey(from.getUniqueId())) {
|
if (tpaRequests.containsKey(from.getUniqueId())) {
|
||||||
tpaRequests.remove(from.getUniqueId());
|
tpaRequests.remove(from.getUniqueId());
|
||||||
requestTimestamps.remove(from.getUniqueId());
|
requestTimestamps.remove(from.getUniqueId());
|
||||||
if (from.isOnline()) from.sendMessage(plugin.getConfigManager().getMessage("tpa-expired"));
|
if (from.isOnline()) {
|
||||||
|
from.sendMessage(plugin.getConfigManager().getMessage("tpa-expired"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, timeout * 20L);
|
}, timeout * 20L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Cross-server TPA — all state lives on the Bungee plugin now;
|
||||||
|
// Spigot just sends the right payload and handles local accept/deny.
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by TpaCommand when the target player is NOT on this server.
|
||||||
|
* Bungee will manage the pending state and route all messages.
|
||||||
|
*/
|
||||||
|
public void sendCrossServerTpaRequest(Player from, String targetName) {
|
||||||
|
BungeeMessenger bm = plugin.getBungeeMessenger();
|
||||||
|
if (bm == null) {
|
||||||
|
from.sendMessage(plugin.getConfigManager().getMessage("player-not-found", "player", targetName));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bm.sendTpaRequest(
|
||||||
|
from, targetName, "TPTO",
|
||||||
|
plugin.getConfigManager().getMessage("tpa-received", "player", from.getName()),
|
||||||
|
plugin.getConfigManager().getMessage("tpa-sent", "player", targetName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called by TpAcceptCommand — handles both same-server and cross-server. */
|
||||||
public boolean acceptTpa(Player target) {
|
public boolean acceptTpa(Player target) {
|
||||||
UUID requester = null;
|
// Same-server accept
|
||||||
|
UUID requesterUUID = null;
|
||||||
for (Map.Entry<UUID, UUID> entry : tpaRequests.entrySet()) {
|
for (Map.Entry<UUID, UUID> entry : tpaRequests.entrySet()) {
|
||||||
if (entry.getValue().equals(target.getUniqueId())) {
|
if (entry.getValue().equals(target.getUniqueId())) {
|
||||||
requester = entry.getKey();
|
requesterUUID = entry.getKey();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (requester == null) return false;
|
if (requesterUUID != null) {
|
||||||
|
Player from = Bukkit.getPlayer(requesterUUID);
|
||||||
Player from = Bukkit.getPlayer(requester);
|
tpaRequests.remove(requesterUUID);
|
||||||
tpaRequests.remove(requester);
|
requestTimestamps.remove(requesterUUID);
|
||||||
requestTimestamps.remove(requester);
|
|
||||||
|
|
||||||
if (from != null && from.isOnline()) {
|
if (from != null && from.isOnline()) {
|
||||||
from.sendMessage(plugin.getConfigManager().getMessage("tpa-accepted", "player", target.getName()));
|
from.sendMessage(plugin.getConfigManager().getMessage("tpa-accepted", "player", target.getName()));
|
||||||
teleport(from, new TeleportLocation(target.getLocation(), plugin.getConfigManager().getServerName()));
|
teleport(from, new TeleportLocation(target.getLocation(),
|
||||||
|
plugin.getConfigManager().getServerName()));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cross-server accept — tell Bungee, it has the pending state
|
||||||
|
BungeeMessenger bm = plugin.getBungeeMessenger();
|
||||||
|
if (bm == null) return false;
|
||||||
|
bm.sendTpaAccept(target, null,
|
||||||
|
plugin.getConfigManager().getMessage("tpa-request-not-found"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called by TpDenyCommand — handles both same-server and cross-server. */
|
||||||
public boolean denyTpa(Player target) {
|
public boolean denyTpa(Player target) {
|
||||||
UUID requester = null;
|
// Same-server deny
|
||||||
|
UUID requesterUUID = null;
|
||||||
for (Map.Entry<UUID, UUID> entry : tpaRequests.entrySet()) {
|
for (Map.Entry<UUID, UUID> entry : tpaRequests.entrySet()) {
|
||||||
if (entry.getValue().equals(target.getUniqueId())) {
|
if (entry.getValue().equals(target.getUniqueId())) {
|
||||||
requester = entry.getKey();
|
requesterUUID = entry.getKey();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (requester == null) return false;
|
if (requesterUUID != null) {
|
||||||
|
Player from = Bukkit.getPlayer(requesterUUID);
|
||||||
Player from = Bukkit.getPlayer(requester);
|
tpaRequests.remove(requesterUUID);
|
||||||
tpaRequests.remove(requester);
|
requestTimestamps.remove(requesterUUID);
|
||||||
requestTimestamps.remove(requester);
|
if (from != null && from.isOnline()) {
|
||||||
|
|
||||||
if (from != null && from.isOnline())
|
|
||||||
from.sendMessage(plugin.getConfigManager().getMessage("tpa-denied", "player", target.getName()));
|
from.sendMessage(plugin.getConfigManager().getMessage("tpa-denied", "player", target.getName()));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-server deny
|
||||||
|
BungeeMessenger bm = plugin.getBungeeMessenger();
|
||||||
|
if (bm == null) return false;
|
||||||
|
bm.sendTpaDeny(target, null,
|
||||||
|
plugin.getConfigManager().getMessage("tpa-denied", "player", target.getName()));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
src/main/resources/bungee.yml
Normal file
5
src/main/resources/bungee.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
name: TeleportSuite-Bungee
|
||||||
|
main: de.teleportsuite.bungee.TeleportSuiteBungee
|
||||||
|
version: 1.0
|
||||||
|
author: TeleportSuite
|
||||||
|
description: BungeeCord-Companion fuer TeleportSuite
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
name: TeleportSuite
|
name: TeleportSuite
|
||||||
version: 1.0.0
|
version: 1.0.2
|
||||||
main: de.teleportsuite.TeleportSuite
|
main: de.teleportsuite.TeleportSuite
|
||||||
api-version: 1.20
|
api-version: 1.20
|
||||||
description: BungeeCord-fähiges Teleport-Komplettpaket
|
description: BungeeCord-fähiges Teleport-Komplettpaket
|
||||||
|
|||||||
Reference in New Issue
Block a user