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
111 changes: 111 additions & 0 deletions src/main/java/com/extantape/voidwell/RewildKeeps.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.extantape.voidwell;

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";
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/extantape/voidwell/VoidwellPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ public void onEnable() {
new MenuListener(), new CommandGuard()))
pm.registerEvents(l, this);

if (getConfig().getBoolean("rewild.keep-islands", true)) {
// OneBlock's world is void: a rewild there would erase every island the one block built.
RewildKeeps keeps = new RewildKeeps(this, w -> {
List<RewildKeeps.Chunk> out = new java.util.ArrayList<>();
for (long[] c : bridge.islandChunks(w)) out.add(new RewildKeeps.Chunk((int) c[0], (int) c[1], "a OneBlock island"));
return out;
}, w -> List.of());
pm.registerEvents(keeps, this);
keeps.start();
}

getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, e ->
e.registrar().register(VoidwellCommand.build(this), "Your void meter and rewards", List.of()));

Expand Down
31 changes: 31 additions & 0 deletions src/main/java/com/extantape/voidwell/oneblock/OneBlockBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,37 @@ public Location oneBlock(int island) {
return new Location(world(), xz[0], OneBlock.getY(), xz[1]);
}

/** How many islands OneBlock has handed out, numbered from 0. */
public int islandCount() {
return PlayerInfo.list.size();
}

/**
* Every chunk an island's square covers, for Rewild: OneBlock's world is void, so a rewild there would erase islands
* the one block built. Empty unless this is OneBlock's world. Package-free math is in {@link #squareChunks}.
*/
public java.util.List<long[]> islandChunks(World w) {
java.util.List<long[]> out = new java.util.ArrayList<>();
World ob = world();
if (ob == null || !ob.getUID().equals(w.getUID())) return out;
java.util.Set<Long> seen = new java.util.HashSet<>();
for (int i = 0; i < islandCount(); i++) {
int[] xz = OneBlock.plugin.getIslandCoordinates(i);
for (long[] c : squareChunks(xz[0], xz[1], spacing())) if (seen.add((c[0] << 32) ^ (c[1] & 0xffffffffL))) out.add(c);
}
return out;
}

/** The chunks of the square of side {@code spacing} centred on an island's one block. */
public static java.util.List<long[]> squareChunks(int x, int z, int spacing) {
int half = Math.max(16, spacing / 2);
java.util.List<long[]> out = new java.util.ArrayList<>();
for (int cx = (x - half) >> 4; cx <= (x + half) >> 4; cx++) {
for (int cz = (z - half) >> 4; cz <= (z + half) >> 4; cz++) out.add(new long[]{cx, cz});
}
return out;
}

/** Distance between islands, in blocks. */
public int spacing() {
return Math.abs(OneBlock.getOffset());
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 @@ -67,6 +67,11 @@ social:
max-team-size: 4

# Saves after a death or a bell trip, so a crash can't roll a player back into a duplicate or a loss.
# With Rewild installed, tells it to leave every OneBlock island alone. OneBlock's world is void, so a rewild there
# would erase what the one block built. Rewild listens once an admin respects Voidwell with /rewild plugins.
rewild:
keep-islands: true

crash-safety:
# Also save the OneBlock world after a death on an island and after a trip out by the bell, so the world on disk
# always matches the saved inventories. Without it, a crash could bring back items a player had already picked up.
Expand Down
61 changes: 61 additions & 0 deletions src/test/java/com/extantape/voidwell/RewildKeepsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.extantape.voidwell;

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")));
}

@Test void anIslandsSquareCoversItsChunks() {
var c = com.extantape.voidwell.oneblock.OneBlockBridge.squareChunks(0, 0, 100);
// 100-block spacing: 50 each way from 0,0, so chunks -4..3 on each axis.
assertEquals(64, c.size());
assertTrue(c.stream().anyMatch(a -> a[0] == -4 && a[1] == 3));
assertTrue(c.stream().noneMatch(a -> a[0] == 4));
}
}
Loading