diff --git a/README.md b/README.md index 4689ebd..014e803 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. @@ -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= 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= node suite.mjs 26.2`. Without `VOIDWELL_WAYSTATION_JAR` they use a small stand-in for Waystation (`test/standin`) instead. ## License diff --git a/build.gradle.kts b/build.gradle.kts index 67df725..f688dad 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,7 @@ plugins { } group = "com.extantape" -version = "0.1.0" +version = "0.1.1" java { toolchain.languageVersion.set(JavaLanguageVersion.of(25)) diff --git a/docs/releases/v0.1.1.md b/docs/releases/v0.1.1.md new file mode 100644 index 0000000..52ef2f4 --- /dev/null +++ b/docs/releases/v0.1.1.md @@ -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. diff --git a/docs/test-audit.md b/docs/test-audit.md index 4546fde..d3de9ae 100644 --- a/docs/test-audit.md +++ b/docs/test-audit.md @@ -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) @@ -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). diff --git a/src/main/java/com/extantape/voidwell/VoidwellPlugin.java b/src/main/java/com/extantape/voidwell/VoidwellPlugin.java index d0aaa64..64d5e01 100644 --- a/src/main/java/com/extantape/voidwell/VoidwellPlugin.java +++ b/src/main/java/com/extantape/voidwell/VoidwellPlugin.java @@ -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(); diff --git a/src/main/java/com/extantape/voidwell/trip/DeathListener.java b/src/main/java/com/extantape/voidwell/trip/DeathListener.java index 820e764..72b85b5 100644 --- a/src/main/java/com/extantape/voidwell/trip/DeathListener.java +++ b/src/main/java/com/extantape/voidwell/trip/DeathListener.java @@ -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. diff --git a/test/lib.mjs b/test/lib.mjs index 59ecd2a..39928e1 100644 --- a/test/lib.mjs +++ b/test/lib.mjs @@ -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)) diff --git a/test/suite.mjs b/test/suite.mjs index b63cacb..f047c02 100644 --- a/test/suite.mjs +++ b/test/suite.mjs @@ -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) }, } @@ -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')