auth-limbo/docs/how-it-works.md
s8n-ru cb746147f0 Rename: LoginLimbo -> AuthLimbo
User wants name to literally be 'auth-limbo' to match the auth_limbo
world the plugin manages. Same functionality, same code, just renamed.
2026-04-30 19:19:26 +01:00

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