diff --git a/README.md b/README.md index 84f8f16..c30e5fd 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ 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. +2. Kopiere die Datei `LandsChunkInfo-1.0.2.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). @@ -296,3 +296,19 @@ Wenn du nicht weiterkommst: Kopiere die **roten Zeilen aus der Server-Konsole** - 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. + +--- + +## 11. PvP-Regionen (neu in 1.0.2) + +Lands blockiert PvP in der Wildnis zwischen Spielern, die im selben Land Mitglied sind. Mit PvP-Regionen kannst du Bereiche festlegen, in denen trotzdem gekämpft werden darf (z. B. eine Arena). + +| Befehl | Was passiert | +|---|---| +| `/chunkinfo pvp pos1` | Setzt die erste Ecke an deiner Position | +| `/chunkinfo pvp pos2` | Setzt die zweite Ecke an deiner Position | +| `/chunkinfo pvp create ` | Legt die Region an | +| `/chunkinfo pvp delete ` | Löscht die Region | +| `/chunkinfo pvp list` | Zeigt alle Regionen | + +Dafür brauchst du das Recht `landschunkinfo.admin`. Die Regionen sind Rechtecke über die ganze Höhe und werden in `plugins/LandsChunkInfo/pvp-regions.yml` gespeichert. PvP wird nur freigegeben, wenn Angreifer und Opfer beide in der Region stehen. diff --git a/pom.xml b/pom.xml index f1ca64e..0a20029 100644 --- a/pom.xml +++ b/pom.xml @@ -1,61 +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 - - - - + + + 4.0.0 + de.landschunkinfo + LandsChunkInfo + 1.0.2 + 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/InfoCommand.java b/src/main/java/de/landschunkinfo/InfoCommand.java index e9e92bd..35f2bce 100644 --- a/src/main/java/de/landschunkinfo/InfoCommand.java +++ b/src/main/java/de/landschunkinfo/InfoCommand.java @@ -21,7 +21,7 @@ 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 static final List SUBS = List.of("top", "skip", "gift", "queue", "watch", "temp", "pvp", "reload"); private final LandsChunkInfo plugin; @@ -58,11 +58,52 @@ final class InfoCommand implements CommandExecutor, TabCompleter { case "queue" -> queue(p, args); case "watch" -> watch(p, args); case "temp" -> temp(p, args); + case "pvp" -> pvp(p, args); default -> info(p, args); } return true; } + private void pvp(Player p, String[] args) { + if (!allowed(p, "admin")) { + return; + } + String action = args.length < 2 ? "" : args[1].toLowerCase(Locale.ROOT); + switch (action) { + case "pos1" -> { + plugin.pvp.setPos1(p); + plugin.send(p, "&aPunkt 1 gesetzt."); + } + case "pos2" -> { + plugin.pvp.setPos2(p); + plugin.send(p, "&aPunkt 2 gesetzt."); + } + case "create" -> { + if (args.length < 3) { + plugin.send(p, "&cNutzung: /chunkinfo pvp create "); + return; + } + String err = plugin.pvp.create(p, args[2]); + plugin.send(p, err == null ? "&aPvP-Region &e" + args[2] + " &aerstellt." : "&c" + err); + } + case "delete" -> { + if (args.length < 3) { + plugin.send(p, "&cNutzung: /chunkinfo pvp delete "); + return; + } + plugin.send(p, plugin.pvp.delete(args[2]) ? "&aRegion geloescht." : "&cRegion nicht gefunden."); + } + case "list" -> { + if (plugin.pvp.all().isEmpty()) { + plugin.send(p, "&7Keine PvP-Regionen vorhanden."); + } + plugin.pvp.all().values().forEach(r -> p.sendMessage(plugin.line("&8- &e" + r.name() + " &7(" + r.world() + + ": " + r.minX() + "," + r.minZ() + " bis " + r.maxX() + "," + r.maxZ() + ")"))); + } + default -> plugin.send(p, "&7/chunkinfo pvp pos1 | pos2 | create | delete | list"); + } + } + private boolean allowed(Player p, String perm) { if (p.hasPermission("landschunkinfo." + perm)) { return true; @@ -467,6 +508,13 @@ final class InfoCommand implements CommandExecutor, TabCompleter { 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 "pvp" -> { + if (args.length == 2) { + out.addAll(List.of("pos1", "pos2", "create", "delete", "list")); + } else if (args[1].equalsIgnoreCase("delete")) { + out.addAll(plugin.pvp.names()); + } + } case "skip", "gift" -> { out.add("confirm"); plugin.landsOf(p.getUniqueId()).forEach(l -> out.add(l.getName())); diff --git a/src/main/java/de/landschunkinfo/LandsChunkInfo.java b/src/main/java/de/landschunkinfo/LandsChunkInfo.java index f378975..1dc7cf7 100644 --- a/src/main/java/de/landschunkinfo/LandsChunkInfo.java +++ b/src/main/java/de/landschunkinfo/LandsChunkInfo.java @@ -49,6 +49,7 @@ public final class LandsChunkInfo extends JavaPlugin implements Listener { LandsIntegration lands; LockStore store; VaultHook vault; + PvpRegions pvp; private long cooldownMillis; private long lastTick = System.currentTimeMillis(); @@ -72,6 +73,8 @@ public final class LandsChunkInfo extends JavaPlugin implements Listener { new ChunkExpansion(this).register(); } + pvp = new PvpRegions(getDataFolder(), getLogger()); + Bukkit.getPluginManager().registerEvents(pvp, this); Bukkit.getPluginManager().registerEvents(this, this); InfoCommand command = new InfoCommand(this); var cmd = getCommand("chunkinfo"); diff --git a/src/main/java/de/landschunkinfo/PvpRegions.java b/src/main/java/de/landschunkinfo/PvpRegions.java new file mode 100644 index 0000000..45a5c8a --- /dev/null +++ b/src/main/java/de/landschunkinfo/PvpRegions.java @@ -0,0 +1,162 @@ +package de.landschunkinfo; + +import org.bukkit.Location; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageByEntityEvent; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +/** + * PvP-Regionen: In diesen Bereichen darf gekaempft werden, auch wenn Lands den Angriff + * (z. B. wegen gemeinsamem Land in der Wildnis) blockiert. Gespeichert in pvp-regions.yml. + * Die Regionen sind Rechtecke ueber die gesamte Hoehe. + */ +final class PvpRegions implements Listener { + + record Region(String name, String world, int minX, int minZ, int maxX, int maxZ) { + boolean contains(Location l) { + return l.getWorld() != null && l.getWorld().getName().equals(world) + && l.getBlockX() >= minX && l.getBlockX() <= maxX + && l.getBlockZ() >= minZ && l.getBlockZ() <= maxZ; + } + } + + private final File file; + private final Logger log; + private final Map regions = new LinkedHashMap<>(); + private final Map pos1 = new ConcurrentHashMap<>(); + private final Map pos2 = new ConcurrentHashMap<>(); + + PvpRegions(File dataFolder, Logger log) { + this.file = new File(dataFolder, "pvp-regions.yml"); + this.log = log; + load(); + } + + private void load() { + regions.clear(); + if (!file.exists()) { + return; + } + YamlConfiguration y = YamlConfiguration.loadConfiguration(file); + ConfigurationSection sec = y.getConfigurationSection("regions"); + if (sec == null) { + return; + } + for (String name : sec.getKeys(false)) { + ConfigurationSection r = sec.getConfigurationSection(name); + if (r == null) { + continue; + } + regions.put(name.toLowerCase(), new Region(name, r.getString("world", ""), + r.getInt("min-x"), r.getInt("min-z"), r.getInt("max-x"), r.getInt("max-z"))); + } + } + + private synchronized void save() { + YamlConfiguration y = new YamlConfiguration(); + for (Region r : regions.values()) { + String base = "regions." + r.name() + "."; + y.set(base + "world", r.world()); + y.set(base + "min-x", r.minX()); + y.set(base + "min-z", r.minZ()); + y.set(base + "max-x", r.maxX()); + y.set(base + "max-z", r.maxZ()); + } + try { + y.save(file); + } catch (IOException e) { + log.warning("pvp-regions.yml konnte nicht gespeichert werden: " + e.getMessage()); + } + } + + // ------------------------------------------------------------ Verwaltung + + void setPos1(Player p) { + pos1.put(p.getUniqueId(), p.getLocation()); + } + + void setPos2(Player p) { + pos2.put(p.getUniqueId(), p.getLocation()); + } + + /** @return null bei Erfolg, sonst Fehlertext */ + String create(Player p, String name) { + Location a = pos1.get(p.getUniqueId()); + Location b = pos2.get(p.getUniqueId()); + if (a == null || b == null) { + return "Setze zuerst &e/chunkinfo pvp pos1 &7und &e/chunkinfo pvp pos2&7."; + } + if (a.getWorld() == null || !a.getWorld().equals(b.getWorld())) { + return "Beide Punkte muessen in derselben Welt liegen."; + } + regions.put(name.toLowerCase(), new Region(name, a.getWorld().getName(), + Math.min(a.getBlockX(), b.getBlockX()), Math.min(a.getBlockZ(), b.getBlockZ()), + Math.max(a.getBlockX(), b.getBlockX()), Math.max(a.getBlockZ(), b.getBlockZ()))); + save(); + return null; + } + + boolean delete(String name) { + boolean removed = regions.remove(name.toLowerCase()) != null; + if (removed) { + save(); + } + return removed; + } + + Map all() { + return Collections.unmodifiableMap(regions); + } + + Set names() { + return regions.values().stream().map(Region::name).collect(java.util.stream.Collectors.toSet()); + } + + boolean inRegion(Location l) { + for (Region r : regions.values()) { + if (r.contains(l)) { + return true; + } + } + return false; + } + + // ----------------------------------------------------------------- Event + + /** + * Laeuft nach Lands (HIGHEST): Wenn Angreifer und Opfer beide in einer PvP-Region stehen, + * wird ein von Lands abgebrochener Schaden wieder freigegeben. + */ + @EventHandler(priority = EventPriority.HIGHEST) + public void onDamage(EntityDamageByEntityEvent e) { + if (!e.isCancelled() || regions.isEmpty() || !(e.getEntity() instanceof Player victim)) { + return; + } + Entity damager = e.getDamager(); + if (damager instanceof Projectile proj && proj.getShooter() instanceof Entity shooter) { + damager = shooter; + } + if (!(damager instanceof Player attacker) || attacker.equals(victim)) { + return; + } + if (inRegion(attacker.getLocation()) && inRegion(victim.getLocation())) { + e.setCancelled(false); + } + } +}