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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ blocks and is never left behind when OneBlock reuses an island.
## Arriving players

- **Lobby gate.** The lobby world's gate to the station stands by the lobby, not by the world's spawn, in its own design: a
wide arch on two lantern posts. With [Latchkey](https://github.com/ExtantApe/Latchkey) 0.3.0 its lobby is used,
read by name, so Latchkey stays optional. Otherwise set `arrival.lobby-world`. Move the lobby and the gate follows.
wide arch on two lantern posts. With [WelcomeWagon](https://github.com/ExtantApe/WelcomeWagon) 0.3.0 its lobby is used,
read by name, so WelcomeWagon stays optional. Otherwise set `arrival.lobby-world`. Move the lobby and the gate follows.
- **Welcome.** Walking into the station the first time, or after 30 days away, you get three short lines saying how many
worlds and platforms you can reach. The time of your last visit is kept on your player file and shown to nobody else.
- **Departures Board.** In front of the centrepiece. Each player sees their own: only the worlds they can use, grouped
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
}

group = "com.extantape"
version = "0.7.1"
version = "0.7.2"

java {
toolchain.languageVersion.set(JavaLanguageVersion.of(25))
Expand Down
4 changes: 4 additions & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@ Dates are when the decision was made. Those marked "by" were made while the firs
| by 2026-11-07 | Gradle with Kotlin DSL, group `com.extantape`, built like BlueMapHUD. Repo and jar named `Waystation`. |
| by 2026-11-07 | One jar, readable source. No economy plugin of its own, no database, no client mod. Money goes through Vault when it's there. |
| by 2026-11-07 | No dependency on Multiverse-Portals. Inventories must never be lost or mixed. Admin rights come from permissions (LuckPerms), not from being an operator. |

## 2026-11-14

- **Other plugins' new names (owner, D194, D196, D197, D198):** Waystation reads WelcomeWagon's lobby and arrival event by their new names, and its docs and tests use OneVoid, Mayor, Sheriff and the other new names. Waystation itself keeps its name until the Waycave rebuild (K649), as the owner chose. Released as 0.7.2.
12 changes: 6 additions & 6 deletions docs/test-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Overkill: `ReturnCommandTest.everyReturnMessageTheCodeUsesIsInTheShippedConfig`
| No usable key means nobody gets in and nobody is sent | unit `withoutAUsableKeyNobodyGetsIn`, `theKeyFileMustBeLongEnoughAndPrivate` (and a key problem never prints the key) |
| A pass is spent on the client once used | bots "B overwrites the pass on the client" |
| A fargate sends nobody to a server that doesn't answer, and says so | bots "with B down, nobody is transferred", "and they are told, kindly" |
| A teleport or respawn that lands a player at a gate can't let the gate take them on their first step | bots "walking straight in from where a teleport put them does nothing", with the control once they've stepped clear; Voidwell's suite plays Multiverse-Inventories' last-location teleport |
| A teleport or respawn that lands a player at a gate can't let the gate take them on their first step | bots "walking straight in from where a teleport put them does nothing", with the control once they've stepped clear; OneVoid's suite plays Multiverse-Inventories' last-location teleport |
| Paper-only servers don't try to build a station every 30 seconds | by reading: `Station.sync` returns before `generate` without Multiverse-Core (no bot check: the return suite runs Paper-only and showed no such warning) |

Acceptance on the stand-in: 153/161, the same 8 as 0.6.0. One check ("op in creative can't break the station floor") read the bot's own view after a refused dig and failed once; it now asks the server, and passes.
Expand All @@ -82,7 +82,7 @@ Acceptance on the stand-in: 153/161, the same 8 as 0.6.0. One check ("op in crea
| A plugin's veto event stops a gate | `acceptance` "plugin event hook vetoes the gate" |
| The hand-over to a world's own plugin | `handover` 28/28 on 26.1.2 |

`test/prepare-small.mjs` now adds Vault or VaultUnlocked to the stand-in when the jar folder has one. Results on Paper 26.1.2: `acceptance` 153/161, with the OneBlock section skipped. The 8 failures are exactly the stand-in list below (the real server's set-up, BlueMap's first render, the flaky op check, and the two crash checks after the OneBlock section). `arrival` passes 39/39 on 26.1.2 with Latchkey 0.3.0, the 10 `--json` checks included.
`test/prepare-small.mjs` now adds Vault or VaultUnlocked to the stand-in when the jar folder has one. Results on Paper 26.1.2: `acceptance` 153/161, with the OneBlock section skipped. The 8 failures are exactly the stand-in list below (the real server's set-up, BlueMap's first render, the flaky op check, and the two crash checks after the OneBlock section). `arrival` passes 39/39 on 26.1.2 with WelcomeWagon 0.3.0, the 10 `--json` checks included.

**Still open:** walking through gates and fargates on 26.3 (bots are disconnected when they move there; the newest ViaVersion builds were checked on 2026-11-10), and everything on the real-client list.

Expand All @@ -92,9 +92,9 @@ Covers what the arrival flow adds, and the first run of the older suites since t

| Promise | Proved by |
| --- | --- |
| The lobby world's return gate stands by the lobby, not by the world's spawn, with the lobby label, leading to the station | `arrival`, without Latchkey (config lobby) and with Latchkey (its lobby). `ArrivalTest` |
| Latchkey's lobby wins over Waystation's own setting, and Latchkey stays optional, read by name | `arrival`: "found Latchkey's lobby by name", "gives way to it"; the run without Latchkey |
| When the lobby moves (config and reload, or Latchkey's lobby and the next arrival), the gate is placed again by the new spot | `arrival`, both runs. `ArrivalTest.whenTheLobbyGateIsPlacedAgain` |
| The lobby world's return gate stands by the lobby, not by the world's spawn, with the lobby label, leading to the station | `arrival`, without WelcomeWagon (config lobby) and with WelcomeWagon (its lobby). `ArrivalTest` |
| WelcomeWagon's lobby wins over Waystation's own setting, and WelcomeWagon stays optional, read by name | `arrival`: "found WelcomeWagon's lobby by name", "gives way to it"; the run without WelcomeWagon |
| When the lobby moves (config and reload, or WelcomeWagon's lobby and the next arrival), the gate is placed again by the new spot | `arrival`, both runs. `ArrivalTest.whenTheLobbyGateIsPlacedAgain` |
| A gate placed as an ordinary return gate before the lobby was known is replaced | `ArrivalTest`. This was a real bug: a removed gate was never placed again until its world's "done" mark was cleared |
| The station welcome comes on a first visit, counts what that player can reach, and doesn't repeat on a quick return or a re-login | `arrival`. The 30-day gap: `ArrivalTest.aWelcomeIsDueOnTheFirstVisitAndAfterTheGap` |
| The Departures Board: one per player in the station, listing only the worlds that player can use, under their platform | `arrival`: two players with different access |
Expand All @@ -109,7 +109,7 @@ Covers what the arrival flow adds, and the first run of the older suites since t
| A reload redraws the board in the new settings (the label backing, for one) | `station`: "label-backing 0 keeps Minecraft's own background". It caught the board keeping its old plate until fixed |
| What per-player data is kept, and that none is shown to other players | `docs/decisions.md`, 2026-11-09 |

`arrival` passes 29/29 on Paper 26.1.2, 26.2 and 26.3, with and without Latchkey 0.3.0. Unit tests: 31. How the board, the wall map and a sealed gate look, and whether the marker turns with you, need a real client (tests.md T12).
`arrival` passes 29/29 on Paper 26.1.2, 26.2 and 26.3, with and without WelcomeWagon 0.3.0. Unit tests: 31. How the board, the wall map and a sealed gate look, and whether the marker turns with you, need a real client (tests.md T12).

### The older suites, on a small made-up copy

Expand Down
38 changes: 19 additions & 19 deletions src/main/java/com/extantape/waystation/Arrival.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,33 @@
/**
* The arrival flow (plan phase 3, owner 2026-11-09): the lobby gate into the station, and a short welcome inside it.
*
* <p>The lobby comes from Latchkey when it's installed and has one set up, otherwise from {@code arrival.lobby-world}.
* Latchkey stays optional: Waystation doesn't build against it, and reads its {@code lobbyLocation()} method and its
* {@code PlayerArrivedEvent} by name (crossing latchkey-waystation). If they don't match, it falls back to the config.
* <p>The lobby comes from WelcomeWagon when it's installed and has one set up, otherwise from {@code arrival.lobby-world}.
* WelcomeWagon stays optional: Waystation doesn't build against it, and reads its {@code lobbyLocation()} method and its
* {@code PlayerArrivedEvent} by name (crossing welcomewagon-waystation). If they don't match, it falls back to the config.
*/
final class Arrival implements Listener {
static final String ARRIVED = "com.extantape.latchkey.api.PlayerArrivedEvent";
static final String ARRIVED = "com.extantape.welcomewagon.api.PlayerArrivedEvent";
/** Furthest a lobby gate may stand from the lobby before it's put back: the placement search radius, and a little. */
static final int LOBBY_REACH = 72;

private final WaystationPlugin plugin;
private final NamespacedKey seen;
private Method latchkeyLobby;
private Plugin latchkey;
private Method welcomewagonLobby;
private Plugin welcomewagon;
private Location lobby;

Arrival(WaystationPlugin plugin) {
this.plugin = plugin;
this.seen = new NamespacedKey(plugin, "station_seen");
}

/** Finds Latchkey's lobby by name, if Latchkey is here. Called once, when every plugin has enabled. */
void hookLatchkey() {
Plugin lk = Bukkit.getPluginManager().getPlugin("Latchkey");
/** Finds WelcomeWagon's lobby by name, if WelcomeWagon is here. Called once, when every plugin has enabled. */
void hookWelcomeWagon() {
Plugin lk = Bukkit.getPluginManager().getPlugin("WelcomeWagon");
if (lk == null || !lk.isEnabled()) return;
try {
latchkeyLobby = lk.getClass().getMethod("lobbyLocation");
latchkey = lk;
welcomewagonLobby = lk.getClass().getMethod("lobbyLocation");
welcomewagon = lk;
Class<? extends Event> arrived = lk.getClass().getClassLoader().loadClass(ARRIVED).asSubclass(Event.class);
Method getLobby = arrived.getMethod("getLobby");
Bukkit.getPluginManager().registerEvent(arrived, this, EventPriority.MONITOR, (l, e) -> {
Expand All @@ -59,27 +59,27 @@ void hookLatchkey() {
Location at = (Location) getLobby.invoke(e);
if (!same(at, lobby)) refresh();
} catch (ReflectiveOperationException ex) {
plugin.getLogger().log(Level.WARNING, "Couldn't read Latchkey's arrival event", ex);
plugin.getLogger().log(Level.WARNING, "Couldn't read WelcomeWagon's arrival event", ex);
}
}, plugin);
plugin.getLogger().info("Using Latchkey's lobby for the lobby gate.");
plugin.getLogger().info("Using WelcomeWagon's lobby for the lobby gate.");
} catch (ReflectiveOperationException | ClassCastException e) {
latchkeyLobby = null;
plugin.getLogger().warning("Latchkey is installed but doesn't offer its lobby the way Waystation expects, so the lobby gate "
welcomewagonLobby = null;
plugin.getLogger().warning("WelcomeWagon is installed but doesn't offer its lobby the way Waystation expects, so the lobby gate "
+ "uses arrival.lobby-world instead. Update both plugins. (" + e.getClass().getSimpleName() + ")");
}
}

/** Where the lobby is now: Latchkey's if it has one, else the configured world. Null for no lobby gate. */
/** Where the lobby is now: WelcomeWagon's if it has one, else the configured world. Null for no lobby gate. */
Location currentLobby() {
Settings.Arrival a = plugin.settings().arrival;
if (!a.lobbyGate()) return null;
if (latchkeyLobby != null && latchkey.isEnabled()) {
if (welcomewagonLobby != null && welcomewagon.isEnabled()) {
try {
Location at = (Location) latchkeyLobby.invoke(latchkey);
Location at = (Location) welcomewagonLobby.invoke(welcomewagon);
if (at != null) return at;
} catch (ReflectiveOperationException | RuntimeException e) {
plugin.getLogger().log(Level.WARNING, "Couldn't ask Latchkey where the lobby is", e);
plugin.getLogger().log(Level.WARNING, "Couldn't ask WelcomeWagon where the lobby is", e);
}
}
if (a.lobbyWorld().isEmpty()) return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public void onEnable() {
if (settings.exampleWorlds) makeExampleWorlds(firstStart);
station.ensureWorld();
Bukkit.getWorlds().forEach(renderer::sweep);
arrival.hookLatchkey();
arrival.hookWelcomeWagon();
arrival.refresh();
station.sync(true);
Bukkit.getScheduler().runTaskTimer(this, renderer::pass, 20L, 20L);
Expand Down
4 changes: 2 additions & 2 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,10 @@ island-posts:

# Arriving players. The lobby gate leads from the lobby into the station. It's the lobby world's return
# gate, placed near the lobby instead of the world's spawn, in its own design so nobody mistakes it for
# a waygate. With Latchkey's lobby set up, Waystation uses that; otherwise the world named here.
# a waygate. With WelcomeWagon's lobby set up, Waystation uses that; otherwise the world named here.
arrival:
lobby-gate: true
lobby-world: "" # only used without Latchkey's lobby. "" for no lobby gate
lobby-world: "" # only used without WelcomeWagon's lobby. "" for no lobby gate
# lobby-spot: {x: 0, y: 65, z: 0} # where in lobby-world; leave out for its spawn
lobby-label: "<gold><bold>Central Station</bold>"
# A short rundown when someone walks into the station for the first time, or after this many days away.
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/paper-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencies:
Vault: {load: BEFORE, required: false, join-classpath: false}
ViaVersion: {load: BEFORE, required: false, join-classpath: false}
# Optional: its lobby places the lobby gate. Read by name, so Waystation builds and runs without it.
Latchkey: {load: BEFORE, required: false, join-classpath: false}
WelcomeWagon: {load: BEFORE, required: false, join-classpath: false}
permissions:
# default: false everywhere an admin is concerned: operators get nothing for free.
waystation.admin:
Expand Down
2 changes: 1 addition & 1 deletion test/acceptance.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,7 @@ else {
await sleep(2500)
const postOf = b => Object.values(b.entities).find(e => e.name === 'interaction' && Math.hypot(e.position.x - b.entity.position.x, e.position.z - b.entity.position.z) < 8)
const hasPosts = !!postOf(obA) && !!postOf(obB)
// Servers that use Voidwell's bell turn the island posts off; the post checks don't apply there.
// Servers that use OneVoid's bell turn the island posts off; the post checks don't apply there.
if (!hasPosts && process.env.WS_NO_POSTS) console.log(' skip: island posts are off on this server (WS_NO_POSTS)')
else check('each island gets a post', hasPosts)
if (hasPosts) {
Expand Down
Loading
Loading