Upload via GUI (27 Dateien)
This commit is contained in:
@@ -1,2 +1,298 @@
|
||||
# LandsChunkInfo
|
||||
# LandsChunkInfo – Anleitung
|
||||
|
||||
Dieses Plugin ist eine Erweiterung für das **Lands**-Plugin. Es zeigt Spielern:
|
||||
|
||||
- wie viele Chunks sie schon besitzen,
|
||||
- wie viele noch frei sind,
|
||||
- **wann wieder ein Chunk frei wird** (nach dem Unclaimen oder durch den Upkeep).
|
||||
|
||||
Du brauchst keine Programmierkenntnisse. Diese Anleitung erklärt alles Schritt für Schritt.
|
||||
|
||||
---
|
||||
|
||||
## 1. Was brauche ich?
|
||||
|
||||
| Was | Pflicht? | Wofür |
|
||||
|---|---|---|
|
||||
| Ein Minecraft-Server mit **Paper** (Version 1.21 bis 26) | Ja | Das Plugin läuft darauf |
|
||||
| **Java 25** auf dem Server | Ja | Lands 8.8.0 braucht das ebenfalls |
|
||||
| Das Plugin **Lands** (Version 8.8.0) | Ja | Ohne Lands startet unser Plugin nicht |
|
||||
| **Vault** + ein Economy-Plugin (z. B. EssentialsX) | Nein | Nur für das Bezahlen mit Spielergeld (`skip`, `gift`) |
|
||||
| **PlaceholderAPI** | Nein | Nur für Platzhalter in Scoreboard, Tab-Liste usw. |
|
||||
|
||||
Folia wird **nicht** unterstützt.
|
||||
|
||||
---
|
||||
|
||||
## 2. Installation (5 Schritte)
|
||||
|
||||
1. Stoppe den Server (oder lass ihn laufen, ein Neustart ist trotzdem später nötig).
|
||||
2. Kopiere die Datei `LandsChunkInfo-1.0.0.jar` in den Ordner `plugins` deines Servers. Dort liegt auch die Lands-Jar.
|
||||
3. Starte den Server neu.
|
||||
4. Schau in die Konsole. Du solltest `[LandsChunkInfo] Enabling LandsChunkInfo` sehen. Steht dort ein roter Fehler, lies Abschnitt 9.
|
||||
5. Im Ordner `plugins/LandsChunkInfo/` wurde jetzt eine Datei `config.yml` erstellt. Dort kannst du alles einstellen (Abschnitt 5).
|
||||
|
||||
Fertig. Spieler können ab sofort `/chunkinfo` benutzen.
|
||||
|
||||
---
|
||||
|
||||
## 3. Was können die Spieler? (Befehle)
|
||||
|
||||
Der Hauptbefehl heißt `/chunkinfo`. Es gehen auch die Kurzformen `/chunks` und `/chunkslots`.
|
||||
|
||||
### Übersicht anzeigen
|
||||
|
||||
| Befehl | Was passiert |
|
||||
|---|---|
|
||||
| `/chunkinfo` | Zeigt alle deine Lands: Chunks belegt, Chunks frei, gesperrte Slots, wann der nächste Chunk frei wird, Upkeep |
|
||||
| `/chunkinfo MeinLand` | Zeigt nur dieses eine Land |
|
||||
| `/chunkinfo top` | Zeigt die Lands mit den meisten Chunks |
|
||||
|
||||
**Beispiel einer Anzeige:**
|
||||
|
||||
```
|
||||
Deine Chunks
|
||||
Von dir geclaimt: 12 / 20
|
||||
MeinLand
|
||||
Chunks: 12 / 20 | Frei: 6 | Gesperrt: 2
|
||||
Nächster Chunk wieder frei in: 3Std 12Min (23.10. 14:30)
|
||||
Sofort freischalten: /chunkinfo skip (Preis: 80,00)
|
||||
Upkeep: 36,00 pro Zyklus, nächster Einzug in 2T 4Std
|
||||
```
|
||||
|
||||
Was bedeuten die Wörter?
|
||||
|
||||
- **Frei** = So viele Chunks kannst du jetzt sofort claimen.
|
||||
- **Gesperrt** = Du hast einen Chunk abgegeben (`/unclaim`). Dieser Platz ist für eine Weile blockiert. Nach der Wartezeit ist er wieder frei.
|
||||
- **Upkeep** = Die regelmäßige Gebühr, die dein Land zahlen muss, damit es seine Chunks behält.
|
||||
|
||||
### Gesperrten Slot sofort freikaufen
|
||||
|
||||
Du willst nicht warten? Dann kauf den Slot frei.
|
||||
|
||||
| Befehl | Was passiert |
|
||||
|---|---|
|
||||
| `/chunkinfo skip` | Zeigt dir den Preis |
|
||||
| `/chunkinfo skip confirm` | Kauft den Slot wirklich. Ohne `confirm` wird **nichts** bezahlt |
|
||||
| `/chunkinfo gift MeinFreundLand` | Zeigt den Preis, um einen Slot in einem **fremden** Land freizukaufen |
|
||||
| `/chunkinfo gift MeinFreundLand confirm` | Kauft den Slot für das fremde Land. Die Mitglieder werden benachrichtigt |
|
||||
|
||||
Der Preis wird günstiger, je kürzer die Wartezeit noch ist.
|
||||
|
||||
### Chunk vormerken (Warteschlange)
|
||||
|
||||
Der Chunk, auf dem du **gerade stehst**, ist frei, aber dein Land hat keinen freien Slot? Dann merk ihn vor. Sobald ein Slot frei wird, claimt das Plugin ihn **automatisch** für dich.
|
||||
|
||||
| Befehl | Was passiert |
|
||||
|---|---|
|
||||
| `/chunkinfo queue` | Merkt den Chunk vor, auf dem du stehst (für dein Land) |
|
||||
| `/chunkinfo queue MeinLand` | Dasselbe, aber für ein bestimmtes Land |
|
||||
| `/chunkinfo queue list` | Zeigt deine vorgemerkten Chunks |
|
||||
| `/chunkinfo queue clear` | Löscht alle deine Vormerkungen |
|
||||
|
||||
Wichtig: **Du musst online sein**, damit der Chunk geclaimt wird.
|
||||
|
||||
### Fremden Chunk beobachten (Watchlist)
|
||||
|
||||
Ein Chunk gehört jemand anderem, und du willst ihn haben, falls er frei wird? Stell dich drauf:
|
||||
|
||||
| Befehl | Was passiert |
|
||||
|---|---|
|
||||
| `/chunkinfo watch` | Du bekommst eine Nachricht, sobald dieser Chunk frei wird |
|
||||
| `/chunkinfo watch list` | Zeigt, was du beobachtest |
|
||||
| `/chunkinfo watch clear` | Hört mit allem auf |
|
||||
|
||||
Tipp: Wenn die Nachricht kommt, nutze danach `/chunkinfo queue`, um ihn dir zu sichern.
|
||||
|
||||
### Chunk nur für kurze Zeit (Temp-Claim)
|
||||
|
||||
Du brauchst einen Chunk nur für ein paar Tage (z. B. für ein Event)? Stell dich in den Chunk (er muss zu **deinem eigenen** Land gehören):
|
||||
|
||||
| Befehl | Was passiert |
|
||||
|---|---|
|
||||
| `/chunkinfo temp 7` | Der Chunk wird nach 7 Tagen automatisch freigegeben |
|
||||
| `/chunkinfo temp list` | Zeigt deine Temp-Chunks |
|
||||
| `/chunkinfo temp cancel` | Nimmt dem Chunk, auf dem du stehst, das Ablaufdatum wieder weg |
|
||||
|
||||
Wenn ein Temp-Chunk abläuft, wird dadurch **kein** Slot gesperrt.
|
||||
|
||||
---
|
||||
|
||||
## 4. Die Platzhalter (optional)
|
||||
|
||||
Nur nötig, wenn du **PlaceholderAPI** hast und die Werte z. B. im Scoreboard oder in der Tab-Liste anzeigen willst. Die Werte beziehen sich auf das Hauptland des Spielers (sein eigenes Land, sonst das erste Land, in dem er vertraut ist).
|
||||
|
||||
| Platzhalter | Zeigt |
|
||||
|---|---|
|
||||
| `%chunkinfo_land%` | Name des Lands |
|
||||
| `%chunkinfo_used%` | Belegte Chunks |
|
||||
| `%chunkinfo_max%` | Maximale Chunks |
|
||||
| `%chunkinfo_free%` | Freie Chunks |
|
||||
| `%chunkinfo_locked%` | Gesperrte Slots |
|
||||
| `%chunkinfo_next_free%` | Zeit, bis der nächste Slot frei wird (`-` wenn keiner gesperrt ist) |
|
||||
| `%chunkinfo_upkeep%` | Upkeep-Kosten pro Zyklus |
|
||||
| `%chunkinfo_upkeep_in%` | Zeit bis zum nächsten Upkeep |
|
||||
| `%chunkinfo_top_1_name%` | Name des Lands auf Platz 1 (statt 1 geht jede Zahl) |
|
||||
| `%chunkinfo_top_1_chunks%` | Chunks des Lands auf Platz 1 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Einstellungen (config.yml)
|
||||
|
||||
Die Datei findest du unter `plugins/LandsChunkInfo/config.yml`. Öffne sie mit einem Texteditor (Notepad++ oder VS Code, **nicht** mit dem Windows-Notepad, wenn du auf Umlaute achtest). Nach einer Änderung gibst du im Spiel oder in der Konsole `/chunkinfo reload` ein.
|
||||
|
||||
**Wichtig:** Lass die Einrückungen (die Leerzeichen am Zeilenanfang) unverändert. Ändere nur die Werte hinter dem Doppelpunkt.
|
||||
|
||||
### Wartezeit nach dem Unclaimen
|
||||
|
||||
```yaml
|
||||
unclaim-cooldown:
|
||||
enabled: true
|
||||
duration: 24h
|
||||
enforce: true
|
||||
```
|
||||
|
||||
- `enabled` – `true` = Funktion an, `false` = aus.
|
||||
- `duration` – Wie lange ein Slot nach `/unclaim` gesperrt bleibt. Schreibweise:
|
||||
- `30m` = 30 Minuten
|
||||
- `12h` = 12 Stunden
|
||||
- `2d` = 2 Tage
|
||||
- `1d12h` = 1 Tag und 12 Stunden
|
||||
- `enforce` – `true` = Spieler können in dieser Zeit wirklich **nicht** neu claimen. `false` = es wird nur angezeigt, aber nichts blockiert.
|
||||
|
||||
### Upkeep-Zeitpunkt
|
||||
|
||||
```yaml
|
||||
upkeep:
|
||||
anchor-date: ""
|
||||
```
|
||||
|
||||
Das Plugin merkt sich automatisch, wann Lands das erste Mal Upkeep eingezogen hat, und rechnet danach weiter. **Bis dahin ist die Anzeige nur geschätzt.** Wenn du genau weißt, an welchem Tag zuletzt eingezogen wurde, trage das Datum ein, zum Beispiel `"2026-10-15"`.
|
||||
|
||||
### Slot freikaufen
|
||||
|
||||
```yaml
|
||||
skip:
|
||||
enabled: true
|
||||
payment: PLAYER
|
||||
price-per-hour: 25.0
|
||||
min-price: 50.0
|
||||
```
|
||||
|
||||
- `payment` – `PLAYER` = das Geld kommt vom Spieler (Vault nötig). `LAND` = das Geld kommt aus der Landkasse (nur der Besitzer darf das). `gift` bezahlt immer der Spieler selbst.
|
||||
- `price-per-hour` – Preis pro verbleibender Stunde Wartezeit.
|
||||
- `min-price` – Mindestpreis, auch wenn nur noch wenige Minuten fehlen.
|
||||
|
||||
Beispiel: Noch 10 Stunden Wartezeit, 25 pro Stunde ergibt 250.
|
||||
|
||||
### Warteschlange, Watchlist, Temp-Claims
|
||||
|
||||
```yaml
|
||||
queue:
|
||||
max: 3
|
||||
watch:
|
||||
max: 5
|
||||
temp-claims:
|
||||
enabled: true
|
||||
max-days: 30
|
||||
max-per-land: 3
|
||||
```
|
||||
|
||||
- `max` – Wie viele Einträge ein Spieler gleichzeitig haben darf.
|
||||
- `max-days` – Längste erlaubte Dauer für einen Temp-Chunk.
|
||||
- `max-per-land` – Wie viele Temp-Chunks ein Land gleichzeitig haben darf.
|
||||
|
||||
### Hinweis beim Einloggen
|
||||
|
||||
```yaml
|
||||
join-reminder:
|
||||
enabled: true
|
||||
upkeep-warning-hours: 48
|
||||
```
|
||||
|
||||
Beim Einloggen sagt das Plugin dem Spieler, wie viele Slots frei sind. Wenn das Guthaben des Lands nicht für den Upkeep reicht und der Einzug in weniger als `upkeep-warning-hours` Stunden ansteht, kommt zusätzlich eine Warnung (nur für den Besitzer).
|
||||
|
||||
### Texte ändern
|
||||
|
||||
Unter `messages:` stehen die Meldungen. Farben schreibst du mit `&`:
|
||||
|
||||
- `&a` = grün, `&c` = rot, `&e` = gelb, `&7` = grau
|
||||
|
||||
`%time%` wird im Spiel durch die Restzeit ersetzt. Lass es stehen.
|
||||
|
||||
---
|
||||
|
||||
## 6. Rechte (Permissions)
|
||||
|
||||
Rechte vergibst du mit einem Rechte-Plugin, z. B. LuckPerms.
|
||||
|
||||
| Recht | Wer bekommt es standardmäßig? | Was erlaubt es? |
|
||||
|---|---|---|
|
||||
| `landschunkinfo.use` | Alle | `/chunkinfo` und `top` benutzen |
|
||||
| `landschunkinfo.skip` | Alle | Eigenen Slot freikaufen |
|
||||
| `landschunkinfo.gift` | Alle | Slot in fremdem Land freikaufen |
|
||||
| `landschunkinfo.queue` | Alle | Warteschlange benutzen |
|
||||
| `landschunkinfo.watch` | Alle | Watchlist benutzen |
|
||||
| `landschunkinfo.temp` | Alle | Temp-Claims benutzen |
|
||||
| `landschunkinfo.bypass` | Nur OPs | Wartezeit nach dem Unclaimen gilt **nicht** |
|
||||
| `landschunkinfo.admin` | Nur OPs | `/chunkinfo reload` |
|
||||
|
||||
**Beispiel:** Du willst, dass niemand außer Teammitgliedern Slots verschenken darf. Entziehe allen das Recht `landschunkinfo.gift` und gib es nur der Team-Gruppe.
|
||||
|
||||
---
|
||||
|
||||
## 7. Häufige Fragen
|
||||
|
||||
**Warum sieht ein Spieler „Gesperrt“, obwohl er nichts gemacht hat?**
|
||||
Jemand aus seinem Land hat einen Chunk abgegeben. Die Sperre gilt für das **ganze Land**, nicht für einzelne Spieler.
|
||||
|
||||
**Warum wird bei manchen Unclaims nichts gesperrt?**
|
||||
Gesperrt wird nur, wenn ein Spieler selbst `/unclaim` benutzt. Nicht gesperrt wird, wenn das Land gelöscht wird, Chunks durch den Upkeep verloren gehen oder ein Temp-Chunk abläuft. Spieler mit dem Recht `landschunkinfo.bypass` lösen ebenfalls keine Sperre aus.
|
||||
|
||||
**Die Upkeep-Zeit stimmt nicht.**
|
||||
Lands verrät dem Plugin nicht, wann genau der Zyklus gestartet ist. Nach dem ersten echten Einzug passt die Anzeige. Alternativ trägst du in `upkeep.anchor-date` ein bekanntes Einzugsdatum ein.
|
||||
|
||||
**Muss ich das Plugin nach jeder Änderung neu starten?**
|
||||
Nein. Für Änderungen an der `config.yml` reicht `/chunkinfo reload`. Nur beim Austausch der Jar-Datei brauchst du einen Neustart.
|
||||
|
||||
**Verlieren Spieler ihre Chunks, wenn ich das Plugin entferne?**
|
||||
Nein. Das Plugin ändert keine Lands-Daten außer bei zwei Dingen: Wenn die Warteschlange einen Chunk claimt oder wenn ein Temp-Chunk abläuft. Entfernst du das Plugin, bleiben alle bestehenden Chunks gleich. Die gespeicherten Sperren und Listen liegen nur in `plugins/LandsChunkInfo/data.yml`.
|
||||
|
||||
**Kann ich einem Spieler mehr Chunk-Slots geben?**
|
||||
Nein, das geht nur in Lands selbst (`player-limits.yml`). Dieses Plugin ändert keine Limits.
|
||||
|
||||
---
|
||||
|
||||
## 8. Datensicherung
|
||||
|
||||
Alles, was sich das Plugin merkt (Sperren, Warteschlange, Watchlist, Temp-Chunks, letzter Upkeep), steht in:
|
||||
|
||||
`plugins/LandsChunkInfo/data.yml`
|
||||
|
||||
Wenn du den Server umziehst oder ein Backup machst, kopiere diese Datei und die `config.yml` mit. **Bearbeite `data.yml` nicht von Hand, solange der Server läuft.** Das Plugin überschreibt die Datei sonst.
|
||||
|
||||
---
|
||||
|
||||
## 9. Wenn etwas nicht klappt
|
||||
|
||||
| Problem | Lösung |
|
||||
|---|---|
|
||||
| Plugin startet nicht, in der Konsole steht etwas mit „Lands“ | Lands fehlt oder ist nicht aktiv. Prüfe, ob die Lands-Jar im Ordner `plugins` liegt und startet |
|
||||
| Fehler mit „Unsupported class file version“ oder „Java“ | Der Server läuft mit einem zu alten Java. Du brauchst **Java 25** |
|
||||
| `/chunkinfo` sagt „Du bist in keinem Land Mitglied“ | Der Spieler hat kein Land und ist in keinem als vertrauenswürdig eingetragen. Das ist normal |
|
||||
| `skip` oder `gift` meldet, Vault fehle | Installiere Vault und ein Economy-Plugin, oder stelle `skip.payment` auf `LAND` (dann geht nur `skip`, `gift` bleibt gesperrt) |
|
||||
| Die Platzhalter zeigen sich als Text, z. B. `%chunkinfo_used%` | PlaceholderAPI fehlt oder wurde nach unserem Plugin geladen. Starte den Server neu |
|
||||
| Änderungen an der `config.yml` wirken nicht | `/chunkinfo reload` ausführen. Falls es trotzdem nicht klappt: Auf Einrückungen und fehlende Anführungszeichen prüfen |
|
||||
| Spieler können nach dem Unclaimen trotzdem sofort claimen | Prüfe `unclaim-cooldown.enforce`, das muss `true` sein. Und prüfe, ob der Spieler das Recht `landschunkinfo.bypass` hat (OPs haben es automatisch) |
|
||||
|
||||
Wenn du nicht weiterkommst: Kopiere die **roten Zeilen aus der Server-Konsole** (oder aus `logs/latest.log`) und gib sie weiter. Daraus lässt sich fast immer die Ursache erkennen.
|
||||
|
||||
---
|
||||
|
||||
## 10. Bekannte Grenzen
|
||||
|
||||
- Das Plugin wurde noch nicht auf einem echten Server getestet. Probiere es erst auf einem Testserver aus.
|
||||
- Die Upkeep-Zeit ist bis zum ersten echten Einzug eine Schätzung.
|
||||
- Die Warteschlange und die Watchlist arbeiten nur für Spieler, die **online** sind.
|
||||
- Temp-Chunks laufen erst ab, wenn der Ersteller oder der Besitzer des Lands online ist.
|
||||
- Lands-Limits (z. B. maximale Chunks) kann das Plugin nicht verändern.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>de.landschunkinfo</groupId>
|
||||
<artifactId>LandsChunkInfo</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.release>25</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>papermc</id>
|
||||
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>1.21.11-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>me.angeschossen</groupId>
|
||||
<artifactId>lands</artifactId>
|
||||
<version>8.8.0</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/libs/Lands-8.8.0.jar</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.milkbowl</groupId>
|
||||
<artifactId>vaultapi</artifactId>
|
||||
<version>1.7.1</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/libs/VaultAPI-1.7.1.jar</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>me.clip</groupId>
|
||||
<artifactId>placeholderapi</artifactId>
|
||||
<version>2.11.7</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/libs/placeholderapi-2.11.7.jar</systemPath>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>LandsChunkInfo-${project.version}</finalName>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,84 @@
|
||||
package de.landschunkinfo;
|
||||
|
||||
import me.angeschossen.lands.api.land.Land;
|
||||
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Platzhalter (Bezug: Hauptland des Spielers, also eigenes Land oder erstes Land mit Vertrauen):
|
||||
* %chunkinfo_land%, _used, _max, _free, _locked, _next_free, _upkeep, _upkeep_in,
|
||||
* %chunkinfo_top_<n>_name%, %chunkinfo_top_<n>_chunks%
|
||||
*/
|
||||
final class ChunkExpansion extends PlaceholderExpansion {
|
||||
|
||||
private final LandsChunkInfo plugin;
|
||||
|
||||
ChunkExpansion(LandsChunkInfo plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
return "chunkinfo";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthor() {
|
||||
return "LandsChunkInfo";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return plugin.getPluginMeta().getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean persist() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onRequest(OfflinePlayer player, String params) {
|
||||
String key = params.toLowerCase(Locale.ROOT);
|
||||
if (key.startsWith("top_")) {
|
||||
String[] p = key.split("_");
|
||||
if (p.length != 3) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
int rank = Integer.parseInt(p[1]);
|
||||
List<Land> top = plugin.top(rank);
|
||||
if (top.size() < rank) {
|
||||
return "-";
|
||||
}
|
||||
Land l = top.get(rank - 1);
|
||||
return p[2].equals("name") ? l.getName() : p[2].equals("chunks") ? String.valueOf(l.getChunksAmount()) : null;
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (player == null) {
|
||||
return "";
|
||||
}
|
||||
Land land = plugin.primaryLand(player.getUniqueId());
|
||||
if (land == null) {
|
||||
return key.equals("land") ? "-" : "0";
|
||||
}
|
||||
List<Long> locked = plugin.cooldownActive() ? plugin.store.active(LandsChunkInfo.landId(land)) : List.of();
|
||||
long now = System.currentTimeMillis();
|
||||
return switch (key) {
|
||||
case "land" -> land.getName();
|
||||
case "used" -> String.valueOf(land.getChunksAmount());
|
||||
case "max" -> land.getMaxChunks() <= 0 ? "unbegrenzt" : String.valueOf(land.getMaxChunks());
|
||||
case "free" -> plugin.freeSlots(land) < 0 ? "unbegrenzt" : String.valueOf(plugin.freeSlots(land));
|
||||
case "locked" -> String.valueOf(locked.size());
|
||||
case "next_free" -> locked.isEmpty() ? "-" : TimeUtil.format(locked.get(0) - now);
|
||||
case "upkeep" -> String.format(Locale.GERMANY, "%.2f", land.getUpkeepCosts());
|
||||
case "upkeep_in" -> TimeUtil.format(plugin.nextUpkeep() - now);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,482 @@
|
||||
package de.landschunkinfo;
|
||||
|
||||
import de.landschunkinfo.LockStore.Entry;
|
||||
import de.landschunkinfo.LockStore.Kind;
|
||||
import me.angeschossen.lands.api.framework.limit.Limit;
|
||||
import me.angeschossen.lands.api.land.Land;
|
||||
import me.angeschossen.lands.api.player.LandPlayer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
final class InfoCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
private static final List<String> SUBS = List.of("top", "skip", "gift", "queue", "watch", "temp", "reload");
|
||||
|
||||
private final LandsChunkInfo plugin;
|
||||
|
||||
InfoCommand(LandsChunkInfo plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ Dispatcher
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length >= 1 && args[0].equalsIgnoreCase("reload")) {
|
||||
if (!sender.hasPermission("landschunkinfo.admin")) {
|
||||
plugin.send(sender, plugin.cfgMsg("no-permission"));
|
||||
return true;
|
||||
}
|
||||
plugin.reload();
|
||||
plugin.send(sender, plugin.cfgMsg("reloaded"));
|
||||
return true;
|
||||
}
|
||||
if (!(sender instanceof Player p)) {
|
||||
plugin.send(sender, plugin.cfgMsg("players-only"));
|
||||
return true;
|
||||
}
|
||||
if (!p.hasPermission("landschunkinfo.use")) {
|
||||
plugin.send(p, plugin.cfgMsg("no-permission"));
|
||||
return true;
|
||||
}
|
||||
String sub = args.length == 0 ? "" : args[0].toLowerCase(Locale.ROOT);
|
||||
switch (sub) {
|
||||
case "top" -> top(p);
|
||||
case "skip" -> skip(p, args, false);
|
||||
case "gift" -> skip(p, args, true);
|
||||
case "queue" -> queue(p, args);
|
||||
case "watch" -> watch(p, args);
|
||||
case "temp" -> temp(p, args);
|
||||
default -> info(p, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean allowed(Player p, String perm) {
|
||||
if (p.hasPermission("landschunkinfo." + perm)) {
|
||||
return true;
|
||||
}
|
||||
plugin.send(p, plugin.cfgMsg("no-permission"));
|
||||
return false;
|
||||
}
|
||||
|
||||
private Land findLand(String name) {
|
||||
for (Object o : plugin.lands.getLands()) {
|
||||
Land l = (Land) o;
|
||||
if (l.getName().equalsIgnoreCase(name)) {
|
||||
return l;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isMember(Land land, UUID uuid) {
|
||||
return uuid.equals(land.getOwnerUID()) || land.isTrusted(uuid);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Info
|
||||
|
||||
private void info(Player p, String[] args) {
|
||||
List<Land> shown = plugin.landsOf(p.getUniqueId());
|
||||
if (args.length >= 1) {
|
||||
String wanted = String.join(" ", args);
|
||||
shown = shown.stream().filter(l -> l.getName().equalsIgnoreCase(wanted)).toList();
|
||||
if (shown.isEmpty()) {
|
||||
plugin.send(p, plugin.cfgMsg("land-not-found"));
|
||||
return;
|
||||
}
|
||||
} else if (shown.isEmpty()) {
|
||||
plugin.send(p, plugin.cfgMsg("no-land"));
|
||||
return;
|
||||
}
|
||||
|
||||
LandPlayer lp = plugin.lands.getLandPlayer(p.getUniqueId());
|
||||
if (lp != null) {
|
||||
Limit limit = Limit.getById("player_chunks");
|
||||
if (limit != null) {
|
||||
int lim = lp.getLimit(limit);
|
||||
p.sendMessage(plugin.line("&8&m &r &aDeine Chunks &8&m "));
|
||||
p.sendMessage(plugin.line("&7Von dir geclaimt: &e" + lp.getChunksClaimed().size()
|
||||
+ " &7/ &e" + (lim < 0 ? "unbegrenzt" : lim)));
|
||||
}
|
||||
}
|
||||
for (Land land : shown) {
|
||||
describe(p, land);
|
||||
}
|
||||
p.sendMessage(plugin.line("&8Mehr: &7/chunkinfo top | skip | gift | queue | watch | temp"));
|
||||
}
|
||||
|
||||
private void describe(Player p, Land land) {
|
||||
int max = land.getMaxChunks();
|
||||
int size = land.getChunksAmount();
|
||||
String id = LandsChunkInfo.landId(land);
|
||||
List<Long> locked = plugin.cooldownActive() ? plugin.store.active(id) : List.of();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
p.sendMessage(plugin.line("&8&m &r &a" + land.getName() + " &8&m "));
|
||||
if (max <= 0) {
|
||||
p.sendMessage(plugin.line("&7Chunks: &e" + size + " &7(kein Limit)"));
|
||||
} else {
|
||||
p.sendMessage(plugin.line("&7Chunks: &e" + size + " &7/ &e" + max + " &8| &7Frei: &a"
|
||||
+ plugin.freeSlots(land) + (locked.isEmpty() ? "" : " &8| &7Gesperrt: &c" + locked.size())));
|
||||
}
|
||||
if (plugin.cooldownActive()) {
|
||||
if (locked.isEmpty()) {
|
||||
p.sendMessage(plugin.line("&7Gesperrte Slots: &akeine"));
|
||||
} else {
|
||||
long first = locked.get(0);
|
||||
p.sendMessage(plugin.line("&7Naechster Chunk wieder frei in: &e" + TimeUtil.format(first - now)
|
||||
+ " &8(" + LandsChunkInfo.fmt(first) + ")"));
|
||||
if (locked.size() > 1) {
|
||||
StringBuilder sb = new StringBuilder("&7Danach: ");
|
||||
locked.stream().skip(1).limit(4).forEach(t -> sb.append("&e")
|
||||
.append(TimeUtil.format(t - now)).append("&7, "));
|
||||
sb.setLength(sb.length() - 2);
|
||||
p.sendMessage(plugin.line(sb + (locked.size() > 5 ? " ..." : "")));
|
||||
}
|
||||
if (plugin.getConfig().getBoolean("skip.enabled", true)) {
|
||||
p.sendMessage(plugin.line("&7Sofort freischalten: &e/chunkinfo skip &7(Preis: &e"
|
||||
+ price(first - now) + "&7)"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Entry t : plugin.store.entries(Kind.TEMP)) {
|
||||
if (t.landId().equals(id)) {
|
||||
p.sendMessage(plugin.line("&7Temp-Chunk &e" + t.x() + ", " + t.z() + " &7laeuft ab in &e"
|
||||
+ TimeUtil.format(t.until() - now)));
|
||||
}
|
||||
}
|
||||
|
||||
double upkeep = land.getUpkeepCosts();
|
||||
if (upkeep > 0) {
|
||||
long next = plugin.nextUpkeep();
|
||||
double balance = land.getBalance();
|
||||
p.sendMessage(plugin.line("&7Upkeep: &e" + money(upkeep) + " &7pro Zyklus, naechster Einzug in &e"
|
||||
+ TimeUtil.format(next - now) + " &8(" + LandsChunkInfo.fmt(next) + ")"));
|
||||
if (balance < upkeep) {
|
||||
String risk = "";
|
||||
YamlConfiguration lc = plugin.landsConfig();
|
||||
if (lc != null && lc.getBoolean("upkeep.unclaim", false) && lc.getDouble("upkeep.per-chunk", 0) > 0) {
|
||||
int lost = (int) Math.min(size, Math.ceil((upkeep - balance) / lc.getDouble("upkeep.per-chunk")));
|
||||
risk = " - " + lost + " Chunk(s) wuerden verloren gehen!";
|
||||
}
|
||||
p.sendMessage(plugin.line("&cGuthaben (" + money(balance) + ") reicht nicht fuer den Upkeep." + risk));
|
||||
}
|
||||
}
|
||||
|
||||
Long inactive = plugin.inactivityRemaining(land);
|
||||
if (inactive != null && inactive >= 0) {
|
||||
boolean ownerOnline = land.getOwnerUID() != null && Bukkit.getPlayer(land.getOwnerUID()) != null;
|
||||
p.sendMessage(plugin.line("&7Inaktivitaet des Besitzers: " + (ownerOnline
|
||||
? "&aBesitzer online"
|
||||
: "&eLand gilt in &c" + TimeUtil.format(inactive) + " &eals inaktiv")));
|
||||
}
|
||||
}
|
||||
|
||||
private static String money(double v) {
|
||||
return String.format(Locale.GERMANY, "%.2f", v);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- Top
|
||||
|
||||
private void top(Player p) {
|
||||
int n = plugin.getConfig().getInt("top.size", 10);
|
||||
p.sendMessage(plugin.line("&8&m &r &aTop-Lands nach Chunks &8&m "));
|
||||
int i = 1;
|
||||
for (Land l : plugin.top(n)) {
|
||||
p.sendMessage(plugin.line("&e#" + i++ + " &7" + l.getName() + " &8- &a" + l.getChunksAmount() + " Chunks"));
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ Skip / Gift
|
||||
|
||||
private String price(long remainingMs) {
|
||||
return money(priceValue(remainingMs));
|
||||
}
|
||||
|
||||
private double priceValue(long remainingMs) {
|
||||
double perHour = plugin.getConfig().getDouble("skip.price-per-hour", 25.0);
|
||||
double min = plugin.getConfig().getDouble("skip.min-price", 50.0);
|
||||
return Math.max(min, remainingMs / 3_600_000.0 * perHour);
|
||||
}
|
||||
|
||||
/** /chunkinfo skip [Land] [confirm] bzw. /chunkinfo gift <Land> [confirm] */
|
||||
private void skip(Player p, String[] args, boolean gift) {
|
||||
if (!allowed(p, gift ? "gift" : "skip")) {
|
||||
return;
|
||||
}
|
||||
if (!plugin.getConfig().getBoolean("skip.enabled", true) || !plugin.cooldownActive()) {
|
||||
plugin.send(p, "&cDiese Funktion ist deaktiviert.");
|
||||
return;
|
||||
}
|
||||
boolean confirm = args.length > 1 && args[args.length - 1].equalsIgnoreCase("confirm");
|
||||
int end = confirm ? args.length - 1 : args.length;
|
||||
String name = end > 1 ? String.join(" ", List.of(args).subList(1, end)) : null;
|
||||
|
||||
Land land;
|
||||
if (name != null) {
|
||||
land = findLand(name);
|
||||
} else if (gift) {
|
||||
plugin.send(p, "&cNutzung: /chunkinfo gift <Land> [confirm]");
|
||||
return;
|
||||
} else {
|
||||
land = plugin.landsOf(p.getUniqueId()).stream()
|
||||
.filter(l -> !plugin.store.active(LandsChunkInfo.landId(l)).isEmpty()).findFirst().orElse(null);
|
||||
if (land == null) {
|
||||
plugin.send(p, "&aDu hast keine gesperrten Slots.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (land == null || (!gift && !isMember(land, p.getUniqueId()))) {
|
||||
plugin.send(p, plugin.cfgMsg("land-not-found"));
|
||||
return;
|
||||
}
|
||||
List<Long> locked = plugin.store.active(LandsChunkInfo.landId(land));
|
||||
if (locked.isEmpty()) {
|
||||
plugin.send(p, "&e" + land.getName() + " &ahat keine gesperrten Slots.");
|
||||
return;
|
||||
}
|
||||
long remaining = locked.get(0) - System.currentTimeMillis();
|
||||
double price = priceValue(remaining);
|
||||
boolean fromLand = !gift && "LAND".equalsIgnoreCase(plugin.getConfig().getString("skip.payment", "PLAYER"));
|
||||
|
||||
if (fromLand && !p.getUniqueId().equals(land.getOwnerUID())) {
|
||||
plugin.send(p, "&cNur der Besitzer darf aus der Landkasse zahlen.");
|
||||
return;
|
||||
}
|
||||
if (!fromLand && plugin.vault == null) {
|
||||
plugin.send(p, "&cDafuer wird Vault mit einem Economy-Plugin benoetigt.");
|
||||
return;
|
||||
}
|
||||
if (!confirm) {
|
||||
plugin.send(p, "&7Slot in &e" + land.getName() + " &7(noch &e" + TimeUtil.format(remaining)
|
||||
+ "&7) freischalten fuer &e" + money(price) + (fromLand ? " &7aus der Landkasse" : "")
|
||||
+ "&7. Bestaetigen: &e/chunkinfo " + (gift ? "gift " : "skip ") + land.getName() + " confirm");
|
||||
return;
|
||||
}
|
||||
|
||||
if (fromLand) {
|
||||
if (land.getBalance() < price) {
|
||||
plugin.send(p, "&cDie Landkasse hat nicht genug Guthaben.");
|
||||
return;
|
||||
}
|
||||
land.setBalance(land.getBalance() - price);
|
||||
} else {
|
||||
if (plugin.vault.balance(p) < price || !plugin.vault.withdraw(p, price)) {
|
||||
plugin.send(p, "&cDu hast nicht genug Geld.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!plugin.store.removeEarliest(LandsChunkInfo.landId(land))) {
|
||||
plugin.send(p, "&cDer Slot ist inzwischen von selbst frei geworden.");
|
||||
return;
|
||||
}
|
||||
plugin.send(p, "&aSlot in &e" + land.getName() + " &afreigeschaltet (&e" + money(price) + " &abezahlt).");
|
||||
if (gift) {
|
||||
for (Player m : plugin.onlineMembers(land)) {
|
||||
plugin.send(m, "&a" + p.getName() + " hat einen Chunk-Slot fuer &e" + land.getName() + " &afreigeschaltet!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- Queue
|
||||
|
||||
private void queue(Player p, String[] args) {
|
||||
if (!allowed(p, "queue")) {
|
||||
return;
|
||||
}
|
||||
String arg = args.length > 1 ? args[1].toLowerCase(Locale.ROOT) : "";
|
||||
if (arg.equals("list")) {
|
||||
listEntries(p, Kind.QUEUE, "Warteschlange");
|
||||
return;
|
||||
}
|
||||
if (arg.equals("clear")) {
|
||||
plugin.send(p, "&7" + plugin.store.removeAll(Kind.QUEUE, p.getUniqueId()) + " Eintraege entfernt.");
|
||||
return;
|
||||
}
|
||||
Land land;
|
||||
if (args.length > 1) {
|
||||
land = findLand(String.join(" ", List.of(args).subList(1, args.length)));
|
||||
} else {
|
||||
land = plugin.primaryLand(p.getUniqueId());
|
||||
}
|
||||
if (land == null || !isMember(land, p.getUniqueId())) {
|
||||
plugin.send(p, plugin.cfgMsg("land-not-found"));
|
||||
return;
|
||||
}
|
||||
World w = p.getWorld();
|
||||
int cx = p.getLocation().getBlockX() >> 4;
|
||||
int cz = p.getLocation().getBlockZ() >> 4;
|
||||
if (plugin.lands.getLandByUnloadedChunk(w, cx, cz) != null) {
|
||||
plugin.send(p, "&cDieser Chunk ist schon geclaimt. Mit &e/chunkinfo watch &ckannst du ihn beobachten.");
|
||||
return;
|
||||
}
|
||||
List<Entry> mine = plugin.store.entries(Kind.QUEUE).stream().filter(e -> e.player().equals(p.getUniqueId())).toList();
|
||||
if (mine.size() >= plugin.getConfig().getInt("queue.max", 3)) {
|
||||
plugin.send(p, "&cDeine Warteschlange ist voll (&e/chunkinfo queue clear&c).");
|
||||
return;
|
||||
}
|
||||
if (mine.stream().anyMatch(e -> e.at(w.getName(), cx, cz))) {
|
||||
plugin.send(p, "&cDieser Chunk ist schon in deiner Warteschlange.");
|
||||
return;
|
||||
}
|
||||
plugin.store.add(Kind.QUEUE, new Entry(p.getUniqueId(), LandsChunkInfo.landId(land), w.getName(), cx, cz, 0));
|
||||
int free = plugin.freeSlots(land);
|
||||
plugin.send(p, "&aChunk &e" + cx + ", " + cz + " &avorgemerkt fuer &e" + land.getName() + "&a. "
|
||||
+ (free == 0 ? "Er wird geclaimt, sobald ein Slot frei wird (du musst dafuer online sein)."
|
||||
: "Er wird in Kuerze geclaimt."));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- Watch
|
||||
|
||||
private void watch(Player p, String[] args) {
|
||||
if (!allowed(p, "watch")) {
|
||||
return;
|
||||
}
|
||||
String arg = args.length > 1 ? args[1].toLowerCase(Locale.ROOT) : "";
|
||||
if (arg.equals("list")) {
|
||||
listEntries(p, Kind.WATCH, "Watchlist");
|
||||
return;
|
||||
}
|
||||
if (arg.equals("clear")) {
|
||||
plugin.send(p, "&7" + plugin.store.removeAll(Kind.WATCH, p.getUniqueId()) + " Eintraege entfernt.");
|
||||
return;
|
||||
}
|
||||
World w = p.getWorld();
|
||||
int cx = p.getLocation().getBlockX() >> 4;
|
||||
int cz = p.getLocation().getBlockZ() >> 4;
|
||||
Land holder = plugin.lands.getLandByUnloadedChunk(w, cx, cz);
|
||||
if (holder == null) {
|
||||
plugin.send(p, "&cDieser Chunk ist frei - du kannst ihn direkt claimen oder mit &e/chunkinfo queue &avormerken.");
|
||||
return;
|
||||
}
|
||||
List<Entry> mine = plugin.store.entries(Kind.WATCH).stream().filter(e -> e.player().equals(p.getUniqueId())).toList();
|
||||
if (mine.size() >= plugin.getConfig().getInt("watch.max", 5)) {
|
||||
plugin.send(p, "&cDeine Watchlist ist voll (&e/chunkinfo watch clear&c).");
|
||||
return;
|
||||
}
|
||||
if (mine.stream().anyMatch(e -> e.at(w.getName(), cx, cz))) {
|
||||
plugin.send(p, "&cDen beobachtest du schon.");
|
||||
return;
|
||||
}
|
||||
plugin.store.add(Kind.WATCH, new Entry(p.getUniqueId(), LandsChunkInfo.landId(holder), w.getName(), cx, cz, 0));
|
||||
plugin.send(p, "&aDu wirst benachrichtigt, sobald Chunk &e" + cx + ", " + cz + " &a(Land &e" + holder.getName() + "&a) frei wird.");
|
||||
}
|
||||
|
||||
private void listEntries(Player p, Kind kind, String title) {
|
||||
List<Entry> mine = plugin.store.entries(kind).stream().filter(e -> e.player().equals(p.getUniqueId())).toList();
|
||||
p.sendMessage(plugin.line("&8&m &r &a" + title + " &8&m "));
|
||||
if (mine.isEmpty()) {
|
||||
p.sendMessage(plugin.line("&7(leer)"));
|
||||
}
|
||||
for (Entry e : mine) {
|
||||
Land land = plugin.landById(e.landId());
|
||||
p.sendMessage(plugin.line("&e" + e.world() + " " + e.x() + ", " + e.z()
|
||||
+ " &8- &7Land: &e" + (land == null ? "?" : land.getName())));
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Temp
|
||||
|
||||
private void temp(Player p, String[] args) {
|
||||
if (!allowed(p, "temp")) {
|
||||
return;
|
||||
}
|
||||
if (!plugin.getConfig().getBoolean("temp-claims.enabled", true)) {
|
||||
plugin.send(p, "&cDiese Funktion ist deaktiviert.");
|
||||
return;
|
||||
}
|
||||
World w = p.getWorld();
|
||||
int cx = p.getLocation().getBlockX() >> 4;
|
||||
int cz = p.getLocation().getBlockZ() >> 4;
|
||||
String arg = args.length > 1 ? args[1].toLowerCase(Locale.ROOT) : "";
|
||||
|
||||
if (arg.equals("list")) {
|
||||
p.sendMessage(plugin.line("&8&m &r &aTemp-Chunks &8&m "));
|
||||
long now = System.currentTimeMillis();
|
||||
plugin.store.entries(Kind.TEMP).stream().filter(e -> plugin.landsOf(p.getUniqueId()).stream()
|
||||
.anyMatch(l -> LandsChunkInfo.landId(l).equals(e.landId()))).forEach(e -> p.sendMessage(plugin.line(
|
||||
"&e" + e.world() + " " + e.x() + ", " + e.z() + " &8- &7noch &e" + TimeUtil.format(e.until() - now))));
|
||||
return;
|
||||
}
|
||||
Land land = plugin.lands.getLandByUnloadedChunk(w, cx, cz);
|
||||
if (land == null || !p.getUniqueId().equals(land.getOwnerUID())) {
|
||||
plugin.send(p, "&cDu musst in einem Chunk deines eigenen Lands stehen.");
|
||||
return;
|
||||
}
|
||||
String id = LandsChunkInfo.landId(land);
|
||||
if (arg.equals("cancel")) {
|
||||
boolean removed = plugin.store.entries(Kind.TEMP).stream()
|
||||
.filter(e -> e.landId().equals(id) && e.at(w.getName(), cx, cz))
|
||||
.anyMatch(e -> plugin.store.remove(Kind.TEMP, e));
|
||||
plugin.send(p, removed ? "&aDie Zeitbegrenzung fuer diesen Chunk wurde entfernt." : "&cDieser Chunk hat keine Zeitbegrenzung.");
|
||||
return;
|
||||
}
|
||||
int days;
|
||||
try {
|
||||
days = Integer.parseInt(arg);
|
||||
} catch (NumberFormatException ex) {
|
||||
plugin.send(p, "&cNutzung: /chunkinfo temp <Tage> | cancel | list");
|
||||
return;
|
||||
}
|
||||
int maxDays = plugin.getConfig().getInt("temp-claims.max-days", 30);
|
||||
if (days < 1 || days > maxDays) {
|
||||
plugin.send(p, "&cErlaubt sind 1 bis " + maxDays + " Tage.");
|
||||
return;
|
||||
}
|
||||
List<Entry> forLand = plugin.store.entries(Kind.TEMP).stream().filter(e -> e.landId().equals(id)).toList();
|
||||
Entry existing = forLand.stream().filter(e -> e.at(w.getName(), cx, cz)).findFirst().orElse(null);
|
||||
if (existing == null && forLand.size() >= plugin.getConfig().getInt("temp-claims.max-per-land", 3)) {
|
||||
plugin.send(p, "&cEs sind schon zu viele Temp-Chunks in diesem Land.");
|
||||
return;
|
||||
}
|
||||
if (existing != null) {
|
||||
plugin.store.remove(Kind.TEMP, existing);
|
||||
}
|
||||
long until = System.currentTimeMillis() + days * 86_400_000L;
|
||||
plugin.store.add(Kind.TEMP, new Entry(p.getUniqueId(), id, w.getName(), cx, cz, until));
|
||||
plugin.send(p, "&aChunk &e" + cx + ", " + cz + " &awird in &e" + days + " Tag(en) &aautomatisch freigegeben ("
|
||||
+ LandsChunkInfo.fmt(until) + ").");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- Tab
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender s, Command c, String alias, String[] args) {
|
||||
if (!(s instanceof Player p)) {
|
||||
return List.of();
|
||||
}
|
||||
String last = args[args.length - 1].toLowerCase(Locale.ROOT);
|
||||
List<String> out = new ArrayList<>();
|
||||
if (args.length == 1) {
|
||||
out.addAll(SUBS);
|
||||
plugin.landsOf(p.getUniqueId()).forEach(l -> out.add(l.getName()));
|
||||
} else {
|
||||
switch (args[0].toLowerCase(Locale.ROOT)) {
|
||||
case "queue", "watch" -> out.addAll(args.length == 2 ? List.of("list", "clear") : List.of());
|
||||
case "temp" -> out.addAll(List.of("list", "cancel", "1", "7", "14", "30"));
|
||||
case "skip", "gift" -> {
|
||||
out.add("confirm");
|
||||
plugin.landsOf(p.getUniqueId()).forEach(l -> out.add(l.getName()));
|
||||
}
|
||||
default -> { }
|
||||
}
|
||||
if (args[0].equalsIgnoreCase("queue")) {
|
||||
plugin.landsOf(p.getUniqueId()).forEach(l -> out.add(l.getName()));
|
||||
}
|
||||
}
|
||||
return out.stream().filter(o -> o.toLowerCase(Locale.ROOT).startsWith(last)).toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
package de.landschunkinfo;
|
||||
|
||||
import de.landschunkinfo.LockStore.Entry;
|
||||
import de.landschunkinfo.LockStore.Kind;
|
||||
import me.angeschossen.lands.api.LandsIntegration;
|
||||
import me.angeschossen.lands.api.applicationframework.util.ULID;
|
||||
import me.angeschossen.lands.api.events.ChunkDeleteEvent;
|
||||
import me.angeschossen.lands.api.events.ChunkPreClaimEvent;
|
||||
import me.angeschossen.lands.api.events.land.DeleteReason;
|
||||
import me.angeschossen.lands.api.events.memberholder.MemberHolderUpkeepEvent;
|
||||
import me.angeschossen.lands.api.land.Land;
|
||||
import me.angeschossen.lands.api.player.LandPlayer;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class LandsChunkInfo extends JavaPlugin implements Listener {
|
||||
|
||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd.MM. HH:mm");
|
||||
private static final long TICK_SECONDS = 30;
|
||||
|
||||
LandsIntegration lands;
|
||||
LockStore store;
|
||||
VaultHook vault;
|
||||
|
||||
private long cooldownMillis;
|
||||
private long lastTick = System.currentTimeMillis();
|
||||
/** Unclaims, die vom Plugin selbst ausgeloest wurden (Temp-Claims) und keinen Slot sperren sollen. */
|
||||
private final Set<String> silent = ConcurrentHashMap.newKeySet();
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
saveDefaultConfig();
|
||||
lands = LandsIntegration.of(this);
|
||||
store = new LockStore(getDataFolder(), getLogger());
|
||||
reload();
|
||||
|
||||
if (Bukkit.getPluginManager().getPlugin("Vault") != null) {
|
||||
vault = VaultHook.create();
|
||||
}
|
||||
if (vault == null) {
|
||||
getLogger().info("Vault/Economy nicht gefunden: Bezahlung nur aus der Landkasse moeglich, /chunkinfo gift ist deaktiviert.");
|
||||
}
|
||||
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
|
||||
new ChunkExpansion(this).register();
|
||||
}
|
||||
|
||||
Bukkit.getPluginManager().registerEvents(this, this);
|
||||
InfoCommand command = new InfoCommand(this);
|
||||
var cmd = getCommand("chunkinfo");
|
||||
if (cmd != null) {
|
||||
cmd.setExecutor(command);
|
||||
cmd.setTabCompleter(command);
|
||||
}
|
||||
Bukkit.getScheduler().runTaskTimer(this, this::tick, 100L, TICK_SECONDS * 20L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (store != null) {
|
||||
store.save();
|
||||
}
|
||||
}
|
||||
|
||||
void reload() {
|
||||
reloadConfig();
|
||||
cooldownMillis = TimeUtil.parse(getConfig().getString("unclaim-cooldown.duration", "24h"));
|
||||
if (cooldownMillis < 0) {
|
||||
getLogger().warning("unclaim-cooldown.duration ist ungueltig, Cooldown wird deaktiviert.");
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- Helfer
|
||||
|
||||
boolean cooldownActive() {
|
||||
return getConfig().getBoolean("unclaim-cooldown.enabled", true) && cooldownMillis > 0;
|
||||
}
|
||||
|
||||
long cooldownMillis() {
|
||||
return cooldownMillis;
|
||||
}
|
||||
|
||||
Component line(String text) {
|
||||
return LegacyComponentSerializer.legacyAmpersand().deserialize(text);
|
||||
}
|
||||
|
||||
void send(org.bukkit.command.CommandSender to, String text) {
|
||||
to.sendMessage(line(getConfig().getString("messages.prefix", "") + text));
|
||||
}
|
||||
|
||||
String cfgMsg(String key, String... repl) {
|
||||
String text = getConfig().getString("messages." + key, key);
|
||||
for (int i = 0; i + 1 < repl.length; i += 2) {
|
||||
text = text.replace(repl[i], repl[i + 1]);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
static String landId(Land land) {
|
||||
return land.getULID().toString();
|
||||
}
|
||||
|
||||
Land landById(String id) {
|
||||
try {
|
||||
return lands.getLandByULID(ULID.fromString(id));
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lands, in denen die Person Besitzer oder vertraut ist. */
|
||||
List<Land> landsOf(UUID uuid) {
|
||||
List<Land> out = new ArrayList<>();
|
||||
for (Object o : lands.getLands()) {
|
||||
Land l = (Land) o;
|
||||
if (uuid.equals(l.getOwnerUID()) || l.isTrusted(uuid)) {
|
||||
out.add(l);
|
||||
}
|
||||
}
|
||||
out.sort(Comparator.comparing((Land l) -> !uuid.equals(l.getOwnerUID())).thenComparing(Land::getName));
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Hauptland: eigenes Land, sonst das erste Land, in dem man vertraut ist. */
|
||||
Land primaryLand(UUID uuid) {
|
||||
List<Land> l = landsOf(uuid);
|
||||
return l.isEmpty() ? null : l.get(0);
|
||||
}
|
||||
|
||||
/** Freie Slots; -1 = kein Limit. */
|
||||
int freeSlots(Land land) {
|
||||
int max = land.getMaxChunks();
|
||||
if (max <= 0) {
|
||||
return -1;
|
||||
}
|
||||
int locked = cooldownActive() ? store.active(landId(land)).size() : 0;
|
||||
return Math.max(0, max - land.getChunksAmount() - locked);
|
||||
}
|
||||
|
||||
List<Player> onlineMembers(Land land) {
|
||||
List<Player> out = new ArrayList<>();
|
||||
for (Object o : land.getOnlinePlayers()) {
|
||||
if (o instanceof Player p) {
|
||||
out.add(p);
|
||||
} else if (o instanceof LandPlayer lp && lp.getPlayer() != null) {
|
||||
out.add(lp.getPlayer());
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Land-Rangliste nach Chunks, absteigend. */
|
||||
List<Land> top(int n) {
|
||||
List<Land> all = new ArrayList<>();
|
||||
for (Object o : lands.getLands()) {
|
||||
all.add((Land) o);
|
||||
}
|
||||
all.sort(Comparator.comparingInt(Land::getChunksAmount).reversed());
|
||||
return all.subList(0, Math.min(n, all.size()));
|
||||
}
|
||||
|
||||
static String fmt(long millis) {
|
||||
return DATE.format(LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault()));
|
||||
}
|
||||
|
||||
private static String chunkKey(String landId, String world, int x, int z) {
|
||||
return landId + "|" + world + "|" + x + "|" + z;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- Events
|
||||
|
||||
/** Manuelles Unclaim sperrt den Slot fuer die konfigurierte Zeit. */
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onUnclaim(ChunkDeleteEvent e) {
|
||||
String id = landId(e.getLand());
|
||||
if (silent.remove(chunkKey(id, e.getWorld().getName(), e.getX(), e.getZ()))) {
|
||||
return;
|
||||
}
|
||||
if (!cooldownActive() || e.getReason() != DeleteReason.DEFAULT
|
||||
|| "LAND_DELETION".equals(String.valueOf(e.getUnclaimType()))) {
|
||||
return;
|
||||
}
|
||||
LandPlayer lp = e.getLandPlayer();
|
||||
Player p = lp == null ? null : lp.getPlayer();
|
||||
if (p != null && p.hasPermission("landschunkinfo.bypass")) {
|
||||
return;
|
||||
}
|
||||
store.add(id, System.currentTimeMillis() + cooldownMillis);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
|
||||
public void onClaim(ChunkPreClaimEvent e) {
|
||||
if (!cooldownActive() || !getConfig().getBoolean("unclaim-cooldown.enforce", true)) {
|
||||
return;
|
||||
}
|
||||
LandPlayer lp = e.getLandPlayer();
|
||||
Player p = lp == null ? null : lp.getPlayer();
|
||||
if (p != null && p.hasPermission("landschunkinfo.bypass")) {
|
||||
return;
|
||||
}
|
||||
Land land = e.getLand();
|
||||
int max = land.getMaxChunks();
|
||||
if (max <= 0) {
|
||||
return;
|
||||
}
|
||||
int size = land.getChunksAmount();
|
||||
List<Long> locked = store.active(landId(land));
|
||||
// Nur eingreifen, wenn der Platz allein wegen der Sperren fehlt; sonst uebernimmt Lands.
|
||||
if (size < max && size + locked.size() >= max) {
|
||||
e.setCancelled(true);
|
||||
if (p != null) {
|
||||
long next = locked.get(Math.max(0, size + locked.size() - max)) - System.currentTimeMillis();
|
||||
send(p, cfgMsg("claim-blocked", "%time%", TimeUtil.format(next)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onUpkeep(MemberHolderUpkeepEvent e) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - store.lastUpkeep() > 600_000L) {
|
||||
store.lastUpkeep(now);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent e) {
|
||||
if (!getConfig().getBoolean("join-reminder.enabled", true)) {
|
||||
return;
|
||||
}
|
||||
Player p = e.getPlayer();
|
||||
Bukkit.getScheduler().runTaskLater(this, () -> joinSummary(p), 60L);
|
||||
}
|
||||
|
||||
private void joinSummary(Player p) {
|
||||
if (!p.isOnline() || !p.hasPermission("landschunkinfo.use")) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
long warnMs = (long) (getConfig().getDouble("join-reminder.upkeep-warning-hours", 48) * 3_600_000L);
|
||||
for (Land land : landsOf(p.getUniqueId())) {
|
||||
int free = freeSlots(land);
|
||||
if (free > 0 && land.getMaxChunks() > 0) {
|
||||
send(p, "&aIn &e" + land.getName() + " &asind &e" + free + " &afreie Chunk-Slots verfuegbar.");
|
||||
}
|
||||
List<Long> locked = cooldownActive() ? store.active(landId(land)) : List.of();
|
||||
if (!locked.isEmpty()) {
|
||||
send(p, "&7" + land.getName() + ": naechster Slot frei in &e" + TimeUtil.format(locked.get(0) - now));
|
||||
}
|
||||
double upkeep = land.getUpkeepCosts();
|
||||
if (upkeep > 0 && land.getBalance() < upkeep && nextUpkeep() - now <= warnMs
|
||||
&& p.getUniqueId().equals(land.getOwnerUID())) {
|
||||
send(p, "&cAchtung: &e" + land.getName() + " &chat zu wenig Guthaben fuer den Upkeep (Einzug in &e"
|
||||
+ TimeUtil.format(nextUpkeep() - now) + "&c).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- Ticker
|
||||
|
||||
private void tick() {
|
||||
long now = System.currentTimeMillis();
|
||||
try {
|
||||
notifyExpiredLocks(now);
|
||||
processTemp(now);
|
||||
processQueue();
|
||||
processWatch();
|
||||
} catch (RuntimeException ex) {
|
||||
getLogger().warning("Fehler im Hintergrund-Task: " + ex);
|
||||
}
|
||||
lastTick = now;
|
||||
}
|
||||
|
||||
private void notifyExpiredLocks(long now) {
|
||||
for (String id : store.lockedLands()) {
|
||||
Land land = landById(id);
|
||||
if (land == null) {
|
||||
store.clearLocks(id);
|
||||
continue;
|
||||
}
|
||||
long expired = store.all(id).stream().filter(t -> t > lastTick && t <= now).count();
|
||||
if (expired > 0) {
|
||||
for (Player p : onlineMembers(land)) {
|
||||
send(p, "&aIn &e" + land.getName() + " &aist wieder &e" + expired + " &aChunk-Slot frei!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processQueue() {
|
||||
Set<String> usedThisTick = new HashSet<>();
|
||||
for (Entry e : store.entries(Kind.QUEUE)) {
|
||||
Player p = Bukkit.getPlayer(e.player());
|
||||
Land land = landById(e.landId());
|
||||
World w = Bukkit.getWorld(e.world());
|
||||
if (land == null || w == null) {
|
||||
store.remove(Kind.QUEUE, e);
|
||||
continue;
|
||||
}
|
||||
if (p == null) {
|
||||
continue;
|
||||
}
|
||||
Land holder = lands.getLandByUnloadedChunk(w, e.x(), e.z());
|
||||
if (holder != null) {
|
||||
if (holder.equals(land)) {
|
||||
store.remove(Kind.QUEUE, e);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (freeSlots(land) == 0 || !usedThisTick.add(e.landId())) {
|
||||
continue;
|
||||
}
|
||||
LandPlayer lp = lands.getLandPlayer(e.player());
|
||||
if (lp == null) {
|
||||
continue;
|
||||
}
|
||||
store.remove(Kind.QUEUE, e);
|
||||
java.util.concurrent.CompletableFuture<?> f = land.claimChunk(lp, w, e.x(), e.z());
|
||||
f.whenComplete((r, ex) -> Bukkit.getScheduler().runTask(this, () -> {
|
||||
if (!p.isOnline()) {
|
||||
return;
|
||||
}
|
||||
String where = e.x() + ", " + e.z();
|
||||
if (ex == null && land.hasChunk(w, e.x(), e.z())) {
|
||||
send(p, "&aWarteschlange: Chunk &e" + where + " &awurde fuer &e" + land.getName() + " &ageclaimt.");
|
||||
} else {
|
||||
send(p, "&cWarteschlange: Chunk &e" + where + " &ckonnte nicht geclaimt werden (Eintrag entfernt).");
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private void processWatch() {
|
||||
for (Entry e : store.entries(Kind.WATCH)) {
|
||||
World w = Bukkit.getWorld(e.world());
|
||||
if (w == null) {
|
||||
store.remove(Kind.WATCH, e);
|
||||
continue;
|
||||
}
|
||||
Player p = Bukkit.getPlayer(e.player());
|
||||
if (p == null) {
|
||||
continue;
|
||||
}
|
||||
if (lands.getLandByUnloadedChunk(w, e.x(), e.z()) == null) {
|
||||
store.remove(Kind.WATCH, e);
|
||||
send(p, "&aDer beobachtete Chunk &e" + e.x() + ", " + e.z() + " &a(" + e.world()
|
||||
+ ") ist jetzt frei! Mit &e/chunkinfo queue &akannst du ihn dir sichern.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processTemp(long now) {
|
||||
for (Entry e : store.entries(Kind.TEMP)) {
|
||||
if (e.until() > now) {
|
||||
continue;
|
||||
}
|
||||
Land land = landById(e.landId());
|
||||
World w = Bukkit.getWorld(e.world());
|
||||
if (land == null || w == null || !land.hasChunk(w, e.x(), e.z())) {
|
||||
store.remove(Kind.TEMP, e);
|
||||
continue;
|
||||
}
|
||||
LandPlayer lp = lands.getLandPlayer(e.player());
|
||||
if (lp == null && land.getOwnerUID() != null) {
|
||||
lp = lands.getLandPlayer(land.getOwnerUID());
|
||||
}
|
||||
if (lp == null) {
|
||||
continue; // wartet, bis ein Verantwortlicher online ist
|
||||
}
|
||||
store.remove(Kind.TEMP, e);
|
||||
String key = chunkKey(e.landId(), e.world(), e.x(), e.z());
|
||||
silent.add(key);
|
||||
Bukkit.getScheduler().runTaskLater(this, () -> silent.remove(key), 200L);
|
||||
land.unclaimChunk(w, e.x(), e.z(), lp);
|
||||
for (Player p : onlineMembers(land)) {
|
||||
send(p, "&7Der zeitlich begrenzte Chunk &e" + e.x() + ", " + e.z() + " &7von &e" + land.getName()
|
||||
+ " &7ist abgelaufen und wurde freigegeben.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- Upkeep
|
||||
|
||||
YamlConfiguration landsConfig() {
|
||||
Plugin pl = Bukkit.getPluginManager().getPlugin("Lands");
|
||||
File f = pl == null ? null : new File(pl.getDataFolder(), "config.yml");
|
||||
return f != null && f.exists() ? YamlConfiguration.loadConfiguration(f) : null;
|
||||
}
|
||||
|
||||
/** Naechster Upkeep-Einzug in epoch-millis, abgeleitet aus Lands economic.interval. */
|
||||
long nextUpkeep() {
|
||||
ZoneId zone = ZoneId.systemDefault();
|
||||
int days = 1;
|
||||
LocalTime time = LocalTime.MIDNIGHT;
|
||||
YamlConfiguration lc = landsConfig();
|
||||
if (lc != null) {
|
||||
days = Math.max(1, lc.getInt("economic.interval.days_2", lc.getInt("economic.interval.days", 1)));
|
||||
try {
|
||||
time = LocalTime.parse(lc.getString("economic.interval.time", "00:00:00"));
|
||||
} catch (Exception ignored) {
|
||||
// Standardwert behalten
|
||||
}
|
||||
}
|
||||
LocalDate anchor = null;
|
||||
if (store.lastUpkeep() > 0) {
|
||||
anchor = Instant.ofEpochMilli(store.lastUpkeep()).atZone(zone).toLocalDate();
|
||||
} else {
|
||||
try {
|
||||
String cfg = getConfig().getString("upkeep.anchor-date", "");
|
||||
if (!cfg.isBlank()) {
|
||||
anchor = LocalDate.parse(cfg);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// ungueltiges Datum -> Schaetzung
|
||||
}
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now(zone);
|
||||
if (anchor == null) {
|
||||
LocalDateTime next = now.toLocalDate().atTime(time);
|
||||
return (next.isAfter(now) ? next : next.plusDays(1)).atZone(zone).toInstant().toEpochMilli();
|
||||
}
|
||||
LocalDateTime next = anchor.atTime(time);
|
||||
while (!next.isAfter(now)) {
|
||||
next = next.plusDays(days);
|
||||
}
|
||||
return next.atZone(zone).toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------- Inaktivitaet
|
||||
|
||||
/**
|
||||
* Restzeit bis der Besitzer laut Lands-Einstellung (expiration.owner_list) als inaktiv gilt.
|
||||
* Gibt null zurueck, wenn die Funktion in Lands nicht aktiv ist, -1 bei "never".
|
||||
*/
|
||||
Long inactivityRemaining(Land land) {
|
||||
YamlConfiguration lc = landsConfig();
|
||||
UUID owner = land.getOwnerUID();
|
||||
if (lc == null || owner == null) {
|
||||
return null;
|
||||
}
|
||||
List<String> list = null;
|
||||
for (String key : lc.getKeys(true)) {
|
||||
if (key.endsWith("expiration.owner_list")) {
|
||||
list = lc.getStringList(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (list == null || list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
OfflinePlayer op = Bukkit.getOfflinePlayer(owner);
|
||||
long playMs = op.getStatistic(Statistic.PLAY_ONE_MINUTE) * 50L;
|
||||
String chosen = null;
|
||||
long chosenPlay = Long.MAX_VALUE;
|
||||
String highest = null;
|
||||
long highestPlay = -1;
|
||||
for (String entry : list) {
|
||||
String[] parts = entry.split(":");
|
||||
long pt = parts.length == 2 ? TimeUtil.parse(parts[0].trim()) : -1;
|
||||
if (pt < 0) {
|
||||
continue;
|
||||
}
|
||||
if (pt >= playMs && pt < chosenPlay) {
|
||||
chosen = parts[1].trim();
|
||||
chosenPlay = pt;
|
||||
}
|
||||
if (pt > highestPlay) {
|
||||
highest = parts[1].trim();
|
||||
highestPlay = pt;
|
||||
}
|
||||
}
|
||||
String offline = chosen != null ? chosen : highest;
|
||||
if (offline == null) {
|
||||
return null;
|
||||
}
|
||||
if (offline.equalsIgnoreCase("never")) {
|
||||
return -1L;
|
||||
}
|
||||
long threshold = TimeUtil.parse(offline);
|
||||
if (threshold < 0) {
|
||||
return null;
|
||||
}
|
||||
long lastSeen = op.isOnline() ? System.currentTimeMillis() : op.getLastSeen();
|
||||
return Math.max(0, threshold - (System.currentTimeMillis() - lastSeen));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package de.landschunkinfo;
|
||||
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/** Persistiert Slot-Sperren, Queue, Watchlist, Temp-Claims und den letzten Upkeep-Einzug. */
|
||||
final class LockStore {
|
||||
|
||||
enum Kind { QUEUE, WATCH, TEMP }
|
||||
|
||||
/** Eintrag fuer Queue/Watch/Temp. {@code until} wird nur bei TEMP genutzt. */
|
||||
record Entry(UUID player, String landId, String world, int x, int z, long until) {
|
||||
|
||||
String serialize() {
|
||||
return player + ";" + landId + ";" + world + ";" + x + ";" + z + ";" + until;
|
||||
}
|
||||
|
||||
static Entry parse(String s) {
|
||||
String[] p = s.split(";");
|
||||
return new Entry(UUID.fromString(p[0]), p[1], p[2], Integer.parseInt(p[3]), Integer.parseInt(p[4]),
|
||||
Long.parseLong(p[5]));
|
||||
}
|
||||
|
||||
boolean at(String world, int x, int z) {
|
||||
return this.world.equals(world) && this.x == x && this.z == z;
|
||||
}
|
||||
}
|
||||
|
||||
private final File file;
|
||||
private final Logger log;
|
||||
private final Map<String, List<Long>> locks = new HashMap<>();
|
||||
private final Map<Kind, List<Entry>> entries = new EnumMap<>(Kind.class);
|
||||
private long lastUpkeep;
|
||||
|
||||
LockStore(File dataFolder, Logger log) {
|
||||
this.file = new File(dataFolder, "data.yml");
|
||||
this.log = log;
|
||||
for (Kind k : Kind.values()) {
|
||||
entries.put(k, new ArrayList<>());
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
private void load() {
|
||||
if (!file.exists()) {
|
||||
return;
|
||||
}
|
||||
YamlConfiguration yml = YamlConfiguration.loadConfiguration(file);
|
||||
lastUpkeep = yml.getLong("last-upkeep", 0);
|
||||
var section = yml.getConfigurationSection("locks");
|
||||
if (section != null) {
|
||||
for (String land : section.getKeys(false)) {
|
||||
locks.put(land, new ArrayList<>(section.getLongList(land)));
|
||||
}
|
||||
}
|
||||
for (Kind k : Kind.values()) {
|
||||
for (String s : yml.getStringList(k.name().toLowerCase())) {
|
||||
try {
|
||||
entries.get(k).add(Entry.parse(s));
|
||||
} catch (RuntimeException e) {
|
||||
log.warning("Ungueltiger Eintrag in data.yml uebersprungen: " + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void save() {
|
||||
YamlConfiguration yml = new YamlConfiguration();
|
||||
yml.set("last-upkeep", lastUpkeep);
|
||||
purge();
|
||||
locks.forEach((land, list) -> yml.set("locks." + land, list));
|
||||
for (Kind k : Kind.values()) {
|
||||
yml.set(k.name().toLowerCase(), entries.get(k).stream().map(Entry::serialize).toList());
|
||||
}
|
||||
try {
|
||||
yml.save(file);
|
||||
} catch (IOException e) {
|
||||
log.warning("data.yml konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void purge() {
|
||||
long now = System.currentTimeMillis();
|
||||
locks.values().forEach(l -> l.removeIf(t -> t <= now));
|
||||
locks.values().removeIf(List::isEmpty);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Sperren
|
||||
|
||||
synchronized void add(String landId, long until) {
|
||||
locks.computeIfAbsent(landId, k -> new ArrayList<>()).add(until);
|
||||
save();
|
||||
}
|
||||
|
||||
/** Aktive Sperren eines Lands, aufsteigend nach Ablaufzeit. */
|
||||
synchronized List<Long> active(String landId) {
|
||||
long now = System.currentTimeMillis();
|
||||
List<Long> list = locks.get(landId);
|
||||
if (list == null) {
|
||||
return List.of();
|
||||
}
|
||||
return list.stream().filter(t -> t > now).sorted().toList();
|
||||
}
|
||||
|
||||
/** Alle Land-IDs, die (evtl. abgelaufene) Sperren halten. */
|
||||
synchronized List<String> lockedLands() {
|
||||
return new ArrayList<>(locks.keySet());
|
||||
}
|
||||
|
||||
/** Alle Sperren eines Lands inkl. abgelaufener, fuer Ablauf-Benachrichtigungen. */
|
||||
synchronized List<Long> all(String landId) {
|
||||
return new ArrayList<>(locks.getOrDefault(landId, List.of()));
|
||||
}
|
||||
|
||||
/** Entfernt die fruehste aktive Sperre. */
|
||||
synchronized boolean removeEarliest(String landId) {
|
||||
List<Long> list = locks.get(landId);
|
||||
if (list == null) {
|
||||
return false;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
Long min = list.stream().filter(t -> t > now).min(Long::compare).orElse(null);
|
||||
if (min == null) {
|
||||
return false;
|
||||
}
|
||||
list.remove(min);
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
synchronized void clearLocks(String landId) {
|
||||
if (locks.remove(landId) != null) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Eintraege
|
||||
|
||||
synchronized List<Entry> entries(Kind kind) {
|
||||
return new ArrayList<>(entries.get(kind));
|
||||
}
|
||||
|
||||
synchronized void add(Kind kind, Entry entry) {
|
||||
entries.get(kind).add(entry);
|
||||
save();
|
||||
}
|
||||
|
||||
synchronized boolean remove(Kind kind, Entry entry) {
|
||||
boolean removed = entries.get(kind).remove(entry);
|
||||
if (removed) {
|
||||
save();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
synchronized int removeAll(Kind kind, UUID player) {
|
||||
int before = entries.get(kind).size();
|
||||
entries.get(kind).removeIf(e -> e.player().equals(player));
|
||||
int removed = before - entries.get(kind).size();
|
||||
if (removed > 0) {
|
||||
save();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Upkeep
|
||||
|
||||
synchronized long lastUpkeep() {
|
||||
return lastUpkeep;
|
||||
}
|
||||
|
||||
synchronized void lastUpkeep(long time) {
|
||||
this.lastUpkeep = time;
|
||||
save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package de.landschunkinfo;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
final class TimeUtil {
|
||||
|
||||
private static final Pattern PART = Pattern.compile("(\\d+)\\s*([smhd])", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private TimeUtil() {
|
||||
}
|
||||
|
||||
/** Parses values like "30s", "15m", "12h", "2d" or "1d12h" to milliseconds. Returns -1 if invalid. */
|
||||
static long parse(String input) {
|
||||
if (input == null || input.isBlank()) {
|
||||
return -1;
|
||||
}
|
||||
Matcher m = PART.matcher(input);
|
||||
long total = 0;
|
||||
int matched = 0;
|
||||
while (m.find()) {
|
||||
long n = Long.parseLong(m.group(1));
|
||||
total += n * switch (Character.toLowerCase(m.group(2).charAt(0))) {
|
||||
case 's' -> 1_000L;
|
||||
case 'm' -> 60_000L;
|
||||
case 'h' -> 3_600_000L;
|
||||
default -> 86_400_000L;
|
||||
};
|
||||
matched += m.group().length();
|
||||
}
|
||||
return matched == input.replace(" ", "").length() && total > 0 ? total : -1;
|
||||
}
|
||||
|
||||
static String format(long millis) {
|
||||
long s = Math.max(0, millis) / 1000;
|
||||
long d = s / 86400;
|
||||
long h = (s % 86400) / 3600;
|
||||
long m = (s % 3600) / 60;
|
||||
if (d > 0) {
|
||||
return d + "T " + h + "Std " + m + "Min";
|
||||
}
|
||||
if (h > 0) {
|
||||
return h + "Std " + m + "Min";
|
||||
}
|
||||
if (m > 0) {
|
||||
return m + "Min " + (s % 60) + "Sek";
|
||||
}
|
||||
return s + "Sek";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.landschunkinfo;
|
||||
|
||||
import net.milkbowl.vault.economy.Economy;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||
|
||||
/** Duenne Huelle um Vault. Wird nur geladen, wenn Vault installiert ist. */
|
||||
final class VaultHook {
|
||||
|
||||
private final Economy economy;
|
||||
|
||||
private VaultHook(Economy economy) {
|
||||
this.economy = economy;
|
||||
}
|
||||
|
||||
static VaultHook create() {
|
||||
RegisteredServiceProvider<Economy> rsp = Bukkit.getServicesManager().getRegistration(Economy.class);
|
||||
return rsp == null ? null : new VaultHook(rsp.getProvider());
|
||||
}
|
||||
|
||||
double balance(OfflinePlayer p) {
|
||||
return economy.getBalance(p);
|
||||
}
|
||||
|
||||
boolean withdraw(OfflinePlayer p, double amount) {
|
||||
return economy.withdrawPlayer(p, amount).transactionSuccess();
|
||||
}
|
||||
|
||||
String format(double amount) {
|
||||
return economy.format(amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
# Unclaim-Cooldown: Nach einem manuellen /unclaim bleibt der Chunk-Slot des Lands
|
||||
# für diese Zeit gesperrt. Format: 30s, 15m, 12h, 2d oder kombiniert (1d12h).
|
||||
unclaim-cooldown:
|
||||
enabled: true
|
||||
duration: 24h
|
||||
# true = Claimen wird blockiert, solange alle Slots belegt oder gesperrt sind.
|
||||
# false = nur Anzeige, kein Eingriff ins Claimen.
|
||||
enforce: true
|
||||
|
||||
# Upkeep-Anzeige. Zeitpunkt wird aus der Lands-config.yml (economic.interval) gelesen.
|
||||
# Der letzte echte Einzug wird automatisch gespeichert. Bis dahin kann ein Startdatum
|
||||
# (Tag eines bekannten Einzugs, yyyy-MM-dd) angegeben werden, sonst wird geschätzt.
|
||||
upkeep:
|
||||
anchor-date: ""
|
||||
|
||||
messages:
|
||||
prefix: "&8[&aChunks&8] &7"
|
||||
no-permission: "&cDazu hast du keine Berechtigung."
|
||||
players-only: "&cNur Spieler können diesen Befehl nutzen."
|
||||
no-land: "&cDu bist in keinem Land Mitglied."
|
||||
land-not-found: "&cLand nicht gefunden oder du bist dort kein Mitglied."
|
||||
reloaded: "&aKonfiguration neu geladen."
|
||||
claim-blocked: "&cAlle freien Chunk-Slots sind nach einem Unclaim noch gesperrt. Nächster Slot frei in &e%time%&c."
|
||||
|
||||
# Gesperrten Slot sofort freischalten (/chunkinfo skip, /chunkinfo gift <Land>).
|
||||
skip:
|
||||
enabled: true
|
||||
# PLAYER = Geld des Spielers (Vault), LAND = Landkasse (nur Besitzer). gift nutzt immer PLAYER.
|
||||
payment: PLAYER
|
||||
# Preis = Restzeit in Stunden * price-per-hour, mindestens min-price.
|
||||
price-per-hour: 25.0
|
||||
min-price: 50.0
|
||||
|
||||
# Auto-Claim-Warteschlange (/chunkinfo queue): claimt vorgemerkte freie Chunks, sobald ein Slot frei ist.
|
||||
# Der Spieler muss dafuer online sein.
|
||||
queue:
|
||||
max: 3
|
||||
|
||||
# Watchlist (/chunkinfo watch): Nachricht, sobald ein fremder Chunk frei wird.
|
||||
watch:
|
||||
max: 5
|
||||
|
||||
# Zeitlich begrenzte Claims (/chunkinfo temp <Tage>): Chunk wird nach Ablauf automatisch freigegeben,
|
||||
# ohne dass dadurch ein Slot gesperrt wird.
|
||||
temp-claims:
|
||||
enabled: true
|
||||
max-days: 30
|
||||
max-per-land: 3
|
||||
|
||||
# Hinweis beim Login: freie Slots, naechster Slot, Upkeep-Warnung.
|
||||
join-reminder:
|
||||
enabled: true
|
||||
upkeep-warning-hours: 48
|
||||
|
||||
top:
|
||||
size: 10
|
||||
@@ -0,0 +1,38 @@
|
||||
name: LandsChunkInfo
|
||||
main: de.landschunkinfo.LandsChunkInfo
|
||||
version: ${project.version}
|
||||
api-version: '1.21'
|
||||
depend: [Lands]
|
||||
softdepend: [Vault, PlaceholderAPI]
|
||||
folia-supported: false
|
||||
description: Zeigt Spielern ihre Chunks und wann wieder ein Chunk frei wird (Unclaim-Cooldown und Upkeep), mit Skip, Queue, Watchlist, Temp-Claims und Platzhaltern.
|
||||
commands:
|
||||
chunkinfo:
|
||||
description: Chunk-Belegung, freie Slots, Upkeep, Queue, Watchlist und mehr
|
||||
usage: /<command> [land|top|skip|gift|queue|watch|temp]
|
||||
aliases: [chunks, chunkslots]
|
||||
permissions:
|
||||
landschunkinfo.use:
|
||||
description: /chunkinfo benutzen
|
||||
default: true
|
||||
landschunkinfo.skip:
|
||||
description: Gesperrten Slot freikaufen
|
||||
default: true
|
||||
landschunkinfo.gift:
|
||||
description: Slot in fremdem Land freikaufen
|
||||
default: true
|
||||
landschunkinfo.queue:
|
||||
description: Auto-Claim-Warteschlange
|
||||
default: true
|
||||
landschunkinfo.watch:
|
||||
description: Chunk-Watchlist
|
||||
default: true
|
||||
landschunkinfo.temp:
|
||||
description: Zeitlich begrenzte Claims
|
||||
default: true
|
||||
landschunkinfo.bypass:
|
||||
description: Unclaim-Cooldown ignorieren
|
||||
default: op
|
||||
landschunkinfo.admin:
|
||||
description: /chunkinfo reload
|
||||
default: op
|
||||
Binary file not shown.
@@ -0,0 +1,56 @@
|
||||
# Unclaim-Cooldown: Nach einem manuellen /unclaim bleibt der Chunk-Slot des Lands
|
||||
# für diese Zeit gesperrt. Format: 30s, 15m, 12h, 2d oder kombiniert (1d12h).
|
||||
unclaim-cooldown:
|
||||
enabled: true
|
||||
duration: 24h
|
||||
# true = Claimen wird blockiert, solange alle Slots belegt oder gesperrt sind.
|
||||
# false = nur Anzeige, kein Eingriff ins Claimen.
|
||||
enforce: true
|
||||
|
||||
# Upkeep-Anzeige. Zeitpunkt wird aus der Lands-config.yml (economic.interval) gelesen.
|
||||
# Der letzte echte Einzug wird automatisch gespeichert. Bis dahin kann ein Startdatum
|
||||
# (Tag eines bekannten Einzugs, yyyy-MM-dd) angegeben werden, sonst wird geschätzt.
|
||||
upkeep:
|
||||
anchor-date: ""
|
||||
|
||||
messages:
|
||||
prefix: "&8[&aChunks&8] &7"
|
||||
no-permission: "&cDazu hast du keine Berechtigung."
|
||||
players-only: "&cNur Spieler können diesen Befehl nutzen."
|
||||
no-land: "&cDu bist in keinem Land Mitglied."
|
||||
land-not-found: "&cLand nicht gefunden oder du bist dort kein Mitglied."
|
||||
reloaded: "&aKonfiguration neu geladen."
|
||||
claim-blocked: "&cAlle freien Chunk-Slots sind nach einem Unclaim noch gesperrt. Nächster Slot frei in &e%time%&c."
|
||||
|
||||
# Gesperrten Slot sofort freischalten (/chunkinfo skip, /chunkinfo gift <Land>).
|
||||
skip:
|
||||
enabled: true
|
||||
# PLAYER = Geld des Spielers (Vault), LAND = Landkasse (nur Besitzer). gift nutzt immer PLAYER.
|
||||
payment: PLAYER
|
||||
# Preis = Restzeit in Stunden * price-per-hour, mindestens min-price.
|
||||
price-per-hour: 25.0
|
||||
min-price: 50.0
|
||||
|
||||
# Auto-Claim-Warteschlange (/chunkinfo queue): claimt vorgemerkte freie Chunks, sobald ein Slot frei ist.
|
||||
# Der Spieler muss dafuer online sein.
|
||||
queue:
|
||||
max: 3
|
||||
|
||||
# Watchlist (/chunkinfo watch): Nachricht, sobald ein fremder Chunk frei wird.
|
||||
watch:
|
||||
max: 5
|
||||
|
||||
# Zeitlich begrenzte Claims (/chunkinfo temp <Tage>): Chunk wird nach Ablauf automatisch freigegeben,
|
||||
# ohne dass dadurch ein Slot gesperrt wird.
|
||||
temp-claims:
|
||||
enabled: true
|
||||
max-days: 30
|
||||
max-per-land: 3
|
||||
|
||||
# Hinweis beim Login: freie Slots, naechster Slot, Upkeep-Warnung.
|
||||
join-reminder:
|
||||
enabled: true
|
||||
upkeep-warning-hours: 48
|
||||
|
||||
top:
|
||||
size: 10
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,38 @@
|
||||
name: LandsChunkInfo
|
||||
main: de.landschunkinfo.LandsChunkInfo
|
||||
version: 1.0.0
|
||||
api-version: '1.21'
|
||||
depend: [Lands]
|
||||
softdepend: [Vault, PlaceholderAPI]
|
||||
folia-supported: false
|
||||
description: Zeigt Spielern ihre Chunks und wann wieder ein Chunk frei wird (Unclaim-Cooldown und Upkeep), mit Skip, Queue, Watchlist, Temp-Claims und Platzhaltern.
|
||||
commands:
|
||||
chunkinfo:
|
||||
description: Chunk-Belegung, freie Slots, Upkeep, Queue, Watchlist und mehr
|
||||
usage: /<command> [land|top|skip|gift|queue|watch|temp]
|
||||
aliases: [chunks, chunkslots]
|
||||
permissions:
|
||||
landschunkinfo.use:
|
||||
description: /chunkinfo benutzen
|
||||
default: true
|
||||
landschunkinfo.skip:
|
||||
description: Gesperrten Slot freikaufen
|
||||
default: true
|
||||
landschunkinfo.gift:
|
||||
description: Slot in fremdem Land freikaufen
|
||||
default: true
|
||||
landschunkinfo.queue:
|
||||
description: Auto-Claim-Warteschlange
|
||||
default: true
|
||||
landschunkinfo.watch:
|
||||
description: Chunk-Watchlist
|
||||
default: true
|
||||
landschunkinfo.temp:
|
||||
description: Zeitlich begrenzte Claims
|
||||
default: true
|
||||
landschunkinfo.bypass:
|
||||
description: Unclaim-Cooldown ignorieren
|
||||
default: op
|
||||
landschunkinfo.admin:
|
||||
description: /chunkinfo reload
|
||||
default: op
|
||||
@@ -0,0 +1,3 @@
|
||||
artifactId=LandsChunkInfo
|
||||
groupId=de.landschunkinfo
|
||||
version=1.0.0
|
||||
@@ -0,0 +1,8 @@
|
||||
de\landschunkinfo\VaultHook.class
|
||||
de\landschunkinfo\LockStore$Entry.class
|
||||
de\landschunkinfo\InfoCommand.class
|
||||
de\landschunkinfo\TimeUtil.class
|
||||
de\landschunkinfo\ChunkExpansion.class
|
||||
de\landschunkinfo\LockStore$Kind.class
|
||||
de\landschunkinfo\LandsChunkInfo.class
|
||||
de\landschunkinfo\LockStore.class
|
||||
@@ -0,0 +1,6 @@
|
||||
C:\Users\Viper\Desktop\Lands-Erweiterung\src\main\java\de\landschunkinfo\ChunkExpansion.java
|
||||
C:\Users\Viper\Desktop\Lands-Erweiterung\src\main\java\de\landschunkinfo\InfoCommand.java
|
||||
C:\Users\Viper\Desktop\Lands-Erweiterung\src\main\java\de\landschunkinfo\LandsChunkInfo.java
|
||||
C:\Users\Viper\Desktop\Lands-Erweiterung\src\main\java\de\landschunkinfo\LockStore.java
|
||||
C:\Users\Viper\Desktop\Lands-Erweiterung\src\main\java\de\landschunkinfo\TimeUtil.java
|
||||
C:\Users\Viper\Desktop\Lands-Erweiterung\src\main\java\de\landschunkinfo\VaultHook.java
|
||||
Reference in New Issue
Block a user