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

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 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.

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:

  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.
  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 and is exactly what PaperLib's AsyncTeleportPaper 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.