Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/main/java/com/extantape/waystation/Protection.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<RewildKeeps.Chunk> rewildChunks(org.bukkit.World w) {
List<RewildKeeps.Chunk> 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<RewildKeeps.Box> rewildBoxes(org.bukkit.World w) {
List<RewildKeeps.Box> 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;
Expand Down
111 changes: 111 additions & 0 deletions src/main/java/com/extantape/waystation/RewildKeeps.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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<World, List<Chunk>> chunks;
private final Function<World, List<Box>> boxes;
private boolean registered;

public RewildKeeps(Plugin owner, Function<World, List<Chunk>> chunks, Function<World, List<Box>> 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<World, List<Chunk>> chunks, Function<World, List<Box>> 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<Object> 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<Object> 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";
}
}
5 changes: 5 additions & 0 deletions src/main/java/com/extantape/waystation/WaystationPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 53 additions & 0 deletions src/test/java/com/extantape/waystation/RewildKeepsTest.java
Original file line number Diff line number Diff line change
@@ -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<Keep> keeps(World world);
default Collection<Area> areas(World world) {
return List.of();
}
}

/** Rewild 0.2: whole chunks only. */
public interface Provider020 {
record Keep(int chunkX, int chunkZ, String reason) {}
Collection<Keep> 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<RewildKeeps.Chunk> CHUNKS = List.of(new RewildKeeps.Chunk(1, -2, "the station"));
static final List<RewildKeeps.Box> 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")));
}
}
Loading