User wants name to literally be 'auth-limbo' to match the auth_limbo world the plugin manages. Same functionality, same code, just renamed.
142 lines
6.8 KiB
Markdown
142 lines
6.8 KiB
Markdown
# How it works
|
|
|
|
A technical walkthrough of the bug AuthLimbo fixes and how the fix is
|
|
implemented.
|
|
|
|
## The bug
|
|
|
|
AuthMe-ReReloaded saves the player's last quit-location into its
|
|
`authme` SQLite table on `PlayerQuitEvent`. On `/login`, AuthMe calls
|
|
its internal `teleportOnLogin` flow, which builds a `Location` from the
|
|
DB row and invokes `Player#teleportAsync(savedLocation)`.
|
|
|
|
In current Paper builds, `teleportAsync` does **not** preload the
|
|
destination chunk before resolving the future. If the saved location is
|
|
in a chunk that is currently unloaded (which is the common case at
|
|
login — the player just connected, almost nothing is loaded yet), Paper
|
|
queues the chunk load asynchronously and returns a future that
|
|
"succeeds" once the player object's position field has been updated.
|
|
|
|
Several things can then race against the actual chunk arrival:
|
|
|
|
- Paper's `unknown-command-message`-style safety chunks get checked,
|
|
the destination still hasn't loaded, and the player gets snapped to
|
|
spawn.
|
|
- Other plugins (Multiverse-Core respawn, Essentials spawn-on-join,
|
|
generic safe-teleport guards) listen at the same priority and rewrite
|
|
the location.
|
|
- A `PlayerJoinEvent` listener in another plugin teleports the player
|
|
while the AuthMe teleport future is still in flight.
|
|
|
|
The end state is the same regardless of which racer wins: the player
|
|
is at world spawn instead of where they logged out. This is documented
|
|
upstream as [PaperMC/Paper#4085](https://github.com/PaperMC/Paper/issues/4085).
|
|
|
|
## The fix
|
|
|
|
AuthLimbo intercepts the post-login flow at two points.
|
|
|
|
### 1. Pre-login: pin the chunk
|
|
|
|
`AuthMeAsyncPreLoginEvent` fires on an AuthMe worker thread before the
|
|
password is checked. We listen at `MONITOR` priority, look up the saved
|
|
quit-location in `authme.db`, and schedule a main-thread task that
|
|
calls `Chunk#addPluginChunkTicket(plugin)` on the chunk containing the
|
|
saved coordinates.
|
|
|
|
A plugin chunk ticket is the canonical Paper API for "keep this chunk
|
|
loaded for me until I release it". It does not survive a server restart
|
|
and it is per-plugin scoped, so we cannot accidentally clobber another
|
|
plugin's ticket.
|
|
|
|
This guarantees the chunk is loaded by the time AuthMe actually
|
|
authenticates the password ~1 second later, so the destination is no
|
|
longer unloaded when the teleport happens.
|
|
|
|
### 2. Post-login: authoritative teleport
|
|
|
|
`LoginEvent` fires after AuthMe authenticates and runs its own
|
|
(broken) restore-teleport. We listen at `MONITOR` priority — the
|
|
documented contract is that monitor listeners run *last* in the chain
|
|
([Bukkit Javadoc on EventPriority][priority]). So at the point our
|
|
handler runs, AuthMe has already done its thing and any other plugin
|
|
that listens at lower priority has too.
|
|
|
|
We schedule a main-thread task with a configurable delay
|
|
(`authme.teleport-delay-ticks`, default 10 ticks ≈ 0.5 s) that:
|
|
|
|
1. Re-reads the saved location from `authme.db` (cheap; SQLite, single
|
|
prepared statement).
|
|
2. Verifies the player is still online and the saved world is loaded.
|
|
3. Adds a plugin chunk ticket on the destination if one is not already
|
|
active from the pre-login phase.
|
|
4. Calls `World#getChunkAtAsyncUrgently(cx, cz)` and chains the
|
|
teleport onto its completion. This is the same pattern PaperLib's
|
|
`AsyncTeleportPaper` uses, and is the fix called out by Paper
|
|
maintainers in [PaperMC/Paper#4085](https://github.com/PaperMC/Paper/issues/4085).
|
|
5. Inside that callback, calls `Player#teleportAsync(saved, TeleportCause.PLUGIN)`.
|
|
6. On the resulting future's success callback, schedules a second task
|
|
to release the chunk ticket 5 seconds later — long enough for the
|
|
client to download the chunk before it is allowed to unload.
|
|
|
|
If any future fails (chunk load timeout, player disconnect mid-teleport,
|
|
teleportAsync rejection), the ticket release runs anyway via the
|
|
`exceptionally` branches. Tickets cannot leak across server restarts.
|
|
|
|
## Why this is reliable
|
|
|
|
- The `addPluginChunkTicket` + `getChunkAtAsyncUrgently` +
|
|
`teleportAsync` chain is the canonical fix called out by Paper
|
|
maintainers in
|
|
[PaperMC/Paper#4085](https://github.com/PaperMC/Paper/issues/4085)
|
|
and is exactly what
|
|
[PaperLib's `AsyncTeleportPaper`](https://github.com/PaperMC/PaperLib)
|
|
does internally.
|
|
- `MONITOR` priority is contractually the last priority that runs.
|
|
Listening here means we are not in a race with other plugins that
|
|
might also try to teleport on login.
|
|
- We never modify AuthMe's database. All reads are through a
|
|
short-lived JDBC connection on a fresh `DriverManager#getConnection`,
|
|
so we cannot deadlock with AuthMe's own connection pool.
|
|
- The shaded SQLite driver
|
|
(`org.xerial:sqlite-jdbc:3.46.1.3`, relocated to
|
|
`ru.authlimbo.shaded.sqlite`) means we do not depend on whatever
|
|
driver AuthMe ships with, and we cannot accidentally clobber it.
|
|
|
|
## Code map
|
|
|
|
All Java sources live under `src/main/java/ru/authlimbo/`.
|
|
|
|
| File | Responsibility |
|
|
|------|----------------|
|
|
| `AuthLimbo.java` | `JavaPlugin` entry point. Loads config, builds the limbo world, registers the listener, exposes `/authlimbo` admin commands. |
|
|
| `AuthMeDatabase.java` | Read-only JDBC wrapper around AuthMe's SQLite DB. One method: `getQuitLocation(playerName)`. Returns a `Location` or null. |
|
|
| `LimboWorldManager.java` | Creates the `auth_limbo` world with `VoidGenerator`, sets game rules, builds the optional barrier platform. |
|
|
| `VoidGenerator.java` | Modern (1.17+) `ChunkGenerator` that produces empty chunks. Disables noise, surface, bedrock, caves, decorations, mobs, and structures. |
|
|
| `LoginListener.java` | The fix itself. Two `@EventHandler` methods on `MONITOR` priority for `AuthMeAsyncPreLoginEvent` (chunk preload) and `LoginEvent` (delayed authoritative teleport). |
|
|
|
|
## Threading model
|
|
|
|
- `AuthMeAsyncPreLoginEvent` fires on an AuthMe worker thread. Our
|
|
handler reads the DB on that thread, then schedules the actual
|
|
`setChunkForceLoaded` call on the main thread because Bukkit world
|
|
state mutation must be single-threaded.
|
|
- `LoginEvent` is dispatched async by AuthMe's HaHaWTH fork. Our
|
|
handler reads the DB on that thread, then uses
|
|
`Bukkit.getScheduler().runTaskLater` to bounce onto the main thread
|
|
for the teleport.
|
|
- `Player#teleportAsync` itself returns a `CompletableFuture<Boolean>`
|
|
whose completion runs on Paper's chunk-loading thread pool. We use
|
|
`thenAccept` for the success callback, which Paper handles correctly.
|
|
|
|
## What this plugin does not do
|
|
|
|
- It does not read or write AuthMe's password column. AuthMe owns
|
|
authentication entirely.
|
|
- It does not manage permissions, groups, or chat formatting.
|
|
- It does not handle `/register`, `/changepassword`, or any other
|
|
AuthMe command.
|
|
- It does not bypass AuthMe's own teleport — AuthMe still runs its
|
|
broken restore. We just run *after* it and overwrite the position.
|
|
|
|
[priority]: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/event/EventPriority.html
|