diff --git a/src/main/java/com/extantape/waystation/Protection.java b/src/main/java/com/extantape/waystation/Protection.java index 2bae644..5f038bc 100644 --- a/src/main/java/com/extantape/waystation/Protection.java +++ b/src/main/java/com/extantape/waystation/Protection.java @@ -71,6 +71,37 @@ void rebuild() { } } + /** + * What Rewild must never rewild: every chunk inside the station's world border, kept whole, and the zone around each + * gate elsewhere, kept in place. Reported whether or not protection is on, because a gate is never wild land. + */ + List rewildChunks(org.bukkit.World w) { + List out = new ArrayList<>(); + if (!w.getName().equals(plugin.settings().stationWorld)) return out; + var border = w.getWorldBorder(); + double half = border.getSize() / 2; + int x0 = (int) Math.floor(border.getCenter().getX() - half) >> 4, x1 = (int) Math.floor(border.getCenter().getX() + half) >> 4; + int z0 = (int) Math.floor(border.getCenter().getZ() - half) >> 4, z1 = (int) Math.floor(border.getCenter().getZ() + half) >> 4; + // A border left at the default (60 million blocks) would list the whole world; the station sets its own, so cap it. + if ((long) (x1 - x0 + 1) * (z1 - z0 + 1) > 65_536) throw new IllegalStateException("the station's world border is too large to report to Rewild"); + for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) out.add(new RewildKeeps.Chunk(x, z, "the station")); + return out; + } + + List rewildBoxes(org.bukkit.World w) { + List out = new ArrayList<>(); + Settings s = plugin.settings(); + for (Gate gate : plugin.store().all()) { + if (!gate.world.equals(w.getName()) || gate.world.equals(s.stationWorld)) continue; + int reach = Math.max(2, s.look(plugin.lookName(gate)).width() / 2 + 1); + int height = s.look(plugin.lookName(gate)).height(); + int along = gate.alongX() ? reach : 2, across = gate.alongX() ? 2 : reach; + out.add(new RewildKeeps.Box(gate.x - along, gate.y - 1, gate.z - across, gate.x + along, gate.y + height, gate.z + across, + "a gate (" + gate.id + ")")); + } + return out; + } + boolean isProtected(Location at) { Settings s = plugin.settings(); if (!s.protectionEnabled) return false; diff --git a/src/main/java/com/extantape/waystation/RewildKeeps.java b/src/main/java/com/extantape/waystation/RewildKeeps.java new file mode 100644 index 0000000..69fbfbc --- /dev/null +++ b/src/main/java/com/extantape/waystation/RewildKeeps.java @@ -0,0 +1,111 @@ +package com.extantape.waystation; + +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.server.PluginEnableEvent; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.ServicePriority; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * Tells Rewild, if it's installed, what this plugin owns, so Rewild never rewilds it: whole chunks to leave alone, and + * block boxes to keep in place while the land around them rewilds. Rewild's interface is reached by name through its + * own class loader, so neither plugin needs the other. The same small class is in each ExtantApe plugin that owns + * blocks; keep the copies alike. + * + *

Rewild only listens once an admin respects this plugin (/rewild plugins). If anything here throws, Rewild resets + * nothing in that world, which is the safe side. + */ +public final class RewildKeeps implements Listener { + static final String PROVIDER = "com.extantape.rewild.api.KeepProvider"; + + /** A box of blocks, corners included, with the reason /rewild why shows. */ + public record Box(int minX, int minY, int minZ, int maxX, int maxY, int maxZ, String reason) {} + + /** A chunk to keep whole, with its reason. */ + public record Chunk(int x, int z, String reason) {} + + private final Plugin owner; + private final Function> chunks; + private final Function> boxes; + private boolean registered; + + public RewildKeeps(Plugin owner, Function> chunks, Function> boxes) { + this.owner = owner; + this.chunks = chunks; + this.boxes = boxes; + } + + /** Registers now if Rewild is already running; otherwise when it enables. */ + public void start() { + Plugin rewild = Bukkit.getPluginManager().getPlugin("Rewild"); + if (rewild != null && rewild.isEnabled()) register(rewild); + } + + @EventHandler + public void onEnable(PluginEnableEvent e) { + if (e.getPlugin().getName().equals("Rewild")) register(e.getPlugin()); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void register(Plugin rewild) { + if (registered) return; + try { + ClassLoader cl = rewild.getClass().getClassLoader(); + Class iface = Class.forName(PROVIDER, true, cl); + Object provider = provider(iface, chunks, boxes); + Bukkit.getServicesManager().register((Class) iface, provider, owner, ServicePriority.Normal); + registered = true; + owner.getLogger().info("Telling Rewild what " + owner.getName() + " owns, so it's never rewilded."); + } catch (ReflectiveOperationException | RuntimeException | LinkageError e) { + owner.getLogger().warning("Rewild is installed but its keep interface doesn't match what " + owner.getName() + + " expects (" + e + "). If an admin respects " + owner.getName() + ", Rewild will reset nothing."); + } + } + + /** A KeepProvider for the given interface. Package-private for tests, which pass a stand-in of the same shape. */ + static Object provider(Class iface, Function> chunks, Function> boxes) + throws ReflectiveOperationException { + Class keepClass = Class.forName(iface.getName() + "$Keep", true, iface.getClassLoader()); + Constructor keep = keepClass.getDeclaredConstructor(int.class, int.class, String.class); + Constructor area; + try { + Class areaClass = Class.forName(iface.getName() + "$Area", true, iface.getClassLoader()); + area = areaClass.getDeclaredConstructor(int.class, int.class, int.class, int.class, int.class, int.class, String.class); + } catch (ClassNotFoundException e) { + area = null; // Rewild before 0.3.0: whole chunks only. + } + Constructor makeArea = area; + return Proxy.newProxyInstance(iface.getClassLoader(), new Class[] {iface}, (proxy, method, args) -> switch (method.getName()) { + case "keeps" -> { + List out = new ArrayList<>(); + for (Chunk c : chunks.apply((World) args[0])) out.add(keep.newInstance(c.x(), c.z(), c.reason())); + yield out; + } + case "areas" -> { + List out = new ArrayList<>(); + if (makeArea != null) { + for (Box b : boxes.apply((World) args[0])) { + out.add(makeArea.newInstance(b.minX(), b.minY(), b.minZ(), b.maxX(), b.maxY(), b.maxZ(), b.reason())); + } + } + yield out; + } + case "hashCode" -> System.identityHashCode(proxy); + case "equals" -> proxy == args[0]; + case "toString" -> owner(proxy); + default -> throw new UnsupportedOperationException(method.getName()); + }); + } + + private static String owner(Object proxy) { + return "keeps for Rewild"; + } +} diff --git a/src/main/java/com/extantape/waystation/WaystationPlugin.java b/src/main/java/com/extantape/waystation/WaystationPlugin.java index 64c037a..538c69b 100644 --- a/src/main/java/com/extantape/waystation/WaystationPlugin.java +++ b/src/main/java/com/extantape/waystation/WaystationPlugin.java @@ -80,6 +80,11 @@ public void onEnable() { getServer().getPluginManager().registerEvents(renderer, this); protection = new Protection(this); getServer().getPluginManager().registerEvents(protection, this); + if (getConfig().getBoolean("rewild.keep-gates", true)) { + RewildKeeps keeps = new RewildKeeps(this, protection::rewildChunks, protection::rewildBoxes); + getServer().getPluginManager().registerEvents(keeps, this); + keeps.start(); + } getServer().getPluginManager().registerEvents(station, this); getServer().getPluginManager().registerEvents(arrival, this); getServer().getPluginManager().registerEvents(board, this); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 449e998..5bc3a62 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -108,6 +108,11 @@ station: # Nobody can change blocks in the station or around a world's gate: no player, no # permission, no operator. Explosions, fire, liquid, pistons and growth are stopped too. # To rebuild the room itself, set enabled: false here and run /gate reload. +# With Rewild installed, tells it to leave the station's world alone and keep every gate in place, so Rewild never +# rewilds them. Rewild listens once an admin respects Waystation with /rewild plugins. Read at startup. +rewild: + keep-gates: true + protection: enabled: true # World-editing commands refused for players anywhere in the station's world, and diff --git a/src/test/java/com/extantape/waystation/RewildKeepsTest.java b/src/test/java/com/extantape/waystation/RewildKeepsTest.java new file mode 100644 index 0000000..ff65341 --- /dev/null +++ b/src/test/java/com/extantape/waystation/RewildKeepsTest.java @@ -0,0 +1,53 @@ +package com.extantape.waystation; + +import org.bukkit.World; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Proxy; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** What this plugin tells Rewild, through stand-ins shaped exactly like Rewild's KeepProvider before and after 0.3.0. */ +class RewildKeepsTest { + /** Same shape as com.extantape.rewild.api.KeepProvider in Rewild 0.3.0. */ + public interface Provider030 { + record Keep(int chunkX, int chunkZ, String reason) {} + record Area(int minX, int minY, int minZ, int maxX, int maxY, int maxZ, String reason) {} + Collection keeps(World world); + default Collection areas(World world) { + return List.of(); + } + } + + /** Rewild 0.2: whole chunks only. */ + public interface Provider020 { + record Keep(int chunkX, int chunkZ, String reason) {} + Collection keeps(World world); + } + + static World world(String name) { + return (World) Proxy.newProxyInstance(World.class.getClassLoader(), new Class[] {World.class}, + (p, m, a) -> m.getName().equals("getName") ? name : null); + } + + static final List CHUNKS = List.of(new RewildKeeps.Chunk(1, -2, "the station")); + static final List BOXES = List.of(new RewildKeeps.Box(1, 60, 1, 5, 70, 3, "a gate")); + + @Test void chunksAndBoxesReachRewild030() throws Exception { + Provider030 p = (Provider030) RewildKeeps.provider(Provider030.class, w -> CHUNKS, w -> BOXES); + assertEquals(List.of(new Provider030.Keep(1, -2, "the station")), List.copyOf(p.keeps(world("w")))); + assertEquals(List.of(new Provider030.Area(1, 60, 1, 5, 70, 3, "a gate")), List.copyOf(p.areas(world("w")))); + } + + @Test void olderRewildGetsChunksOnly() throws Exception { + Provider020 p = (Provider020) RewildKeeps.provider(Provider020.class, w -> CHUNKS, w -> BOXES); + assertEquals(List.of(new Provider020.Keep(1, -2, "the station")), List.copyOf(p.keeps(world("w")))); + } + + @Test void aFailureReachesRewildSoItResetsNothing() throws Exception { + Provider030 p = (Provider030) RewildKeeps.provider(Provider030.class, w -> { throw new IllegalStateException("broken"); }, w -> BOXES); + assertThrows(IllegalStateException.class, () -> p.keeps(world("w"))); + } +}