Skip to content

Addon Development

Leaf26 edited this page Jun 18, 2026 · 2 revisions

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


Why LeafRTP has an addon API at all (the "why")

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 RTPScheduler SPI (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.


Happy path: a working addon in three steps

This is the shortest path from nothing to a loaded addon that registers a custom region shape named BIGSQUARE.

1. Depend on LeafRTP

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.

2. Register your addon for ServiceLoader

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

3. Implement RTPAddon and register a shape

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


The integration seams

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.

A more complete example

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

Bukkit events (Bukkit-family plugins only)

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.


Recommended reading order

  1. docs/dev/CONCEPTS.md - the queue, shapes, vertical adjustors, spiral math, and teleport pipeline your addon plugs into.
  2. docs/dev/ARCHITECTURE.md - what lives in rtp-api vs rtp-core vs platform adapters, and the import boundary you must respect.
  3. docs/dev/DESIGN.md - the bounded execution model, concurrency guarantees, and fault-tolerance contracts.
  4. docs/dev/ADDON_LOADING.md - how LeafRTP discovers, loads, and unloads addons per platform.
  5. docs/dev/EXTERNAL_HOOKS.md - the hook catalog and the RTP.addShape / RTP.addVerticalAdjustor factory entry points.

For building LeafRTP itself from source, see Compiling and Editing.

Clone this wiki locally