Upload via GUI (6 Dateien)

This commit is contained in:
2026-10-26 21:35:36 +00:00
parent 62762ed71e
commit 2d8f7cf4bf
6 changed files with 602 additions and 0 deletions
@@ -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<UUID, Map<Long, Integer>> 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<Long, Integer> 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<Long, Integer> 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<Long, Integer> 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<UUID, List<String>> snapshot() {
Map<UUID, List<String>> snap = new HashMap<>();
counts.forEach((world, map) -> {
List<String> 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<UUID, List<String>> 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());
}
}
}
@@ -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<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);
}
}