300 lines
12 KiB
Java
300 lines
12 KiB
Java
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<String> 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<UUID, List<String>> 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<ChunkCoordinate> 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<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
|
|
if (args.length != 1) return List.of();
|
|
List<String> 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<long[]> queue = new ArrayDeque<>();
|
|
List<World> worldList = Bukkit.getWorlds();
|
|
for (Land land : lands.getLands()) {
|
|
for (int i = 0; i < worldList.size(); i++) {
|
|
Collection<ChunkCoordinate> 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);
|
|
}
|
|
}
|