User wants name to literally be 'auth-limbo' to match the auth_limbo world the plugin manages. Same functionality, same code, just renamed.
6.8 KiB
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
PlayerJoinEventlistener 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.
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). 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:
- Re-reads the saved location from
authme.db(cheap; SQLite, single prepared statement). - Verifies the player is still online and the saved world is loaded.
- Adds a plugin chunk ticket on the destination if one is not already active from the pre-login phase.
- Calls
World#getChunkAtAsyncUrgently(cx, cz)and chains the teleport onto its completion. This is the same pattern PaperLib'sAsyncTeleportPaperuses, and is the fix called out by Paper maintainers in PaperMC/Paper#4085. - Inside that callback, calls
Player#teleportAsync(saved, TeleportCause.PLUGIN). - 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+teleportAsyncchain is the canonical fix called out by Paper maintainers in PaperMC/Paper#4085 and is exactly what PaperLib'sAsyncTeleportPaperdoes internally. MONITORpriority 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 toru.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
AuthMeAsyncPreLoginEventfires on an AuthMe worker thread. Our handler reads the DB on that thread, then schedules the actualsetChunkForceLoadedcall on the main thread because Bukkit world state mutation must be single-threaded.LoginEventis dispatched async by AuthMe's HaHaWTH fork. Our handler reads the DB on that thread, then usesBukkit.getScheduler().runTaskLaterto bounce onto the main thread for the teleport.Player#teleportAsyncitself returns aCompletableFuture<Boolean>whose completion runs on Paper's chunk-loading thread pool. We usethenAcceptfor 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.