diff --git a/README.md b/README.md index 8319473..84f8f16 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/libs/Lands-8.8.0.jar b/libs/Lands-8.8.0.jar new file mode 100644 index 0000000..5d1da16 Binary files /dev/null and b/libs/Lands-8.8.0.jar differ diff --git a/libs/VaultAPI-1.7.1.jar b/libs/VaultAPI-1.7.1.jar new file mode 100644 index 0000000..487b7af Binary files /dev/null and b/libs/VaultAPI-1.7.1.jar differ diff --git a/libs/placeholderapi-2.11.7.jar b/libs/placeholderapi-2.11.7.jar new file mode 100644 index 0000000..27e1cd7 Binary files /dev/null and b/libs/placeholderapi-2.11.7.jar differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f1ca64e --- /dev/null +++ b/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + de.landschunkinfo + LandsChunkInfo + 1.0.0 + jar + + + 25 + UTF-8 + + + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + + + + io.papermc.paper + paper-api + 1.21.11-R0.1-SNAPSHOT + provided + + + me.angeschossen + lands + 8.8.0 + system + ${project.basedir}/libs/Lands-8.8.0.jar + + + net.milkbowl + vaultapi + 1.7.1 + system + ${project.basedir}/libs/VaultAPI-1.7.1.jar + + + me.clip + placeholderapi + 2.11.7 + system + ${project.basedir}/libs/placeholderapi-2.11.7.jar + + + + + LandsChunkInfo-${project.version} + + + src/main/resources + true + + + + diff --git a/src/main/java/de/landschunkinfo/ChunkExpansion.java b/src/main/java/de/landschunkinfo/ChunkExpansion.java new file mode 100644 index 0000000..7c4c946 --- /dev/null +++ b/src/main/java/de/landschunkinfo/ChunkExpansion.java @@ -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__name%, %chunkinfo_top__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 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 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; + }; + } +} diff --git a/src/main/java/de/landschunkinfo/InfoCommand.java b/src/main/java/de/landschunkinfo/InfoCommand.java new file mode 100644 index 0000000..e9e92bd --- /dev/null +++ b/src/main/java/de/landschunkinfo/InfoCommand.java @@ -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 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 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 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 [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 [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 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 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 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 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 | 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 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 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 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(); + } +} diff --git a/src/main/java/de/landschunkinfo/LandsChunkInfo.java b/src/main/java/de/landschunkinfo/LandsChunkInfo.java new file mode 100644 index 0000000..f378975 --- /dev/null +++ b/src/main/java/de/landschunkinfo/LandsChunkInfo.java @@ -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 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 landsOf(UUID uuid) { + List 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 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 onlineMembers(Land land) { + List 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 top(int n) { + List 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 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 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 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 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)); + } +} diff --git a/src/main/java/de/landschunkinfo/LockStore.java b/src/main/java/de/landschunkinfo/LockStore.java new file mode 100644 index 0000000..419f95a --- /dev/null +++ b/src/main/java/de/landschunkinfo/LockStore.java @@ -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> locks = new HashMap<>(); + private final Map> 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 active(String landId) { + long now = System.currentTimeMillis(); + List 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 lockedLands() { + return new ArrayList<>(locks.keySet()); + } + + /** Alle Sperren eines Lands inkl. abgelaufener, fuer Ablauf-Benachrichtigungen. */ + synchronized List all(String landId) { + return new ArrayList<>(locks.getOrDefault(landId, List.of())); + } + + /** Entfernt die fruehste aktive Sperre. */ + synchronized boolean removeEarliest(String landId) { + List 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 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(); + } +} diff --git a/src/main/java/de/landschunkinfo/TimeUtil.java b/src/main/java/de/landschunkinfo/TimeUtil.java new file mode 100644 index 0000000..177c9ae --- /dev/null +++ b/src/main/java/de/landschunkinfo/TimeUtil.java @@ -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"; + } +} diff --git a/src/main/java/de/landschunkinfo/VaultHook.java b/src/main/java/de/landschunkinfo/VaultHook.java new file mode 100644 index 0000000..b643158 --- /dev/null +++ b/src/main/java/de/landschunkinfo/VaultHook.java @@ -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 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); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..67513a2 --- /dev/null +++ b/src/main/resources/config.yml @@ -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 ). +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 ): 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 diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..01c5601 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -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: / [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 diff --git a/target/LandsChunkInfo-1.0.0.jar b/target/LandsChunkInfo-1.0.0.jar new file mode 100644 index 0000000..b4d706b Binary files /dev/null and b/target/LandsChunkInfo-1.0.0.jar differ diff --git a/target/classes/config.yml b/target/classes/config.yml new file mode 100644 index 0000000..67513a2 --- /dev/null +++ b/target/classes/config.yml @@ -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 ). +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 ): 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 diff --git a/target/classes/de/landschunkinfo/ChunkExpansion.class b/target/classes/de/landschunkinfo/ChunkExpansion.class new file mode 100644 index 0000000..3714df2 Binary files /dev/null and b/target/classes/de/landschunkinfo/ChunkExpansion.class differ diff --git a/target/classes/de/landschunkinfo/InfoCommand.class b/target/classes/de/landschunkinfo/InfoCommand.class new file mode 100644 index 0000000..40319f5 Binary files /dev/null and b/target/classes/de/landschunkinfo/InfoCommand.class differ diff --git a/target/classes/de/landschunkinfo/LandsChunkInfo.class b/target/classes/de/landschunkinfo/LandsChunkInfo.class new file mode 100644 index 0000000..1c65b11 Binary files /dev/null and b/target/classes/de/landschunkinfo/LandsChunkInfo.class differ diff --git a/target/classes/de/landschunkinfo/LockStore$Entry.class b/target/classes/de/landschunkinfo/LockStore$Entry.class new file mode 100644 index 0000000..7afc9b5 Binary files /dev/null and b/target/classes/de/landschunkinfo/LockStore$Entry.class differ diff --git a/target/classes/de/landschunkinfo/LockStore$Kind.class b/target/classes/de/landschunkinfo/LockStore$Kind.class new file mode 100644 index 0000000..37e4fdf Binary files /dev/null and b/target/classes/de/landschunkinfo/LockStore$Kind.class differ diff --git a/target/classes/de/landschunkinfo/LockStore.class b/target/classes/de/landschunkinfo/LockStore.class new file mode 100644 index 0000000..2c487eb Binary files /dev/null and b/target/classes/de/landschunkinfo/LockStore.class differ diff --git a/target/classes/de/landschunkinfo/TimeUtil.class b/target/classes/de/landschunkinfo/TimeUtil.class new file mode 100644 index 0000000..3865dc8 Binary files /dev/null and b/target/classes/de/landschunkinfo/TimeUtil.class differ diff --git a/target/classes/de/landschunkinfo/VaultHook.class b/target/classes/de/landschunkinfo/VaultHook.class new file mode 100644 index 0000000..e88bd18 Binary files /dev/null and b/target/classes/de/landschunkinfo/VaultHook.class differ diff --git a/target/classes/plugin.yml b/target/classes/plugin.yml new file mode 100644 index 0000000..1324240 --- /dev/null +++ b/target/classes/plugin.yml @@ -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: / [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 diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties new file mode 100644 index 0000000..af117c4 --- /dev/null +++ b/target/maven-archiver/pom.properties @@ -0,0 +1,3 @@ +artifactId=LandsChunkInfo +groupId=de.landschunkinfo +version=1.0.0 diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..3d56fa0 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -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 diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..db38ef3 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -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