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); } }