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
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A OneBlock add-on for Paper. Players walk into OneBlock through the Waystation, ring their island's bell to leave, and get something back for whatever they lose to the void.

**Status:** 0.1.0, the first release. It passes all 82 of its bot checks on a throwaway Paper 26.2 server, with Waystation 0.4.0, OneBlock 1.6.4, Multiverse-Core 5.8.2, Multiverse-Inventories 5.3.6, LuckPerms and HouseRules 0.1.1. The bots walk through Waystation's real OneBlock gate. The bots are 26.1 clients connecting through ViaVersion. On Paper 26.3 it loads, sets up and reports no problems, but no bot could play there: 26.3 disconnects clients that come through ViaVersion ("Invalid move player packet"), even ones standing still. Nothing has been seen in a real game client yet. See [Tested](#tested) for the details.
**Status:** 0.1.1. It passes all 86 of its bot checks on a throwaway Paper 26.2 server, with Waystation 0.6.0, OneBlock 1.6.4, Multiverse-Core 5.8.2, Multiverse-Inventories 5.3.6, LuckPerms and HouseRules 0.1.1. The bots walk through Waystation's real OneBlock gate. The bots are 26.1 clients connecting through ViaVersion. On Paper 26.3 it loads, sets up and reports no problems, but no bot could play there: 26.3 disconnects clients that come through ViaVersion ("Invalid move player packet"), even ones standing still. Nothing has been seen in a real game client yet. See [Tested](#tested) for the details.

## What it does

Expand Down Expand Up @@ -36,7 +36,7 @@ Paper 26.1 or later, Java 25. Waystation 0.4.0 or later and OneBlock 1.6.4 (plug

With the server stopped:

1. Copy `Voidwell-0.1.0.jar` to `plugins/`.
1. Copy `Voidwell-0.1.1.jar` to `plugins/`.
2. In OneBlock, turn off two settings that fight Voidwell: `/ob autojoin false` and `/ob rebirth_on_the_island false`. With autojoin on, OneBlock cancels the trip back to a player's bell.
3. In Waystation, set the OneBlock world's gate to `hand-over: true` and turn island posts off (`island-posts.enabled: false`). The bell replaces them.
4. Put the OneBlock world in its own Multiverse-Inventories group, and the Waystation's world in a group with the lobby, so a death in OneBlock is a plain OneBlock-to-lobby swap.
Expand Down Expand Up @@ -86,13 +86,17 @@ Not tested yet:
- Anything with a player on Paper 26.3, for the reason above. It awaits a newer bot client or a real one.
- A real client: whether the bell looks and feels deliberate and is easy to find, how the meter and labels read day and night and for colour-blind players, whether payouts feel exciting and a void death feels like a consolation.

## Troubleshooting

- **A player respawns on their island after a OneBlock death.** Voidwell sets the respawn at the Waystation's OneBlock gate, and since 0.1.1 it checks half a second later. If another plugin has moved the player back into OneBlock, Voidwell puts them at the gate and writes a console warning naming where they were moved. To find which plugin did it, start the server with `-Dvoidwell.trace=true`: every teleport and respawn is then logged with the code that caused it. Leave it off otherwise.

## Build

```bash
./gradlew build
```

The jar is `build/libs/Voidwell-0.1.0.jar`. To run the bot tests: `cd test && npm install && node fetch.mjs && VOIDWELL_WAYSTATION_JAR=<path to Waystation-0.4.0.jar> node suite.mjs 26.2`. Without `VOIDWELL_WAYSTATION_JAR` they use a small stand-in for Waystation (`test/standin`) instead.
The jar is `build/libs/Voidwell-0.1.1.jar`. To run the bot tests: `cd test && npm install && node fetch.mjs && VOIDWELL_WAYSTATION_JAR=<path to Waystation-0.4.0.jar> node suite.mjs 26.2`. Without `VOIDWELL_WAYSTATION_JAR` they use a small stand-in for Waystation (`test/standin`) instead.

## License

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
}

group = "com.extantape"
version = "0.1.0"
version = "0.1.1"

java {
toolchain.languageVersion.set(JavaLanguageVersion.of(25))
Expand Down
18 changes: 18 additions & 0 deletions docs/releases/v0.1.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
A fix for one case where Voidwell's promise about deaths didn't hold.

## Fixed

- **A OneBlock death always ends at the Waystation's OneBlock gate.** On a rehearsal copy of a real server, a player who had rung their bell, gone back through the gate and then died in the void respawned on their island, while Voidwell told them "You're back at the Waystation". Voidwell sets the respawn at the gate, and in its own tests it lands there; something else on that server moved the player afterwards. Voidwell now checks half a second after every OneBlock respawn and, if the player has been moved back into OneBlock, puts them at the gate and writes a console warning saying where they'd been moved. Nothing was ever lost: the void meter charged correctly throughout.

## New

- **`-Dvoidwell.trace=true`**, for troubleshooting: logs every teleport and respawn with the code that caused it, so an admin can see which plugin moves players. Off unless set.
- The bot suite reports a failed set-up as a failure, instead of "0 passed, 0 failed".

## Upgrading

Replace the jar. No config or data changes.

## Tested

Bot tests on a throwaway Paper 26.2 server with Waystation 0.6.0, OneBlock 1.6.4, Multiverse and LuckPerms: the rehearsal's repro (bell trip, back, void death) respawns at the gate, and when the console moves the player back onto the island right after the respawn, Voidwell puts them at the gate and logs it. The whole suite passed 86/86 (82 before plus 4 new). Not run on 26.3 (bots can't stay connected there), and the plugin that moved the player on the real server isn't identified yet.
16 changes: 14 additions & 2 deletions docs/test-audit.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
# Test audit

Kept by this repo's owning lead (Minecraft: Plugins Lead), per `~/Code/CLAUDE.md`, "Test self-audits". Newest first. Taken over from code-44 with its agreement on 2026-11-10.
Kept by this repo's owning lead (Minecraft: Plugins Lead), per `~/Code/CLAUDE.md`, "Test self-audits". Newest first.

## 2026-11-10: the respawn after a bell trip (0.1.1)

The server session's rehearsal on a copy of the real server (Paper 26.2, Waystation 0.6.0, OneBlock 1.6.4) found a void death after a bell trip and a return putting the player on their island while Voidwell said "You're back at the Waystation", in 3 of 3 runs. In this repo's suite the same steps respawn at the gate; a teleport trace showed only Voidwell's respawn there. So something on the real server moves the player afterwards.

| Promise | Covered by |
| --- | --- |
| After a bell trip and back, a void death still respawns outside the gate, and says so | bots "way bell: after a bell trip and back, a void death still respawns outside the gate", "and says so" (the rehearsal's repro) |
| If another plugin moves the player back into OneBlock after the respawn, Voidwell still ends with them at the gate and the console says so | bots "if another plugin moves them back after the respawn…", "and the console says what happened" (the console plays the other plugin) |

Gap: which plugin on the real server does it isn't known yet. `-Dvoidwell.trace=true` logs every teleport with its cause, for the server session's next rehearsal.
Taken over from code-44 with its agreement on 2026-11-10.

## 2026-11-10: first full audit (at 0.1.0)

Expand Down Expand Up @@ -30,7 +42,7 @@ After the refactor, the bot suite's 82 checks all passed on Paper 26.2 (2026-11-

### Gaps

- **A visitor picking up an item that isn't theirs.** Pickup has its own rules (your own throws and death drops are allowed), so the break check doesn't cover it. A bot check was tried on 2026-11-10: items summoned at a visitor's feet on another island never appeared, and the suite keeps only the last server's console, so the cause wasn't found. Not covered yet. Next try: the island owner's bot drops an item next to the visitor, with a control for the visitor's own throw.
- **A visitor picking up an item that isn't theirs.** Pickup has its own rules (your own throws and death drops are allowed), so the break check doesn't cover it. A bot check was tried on 2026-11-10: items summoned at a visitor's feet on another island never appeared, and the suite keeps only the last server's console, so the cause wasn't found. Not covered yet. Next try: the island owner's bot drops an item next to the visitor, with a control for the visitor's own throw. Later finding (2026-11-10): nothing in Voidwell removes an item entity except the void, so the emerald most likely fell off the island's edge; it isn't data loss.

- **Anything with a player on Paper 26.3.** ViaVersion clients are disconnected there when they move. Checked again on 2026-11-10: the newest ViaVersion and ViaBackwards builds on Modrinth are the ones already in use, and the bot library has no native 26.3 support. Server-only checks run there. This waits for a newer bot client or a real one (T21).
- **A real client:** how the bell, meter and labels look and feel (T21).
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/extantape/voidwell/VoidwellPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,19 @@ public void onWorldChange(PlayerChangedWorldEvent e) {
Bukkit.getScheduler().runTaskLater(this, () -> rewards.deliver(e.getPlayer()), 20L);
}

// Troubleshooting: with -Dvoidwell.trace=true, log every teleport and respawn with the plugin code that caused it.
@EventHandler(priority = EventPriority.MONITOR)
public void traceTp(org.bukkit.event.player.PlayerTeleportEvent e) {
if (!Boolean.getBoolean("voidwell.trace")) return;
StringBuilder sb = new StringBuilder();
for (StackTraceElement el : new Throwable().getStackTrace()) { String c = el.getClassName(); if (!c.startsWith("java.") && !c.startsWith("jdk.")) sb.append(" <").append(c.replaceAll("^.*\\.", "")).append('.').append(el.getMethodName()); if (sb.length() > 900) break; }
getLogger().info("TRACE tp " + e.getPlayer().getName() + " " + e.getCause() + " -> " + e.getTo().getWorld().getName() + " " + e.getTo().getBlockX() + "," + e.getTo().getBlockY() + "," + e.getTo().getBlockZ() + " cancelled=" + e.isCancelled() + sb);
}
@EventHandler(priority = EventPriority.MONITOR)
public void traceRespawn(org.bukkit.event.player.PlayerRespawnEvent e) {
if (Boolean.getBoolean("voidwell.trace")) getLogger().info("TRACE respawn " + e.getPlayer().getName() + " -> " + e.getRespawnLocation().getWorld().getName() + " " + e.getRespawnLocation().getBlockX() + "," + e.getRespawnLocation().getBlockY() + "," + e.getRespawnLocation().getBlockZ());
}

@EventHandler
public void onQuit(PlayerQuitEvent e) {
var id = e.getPlayer().getUniqueId();
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/extantape/voidwell/trip/DeathListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ public void onRespawn(PlayerRespawnEvent e) {
p.saveData();
Text.tell(p, "You're back at the Waystation. Walk through the OneBlock gate to return to your island.");
});
// Another plugin can still move a player after the respawn (seen once on a real server's rehearsal, not
// in this repo's tests). The promise is the gate, so put them there, and say so in the console, so an
// admin can find which plugin did it (start Java with -Dvoidwell.trace=true to log every teleport).
Location promised = gate.get().clone();
Bukkit.getScheduler().runTaskLater(plugin, () -> {
if (!p.isOnline() || !plugin.bridge.inWorld(p.getLocation())) return;
Location at = p.getLocation();
plugin.getLogger().warning("Something moved " + p.getName() + " back into OneBlock after their respawn (to "
+ at.getBlockX() + ", " + at.getBlockY() + ", " + at.getBlockZ() + "). Sending them to the Waystation's OneBlock gate, as Voidwell promised."
+ " To find what moved them, start the server with -Dvoidwell.trace=true.");
p.teleportAsync(promised, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.PLUGIN);
}, 10L);
} else {
// Waystation can't say where its gate is. Respawning on the island at least keeps OneBlock's inventory
// in the OneBlock world. The player is told, rather than left wondering.
Expand Down
2 changes: 1 addition & 1 deletion test/lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class Server {

async start(timeout = 240000) {
this.log = ''
this.proc = spawn(JAVA, ['-Xms1G', '-Xmx2G', '-Dpaper.disableChannelLimit=true', '-jar', 'paper.jar', '--nogui'], { cwd: this.dir })
this.proc = spawn(JAVA, ['-Xms1G', '-Xmx2G', '-Dpaper.disableChannelLimit=true', ...(process.env.VOIDWELL_TRACE ? ['-Dvoidwell.trace=true'] : []), '-jar', 'paper.jar', '--nogui'], { cwd: this.dir })
this.proc.stdout.on('data', d => { this.log += d.toString().replace(/\x1b\[[0-9;]*m/g, '') })
this.proc.stderr.on('data', d => { this.log += d.toString() })
this.exited = new Promise(r => this.proc.on('exit', r))
Expand Down
22 changes: 22 additions & 0 deletions test/suite.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,25 @@ const tests = {
check('way bell: back where the bell was rung', back.dim === 'minecraft:oneblock' && Math.hypot(back.x - spot.x, back.z - spot.z) < 0.6 && Math.abs(back.y - spot.y) < 0.6, JSON.stringify({ back, spot }))
const after = { inv: summary(a), xp: a.experience.level, hp: Math.round(a.health), food: a.food }
check('way bell: same inventory, XP, health and hunger', JSON.stringify(after) === JSON.stringify(mid), JSON.stringify({ mid, after }))
// The server session's repro (2026-11-10): after a bell trip and back, a void death must still respawn outside
// the OneBlock gate, not on the island.
const m = a.chatLog.length
await s.run('execute in minecraft:oneblock run tp Ash 0.5 -75 0.5', /Teleported/)
await sleep(6000)
const died = await where('Ash')
check('way bell: after a bell trip and back, a void death still respawns outside the gate', atLanding(died), JSON.stringify(died))
await heard(a, /back at the Waystation/, 3000, m).then(() => check('way bell: and says so', true)).catch(e => check('way bell: and says so', false, e.message))
// What the real server's rehearsal saw: something moves the player back onto the island right after the respawn.
// Played here by the console, within the first tick or two. Voidwell must still end with them at the gate.
await s.run('execute in minecraft:oneblock run tp Ash 2.5 101 0.5', /Teleported/)
const mark = s.mark()
await s.run('execute in minecraft:oneblock run tp Ash 0.5 -75 0.5', /Teleported/)
await new Promise(res => { a.once('spawn', res); setTimeout(res, 8000) })
s.cmd('execute in minecraft:oneblock run tp Ash 2.5 101 0.5')
await sleep(3000)
const moved = await where('Ash')
check('way bell: if another plugin moves them back after the respawn, Voidwell still puts them at the gate', atLanding(moved), JSON.stringify(moved))
check('way bell: and the console says what happened', /Something moved Ash back into OneBlock after their respawn/.test(s.since(mark)))
a.quit(); await sleep(1000)
},
}
Expand All @@ -713,6 +732,9 @@ try {
console.log(`--- ${name}`)
try { await fn() } catch (e) { check(name + ' (ran to the end)', false, e.message.slice(0, 600)) }
}
} catch (e) {
// A set-up that fails must count as a failure, not as "0 passed, 0 failed".
check('set-up finished', false, e.message.slice(0, 600))
} finally {
const errs = s ? s.errors() : []
console.log('server errors:', errs.length ? '\n' + errs.slice(0, 15).join('\n') : 'none')
Expand Down
Loading