# How it works A technical walkthrough of the bug LoginLimbo 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 LoginLimbo 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.loginlimbo.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/racked/limbo/`. | File | Responsibility | |------|----------------| | `LoginLimbo.java` | `JavaPlugin` entry point. Loads config, builds the limbo world, registers the listener, exposes `/loginlimbo` 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` 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