diff --git a/README.md b/README.md
index fa7c236..b4cd52e 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,102 @@
# LandHopperLimit
+Dieses Plugin begrenzt, wie viele **Trichter** (Hopper) ein **Land** haben darf.
+Standard: **300 Trichter pro Land**. Ist das Limit erreicht, kann niemand mehr einen neuen Trichter in dem Land setzen.
+
+Bereits gesetzte Trichter bleiben immer stehen, es wird nichts gelöscht.
+
+---
+
+## Was du brauchst
+
+- Einen **Paper-Server** (Version 1.21.4)
+- Das Plugin **Lands**
+
+---
+
+## Installation (5 Schritte)
+
+1. Lade die Datei `LandHopperLimit-1.0.0.jar` herunter.
+2. Kopiere sie in den Ordner `plugins` deines Servers.
+3. Starte den Server neu.
+4. Gib einmal im Spiel (oder in der Konsole) diesen Befehl ein:
+ ```
+ /lhl scan
+ ```
+ Das zählt alle schon vorhandenen Trichter. Das dauert je nach Servergröße ein paar Minuten. Warte, bis "Scan fertig" erscheint.
+5. Fertig!
+
+Den Scan musst du nur **einmal** machen.
+
+---
+
+## Befehle
+
+| Befehl | Was er macht | Wer darf das |
+|---|---|---|
+| `/lhl` oder `/lhl info` | Zeigt, wie viele Trichter das Land hat, in dem du stehst | Alle |
+| `/lhl scan` | Zählt alle Trichter aller Länder neu | Nur Admins |
+| `/lhl reload` | Lädt die Einstellungen neu | Nur Admins |
+
+`/lhl` kannst du auch als `/landhopperlimit` oder `/trichterlimit` schreiben.
+
+---
+
+## Rechte (Permissions)
+
+| Recht | Was es bewirkt | Standard |
+|---|---|---|
+| `landhopperlimit.info` | `/lhl info` benutzen | Alle |
+| `landhopperlimit.admin` | `/lhl scan` und `/lhl reload` benutzen | Nur OPs |
+| `landhopperlimit.bypass` | Das Limit ignorieren | Niemand |
+
+Wer `landhopperlimit.bypass` bekommt, darf unbegrenzt Trichter setzen (zum Beispiel für Teammitglieder).
+
+---
+
+## Einstellungen ändern
+
+Öffne die Datei `plugins/LandHopperLimit/config.yml` mit einem Texteditor.
+
+**Limit ändern:**
+```yaml
+limit: 300
+```
+Ersetze `300` durch die Zahl, die du willst.
+
+**Nur bestimmte Welten:**
+```yaml
+worlds:
+ - world
+ - world_the_end
+```
+Lässt du die Liste leer (`worlds: []`), gilt das Limit in **allen** Welten.
+
+**Nachrichten ändern:** Alle Texte stehen unter `messages:`. Farben schreibst du mit `&`, zum Beispiel `&c` für rot und `&e` für gelb.
+
+Nach jeder Änderung im Spiel `/lhl reload` eingeben. Ein Neustart ist nicht nötig.
+
+---
+
+## Gut zu wissen
+
+- Das Limit gilt für **alle** Trichter, auch für spezielle Trichter von anderen Plugins, solange sie Trichter-Blöcke sind.
+- Steht man **außerhalb eines Lands** (Wildnis), gibt es kein Limit.
+- Die Zahlen werden in der Datei `plugins/LandHopperLimit/hopper-counts.yml` gespeichert. Diese Datei bitte **nicht von Hand ändern**.
+- Wurden Trichter mit anderen Plugins oder Tools gesetzt (zum Beispiel WorldEdit), kann die Zahl kurz falsch sein. Mit `/lhl scan` stimmt sie wieder.
+
+---
+
+## Hilfe, es klappt nicht!
+
+| Problem | Lösung |
+|---|---|
+| Plugin startet nicht | Prüfe, ob **Lands** installiert ist und der Server **Paper 1.21.4** ist |
+| "Du stehst in keinem Land" | Stell dich in ein Land, in der Wildnis gibt es nichts zu zählen |
+| Die Zahl stimmt nicht | `/lhl scan` ausführen |
+| "Dafür hast du keine Rechte" | Du brauchst das Recht `landhopperlimit.admin` |
+| Ein Spieler soll das Limit umgehen | Gib ihm das Recht `landhopperlimit.bypass` |
+
+---
+
+Von Lukas (Viper Network)
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..40bc30f
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,24 @@
+
+
+ 4.0.0
+ de.vipernetwork
+ LandHopperLimit
+ 1.0.0
+ jar
+
+ 21
+ UTF-8
+
+
+ papermchttps://repo.papermc.io/repository/maven-public/
+ jitpackhttps://jitpack.io
+
+
+ io.papermc.paperpaper-api1.21.4-R0.1-SNAPSHOTprovided
+ com.github.angeschossenLandsAPI7.25.4provided
+
+
+ LandHopperLimit-${project.version}
+ src/main/resourcestrue
+
+
diff --git a/src/main/java/de/vipernetwork/landhopperlimit/ChunkCache.java b/src/main/java/de/vipernetwork/landhopperlimit/ChunkCache.java
new file mode 100644
index 0000000..b8770c0
--- /dev/null
+++ b/src/main/java/de/vipernetwork/landhopperlimit/ChunkCache.java
@@ -0,0 +1,129 @@
+package de.vipernetwork.landhopperlimit;
+
+import org.bukkit.Chunk;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockState;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.file.YamlConfiguration;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Logger;
+
+/**
+ * Speichert die Anzahl der Trichter pro Chunk, damit auch nicht geladene Chunks
+ * eines Lands mitgezählt werden können, ohne sie laden zu müssen.
+ */
+public final class ChunkCache {
+
+ private final Map> counts = new ConcurrentHashMap<>();
+ private final File file;
+ private final Logger logger;
+ private volatile boolean dirty;
+
+ public ChunkCache(File file, Logger logger) {
+ this.file = file;
+ this.logger = logger;
+ }
+
+ static long key(int x, int z) {
+ return ((long) x << 32) | (z & 0xFFFFFFFFL);
+ }
+
+ public int get(UUID world, int x, int z) {
+ Map map = counts.get(world);
+ if (map == null) return 0;
+ return map.getOrDefault(key(x, z), 0);
+ }
+
+ public void set(UUID world, int x, int z, int amount) {
+ Map map = counts.computeIfAbsent(world, w -> new ConcurrentHashMap<>());
+ long k = key(x, z);
+ Integer old = amount <= 0 ? map.remove(k) : map.put(k, amount);
+ if (old == null ? amount > 0 : old != amount) dirty = true;
+ }
+
+ public void add(UUID world, int x, int z, int delta) {
+ set(world, x, z, Math.max(0, get(world, x, z) + delta));
+ }
+
+ /** Zählt die Trichter in einem geladenen Chunk. Der Block "exclude" wird nicht mitgezählt. */
+ public static int countHoppers(Chunk chunk, Block exclude) {
+ int n = 0;
+ for (BlockState state : chunk.getTileEntities(false)) {
+ if (state.getType() != Material.HOPPER) continue;
+ if (exclude != null && state.getX() == exclude.getX() && state.getY() == exclude.getY()
+ && state.getZ() == exclude.getZ()) continue;
+ n++;
+ }
+ return n;
+ }
+
+ /** Zählt einen geladenen Chunk neu und speichert das Ergebnis. */
+ public int recount(Chunk chunk, Block exclude) {
+ int n = countHoppers(chunk, exclude);
+ set(chunk.getWorld().getUID(), chunk.getX(), chunk.getZ(), n);
+ return n;
+ }
+
+ public void load() {
+ if (!file.exists()) return;
+ YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
+ ConfigurationSection worlds = yaml.getConfigurationSection("worlds");
+ if (worlds == null) return;
+ for (String worldId : worlds.getKeys(false)) {
+ UUID uuid;
+ try {
+ uuid = UUID.fromString(worldId);
+ } catch (IllegalArgumentException e) {
+ continue;
+ }
+ Map map = counts.computeIfAbsent(uuid, w -> new ConcurrentHashMap<>());
+ for (String entry : worlds.getStringList(worldId)) {
+ String[] p = entry.split(",");
+ if (p.length != 3) continue;
+ try {
+ int amount = Integer.parseInt(p[2].trim());
+ if (amount > 0) map.put(key(Integer.parseInt(p[0].trim()), Integer.parseInt(p[1].trim())), amount);
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ }
+ }
+
+ /** Erstellt eine Kopie der Daten (auf dem Main-Thread aufrufen), die dann async gespeichert werden kann. */
+ public Map> snapshot() {
+ Map> snap = new HashMap<>();
+ counts.forEach((world, map) -> {
+ List list = new ArrayList<>(map.size());
+ map.forEach((k, v) -> list.add((int) (k >> 32) + "," + (int) (long) k + "," + v));
+ snap.put(world, list);
+ });
+ dirty = false;
+ return snap;
+ }
+
+ public boolean isDirty() {
+ return dirty;
+ }
+
+ public synchronized void write(Map> snap) {
+ YamlConfiguration yaml = new YamlConfiguration();
+ yaml.options().setHeader(List.of("Automatisch erstellt - nicht von Hand bearbeiten.",
+ "Format: chunkX,chunkZ,anzahlTrichter"));
+ snap.forEach((world, list) -> yaml.set("worlds." + world, list));
+ try {
+ file.getParentFile().mkdirs();
+ yaml.save(file);
+ } catch (IOException e) {
+ logger.warning("Konnte " + file.getName() + " nicht speichern: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/de/vipernetwork/landhopperlimit/LandHopperLimit.java b/src/main/java/de/vipernetwork/landhopperlimit/LandHopperLimit.java
new file mode 100644
index 0000000..44ffc5e
--- /dev/null
+++ b/src/main/java/de/vipernetwork/landhopperlimit/LandHopperLimit.java
@@ -0,0 +1,299 @@
+package de.vipernetwork.landhopperlimit;
+
+import me.angeschossen.lands.api.LandsIntegration;
+import me.angeschossen.lands.api.land.ChunkCoordinate;
+import me.angeschossen.lands.api.land.Land;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import org.bukkit.Bukkit;
+import org.bukkit.Chunk;
+import org.bukkit.Material;
+import org.bukkit.World;
+import org.bukkit.block.Block;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabCompleter;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.block.BlockExplodeEvent;
+import org.bukkit.event.block.BlockPlaceEvent;
+import org.bukkit.event.entity.EntityExplodeEvent;
+import org.bukkit.event.world.ChunkLoadEvent;
+import org.bukkit.event.world.ChunkUnloadEvent;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public final class LandHopperLimit extends JavaPlugin implements Listener, TabCompleter {
+
+ private static final LegacyComponentSerializer LEGACY = LegacyComponentSerializer.legacyAmpersand();
+
+ private LandsIntegration lands;
+ private ChunkCache cache;
+ private int limit;
+ private Set worlds;
+ private boolean scanning;
+
+ @Override
+ public void onEnable() {
+ saveDefaultConfig();
+ loadSettings();
+
+ lands = LandsIntegration.of(this);
+ cache = new ChunkCache(new File(getDataFolder(), "hopper-counts.yml"), getLogger());
+ cache.load();
+
+ // Bereits geladene Chunks direkt zählen
+ for (World world : Bukkit.getWorlds()) {
+ if (!worldEnabled(world)) continue;
+ for (Chunk chunk : world.getLoadedChunks()) cache.recount(chunk, null);
+ }
+
+ getServer().getPluginManager().registerEvents(this, this);
+ var cmd = getCommand("landhopperlimit");
+ if (cmd != null) {
+ cmd.setExecutor(this);
+ cmd.setTabCompleter(this);
+ }
+
+ // Alle 5 Minuten speichern, falls sich etwas geändert hat
+ Bukkit.getScheduler().runTaskTimer(this, this::saveAsync, 20L * 300, 20L * 300);
+ getLogger().info("Limit: " + limit + " Trichter pro Land.");
+ }
+
+ @Override
+ public void onDisable() {
+ if (cache != null) cache.write(cache.snapshot());
+ }
+
+ private void loadSettings() {
+ reloadConfig();
+ limit = getConfig().getInt("limit", 300);
+ worlds = new HashSet<>(getConfig().getStringList("worlds"));
+ }
+
+ private void saveAsync() {
+ if (!cache.isDirty()) return;
+ Map> snap = cache.snapshot();
+ Bukkit.getScheduler().runTaskAsynchronously(this, () -> cache.write(snap));
+ }
+
+ private boolean worldEnabled(World world) {
+ return worlds.isEmpty() || worlds.contains(world.getName());
+ }
+
+ // ---------------------------------------------------------------- Zählen
+
+ /** Zählt alle Trichter eines Lands. Geladene Chunks werden live gezählt, die anderen kommen aus dem Cache. */
+ private int countLand(Land land, Block exclude) {
+ int total = 0;
+ for (World world : Bukkit.getWorlds()) {
+ Collection chunks = land.getChunks(world);
+ if (chunks == null || chunks.isEmpty()) continue;
+ UUID uid = world.getUID();
+ for (ChunkCoordinate cc : chunks) {
+ int x = cc.getX(), z = cc.getZ();
+ if (world.isChunkLoaded(x, z)) {
+ Block ex = exclude != null && exclude.getWorld().equals(world)
+ && (exclude.getX() >> 4) == x && (exclude.getZ() >> 4) == z ? exclude : null;
+ total += cache.recount(world.getChunkAt(x, z), ex);
+ } else {
+ total += cache.get(uid, x, z);
+ }
+ }
+ }
+ return total;
+ }
+
+ // ---------------------------------------------------------------- Events
+
+ @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
+ public void onPlaceCheck(BlockPlaceEvent event) {
+ Block block = event.getBlockPlaced();
+ if (block.getType() != Material.HOPPER) return;
+ if (!worldEnabled(block.getWorld())) return;
+
+ Player player = event.getPlayer();
+ if (player.hasPermission("landhopperlimit.bypass")) return;
+
+ Land land = lands.getLandByChunk(block.getWorld(), block.getX() >> 4, block.getZ() >> 4);
+ if (land == null) return; // Wildnis - kein Land-Limit
+
+ int count = countLand(land, block);
+ if (count >= limit) {
+ event.setCancelled(true);
+ send(player, "messages.limit-reached", land.getName(), count);
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onPlaced(BlockPlaceEvent event) {
+ Block b = event.getBlockPlaced();
+ if (b.getType() == Material.HOPPER) cache.add(b.getWorld().getUID(), b.getX() >> 4, b.getZ() >> 4, 1);
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onBreak(BlockBreakEvent event) {
+ removed(event.getBlock());
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onEntityExplode(EntityExplodeEvent event) {
+ event.blockList().forEach(this::removed);
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onBlockExplode(BlockExplodeEvent event) {
+ event.blockList().forEach(this::removed);
+ }
+
+ private void removed(Block b) {
+ if (b.getType() == Material.HOPPER) cache.add(b.getWorld().getUID(), b.getX() >> 4, b.getZ() >> 4, -1);
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onChunkLoad(ChunkLoadEvent event) {
+ if (!worldEnabled(event.getWorld())) return;
+ cache.recount(event.getChunk(), null);
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onChunkUnload(ChunkUnloadEvent event) {
+ if (!worldEnabled(event.getWorld())) return;
+ cache.recount(event.getChunk(), null);
+ }
+
+ // ---------------------------------------------------------------- Befehle
+
+ @Override
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
+ String sub = args.length > 0 ? args[0].toLowerCase() : "info";
+ switch (sub) {
+ case "info" -> {
+ if (!(sender instanceof Player p)) {
+ sender.sendMessage("Nur ingame nutzbar.");
+ return true;
+ }
+ if (!p.hasPermission("landhopperlimit.info")) {
+ send(p, "messages.no-permission", null, 0);
+ return true;
+ }
+ Land land = lands.getLandByChunk(p.getWorld(), p.getLocation().getBlockX() >> 4, p.getLocation().getBlockZ() >> 4);
+ if (land == null) {
+ send(p, "messages.no-land", null, 0);
+ return true;
+ }
+ send(p, "messages.info", land.getName(), countLand(land, null));
+ }
+ case "scan" -> {
+ if (!sender.hasPermission("landhopperlimit.admin")) {
+ send(sender, "messages.no-permission", null, 0);
+ return true;
+ }
+ startScan(sender);
+ }
+ case "reload" -> {
+ if (!sender.hasPermission("landhopperlimit.admin")) {
+ send(sender, "messages.no-permission", null, 0);
+ return true;
+ }
+ loadSettings();
+ send(sender, "messages.reloaded", null, 0);
+ }
+ default -> sender.sendMessage(LEGACY.deserialize("&7/" + label + " info &8| &7scan &8| &7reload"));
+ }
+ return true;
+ }
+
+ @Override
+ public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
+ if (args.length != 1) return List.of();
+ List options = sender.hasPermission("landhopperlimit.admin") ? List.of("info", "scan", "reload") : List.of("info");
+ return options.stream().filter(s -> s.startsWith(args[0].toLowerCase())).toList();
+ }
+
+ /** Lädt alle Chunks aller Länder nacheinander und zählt die Trichter. Nur einmal nach der Installation nötig. */
+ private void startScan(CommandSender sender) {
+ if (scanning) {
+ send(sender, "messages.scan-running", null, 0);
+ return;
+ }
+ Deque queue = new ArrayDeque<>();
+ List worldList = Bukkit.getWorlds();
+ for (Land land : lands.getLands()) {
+ for (int i = 0; i < worldList.size(); i++) {
+ Collection chunks = land.getChunks(worldList.get(i));
+ if (chunks == null) continue;
+ for (ChunkCoordinate cc : chunks) queue.add(new long[]{i, cc.getX(), cc.getZ()});
+ }
+ }
+ int total = queue.size();
+ if (total == 0) {
+ sender.sendMessage(LEGACY.deserialize(msg("messages.scan-done").replace("{chunks}", "0")));
+ return;
+ }
+ scanning = true;
+ sender.sendMessage(LEGACY.deserialize(msg("messages.scan-start").replace("{chunks}", String.valueOf(total))));
+
+ AtomicInteger done = new AtomicInteger();
+ // Pro Tick ein paar Chunks async laden, damit der Server nicht laggt
+ Bukkit.getScheduler().runTaskTimer(this, task -> {
+ for (int n = 0; n < 4 && !queue.isEmpty(); n++) {
+ long[] e = queue.poll();
+ World world = worldList.get((int) e[0]);
+ int x = (int) e[1], z = (int) e[2];
+ if (world.isChunkLoaded(x, z)) {
+ cache.recount(world.getChunkAt(x, z), null);
+ progress(sender, done.incrementAndGet(), total);
+ continue;
+ }
+ world.getChunkAtAsync(x, z, false).handle((chunk, error) -> {
+ if (chunk != null && error == null) cache.recount(chunk, null);
+ progress(sender, done.incrementAndGet(), total);
+ return null;
+ });
+ }
+ if (queue.isEmpty()) task.cancel();
+ }, 1L, 1L);
+ }
+
+ private void progress(CommandSender sender, int done, int total) {
+ if (done % 500 == 0 && done < total) {
+ sender.sendMessage(LEGACY.deserialize(msg("messages.scan-progress")
+ .replace("{done}", String.valueOf(done)).replace("{chunks}", String.valueOf(total))));
+ }
+ if (done == total) {
+ scanning = false;
+ saveAsync();
+ sender.sendMessage(LEGACY.deserialize(msg("messages.scan-done").replace("{chunks}", String.valueOf(total))));
+ }
+ }
+
+ // ---------------------------------------------------------------- Nachrichten
+
+ private String msg(String path) {
+ return getConfig().getString("messages.prefix", "") + getConfig().getString(path, path);
+ }
+
+ private void send(CommandSender to, String path, String land, int count) {
+ String text = msg(path)
+ .replace("{limit}", String.valueOf(limit))
+ .replace("{count}", String.valueOf(count))
+ .replace("{land}", land == null ? "-" : land);
+ Component c = LEGACY.deserialize(text);
+ to.sendMessage(c);
+ }
+}
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
new file mode 100644
index 0000000..8dd9f64
--- /dev/null
+++ b/src/main/resources/config.yml
@@ -0,0 +1,28 @@
+# LandHopperLimit
+# Begrenzt ALLE Trichter (normale + upgradebare) pro Land.
+# Bereits gesetzte Trichter bleiben stehen. Ist ein Land schon über dem Limit,
+# können dort nur keine neuen Trichter gesetzt werden.
+
+# Maximale Anzahl Trichter pro Land
+limit: 300
+
+# Welten, in denen das Limit gilt. Leer lassen = alle Welten.
+# Beispiel:
+# worlds:
+# - world
+# - world_the_end
+worlds: []
+
+# Farbcodes mit & (z.B. &c = rot, &e = gelb)
+# Platzhalter: {limit}, {count}, {land}, {chunks}, {done}
+messages:
+ prefix: '&8[&6Trichter&8] '
+ limit-reached: '&cDas Land &e{land} &chat das Maximum von &e{limit} &cTrichtern erreicht.'
+ info: '&7Land &e{land}&7: &e{count}&7/&e{limit} &7Trichter'
+ no-land: '&cDu stehst in keinem Land.'
+ no-permission: '&cDafür hast du keine Rechte.'
+ reloaded: '&aConfig neu geladen.'
+ scan-start: '&7Scanne &e{chunks} &7Chunks aller Länder...'
+ scan-progress: '&7Scan: &e{done}&7/&e{chunks} &7Chunks'
+ scan-done: '&aScan fertig! &e{chunks} &aChunks gezählt.'
+ scan-running: '&cEin Scan läuft bereits.'
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
new file mode 100644
index 0000000..cb53341
--- /dev/null
+++ b/src/main/resources/plugin.yml
@@ -0,0 +1,22 @@
+name: LandHopperLimit
+version: '${project.version}'
+main: de.vipernetwork.landhopperlimit.LandHopperLimit
+api-version: '1.20'
+author: Lukas (Viper Network)
+description: Begrenzt die Anzahl aller Trichter pro Lands-Land.
+depend: [Lands]
+commands:
+ landhopperlimit:
+ description: Trichter-Limit pro Land
+ usage: / [info|scan|reload]
+ aliases: [lhl, trichterlimit]
+permissions:
+ landhopperlimit.info:
+ description: Zeigt die Trichter-Anzahl im aktuellen Land
+ default: true
+ landhopperlimit.admin:
+ description: Darf scan und reload nutzen
+ default: op
+ landhopperlimit.bypass:
+ description: Ignoriert das Limit beim Platzieren
+ default: false