Skip to content

Hooks and Verifiers

Leaf26 edited this page Jun 18, 2026 · 2 revisions

LeafRTP exposes a single, platform-neutral surface for modifying its behavior - vetoing destinations, charging money, exposing placeholders, overriding the world border, replacing the combat gate, replacing what a bare /rtp does, and supplying an arrival platform. This page is the catalog of those hooks. The decision behind it is ADR-026.

If you only want to react to teleports or register a custom shape, you do not need this page - see Addon development instead.


The one entry point

import io.github.dailystruggle.rtp.api.RTPAPI;
import io.github.dailystruggle.rtp.api.hooks.RTPHooks;

RTPHooks hooks = RTPAPI.hooks(); // throws IllegalStateException if core not yet loaded (S-006)

From RTPHooks you reach every behavior-modification registry. Call it from your addon's onLoad() (where core is guaranteed loaded), and undo single-binding hooks in onUnload().

!!! note "Single-binding vs multi-binding" Registries (verifiers, placeholders) accept many providers via register(...) / unregister(...). Single-binding slots (economy, world border, anvil pre-filter, PvP, root action, platform creator) hold one provider via bind(...) / clear(...), where the last bind wins. Bind a single-binding slot only when your plugin is genuinely the authority for it.


Hook catalog

1. Region verifiers - RTPHooks#verifiers()

Vetoes a candidate teleport location before the player is sent there. Invoked on every per-attempt verification pass. A throwing verifier is logged at WARNING and treated as false (location rejected) - LeafRTP never silently swallows failures (S-004). This is the seam the twelve bundled claim integrations use.

RTPAPI.hooks().verifiers().register(coords -> myCheckReturnsTrueIfSafe(coords));
RTPAPI.hooks().verifiers().registerAsync(coords -> myAsyncCheck(coords)); // returns CompletableFuture

!!! warning "Verifiers must not block" Sync verifiers run on the verification chain; async verifiers return a CompletableFuture and may do off-thread I/O but must not block a region/tick thread. No synchronous chunk I/O (S-005). return true to accept, false to reject (LeafRTP rerolls).

2. Economy - RTPHooks#economy()

Charges and refunds for /rtp invocations. Implementations must be thread-safe (may be called from the async generation pipeline). When no provider is bound, a platform no-op is used and /rtp cost configuration is silently treated as "free". The bundled Vault/EssentialsX integration binds here.

RTPAPI.hooks().economy().bind(myRTPEconomy);

3. Placeholders - RTPHooks#placeholders()

Names exposed by LeafRTP to PlaceholderAPI / chat plugins (%rtp_<key>%). Resolvers may be invoked from any thread and must not block server APIs.

RTPAPI.hooks().placeholders().register("my_metric",
    (uuid, key) -> Integer.toString(myCounter.get(uuid)));

4. World border - RTPHooks#worldBorder()

Constrains the candidate radius and per-attempt sampling to "inside the border". isInside must not block. The bundled Chunky/ChunkyBorder integration binds here; the platform-native WorldBorder is the fallback.

RTPAPI.hooks().worldBorder().bind((world, x, z) -> myBorder.contains(world, x, z));

5. Anvil pre-filter - RTPHooks#anvilPrefilter()

Lets region scanning skip whole chunks without loading them, by reading anvil/NBT data directly (ACCEPT / REJECT / UNKNOWN). Off-main-thread on Folia; implementations must be thread-safe. The rtp-anvil module is the built-in producer; no bound provider falls back to per-attempt chunk loads (slower but correct).

RTPAPI.hooks().anvilPrefilter().bind((world, cx, cz) -> myDecision(world, cx, cz));

6. PvP combat state - RTPHooks#pvpCombatState()

Replaces LeafRTP's native PvP tracker as the authority for the optional /rtp combat gate (refuse / delay / cancel a teleport for a combat-tagged player). The gate is off by default (safety.yml#pvpCheckEnabled). Called from the command thread and the teleport pipeline, so it must be thread-safe and non-blocking; a throwing provider is logged once and treated as "not in combat" (S-004) so a buggy integration never traps players.

RTPAPI.hooks().pvpCombatState().bind(uuid -> myCombatPlugin.isTagged(uuid));

!!! tip "You do not need a bundled *Checker" Third-party combat plugins bind their own provider directly - you do not have to wait for LeafRTP to ship an adapter. Provider is a functional interface (boolean isInCombat(UUID)), so a lambda or method reference is enough. Call clear() from onUnload() to restore the native tracker.

7. Bare-/rtp root action - RTPHooks#rootAction()

Replaces what a bare /rtp (no arguments) does - e.g. open an addon GUI instead of teleporting. Sub-commands (/rtp admin, ...) are never affected. Runs on the command thread; must be non-blocking with no synchronous chunk I/O (S-005). return true to handle the command (classic teleport suppressed); return false to defer to the classic teleport.

RTPAPI.hooks().rootAction().bind((uuid, feedback) -> { openMyMenu(uuid); return true; });

8. Arrival platform creator - RTPHooks#platformCreator()

Replaces LeafRTP's built-in emergency block disc with an addon-supplied arrival platform - a procedural pad, a lobby structure, a pasted schematic. Two-phase: prepare(at) runs off the region thread (do blocking I/O here) and createPlatform(at, prepared) runs on the region-owning thread (block writes only - no synchronous file/network/chunk I/O, S-005).

// Single-phase: nothing to pre-load. Runs on the region thread.
RTPAPI.hooks().platformCreator().bind(new PlatformCreator() {
  @Override public String creatorName() { return "MyLobbyPad"; }
  @Override public boolean createPlatform(RTPLocation at) {
    return myPadBuilder.build(at); // no blocking I/O here
  }
});

// Two-phase: do the blocking load in prepare(), consume it on the region thread.
RTPAPI.hooks().platformCreator().bind(new PlatformCreator() {
  @Override public String creatorName() { return "MyStructurePad"; }
  @Override public CompletableFuture<?> prepare(RTPLocation at) {
    return CompletableFuture.completedFuture(loadStructure(at)); // blocking I/O is fine here
  }
  @Override public boolean createPlatform(RTPLocation at, Object prepared) {
    if (prepared == null) return false;          // declined -> default platform
    return ((Structure) prepared).writeAt(at);   // region thread: block writes only
  }
});

!!! warning "Route all async work through RTP.scheduler" LeafRTP already invokes prepare() on the pipeline's load (non-region) thread, so do the blocking work inline and return a completed future - do not spin up your own executor or CompletableFuture.supplyAsync. All async work on a backend JVM must go through RTP.scheduler, never a raw thread pool.


Shape and vertical-adjustor factories (not in RTPHooks)

Registering a custom shape or vertical adjustor is a construction-time registration, not a behavior-modification seam, so it lives on the RTP facade rather than RTPHooks:

RTP.addShape(myShape);
RTP.addVerticalAdjustor(myAdjustor);

Deriving a custom shape needs the concrete rtp-core base classes (two-tier API model), which is why these are RTP.* calls. See Addon development for the shape happy path and Shapes for the geometry model.


When a target plugin is absent

In every case below, LeafRTP logs a single line and continues - it never silently swallows a failure (S-004):

Hook Target missing -> behavior
Region verifiers (claim plugins) Verifier simply not registered.
Economy Stays the platform no-op; /rtp treated as free.
Placeholders Not exported.
World border Falls back to platform World#getWorldBorder() + config radius.
Anvil pre-filter Falls back to per-attempt chunk loads.
PvP combat state Falls back to the native combat tracker.
Arrival platform creator Falls back to the region schematic path, then the emergency block disc.

See also

Clone this wiki locally