Skip to content

Extending RTP

Leaf26 edited this page Jun 17, 2026 · 1 revision

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.

The two tiers

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.

The extension surfaces at a glance

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.


Menus and renderers

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 a MenuRenderer under 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.

Commands and triggers

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

Verifiers, shapes, and vertical adjustors

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.

Effects (the warmup window)

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.

Maps

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.

Metrics

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

Config and scheduling (the plumbing every addon reuses)

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 raw Thread / Executors in addon code - that bypasses Folia region ownership and the shutdown drain.

See Addon development for both.


Going cross-server: piggybacking the network transport

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.


Where to go next

Clone this wiki locally