-
Notifications
You must be signed in to change notification settings - Fork 10
Extending RTP
LeafRTP is built to be extended. The plugin you drop in is itself assembled from the same seams it exposes to addons: the bundled menu, the biome maps, the scheduler, the config system, and the proxy transport are all consumed through public surfaces, not private internals. That means almost anything the core does, an addon can do too - without re-implementing safety.
This page is the map. It lists every extension surface, what you build on it, and where the detailed how-to lives. Start here, then jump to the page for the seam you need.
!!! note "The one rule that makes all of this safe"
An addon never re-implements destination safety. It composes an existing surface and adds one new behavior on top. Permission gating, cooldown/cost resolution, claim avoidance, and the S-001..S-007 prohibitions all stay inside the engine, behind the API. The only mutating call most addons make is teleport(...), which re-validates everything server-side and always returns a result (never a silent no-op, per REQ-RTP-S-004). This is why the reference GUI addon is only about a dozen classes.
LeafRTP splits its API into two tiers (see ADR-051). Knowing which tier you are on tells you what to depend on and how stable it is.
| Tier | Module | What it is | Examples |
|---|---|---|---|
| Contract tier | rtp-api |
Stable, platform-neutral interfaces for addon authors. No org.bukkit.*. |
RTPAPI.getAllowedTargets, teleport, getTargetStatus, getMetricsSnapshot, RTPAPI.hooks(), MenuRenderer
|
| Implementation-extension tier | rtp-core |
Deriving new engine pieces (a new geometry, a new height picker). Less stable; you derive concrete engine types. |
RTP.addShape(Shape), RTP.addVerticalAdjustor(VerticalAdjustor)
|
!!! tip "Pick the lowest tier that does the job"
Most addons (menus, triggers, exporters, effects) live entirely on the contract tier and only depend on rtp-api. Reach into rtp-core only when you are genuinely adding a new kind of destination geometry or vertical placement.
| You want to... | Surface | Tier | Detail page |
|---|---|---|---|
| Put a clickable destination menu / GUI in front of players | typed targets + a MenuRenderer
|
contract | Building a menu |
| Add a new menu style (book, sign board, map wall) on the same model |
MenuRenderer + GuiRenderers registry |
contract | Building a menu |
Add a /rtp <verb> sub-command |
commands-api command tree |
contract | Sub-commands |
| Change where players may land (claim skip, "near structures", "avoid water") | verifier hooks | contract | Hooks and verifiers |
| Add a new region geometry | RTP.addShape(Shape) |
implementation | Shapes + Hooks and verifiers |
| Change how the final Y is chosen | RTP.addVerticalAdjustor(VerticalAdjustor) |
implementation | Vertical adjustors + Hooks and verifiers |
| Play something during the warmup delay (particles, bossbar, fake-block "rift") | effects-api |
contract | Effects |
| Draw a live biome / coverage map |
maps-api MapView path |
contract |
Building a menu (regionIconStyle) |
| Read cache depth, queue length, latency, TPS/MSPT | RTPAPI.getMetricsSnapshot() |
contract | Metrics |
| Run RTP on join / first-join / death / world-change |
rtp.onevent.* permissions (no code) |
n/a | Common RTP recipes |
| Offer destinations on other servers behind a proxy |
NetworkTransport SPI |
proxy SPI | this page, below |
Ship your own config file that reloads with /rtp reload
|
ConfigParser |
core | Addon development |
| Schedule work safely on every platform (incl. Folia) | RTP.scheduler |
core | Addon development |
The sections below add the "why" and the non-obvious gotchas for each. Read the linked page for copy-pasteable code.
The most common reason to hook an RTP plugin is to put a destination picker in front of players. LeafRTP gives you two levels:
- Use the bundled menu - drop in the GUI addon, no code. See Menu.
-
Build your own - list the player's allowed destinations (
getAllowedTargets), decorate each with live status (getTargetStatus), submit on click (teleport), and draw it however you like by registering aMenuRendererunder a style key. See Building a menu.
Because the menu model is platform-neutral and immutable, a new renderer (chest, book, sign board, hologram, map wall) is purely a drawing concern - it never re-derives targets or re-checks safety.
Add a /rtp <verb> through the commands-api tree instead of forking the command (see Sub-commands). For NPC / sign / item triggers, prefer binding the typed RTPAPI.teleport(...) over shelling out to the command string, so your trigger gets a real success/queued/reason result and can give the player accurate feedback. The bare /rtp root action is itself a bindable hook (it is how the GUI addon opens its menu).
These change RTP's decision, not its presentation:
- Verifiers reject a candidate destination and force a re-roll (custom claim plugins, "only near villages", "no lava nearby"). They run inside the bounded spiral selector, so they cannot loop forever and cannot bypass the S-00x prohibitions. See Hooks and verifiers.
-
Shapes add a new region geometry via
RTP.addShape(Shape). See Shapes. -
Vertical adjustors change how the final Y is chosen via
RTP.addVerticalAdjustor(VerticalAdjustor). See Vertical adjustors.
!!! warning "Shapes and adjustors are implementation-tier"
addShape / addVerticalAdjustor live on RTP in rtp-core, not on rtp-api. Register them only after core has loaded - calling an extension entry point before rtp-core finishes loading throws IllegalStateException (S-006), it does not silently no-op.
The teleport warmup delay is an extension point. Register an effect through effects-api to run particles, a countdown bossbar, a sound cue, a screen fade, or the planned client-side "rift" block animation - all during the delay, with no world edits and no chunk loads on a region thread. See Effects.
LeafRTP bundles a maps-api MapView path, already used to render a region's biome chart onto a FILLED_MAP icon (regionIconStyle: biome-map in the GUI addon). An addon can reuse it for a wall-map of scanned coverage or a per-player "where could I land" preview. See the regionIconStyle section of Building a menu.
RTPAPI.getMetricsSnapshot() exposes the same runtime health the GUI addon's dashboard tile shows: cache depth, queue length, pipeline latency percentiles, TPS/MSPT, spatial-memory coverage. Build a scoreboard board, a /rtp health command, or a Prometheus / InfluxDB exporter with zero core changes. See Metrics and PlaceholderAPI (the same numbers are exposed as %rtp_*% placeholders for no-code consumers).
Two engine services keep your addon platform-neutral and reload-aware:
-
ConfigParser- register your own YAML file; it is created on first boot in the RTP data folder, reloads on/rtp reload, and never needs manual pasting. -
RTP.scheduler- schedule delayed / repeating / async work through one SPI that the platform adapter maps to the right thread (region thread on Folia, async pool, server-thread executor on Fabric/NeoForge). Never use rawThread/Executorsin addon code - that bypasses Folia region ownership and the shutdown drain.
See Addon development for both.
When LeafRTP runs in network mode behind a proxy (Velocity / BungeeCord), it maintains a shared view of every backend through the NetworkTransport SPI. An addon can ride that same transport to offer destinations on other servers - for example a warp menu whose rows are remote backends, or a /rtp that scatters you onto a different survival shard.
The whole flow reuses primitives that already exist; an addon adds layout and two calls, not a new transport:
| Step | Existing seam |
|---|---|
| Discover online backends and their regions |
NetworkTransport.readSnapshot() -> NetworkSnapshot (per-backend BackendHeartbeat) |
| Decorate a remote row (player count, TPS, "region ready") | fields already carried in the heartbeat snapshot |
| Reserve a coordinate on the remote backend |
NetworkTransport.claim(serverId, playerId, ttl, Optional.of(regionKey)) -> ReservationToken
|
| Route the player and re-attach the region on arrival | the backend's join trigger redeems the token and runs rtp region=<server>:<region>
|
The wire form /rtp region=<server>:<region> and the region-carrying claim(...) overload are already shipped, so "RTP onto another server" is a presentation feature on top of the existing transport.
!!! warning "Respect the transport threading contract"
Every NetworkTransport future completes on a transport-owned executor. You must hop back to the host scheduler (RTP.scheduler) before touching world or player state (S-005). Treat a missing reservation as "no reservation, fall through to local routing", never as a fatal error (S-004).
!!! note "Network rows are proxy-gated"
Remote destinations only exist when a NetworkTransport binding (Redis / SQL / in-memory) is installed. With no binding, degrade to local targets - exactly as the GUI addon falls back to the classic teleport when no renderer is registered. Never silently no-op.
The proxy SPI types (NetworkTransport, NetworkSnapshot, BackendSelector, ReservationToken) live in rtp-proxy-common, not in rtp-api / rtp-core. See the network.yml page for the admin side and ADR-036 for the design.
- New to building addons? Start at Addon development, then Example addon.
- Need the loader /
ServiceLoadermechanics? Addon loading. - Want the full hook catalog with code? Hooks and verifiers.
- Building UI? Building a menu.
- Just want a behavior without code? Common RTP recipes.