Initial commit: RackedLimbo 1.0.0
Auth-limbo + login-restore fix for Paper 1.21+. Bypasses the AuthMe `teleportOnLogin` race (PaperMC/Paper#4085) by listening at MONITOR priority, reading the player's saved quit-location from AuthMe's SQLite DB, pinning the destination chunk via addPluginChunkTicket, then chaining getChunkAtAsyncUrgently and teleportAsync. Bundles a void auth_limbo world via custom ChunkGenerator so the plugin removes the need for Multiverse-Core for offline-mode auth flows.
This commit is contained in:
commit
8cd92694e7
21 changed files with 1615 additions and 0 deletions
61
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
61
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Report a problem with RackedLimbo
|
||||
title: "[bug] "
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
A one-paragraph description of what is broken.
|
||||
|
||||
## Environment
|
||||
|
||||
| Field | Value |
|
||||
|-------------------------|----------------------------------------|
|
||||
| RackedLimbo version | e.g. 1.0.0 |
|
||||
| Server software | Paper / Purpur / Folia |
|
||||
| Server version | e.g. 1.21.11 build #142 |
|
||||
| Java version | output of `java -version` |
|
||||
| AuthMe variant | e.g. AuthMe-ReReloaded HaHaWTH b49 |
|
||||
| OS | e.g. Debian 13, Windows 11, Docker |
|
||||
| Other auth/world plugins| e.g. Multiverse-Core, Essentials, ... |
|
||||
|
||||
## Steps to reproduce
|
||||
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
|
||||
## Expected behaviour
|
||||
|
||||
What you expected to happen.
|
||||
|
||||
## Actual behaviour
|
||||
|
||||
What actually happened. Include exact log lines.
|
||||
|
||||
## Logs
|
||||
|
||||
Paste the relevant section of `logs/latest.log` here. Search for
|
||||
`RackedLimbo` to filter our log lines. If you are running with
|
||||
`debug: true` in `config.yml`, please include those lines as well.
|
||||
|
||||
```
|
||||
(paste logs)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
If you have customised `plugins/RackedLimbo/config.yml`, paste the
|
||||
relevant keys here.
|
||||
|
||||
```yaml
|
||||
(paste config)
|
||||
```
|
||||
|
||||
## Additional context
|
||||
|
||||
Anything else that might be relevant. Screenshots, stack traces,
|
||||
related plugin versions, recent changes to the server.
|
||||
46
.github/workflows/build.yml
vendored
Normal file
46
.github/workflows/build.yml
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 21 (Temurin)
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '21'
|
||||
cache: maven
|
||||
|
||||
# The AuthMe jar is a system-scope dependency referenced by pom.xml
|
||||
# but is not redistributed in this repo. Set the AUTHME_JAR_URL repo
|
||||
# secret to a direct download URL for AuthMe-5.6.0-FORK-Universal.jar
|
||||
# before this workflow can succeed. See lib/README.md for context.
|
||||
- name: Fetch AuthMe jar
|
||||
env:
|
||||
AUTHME_JAR_URL: ${{ secrets.AUTHME_JAR_URL }}
|
||||
run: |
|
||||
if [ -z "$AUTHME_JAR_URL" ]; then
|
||||
echo "::error::AUTHME_JAR_URL secret is not set. See lib/README.md."
|
||||
exit 1
|
||||
fi
|
||||
curl -fsSL "$AUTHME_JAR_URL" -o lib/AuthMe-5.6.0-FORK-Universal.jar
|
||||
ls -la lib/
|
||||
|
||||
- name: Build with Maven
|
||||
run: mvn -B -ntp package
|
||||
|
||||
- name: Upload jar artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: RackedLimbo-jar
|
||||
path: target/RackedLimbo-*.jar
|
||||
if-no-files-found: error
|
||||
44
.github/workflows/release.yml
vendored
Normal file
44
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 21 (Temurin)
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '21'
|
||||
cache: maven
|
||||
|
||||
# See .github/workflows/build.yml for context on this secret.
|
||||
- name: Fetch AuthMe jar
|
||||
env:
|
||||
AUTHME_JAR_URL: ${{ secrets.AUTHME_JAR_URL }}
|
||||
run: |
|
||||
if [ -z "$AUTHME_JAR_URL" ]; then
|
||||
echo "::error::AUTHME_JAR_URL secret is not set. See lib/README.md."
|
||||
exit 1
|
||||
fi
|
||||
curl -fsSL "$AUTHME_JAR_URL" -o lib/AuthMe-5.6.0-FORK-Universal.jar
|
||||
|
||||
- name: Build with Maven
|
||||
run: mvn -B -ntp package
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: target/RackedLimbo-*.jar
|
||||
generate_release_notes: true
|
||||
fail_on_unmatched_files: true
|
||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Build output
|
||||
target/
|
||||
*.class
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
.vscode/
|
||||
.project
|
||||
.classpath
|
||||
.settings/
|
||||
|
||||
# Logs and OS
|
||||
*.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Maven wrapper (we don't ship one)
|
||||
.mvn/
|
||||
mvnw
|
||||
mvnw.cmd
|
||||
lib/AuthMe-*.jar
|
||||
35
CHANGELOG.md
Normal file
35
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to RackedLimbo are documented here.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.0] - 2026-04-30
|
||||
|
||||
Initial public release.
|
||||
|
||||
### Added
|
||||
- Auth-limbo void world manager. Creates and configures `auth_limbo` world
|
||||
with no Multiverse-Core dependency.
|
||||
- AuthMe `LoginEvent` listener at `MONITOR` priority. Reads saved quit
|
||||
coordinates directly from `plugins/AuthMe/authme.db` (SQLite, read-only)
|
||||
and forces a delayed `teleportAsync` to those coordinates after AuthMe's
|
||||
own broken teleport runs.
|
||||
- `AuthMeAsyncPreLoginEvent` listener for chunk preload. Pins the
|
||||
destination chunk via `Chunk#addPluginChunkTicket` before login
|
||||
completes to avoid the unloaded-chunk race documented in
|
||||
[Paper #4085](https://github.com/PaperMC/Paper/issues/4085). Tickets
|
||||
are released 5 seconds after the teleport completes.
|
||||
- Optional 5x5 barrier platform at limbo spawn so unauth players can't
|
||||
fall into the void.
|
||||
- `/rackedlimbo reload` and `/rackedlimbo tp <player>` admin commands
|
||||
gated on `rackedlimbo.admin`.
|
||||
- Shaded SQLite JDBC driver (`org.xerial:sqlite-jdbc 3.46.1.3`,
|
||||
relocated to `ru.racked.limbo.shaded.sqlite`) so the plugin reads
|
||||
AuthMe's database without classpath collisions.
|
||||
|
||||
### Compatibility
|
||||
- Paper / Purpur 1.21.11 (api-version `1.21`).
|
||||
- Java 21.
|
||||
- AuthMe-ReReloaded HaHaWTH fork b49 (`fr.xephi:authme:5.6.0-FORK-b49`)
|
||||
as a hard dependency.
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 s8n-ru / racked.ru
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
162
README.md
Normal file
162
README.md
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
# RackedLimbo
|
||||
|
||||
Auth-limbo + login-restore fix for Paper 1.21+.
|
||||
|
||||
[](https://github.com/s8n-ru/racked-limbo/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://papermc.io/)
|
||||
[](https://adoptium.net/)
|
||||
|
||||
A small Paper plugin that fixes a chronic AuthMe-ReReloaded post-login
|
||||
teleport bug and bundles a void auth-limbo world so you don't need
|
||||
Multiverse-Core just to host one void world.
|
||||
|
||||
---
|
||||
|
||||
## The problem
|
||||
|
||||
After a player runs `/login`, AuthMe is supposed to teleport them back
|
||||
to the coordinates they had when they last quit. In current
|
||||
AuthMe-ReReloaded forks (verified on the HaHaWTH fork b49), this
|
||||
restore-teleport often fails: players land at world spawn instead of
|
||||
their saved location.
|
||||
|
||||
Root cause is a known Paper teleport-race documented at
|
||||
[PaperMC/Paper#4085](https://github.com/PaperMC/Paper/issues/4085).
|
||||
AuthMe's `teleportOnLogin` flow calls `Player#teleportAsync` without
|
||||
preloading the destination chunk first. The future resolves before the
|
||||
chunk is loaded, Paper's safety logic then snaps the player back to the
|
||||
nearest loaded position, and that position is world spawn.
|
||||
|
||||
We tried fixing this in AuthMe config. We tried removing Multiverse.
|
||||
The bug kept reappearing. RackedLimbo is the long-term fix.
|
||||
|
||||
---
|
||||
|
||||
## What this plugin does
|
||||
|
||||
Two things, narrowly.
|
||||
|
||||
1. **Hosts a void `auth_limbo` world.** A custom `ChunkGenerator`
|
||||
produces empty chunks, an optional 5x5 barrier platform sits under
|
||||
spawn, and the world is configured with no daylight cycle, no
|
||||
weather, no mob spawning, and no PvP. AuthMe can use it as its
|
||||
pre-auth limbo without you installing Multiverse-Core.
|
||||
|
||||
2. **Overrides AuthMe's restore-teleport.** A `LoginEvent` listener at
|
||||
`MONITOR` priority runs *after* AuthMe's own broken teleport, reads
|
||||
the player's saved quit-location directly from
|
||||
`plugins/AuthMe/authme.db`, pins the destination chunk via
|
||||
`Chunk#addPluginChunkTicket`, then chains
|
||||
`World#getChunkAtAsyncUrgently` into an authoritative
|
||||
`Player#teleportAsync`. This is the canonical fix described in
|
||||
[PaperMC/Paper#4085](https://github.com/PaperMC/Paper/issues/4085)
|
||||
and used by
|
||||
[PaperLib's `AsyncTeleportPaper`](https://github.com/PaperMC/PaperLib).
|
||||
|
||||
That is the entire surface area. No password handling, no permissions
|
||||
beyond admin commands, no register flow, no chat formatting. AuthMe owns
|
||||
all of that.
|
||||
|
||||
---
|
||||
|
||||
## Install
|
||||
|
||||
1. Download `RackedLimbo-1.0.0.jar` from the
|
||||
[Releases page](https://github.com/s8n-ru/racked-limbo/releases).
|
||||
2. Drop it into your server's `plugins/` directory.
|
||||
3. Restart the server (do not use `/reload`).
|
||||
|
||||
AuthMe-ReReloaded is a **hard dependency**. The plugin will refuse to
|
||||
load without it.
|
||||
|
||||
For the `itzg/minecraft-server` Docker image, see
|
||||
[`docs/installation.md`](docs/installation.md) for the `PLUGINS:`
|
||||
environment variable form.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
`plugins/RackedLimbo/config.yml` is created on first start. Defaults:
|
||||
|
||||
```yaml
|
||||
limbo:
|
||||
world: auth_limbo # Bukkit world name
|
||||
spawn-x: 0.5
|
||||
spawn-y: 128.0
|
||||
spawn-z: 0.5
|
||||
build-platform: true # 5x5 barrier under spawn
|
||||
platform-y: 127
|
||||
|
||||
authme:
|
||||
db-path: plugins/AuthMe/authme.db
|
||||
teleport-delay-ticks: 10 # ticks to wait after LoginEvent
|
||||
preload-chunks: true # forceload chunk before teleport
|
||||
|
||||
debug: false # verbose logging
|
||||
```
|
||||
|
||||
Full reference in [`docs/configuration.md`](docs/configuration.md).
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Permission | Effect |
|
||||
|----------------------------|---------------------|-----------------------------------------------------|
|
||||
| `/rackedlimbo reload` | `rackedlimbo.admin` | Reload `config.yml`. |
|
||||
| `/rackedlimbo tp <player>` | `rackedlimbo.admin` | Manually teleport a player to their saved location. |
|
||||
|
||||
Aliases: `/rlimbo`.
|
||||
|
||||
---
|
||||
|
||||
## Compatibility
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------------------------------|----------|----------------------------------------|
|
||||
| Paper 1.21.11 | Yes | Primary target. |
|
||||
| Purpur 1.21.11 | Yes | Same Paper API surface. |
|
||||
| Folia | Unknown | Untested. Login event threading may differ. |
|
||||
| AuthMe-ReReloaded (HaHaWTH b49) | Yes | Verified on production server. |
|
||||
| AuthMe-ReReloaded other 5.x forks | Untested | Schema is the same, should work. |
|
||||
| Multiverse-Core | Untested | Not required. Possible teleport-intercept conflict — see [`docs/compatibility.md`](docs/compatibility.md). |
|
||||
|
||||
---
|
||||
|
||||
## Build from source
|
||||
|
||||
```bash
|
||||
mvn clean package
|
||||
```
|
||||
|
||||
The shaded jar lands at `target/RackedLimbo-1.0.0.jar`. Requires Java
|
||||
21+ and a Maven 3.9+ install. The `lib/AuthMe-5.6.0-FORK-Universal.jar`
|
||||
in the repo is referenced as a `system`-scope dependency so the build
|
||||
does not need any private repository credentials.
|
||||
|
||||
---
|
||||
|
||||
## Why not just use Multiverse-Core for the void world?
|
||||
|
||||
Multiverse-Core is a 2 MB plugin that does world creation, world
|
||||
listing, dimension portals, world-specific permissions, world inventory
|
||||
separation, and a dozen other things. It also intercepts teleports for
|
||||
its own portal and respawn logic, which is exactly the contention point
|
||||
we are trying to avoid here. RackedLimbo is ~400 lines of code and only
|
||||
manages the one void world AuthMe needs. If you are already running
|
||||
Multiverse for other reasons, you can ignore the limbo manager and only
|
||||
benefit from the LoginEvent fix.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT. See [`LICENSE`](LICENSE).
|
||||
|
||||
---
|
||||
|
||||
## Author
|
||||
|
||||
Built by [s8n-ru](https://github.com/s8n-ru) for [racked.ru](https://racked.ru/).
|
||||
51
docs/compatibility.md
Normal file
51
docs/compatibility.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Compatibility
|
||||
|
||||
## Tested matrix
|
||||
|
||||
| Component | Version | Status | Notes |
|
||||
|-----------|---------|--------|-------|
|
||||
| Paper | 1.21.11 | Verified | Primary target. Builds against `1.21.11-R0.1-SNAPSHOT`. |
|
||||
| Purpur | 1.21.11 | Expected to work | Purpur is a Paper fork with a superset of the Paper API used here. Untested in CI but the relevant API surface (`teleportAsync`, `setChunkForceLoaded`, modern `ChunkGenerator`) is identical. |
|
||||
| Folia | any | Untested | Folia's regionised threading model changes how `LoginEvent` is dispatched. The current `Bukkit.getScheduler().runTaskLater` calls would need to be reworked to use Folia's region scheduler. Do not run this plugin on Folia until it has been audited. |
|
||||
| Java | 21 | Required | `pom.xml` targets Java 21. |
|
||||
| Java | 22 / 23 / 24 | Expected to work | Source compatibility level is 21. No reflection or unsafe APIs are used. |
|
||||
|
||||
## Auth plugins
|
||||
|
||||
| Plugin | Version | Status | Notes |
|
||||
|--------|---------|--------|-------|
|
||||
| AuthMe-ReReloaded (HaHaWTH fork) | b49 | Verified | Hard dependency. The fork's SQLite schema is the one we read. |
|
||||
| AuthMe-ReReloaded (other 5.x forks) | any | Expected to work | Other forks track the same upstream schema (`authme` table with `username, x, y, z, world, yaw, pitch`). The plugin will compile and load against any 5.x AuthMe binary because we depend on `fr.xephi.authme.events.LoginEvent` which has been stable since 5.0. |
|
||||
| AuthMe-ReReloaded (6.x) | any | Untested | API likely the same; no reason to expect a break. |
|
||||
| Original AuthMe (`fr.xephi:authme` 5.x) | any | Expected to work | Same Java package, same event class. |
|
||||
| nLogin / FastLogin / SimpleAuth / others | n/a | Not supported | This plugin is hard-coupled to AuthMe events. It will refuse to load without `AuthMe` in `plugin.yml`. |
|
||||
| MySQL-backed AuthMe | n/a | Not supported (yet) | The plugin reads `authme.db` directly via SQLite JDBC. If your AuthMe stores data in MySQL, the LoginEvent restore is a no-op. |
|
||||
|
||||
## Other plugins
|
||||
|
||||
| Plugin | Status | Notes |
|
||||
|--------|--------|-------|
|
||||
| Multiverse-Core | Untested | Multiverse intercepts teleports for portals and respawn. The two plugins should coexist because RackedLimbo runs at `MONITOR` priority and overrides whatever Multiverse does to the post-login location. If you only run Multiverse for the limbo world, you do not need it any more — RackedLimbo's `LimboWorldManager` covers that case. |
|
||||
| EssentialsX | Untested | Essentials' spawn-on-join (`spawn-on-join` in `essentials.yml`) is the most common conflicting feature. With Essentials' default ordering, RackedLimbo's `MONITOR` listener still runs last and wins. If you see the spawn-on-join location winning, set `spawn-on-join: false`. |
|
||||
| WorldGuard | Should not interfere | WorldGuard does not teleport on login. |
|
||||
| BetterReload / PlugManX | Avoid | Hot-reloading any of (RackedLimbo, AuthMe, Paper itself) will leave dangling listeners. Always restart. |
|
||||
|
||||
## Database compatibility
|
||||
|
||||
The plugin opens `plugins/AuthMe/authme.db` with a fresh JDBC connection
|
||||
per query. AuthMe also keeps a connection open to the same file. SQLite
|
||||
supports multiple readers under WAL mode, which is AuthMe's default; we
|
||||
never `PRAGMA journal_mode` ourselves, never write, and never wrap the
|
||||
read in a transaction. There is no risk of corruption from this access
|
||||
pattern.
|
||||
|
||||
If AuthMe is configured to use MySQL or PostgreSQL instead of SQLite,
|
||||
this plugin will log a warning on first lookup and the LoginEvent
|
||||
restore will be skipped. A MySQL backend is on the wishlist but not
|
||||
implemented.
|
||||
|
||||
## Reporting compatibility results
|
||||
|
||||
If you run this plugin on a configuration not covered above, please
|
||||
[open an issue](https://github.com/s8n-ru/racked-limbo/issues/new?template=bug_report.md)
|
||||
or PR an update to this table.
|
||||
142
docs/configuration.md
Normal file
142
docs/configuration.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# Configuration reference
|
||||
|
||||
`plugins/RackedLimbo/config.yml` is created on first start with the
|
||||
defaults below. Reload at runtime with `/rackedlimbo reload`.
|
||||
|
||||
```yaml
|
||||
limbo:
|
||||
world: auth_limbo
|
||||
spawn-x: 0.5
|
||||
spawn-y: 128.0
|
||||
spawn-z: 0.5
|
||||
build-platform: true
|
||||
platform-y: 127
|
||||
|
||||
authme:
|
||||
db-path: plugins/AuthMe/authme.db
|
||||
teleport-delay-ticks: 10
|
||||
preload-chunks: true
|
||||
|
||||
debug: false
|
||||
```
|
||||
|
||||
## `limbo.*`
|
||||
|
||||
Settings for the void auth-limbo world. The plugin creates this world
|
||||
on enable if it does not already exist.
|
||||
|
||||
### `limbo.world`
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Type | string |
|
||||
| Default | `auth_limbo` |
|
||||
|
||||
The Bukkit world name. The world folder will live at the server data
|
||||
root (`<world-name>/`). Changing this **after** the world has been
|
||||
created will cause the plugin to create a second world with the new
|
||||
name on next start; the old one is left on disk and you can delete it
|
||||
manually.
|
||||
|
||||
### `limbo.spawn-x`, `limbo.spawn-y`, `limbo.spawn-z`
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Type | double |
|
||||
| Defaults | `0.5`, `128.0`, `0.5` |
|
||||
|
||||
World spawn coordinates inside `auth_limbo`. The `.5` offsets put the
|
||||
player on the centre of a block, which avoids fall-through edge cases
|
||||
when standing on the barrier platform.
|
||||
|
||||
### `limbo.build-platform`
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Type | boolean |
|
||||
| Default | `true` |
|
||||
|
||||
If true, the plugin builds a 5x5 patch of `BARRIER` blocks at
|
||||
`platform-y` directly under spawn so unauth players cannot fall into
|
||||
the void. Disable if you want to manage the platform yourself with a
|
||||
schematic.
|
||||
|
||||
### `limbo.platform-y`
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Type | integer |
|
||||
| Default | `127` |
|
||||
|
||||
Y-level of the barrier platform. Should be exactly one below `spawn-y`
|
||||
so players land on top of the barriers.
|
||||
|
||||
## `authme.*`
|
||||
|
||||
Settings for the AuthMe integration.
|
||||
|
||||
### `authme.db-path`
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Type | string |
|
||||
| Default | `plugins/AuthMe/authme.db` |
|
||||
|
||||
Path to AuthMe's SQLite database. Resolved relative to the server data
|
||||
directory if not absolute. Only SQLite is supported — if your AuthMe
|
||||
deployment uses MySQL, this plugin won't read your data and the
|
||||
LoginEvent restore will be a no-op.
|
||||
|
||||
### `authme.teleport-delay-ticks`
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Type | long |
|
||||
| Default | `10` (≈ 0.5 s at 20 TPS) |
|
||||
|
||||
How many ticks to wait after AuthMe's `LoginEvent` fires before issuing
|
||||
our authoritative teleport. The delay exists so AuthMe's own (broken)
|
||||
teleport runs first; we then overwrite the player position with the
|
||||
correct one.
|
||||
|
||||
If you see players briefly appearing at world spawn before snapping to
|
||||
their saved location, lower this value. If you see the saved-location
|
||||
teleport fail because some other plugin teleports the player in
|
||||
between, raise it.
|
||||
|
||||
### `authme.preload-chunks`
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Type | boolean |
|
||||
| Default | `true` |
|
||||
|
||||
If true, the plugin listens to `AuthMeAsyncPreLoginEvent` and pins the
|
||||
chunk at the saved quit-location with `Chunk#addPluginChunkTicket`
|
||||
before the password check completes. This warms the chunk so the actual
|
||||
`getChunkAtAsyncUrgently` + `teleportAsync` chain roughly 1 second
|
||||
later does not race against an unloaded chunk
|
||||
([PaperMC/Paper#4085](https://github.com/PaperMC/Paper/issues/4085)).
|
||||
|
||||
The ticket is released ~5 seconds after the teleport completes — long
|
||||
enough for the client to download the chunk before it can unload.
|
||||
|
||||
Disabling this is safe — the post-login `getChunkAtAsyncUrgently` call
|
||||
will still load the chunk before teleporting — but you may see the
|
||||
original teleport bug re-emerge under load if AuthMe's own teleport
|
||||
fires before the chunk is ready.
|
||||
|
||||
## `debug`
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Type | boolean |
|
||||
| Default | `false` |
|
||||
|
||||
If true, the plugin logs every chunk forceload, every teleport result,
|
||||
and every "no saved location" decision at INFO level. Useful when
|
||||
diagnosing a player report; noisy in production.
|
||||
|
||||
`limbo.world` cannot be changed without a full server restart — the
|
||||
limbo world is created during `onEnable`. Other keys take effect on
|
||||
`/rackedlimbo reload`.
|
||||
142
docs/how-it-works.md
Normal file
142
docs/how-it-works.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# How it works
|
||||
|
||||
A technical walkthrough of the bug RackedLimbo 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
|
||||
|
||||
RackedLimbo 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.racked.limbo.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 |
|
||||
|------|----------------|
|
||||
| `RackedLimbo.java` | `JavaPlugin` entry point. Loads config, builds the limbo world, registers the listener, exposes `/rackedlimbo` 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
|
||||
100
docs/installation.md
Normal file
100
docs/installation.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Installation
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Component | Minimum | Notes |
|
||||
|-----------|---------|-------|
|
||||
| Server software | Paper 1.21.11 (or Purpur on the same MC version) | Folia is untested. |
|
||||
| Java | 21 | Required by Paper 1.21+. |
|
||||
| AuthMe-ReReloaded | any 5.x or 6.x fork | Verified on the HaHaWTH fork build 49 (`5.6.0-FORK-b49`). Hard dependency. |
|
||||
|
||||
## Bare-metal / VPS install
|
||||
|
||||
1. Download `RackedLimbo-X.Y.Z.jar` from the
|
||||
[Releases page](https://github.com/s8n-ru/racked-limbo/releases/latest).
|
||||
2. Drop the jar into your server's `plugins/` directory.
|
||||
3. Restart the server. Do **not** use `/reload` — the listener wiring
|
||||
does not survive a hot reload.
|
||||
4. On first start the plugin will create:
|
||||
- `plugins/RackedLimbo/config.yml`
|
||||
- The `auth_limbo/` world directory at the server data root.
|
||||
5. Tail the log and look for:
|
||||
|
||||
```
|
||||
[RackedLimbo] Using AuthMe DB at: /path/to/plugins/AuthMe/authme.db
|
||||
[RackedLimbo] Limbo world 'auth_limbo' already loaded.
|
||||
[RackedLimbo] Enabled. Listening for AuthMe LoginEvent at MONITOR.
|
||||
```
|
||||
|
||||
If you see "No saved location for ..." on a successful `/login`, that
|
||||
user has no row in `authme.db` yet — log out, log back in, and the
|
||||
row will be written by AuthMe on quit.
|
||||
|
||||
## Docker (`itzg/minecraft-server`)
|
||||
|
||||
This is the recommended distribution. Add the plugin via the auto-loader.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
mc:
|
||||
image: itzg/minecraft-server:java21
|
||||
environment:
|
||||
EULA: "TRUE"
|
||||
TYPE: PAPER
|
||||
VERSION: "1.21.11"
|
||||
PLUGINS: |
|
||||
https://github.com/s8n-ru/racked-limbo/releases/download/v1.0.0/RackedLimbo-1.0.0.jar
|
||||
# Pin the version — itzg's auto-loader purges unrecognised jars on restart.
|
||||
REMOVE_OLD_MODS: "TRUE"
|
||||
REMOVE_OLD_MODS_INCLUDE: "*.jar"
|
||||
REMOVE_OLD_MODS_EXCLUDE: "RackedLimbo*.jar,AuthMe*.jar,(other-plugins)*.jar"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
ports:
|
||||
- "25565:25565"
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Replace `v1.0.0` and `1.0.0` with the version you want to pin.
|
||||
- `REMOVE_OLD_MODS_EXCLUDE` must list every jar you want kept; everything
|
||||
else gets wiped on restart.
|
||||
- AuthMe must also be in `PLUGINS` (or otherwise present in `plugins/`)
|
||||
before this plugin loads — Paper enforces the `depend` ordering from
|
||||
`plugin.yml`.
|
||||
|
||||
## Verifying the install
|
||||
|
||||
Run as an op:
|
||||
|
||||
```
|
||||
/rackedlimbo
|
||||
```
|
||||
|
||||
You should see:
|
||||
|
||||
```
|
||||
RackedLimbo 1.0.0 - sub: reload | tp <player>
|
||||
```
|
||||
|
||||
To confirm the DB read path is working, log out a test account, then run
|
||||
`/rackedlimbo tp <name>` — the plugin should report the world and
|
||||
coordinates it pulled from `authme.db`.
|
||||
|
||||
## Updating
|
||||
|
||||
1. Stop the server.
|
||||
2. Replace the old jar with the new one.
|
||||
3. Start the server.
|
||||
|
||||
`config.yml` is preserved across updates. Breaking config changes will
|
||||
be flagged in `CHANGELOG.md` for the affected version.
|
||||
|
||||
## Uninstalling
|
||||
|
||||
1. Stop the server.
|
||||
2. Delete `plugins/RackedLimbo-*.jar` and the `plugins/RackedLimbo/`
|
||||
folder.
|
||||
3. Optionally delete the `auth_limbo/` world directory if you no longer
|
||||
want the void world. AuthMe will fall back to whatever
|
||||
`Settings.RestrictionsSettings.unrestrictedWorlds` / spawn it was
|
||||
using before.
|
||||
0
lib/.gitkeep
Normal file
0
lib/.gitkeep
Normal file
46
lib/README.md
Normal file
46
lib/README.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# `lib/` — system-scope dependencies
|
||||
|
||||
This directory holds binary dependencies that are not available from a
|
||||
public Maven repository and so are referenced as `system`-scope in
|
||||
`pom.xml`.
|
||||
|
||||
## `AuthMe-5.6.0-FORK-Universal.jar`
|
||||
|
||||
The HaHaWTH fork of AuthMe-ReReloaded, build 49. Used at compile time
|
||||
only — `system` scope means it is never bundled into the shaded plugin
|
||||
jar.
|
||||
|
||||
This jar is not checked in to the public repository because:
|
||||
|
||||
1. It is not licensed by us — it ships under AuthMe-ReReloaded's GPL.
|
||||
2. It is several MB; redistributing it bloats the repo for no benefit.
|
||||
3. Any 5.x AuthMe-ReReloaded fork exposes the same `LoginEvent` and
|
||||
`AuthMeAsyncPreLoginEvent` API and works as a drop-in compile target.
|
||||
|
||||
To build locally:
|
||||
|
||||
1. Download the AuthMe jar that matches the runtime you intend to
|
||||
target. Verified against
|
||||
[HaHaWTH/AuthMeReloaded](https://github.com/HaHaWTH/AuthMeReloaded)
|
||||
build 49 (`AuthMe-5.6.0-FORK-Universal.jar`).
|
||||
2. Drop it into this directory with the exact filename
|
||||
`AuthMe-5.6.0-FORK-Universal.jar`.
|
||||
3. Run `mvn clean package` from the repo root.
|
||||
|
||||
## CI builds
|
||||
|
||||
`.github/workflows/build.yml` and `.github/workflows/release.yml` both
|
||||
include a `Fetch AuthMe jar` step that downloads the dependency before
|
||||
running Maven. The download URL is read from the repository secret
|
||||
`AUTHME_JAR_URL`. To make CI builds succeed:
|
||||
|
||||
1. Go to the repo's **Settings → Secrets and variables → Actions**.
|
||||
2. Add a new repository secret named `AUTHME_JAR_URL`.
|
||||
3. Set the value to a direct download URL for the AuthMe jar that
|
||||
matches the version pinned in `pom.xml` (currently
|
||||
`5.6.0-FORK-b49`). A GitHub release asset URL works.
|
||||
|
||||
Until that secret is set, the `Build` and `Release` workflows will fail
|
||||
on the first run with `AUTHME_JAR_URL secret is not set`. This is the
|
||||
expected gating — do not remove the check; it stops a broken jar from
|
||||
shipping silently.
|
||||
115
pom.xml
Normal file
115
pom.xml
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>ru.racked</groupId>
|
||||
<artifactId>RackedLimbo</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>RackedLimbo</name>
|
||||
<description>AuthMe-aware void limbo + reliable post-login teleport for Paper 1.21.11.</description>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<paper.api.version>1.21.11-R0.1-SNAPSHOT</paper.api.version>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>papermc</id>
|
||||
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>sonatype</id>
|
||||
<url>https://oss.sonatype.org/content/groups/public/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<!-- Paper API (provided by the server runtime) -->
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>${paper.api.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- AuthMe-ReReloaded (HaHaWTH fork b49) — provided by the server runtime.
|
||||
We compile against the bundled jar and never shade it. -->
|
||||
<dependency>
|
||||
<groupId>fr.xephi</groupId>
|
||||
<artifactId>authme</artifactId>
|
||||
<version>5.6.0-FORK-b49</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/AuthMe-5.6.0-FORK-Universal.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
<!-- SQLite JDBC driver — shaded into the plugin jar so we read AuthMe's authme.db -->
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.46.1.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>${project.name}-${project.version}</finalName>
|
||||
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<release>21</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<minimizeJar>false</minimizeJar>
|
||||
<relocations>
|
||||
<relocation>
|
||||
<pattern>org.sqlite</pattern>
|
||||
<shadedPattern>ru.racked.limbo.shaded.sqlite</shadedPattern>
|
||||
</relocation>
|
||||
</relocations>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
<exclude>META-INF/MANIFEST.MF</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
97
src/main/java/ru/racked/limbo/AuthMeDatabase.java
Normal file
97
src/main/java/ru/racked/limbo/AuthMeDatabase.java
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package ru.racked.limbo;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Read-only access to AuthMe's SQLite {@code authme} table.
|
||||
*
|
||||
* Schema (from AuthMe-ReReloaded fork b49):
|
||||
* username VARCHAR PRIMARY KEY (lowercased real name)
|
||||
* realname VARCHAR
|
||||
* x DOUBLE, y DOUBLE, z DOUBLE
|
||||
* world VARCHAR (default 'world')
|
||||
* yaw FLOAT, pitch FLOAT
|
||||
*
|
||||
* We never write to this DB — AuthMe owns it. We only read on login to
|
||||
* find the player's last quit-location, then teleport them ourselves.
|
||||
*/
|
||||
public final class AuthMeDatabase {
|
||||
|
||||
private final RackedLimbo plugin;
|
||||
private final File dbFile;
|
||||
private final String url;
|
||||
|
||||
public AuthMeDatabase(RackedLimbo plugin, File dbFile) {
|
||||
this.plugin = plugin;
|
||||
this.dbFile = dbFile;
|
||||
this.url = "jdbc:sqlite:" + dbFile.getAbsolutePath();
|
||||
|
||||
// Force-load shaded SQLite driver class so DriverManager finds it.
|
||||
try {
|
||||
Class.forName("ru.racked.limbo.shaded.sqlite.JDBC");
|
||||
} catch (ClassNotFoundException e) {
|
||||
plugin.getLogger().log(Level.SEVERE,
|
||||
"[RackedLimbo] Shaded SQLite driver class missing — build is broken!", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a player's saved quit location by exact name (case-insensitive).
|
||||
* Returns null if no row, no world, or any error.
|
||||
*/
|
||||
public Location getQuitLocation(String playerName) {
|
||||
if (playerName == null || playerName.isEmpty()) return null;
|
||||
String lower = playerName.toLowerCase();
|
||||
|
||||
String sql = "SELECT x, y, z, world, yaw, pitch FROM authme WHERE LOWER(username) = ? LIMIT 1";
|
||||
|
||||
try (Connection con = DriverManager.getConnection(url);
|
||||
PreparedStatement ps = con.prepareStatement(sql)) {
|
||||
|
||||
ps.setString(1, lower);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (!rs.next()) {
|
||||
if (plugin.debug()) {
|
||||
plugin.getLogger().info("[RackedLimbo][debug] No DB row for " + playerName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
double x = rs.getDouble("x");
|
||||
double y = rs.getDouble("y");
|
||||
double z = rs.getDouble("z");
|
||||
String worldName = rs.getString("world");
|
||||
float yaw = rs.getFloat("yaw");
|
||||
float pitch = rs.getFloat("pitch");
|
||||
|
||||
if (worldName == null || worldName.isEmpty()) worldName = "world";
|
||||
World world = Bukkit.getWorld(worldName);
|
||||
if (world == null) {
|
||||
plugin.getLogger().warning("[RackedLimbo] World '" + worldName
|
||||
+ "' from authme.db is not loaded — cannot restore "
|
||||
+ playerName + ".");
|
||||
return null;
|
||||
}
|
||||
return new Location(world, x, y, z, yaw, pitch);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.WARNING,
|
||||
"[RackedLimbo] Failed to read authme.db for " + playerName, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
// No persistent connection — DriverManager.getConnection() opens a new
|
||||
// one per query. Nothing to close globally.
|
||||
}
|
||||
}
|
||||
92
src/main/java/ru/racked/limbo/LimboWorldManager.java
Normal file
92
src/main/java/ru/racked/limbo/LimboWorldManager.java
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package ru.racked.limbo;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.GameRule;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.WorldCreator;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
|
||||
/**
|
||||
* Creates and configures the {@code auth_limbo} world.
|
||||
*
|
||||
* Why a separate manager: AuthMe's pre-auth limbo logic is independent of
|
||||
* Multiverse — it only needs a Bukkit world to teleport unauth players into.
|
||||
* We create that world ourselves with a void generator and a tiny barrier
|
||||
* platform at spawn so falling unauth players don't take fall damage in void.
|
||||
*/
|
||||
public final class LimboWorldManager {
|
||||
|
||||
private final RackedLimbo plugin;
|
||||
|
||||
public LimboWorldManager(RackedLimbo plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void ensureLimbo() {
|
||||
FileConfiguration cfg = plugin.getConfig();
|
||||
String name = cfg.getString("limbo.world", "auth_limbo");
|
||||
|
||||
World world = Bukkit.getWorld(name);
|
||||
if (world == null) {
|
||||
plugin.getLogger().info("[RackedLimbo] Creating limbo world '" + name + "'...");
|
||||
WorldCreator wc = new WorldCreator(name)
|
||||
.environment(World.Environment.THE_END)
|
||||
.generator(new VoidGenerator())
|
||||
.generateStructures(false);
|
||||
world = wc.createWorld();
|
||||
if (world == null) {
|
||||
plugin.getLogger().severe("[RackedLimbo] Failed to create limbo world!");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
plugin.getLogger().info("[RackedLimbo] Limbo world '" + name + "' already loaded.");
|
||||
}
|
||||
|
||||
double sx = cfg.getDouble("limbo.spawn-x", 0.5);
|
||||
double sy = cfg.getDouble("limbo.spawn-y", 128.0);
|
||||
double sz = cfg.getDouble("limbo.spawn-z", 0.5);
|
||||
world.setSpawnLocation((int) sx, (int) sy, (int) sz);
|
||||
|
||||
// World rules: no time progression, no weather, no mobs, no PvP.
|
||||
world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
|
||||
world.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
|
||||
world.setGameRule(GameRule.DO_MOB_SPAWNING, false);
|
||||
world.setGameRule(GameRule.DO_FIRE_TICK, false);
|
||||
world.setPVP(false);
|
||||
world.setKeepSpawnInMemory(true);
|
||||
|
||||
if (cfg.getBoolean("limbo.build-platform", true)) {
|
||||
int py = cfg.getInt("limbo.platform-y", 127);
|
||||
int cx = (int) sx;
|
||||
int cz = (int) sz;
|
||||
int built = 0;
|
||||
for (int dx = -2; dx <= 2; dx++) {
|
||||
for (int dz = -2; dz <= 2; dz++) {
|
||||
Block b = world.getBlockAt(cx + dx, py, cz + dz);
|
||||
if (b.getType() != Material.BARRIER) {
|
||||
b.setType(Material.BARRIER, false);
|
||||
built++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (built > 0) {
|
||||
plugin.getLogger().info("[RackedLimbo] Built " + built + " barrier blocks at "
|
||||
+ cx + "," + py + "," + cz + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Location spawn() {
|
||||
FileConfiguration cfg = plugin.getConfig();
|
||||
String name = cfg.getString("limbo.world", "auth_limbo");
|
||||
World world = Bukkit.getWorld(name);
|
||||
if (world == null) return null;
|
||||
return new Location(world,
|
||||
cfg.getDouble("limbo.spawn-x", 0.5),
|
||||
cfg.getDouble("limbo.spawn-y", 128.0),
|
||||
cfg.getDouble("limbo.spawn-z", 0.5));
|
||||
}
|
||||
}
|
||||
193
src/main/java/ru/racked/limbo/LoginListener.java
Normal file
193
src/main/java/ru/racked/limbo/LoginListener.java
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
package ru.racked.limbo;
|
||||
|
||||
import fr.xephi.authme.events.AuthMeAsyncPreLoginEvent;
|
||||
import fr.xephi.authme.events.LoginEvent;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Listens for AuthMe's two relevant events:
|
||||
*
|
||||
* 1. {@link AuthMeAsyncPreLoginEvent} — fired before AuthMe authenticates
|
||||
* the password. We pin the destination chunk via a plugin chunk-ticket
|
||||
* so it's fully loaded by the time the actual teleport runs.
|
||||
*
|
||||
* 2. {@link LoginEvent} — fired AFTER AuthMe successfully authenticates
|
||||
* and runs its own (often broken) post-login teleport. We listen at
|
||||
* MONITOR priority so we are LAST in the chain, then fire an
|
||||
* authoritative teleport that overrides whatever AuthMe / Paper safety
|
||||
* checks did to the player's location.
|
||||
*
|
||||
* Threading:
|
||||
* - AuthMeAsyncPreLoginEvent fires async (AuthMe worker thread).
|
||||
* - LoginEvent in this fork fires on the main thread, but we treat it
|
||||
* as if it could be async — all chunk/teleport work goes through
|
||||
* scheduler hops to the main thread.
|
||||
* - {@code teleportAsync} and {@code getChunkAtAsyncUrgently} are
|
||||
* thread-safe per Paper docs.
|
||||
*
|
||||
* Pattern (from Paper Issue #4085 + PaperLib AsyncTeleportPaper):
|
||||
* addPluginChunkTicket(cx, cz)
|
||||
* -> getChunkAtAsyncUrgently(cx, cz, true)
|
||||
* .thenAccept(chunk ->
|
||||
* player.teleportAsync(loc, PLUGIN)
|
||||
* .thenAccept(ok -> removePluginChunkTicket(cx, cz) after 5s)
|
||||
* );
|
||||
*/
|
||||
public final class LoginListener implements Listener {
|
||||
|
||||
private final RackedLimbo plugin;
|
||||
private final AuthMeDatabase db;
|
||||
|
||||
/** Tracks active plugin-chunk-tickets so we don't double-add or fail to release. */
|
||||
private final Set<String> activeTickets = new HashSet<>();
|
||||
|
||||
public LoginListener(RackedLimbo plugin, AuthMeDatabase db) {
|
||||
this.plugin = plugin;
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
/* ---------------- Pre-login: pin the chunk early ---------------- */
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onAsyncPreLogin(AuthMeAsyncPreLoginEvent event) {
|
||||
if (!plugin.getConfig().getBoolean("authme.preload-chunks", true)) return;
|
||||
|
||||
Player player = event.getPlayer();
|
||||
if (player == null) return;
|
||||
final String name = player.getName();
|
||||
|
||||
final Location saved = db.getQuitLocation(name);
|
||||
if (saved == null || saved.getWorld() == null) return;
|
||||
|
||||
final World world = saved.getWorld();
|
||||
final int cx = saved.getBlockX() >> 4;
|
||||
final int cz = saved.getBlockZ() >> 4;
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
String key = world.getName() + ":" + cx + ":" + cz;
|
||||
if (activeTickets.add(key)) {
|
||||
try {
|
||||
world.getChunkAt(cx, cz).addPluginChunkTicket(plugin);
|
||||
if (plugin.debug()) {
|
||||
plugin.getLogger().info("[RackedLimbo][debug] Chunk-ticket added "
|
||||
+ key + " for " + name);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning("[RackedLimbo] addPluginChunkTicket failed for "
|
||||
+ name + ": " + t.getMessage());
|
||||
activeTickets.remove(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- Post-login: authoritative teleport ---------------- */
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onLogin(LoginEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
if (player == null) return;
|
||||
final String name = player.getName();
|
||||
|
||||
final Location saved = db.getQuitLocation(name);
|
||||
if (saved == null) {
|
||||
plugin.getLogger().info("[RackedLimbo] No saved location for "
|
||||
+ name + " — leaving where AuthMe put them.");
|
||||
return;
|
||||
}
|
||||
|
||||
long delay = Math.max(0, plugin.getConfig().getLong("authme.teleport-delay-ticks", 10L));
|
||||
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> doTeleport(player, name, saved), delay);
|
||||
}
|
||||
|
||||
/* ---------------- Core teleport with chunk-prep ---------------- */
|
||||
|
||||
private void doTeleport(Player player, String name, Location saved) {
|
||||
if (!player.isOnline()) {
|
||||
plugin.getLogger().info("[RackedLimbo] " + name
|
||||
+ " went offline before restore — skipping.");
|
||||
return;
|
||||
}
|
||||
World world = saved.getWorld();
|
||||
if (world == null) {
|
||||
plugin.getLogger().warning("[RackedLimbo] Saved world for "
|
||||
+ name + " is no longer loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getLogger().info(String.format(
|
||||
"[RackedLimbo] Restoring %s to %s(%.1f, %.1f, %.1f)",
|
||||
name, world.getName(), saved.getX(), saved.getY(), saved.getZ()));
|
||||
|
||||
final int cx = saved.getBlockX() >> 4;
|
||||
final int cz = saved.getBlockZ() >> 4;
|
||||
final String key = world.getName() + ":" + cx + ":" + cz;
|
||||
|
||||
// Make sure the chunk is loaded before teleporting. Paper's
|
||||
// teleportAsync alone has been observed to drop the player at
|
||||
// world spawn briefly when the destination chunk isn't ready.
|
||||
try {
|
||||
if (activeTickets.add(key)) {
|
||||
world.getChunkAt(cx, cz).addPluginChunkTicket(plugin);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
// non-fatal — fall through to chunk-load + teleport
|
||||
}
|
||||
|
||||
world.getChunkAtAsyncUrgently(cx, cz).thenAccept((Chunk chunk) -> {
|
||||
player.teleportAsync(saved, PlayerTeleportEvent.TeleportCause.PLUGIN)
|
||||
.thenAccept(success -> {
|
||||
if (Boolean.TRUE.equals(success)) {
|
||||
if (plugin.debug()) {
|
||||
plugin.getLogger().info("[RackedLimbo][debug] Teleport ok for " + name);
|
||||
}
|
||||
} else {
|
||||
plugin.getLogger().warning("[RackedLimbo] teleportAsync returned false for "
|
||||
+ name + " — Paper may have rejected the location.");
|
||||
}
|
||||
// Release the ticket 5s later — gives the client time to
|
||||
// download the chunk before we let it unload.
|
||||
scheduleTicketRelease(world, cx, cz, key);
|
||||
})
|
||||
.exceptionally(ex -> {
|
||||
plugin.getLogger().warning("[RackedLimbo] teleportAsync threw for "
|
||||
+ name + ": " + ex.getMessage());
|
||||
scheduleTicketRelease(world, cx, cz, key);
|
||||
return null;
|
||||
});
|
||||
}).exceptionally(ex -> {
|
||||
plugin.getLogger().warning("[RackedLimbo] getChunkAtAsyncUrgently threw for "
|
||||
+ name + ": " + ex.getMessage());
|
||||
scheduleTicketRelease(world, cx, cz, key);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private void scheduleTicketRelease(World world, int cx, int cz, String key) {
|
||||
if (!activeTickets.contains(key)) return;
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
try {
|
||||
world.getChunkAt(cx, cz).removePluginChunkTicket(plugin);
|
||||
} catch (Throwable t) {
|
||||
// best-effort
|
||||
} finally {
|
||||
activeTickets.remove(key);
|
||||
if (plugin.debug()) {
|
||||
plugin.getLogger().info("[RackedLimbo][debug] Chunk-ticket released " + key);
|
||||
}
|
||||
}
|
||||
}, 20L * 5L);
|
||||
}
|
||||
}
|
||||
125
src/main/java/ru/racked/limbo/RackedLimbo.java
Normal file
125
src/main/java/ru/racked/limbo/RackedLimbo.java
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
package ru.racked.limbo;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* RackedLimbo — companion plugin for AuthMe-ReReloaded.
|
||||
*
|
||||
* Two responsibilities:
|
||||
* 1. Provide a void {@code auth_limbo} world (no Multiverse required).
|
||||
* 2. Listen to {@link fr.xephi.authme.events.LoginEvent} at MONITOR priority,
|
||||
* then teleport the player to their saved quit-location from authme.db
|
||||
* AFTER AuthMe's own (broken) teleport has run.
|
||||
*
|
||||
* The DB is read directly via JDBC SQLite — we never modify it.
|
||||
*/
|
||||
public final class RackedLimbo extends JavaPlugin {
|
||||
|
||||
private static RackedLimbo instance;
|
||||
private LimboWorldManager limboManager;
|
||||
private AuthMeDatabase authmeDb;
|
||||
private LoginListener loginListener;
|
||||
|
||||
public static RackedLimbo getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
instance = this;
|
||||
saveDefaultConfig();
|
||||
|
||||
FileConfiguration cfg = getConfig();
|
||||
|
||||
// Resolve AuthMe DB path. Server data dir is the JVM working dir under itzg.
|
||||
String dbPathStr = cfg.getString("authme.db-path", "plugins/AuthMe/authme.db");
|
||||
File dbFile = new File(dbPathStr);
|
||||
if (!dbFile.isAbsolute()) {
|
||||
dbFile = new File(getServer().getWorldContainer(), dbPathStr);
|
||||
}
|
||||
getLogger().info("[RackedLimbo] Using AuthMe DB at: " + dbFile.getAbsolutePath());
|
||||
this.authmeDb = new AuthMeDatabase(this, dbFile);
|
||||
|
||||
// Build the limbo world before AuthMe gets a chance to teleport players there.
|
||||
this.limboManager = new LimboWorldManager(this);
|
||||
this.limboManager.ensureLimbo();
|
||||
|
||||
// Register listener LAST — AuthMe is depend so it's already enabled.
|
||||
this.loginListener = new LoginListener(this, authmeDb);
|
||||
Bukkit.getPluginManager().registerEvents(loginListener, this);
|
||||
|
||||
getLogger().info("[RackedLimbo] Enabled. Listening for AuthMe LoginEvent at MONITOR.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (authmeDb != null) authmeDb.close();
|
||||
getLogger().info("[RackedLimbo] Disabled.");
|
||||
}
|
||||
|
||||
public LimboWorldManager limbo() {
|
||||
return limboManager;
|
||||
}
|
||||
|
||||
public AuthMeDatabase db() {
|
||||
return authmeDb;
|
||||
}
|
||||
|
||||
public boolean debug() {
|
||||
return getConfig().getBoolean("debug", false);
|
||||
}
|
||||
|
||||
/* ---------------- /rackedlimbo admin command ---------------- */
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (!command.getName().equalsIgnoreCase("rackedlimbo")) return false;
|
||||
|
||||
if (args.length == 0) {
|
||||
sender.sendMessage("RackedLimbo " + getDescription().getVersion()
|
||||
+ " — sub: reload | tp <player>");
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "reload" -> {
|
||||
reloadConfig();
|
||||
sender.sendMessage("[RackedLimbo] config reloaded.");
|
||||
return true;
|
||||
}
|
||||
case "tp" -> {
|
||||
if (args.length < 2) {
|
||||
sender.sendMessage("Usage: /rackedlimbo tp <player>");
|
||||
return true;
|
||||
}
|
||||
Player target = Bukkit.getPlayerExact(args[1]);
|
||||
if (target == null) {
|
||||
sender.sendMessage("Player not online: " + args[1]);
|
||||
return true;
|
||||
}
|
||||
Location saved = authmeDb.getQuitLocation(target.getName());
|
||||
if (saved == null) {
|
||||
sender.sendMessage("No saved location in authme.db for " + target.getName());
|
||||
return true;
|
||||
}
|
||||
target.teleportAsync(saved);
|
||||
sender.sendMessage("Teleported " + target.getName() + " to "
|
||||
+ saved.getWorld().getName() + "("
|
||||
+ (int) saved.getX() + "," + (int) saved.getY() + "," + (int) saved.getZ() + ")");
|
||||
return true;
|
||||
}
|
||||
default -> {
|
||||
sender.sendMessage("Unknown subcommand: " + args[0]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/main/java/ru/racked/limbo/VoidGenerator.java
Normal file
62
src/main/java/ru/racked/limbo/VoidGenerator.java
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package ru.racked.limbo;
|
||||
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.bukkit.generator.WorldInfo;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Empty-chunk generator for the limbo world. Every chunk is air — players will
|
||||
* fall into the void unless we drop them onto a barrier platform at spawn.
|
||||
*
|
||||
* Uses Paper's modern generator API (1.17+) — does NOT override the legacy
|
||||
* {@code generateChunkData} so it works correctly with chunk caching.
|
||||
*/
|
||||
public final class VoidGenerator extends ChunkGenerator {
|
||||
|
||||
@Override
|
||||
public void generateNoise(@NotNull WorldInfo worldInfo, @NotNull Random random,
|
||||
int chunkX, int chunkZ, @NotNull ChunkData chunkData) {
|
||||
// no-op: leave chunk as air
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateSurface(@NotNull WorldInfo worldInfo, @NotNull Random random,
|
||||
int chunkX, int chunkZ, @NotNull ChunkData chunkData) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateBedrock(@NotNull WorldInfo worldInfo, @NotNull Random random,
|
||||
int chunkX, int chunkZ, @NotNull ChunkData chunkData) {
|
||||
// no-op — we don't even want a floor
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateCaves(@NotNull WorldInfo worldInfo, @NotNull Random random,
|
||||
int chunkX, int chunkZ, @NotNull ChunkData chunkData) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateNoise() { return false; }
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateSurface() { return false; }
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateBedrock() { return false; }
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateCaves() { return false; }
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateDecorations() { return false; }
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateMobs() { return false; }
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateStructures() { return false; }
|
||||
}
|
||||
28
src/main/resources/config.yml
Normal file
28
src/main/resources/config.yml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# RackedLimbo configuration
|
||||
|
||||
# Limbo world settings — pre-auth players are kept here.
|
||||
limbo:
|
||||
# Bukkit world name. Folder will be created at /data/<world-name>/.
|
||||
world: auth_limbo
|
||||
# Spawn coordinates inside the limbo world.
|
||||
spawn-x: 0.5
|
||||
spawn-y: 128.0
|
||||
spawn-z: 0.5
|
||||
# Build a 5x5 barrier platform under spawn so unauth players don't fall.
|
||||
build-platform: true
|
||||
platform-y: 127
|
||||
|
||||
# Where AuthMe's SQLite DB lives, relative to the server data dir.
|
||||
# Default matches itzg/minecraft-server with a /data bind mount.
|
||||
authme:
|
||||
db-path: plugins/AuthMe/authme.db
|
||||
# Tick delay between LoginEvent firing and us forcing the teleport.
|
||||
# 10 ticks (~0.5s) lets AuthMe's own broken teleport finish first
|
||||
# so our async teleport is the LAST one to run.
|
||||
teleport-delay-ticks: 10
|
||||
# Whether to forceload the chunk at the saved quit-loc 1s before login
|
||||
# completes (via AuthMeAsyncPreLoginEvent), then unforce after teleport.
|
||||
preload-chunks: true
|
||||
|
||||
# Set to true for verbose logs. False = only INFO on actual restores.
|
||||
debug: false
|
||||
29
src/main/resources/plugin.yml
Normal file
29
src/main/resources/plugin.yml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
name: RackedLimbo
|
||||
main: ru.racked.limbo.RackedLimbo
|
||||
version: ${project.version}
|
||||
api-version: '1.21'
|
||||
authors: [s8n / racked.ru]
|
||||
description: AuthMe-aware void limbo + reliable post-login teleport.
|
||||
website: https://racked.ru/
|
||||
|
||||
# Hard depend on AuthMe — we listen to fr.xephi.authme.events.LoginEvent
|
||||
# so AuthMe must be present and load first.
|
||||
depend:
|
||||
- AuthMe
|
||||
|
||||
# We want to run AFTER VoidWorldGenerator (if present) so it can serve our
|
||||
# limbo world with a vanilla void chunk generator if our own generator misses.
|
||||
softdepend:
|
||||
- VoidWorldGenerator
|
||||
|
||||
commands:
|
||||
rackedlimbo:
|
||||
description: RackedLimbo admin commands.
|
||||
aliases: [rlimbo]
|
||||
permission: rackedlimbo.admin
|
||||
usage: /rackedlimbo <reload|tp <player>>
|
||||
|
||||
permissions:
|
||||
rackedlimbo.admin:
|
||||
description: Manage RackedLimbo at runtime.
|
||||
default: op
|
||||
Loading…
Reference in a new issue