-
Notifications
You must be signed in to change notification settings - Fork 10
Hooks and Verifiers
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.
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.
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 eight 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).
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);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)));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));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));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.
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; });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.
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.
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. |
- Addon development - landing page and happy path.
- Example addon - a worked addon using the verifier and config seams.
-
docs/dev/EXTERNAL_HOOKS.md- the authoritative, exhaustive hook reference.