-
Notifications
You must be signed in to change notification settings - Fork 10
Addon Development
This page is the landing page for addon and integration developers who want to extend or drive LeafRTP from their own plugin or mod. If you are a server administrator looking for configs, commands, or permissions, start at Home instead - this page does not cover operator setup.
By the end of this page you will have a working, platform-agnostic addon that registers a custom region shape, and you will know which platform-neutral seam to reach for when you want to do more (config, safety hooks, teleport-lifecycle callbacks, scheduling, sub-commands).
Legacy random-teleport plugins do their work on the main thread, on the hot path: a /rtp click picks a coordinate, loads the chunk synchronously, checks if it is safe, and - if not - rerolls and loads another, with nothing bounding the loop. That design has no room for a stable extension API, because every hook you add runs inside the lag spike.
LeafRTP is structurally different, and that difference is what makes the addon API safe to build against:
-
Off-tick, pre-warmed queue. Destinations are generated and safety-verified in a background queue before anyone runs
/rtp. Your verifier, config, and lifecycle hooks run there - not on the tick that teleports the player - so a downstream plugin cannot stall the server's TPS. -
Native regional-threading (Folia) compatibility. All scheduling is routed through the
RTPSchedulerSPI (RTP.scheduler/RTPRunnable), which lands work on the correct thread per platform (main thread on Spigot/Paper, the owning entity's scheduler or a region thread on Folia, the server-thread executor on Fabric/NeoForge). Your addon inherits Folia-correct, region-safe scheduling for free without importing a single platform scheduler API. -
2D-to-1D Archimedean-spiral coordinate mapping. Region shapes map a 1D index onto a bounded 2D spiral (
$r = a + b\theta$ ), which is what lets the selector step over known-bad ground learned in persistent spatial memory instead of rerolling. A custom shape plugs into this same bounded contract - so it benefits from the same learning and stays within the same performance envelope. - Persistent spatial memory. Failed sectors are remembered per region, so verification cost amortises across teleports. Your verifier contributes to that memory rather than re-paying its cost on every attempt.
The upshot: you write behavior, and the engine guarantees when and where it runs.
!!! note "One module, every platform"
Most addons need no platform-specific code. You compile one module against rtp-api (and rtp-core for core types), and the same jar loads unchanged on Spigot, Paper, Folia, Fabric, and NeoForge. You do not write a Bukkit plugin.yml, a Fabric ModInitializer, or a NeoForge @Mod entry point. The reference is addons/RTP_ExampleAddon - three Java files, zero org.bukkit.* imports.
This is the shortest path from nothing to a loaded addon that registers a custom region shape named BIGSQUARE.
rtp-api carries the RTPAddon SPI; rtp-core carries the RTP facade and the Shape hierarchy. Both are provided by LeafRTP at runtime, so depend on them as compileOnly / provided.
=== "Gradle"
```gradle
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
dependencies {
compileOnly 'com.github.DailyStruggle.RTP:rtp-api:3.0.1' // RTPAddon SPI
compileOnly 'com.github.DailyStruggle.RTP:rtp-core:3.0.1' // RTP.addShape, Shape, built-in shapes
}
```
=== "Maven"
```xml
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.DailyStruggle.RTP</groupId>
<artifactId>rtp-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.DailyStruggle.RTP</groupId>
<artifactId>rtp-core</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
```
!!! tip "Picking a version"
Use a release tag (e.g. 3.0.1), a branch (V3-SNAPSHOT), or a commit hash for the version. See docs/dev/PUBLISHING.md for the publishing setup and the Maven Central path.
Create exactly one resource file so LeafRTP can discover your addon on every platform:
src/main/resources/META-INF/services/io.github.dailystruggle.rtp.api.addon.RTPAddon
containing a single line naming your implementation class:
com.example.myrtpaddon.MyRtpAddon
onLoad() runs once, after rtp-core has finished initialising, on an RTP task thread. That is the moment to register your shape. The simplest custom shape is a re-configured clone of a built-in one, registered under a new name so operators can select it exactly like any built-in shape.
package com.example.myrtpaddon;
import io.github.dailystruggle.rtp.api.addon.RTPAddon;
import io.github.dailystruggle.rtp.common.RTP;
import io.github.dailystruggle.rtp.common.selection.region.selectors.memory.shapes.Square;
import io.github.dailystruggle.rtp.common.selection.region.selectors.memory.shapes.enums.GenericMemoryShapeParams;
import java.util.logging.Level;
public final class MyRtpAddon implements RTPAddon {
@Override
public void onLoad() {
// A 4096-radius square ring, registered under a new name.
Square bigSquare = new Square("BIGSQUARE");
bigSquare.set(GenericMemoryShapeParams.radius, 4096);
bigSquare.set(GenericMemoryShapeParams.centerRadius, 512);
RTP.addShape(bigSquare);
RTP.log(Level.INFO, "[MyRtpAddon] registered shape BIGSQUARE");
}
@Override
public void onUnload() {
// Release tasks / chunk tickets / DB writes here. A pure shape needs no teardown.
}
}Build it, drop the jar on LeafRTP's classpath (see docs/dev/ADDON_LOADING.md), and BIGSQUARE is now selectable wherever a built-in shape is - on every platform. That is the entire addon.
!!! warning "Always clean up in onUnload()"
If your addon allocates anything that outlives a single call - scheduled tasks via RTP.scheduler, chunk tickets, open database handles - you must release it in onUnload(). LeafRTP calls onUnload() on /rtp reload and shutdown; a leaked chunk ticket is an S-002 violation (permanently force-loaded chunk).
When your addon needs more than a shape, reach for the platform-neutral seam below rather than talking to the server directly. Each one is the abstraction that keeps your addon cross-platform and off the hot path.
| You want to... | Use this (platform-neutral) | Do not touch |
|---|---|---|
| Be discovered and loaded on every platform |
RTPAddon + the META-INF/services descriptor (ServiceLoader) |
Bukkit plugin.yml, Fabric fabric.mod.json, NeoForge @Mod
|
| Schedule async / delayed / repeating / region-correct work |
RTP.scheduler (the RTPScheduler SPI) and RTPRunnable.schedule()
|
Bukkit.getScheduler(), Folia schedulers, raw Thread / Executors
|
| Add or veto a teleport destination |
RTPAPI.hooks().verifiers().register(...) (claim/biome/distance verifier seam, ADR-026) |
Direct claim-plugin calls in the hot path |
| React to a completed teleport | TeleportPipelineTask.teleportPostActions |
Bukkit PostTeleportEvent listeners |
Read/write your own config + honor /rtp reload
|
RTP.configs.putParser(...), Configs.onReload(...)
|
Platform config loaders |
| Register a custom shape or vertical adjustor |
RTP.addShape(...) / the adjustor registry |
Platform-specific anything |
Add a /rtp sub-command |
RTP.baseCommand.addSubCommand(...) (commands-api) |
A separate Bukkit command |
| Resolve players / worlds / locations |
RTP.serverAccessor (RTPServerAccessor) + RTPPlayer / RTPWorld / RTPLocation wrappers |
org.bukkit.*, net.minecraft.*
|
| Get a region by name | RTP.selectionAPI.getRegion("name") |
- |
| Log | RTP.log(...) |
Bukkit.getLogger(), System.out
|
!!! warning "Verifiers run asynchronously"
A verifier registered via RTPAPI.hooks().verifiers().register(coords -> ...) runs on the off-tick generation queue. It must not block, perform main-thread chunk I/O, or swallow exceptions (S-004 / S-005). Return your verdict; never silently return on failure.
The verifier seam below vetoes any candidate inside a hypothetical protected area - the same pattern the twelve bundled claim integrations use:
import io.github.dailystruggle.rtp.api.RTPAPI;
RTPAPI.hooks().verifiers().register(coords -> {
// runs off-tick, in the pre-generation queue; must not block.
return !myClaimService.isProtected(coords.x(), coords.z());
});To observe completed teleports without a Bukkit listener:
import io.github.dailystruggle.rtp.common.tasks.teleport.TeleportPipelineTask;
TeleportPipelineTask.teleportPostActions.add(task -> {
// fires after a successful teleport, on the correct platform thread.
});If your plugin targets only the Bukkit family (Spigot / Paper / Folia) and you would rather use the platform's event bus than the cross-platform SPI, LeafRTP fires custom Bukkit events:
| Event | Fires when |
|---|---|
PlayerQueuePushEvent |
a player begins waiting for a new location to generate |
PlayerQueuePopEvent |
a player is selected for teleportation |
PreSetupTeleportEvent / PostSetupTeleportEvent
|
around selection tasks |
PreLoadChunksEvent / PostLoadChunksEvent
|
around chunk preload |
PreTeleportEvent / PostTeleportEvent
|
around the teleport itself |
RandomSelectQueueEvent |
the plugin prepares a location asynchronously |
TeleportCancelEvent |
a player cancels teleportation |
TeleportCommandFailEvent / TeleportCommandSuccessEvent
|
command outcome |
public final class OnRandomTeleport implements Listener {
@EventHandler(priority = EventPriority.HIGHEST)
public void onRandomTeleport(PostTeleportEvent event) {
// runs last, after other listeners may have adjusted the destination
}
}Register it the usual way: getServer().getPluginManager().registerEvents(new OnRandomTeleport(), this);
!!! note "Prefer the cross-platform SPI"
Bukkit events only fire on the Bukkit family. If you want one jar that also runs on Fabric and NeoForge, use TeleportPipelineTask.teleportPostActions and the verifier seam instead.
-
docs/dev/CONCEPTS.md- the queue, shapes, vertical adjustors, spiral math, and teleport pipeline your addon plugs into. -
docs/dev/ARCHITECTURE.md- what lives inrtp-apivsrtp-corevs platform adapters, and the import boundary you must respect. -
docs/dev/DESIGN.md- the bounded execution model, concurrency guarantees, and fault-tolerance contracts. -
docs/dev/ADDON_LOADING.md- how LeafRTP discovers, loads, and unloads addons per platform. -
docs/dev/EXTERNAL_HOOKS.md- the hook catalog and theRTP.addShape/RTP.addVerticalAdjustorfactory entry points.
For building LeafRTP itself from source, see Compiling and Editing.