Skip to content

Example Addon

Leaf26 edited this page Jun 22, 2026 · 2 revisions

addons/LeafRTPCountdownAddon is the canonical reference addon. It is platform-agnostic: it implements the RTPAddon SPI, is discovered via ServiceLoader, and runs unchanged on Bukkit / Spigot / Paper / Folia, Fabric, and NeoForge - with zero org.bukkit.* imports and no plugin loader. It deliberately demonstrates the four interfaces most addons need without any real business logic, so it doubles as a working template.

Read it end-to-end before writing your own. This page summarises the four interfaces it exercises.


Project layout

File Role
build.gradle compileOnly for rtp-api (the RTPAddon SPI) and rtp-core (Configs / ConfigParser / TeleportPipelineTask). No spigot-api.
META-INF/services/io.github.dailystruggle.rtp.api.addon.RTPAddon The ServiceLoader descriptor naming the RTPAddon implementation. This is how LeafRTP discovers the addon on every platform.
countdown.yml Example YAML config the addon ships with. Keys match the enum constants in CountdownKeys.
CountdownKeys.java Typed enum of the YAML keys. ConfigParser<E> is generic over this enum.
RTPCountdownAddon.java The RTPAddon implementation. Wires everything up in onLoad().
ExampleCountdownHooks.java Two platform-agnostic teleport countdowns built on RTP.scheduler / RTP.serverAccessor - no org.bukkit.*.

The four interfaces

1. Configuration - ConfigParser<E extends Enum<E>>

Create an enum (CountdownKeys) whose constants name your YAML keys, then register a ConfigParser against LeafRTP's Configs registry so that /rtp reload picks up your file and /rtp config tab-completes it:

RTP.configs.putParser(
    new ConfigParser<>(
        CountdownKeys.class,
        "example",            // YAML basename => countdown.yml
        "1.0",                // schema version
        RTP.serverAccessor.getPluginDirectory(),
        null,
        RTP.configs.fileDatabase,
        this.getClass().getClassLoader()));

2. Safety contribution - RTPAPI.hooks().verifiers()

Contribute a predicate the pipeline evaluates asynchronously for every candidate location:

RTPAPI.hooks().verifiers().register(coords -> myCheckReturnsTrueIfSafe(coords));

!!! warning "Rules of engagement" Return quickly (the call happens on a worker thread). No synchronous chunk I/O on the main thread (S-005). Do not swallow failures silently (S-004) - log with RTP.log(Level.WARNING, msg, t). return true to accept the location, false to reject it (LeafRTP rerolls). For checks that await a database/network call, use registerAsync(...). See Hooks and verifiers.

3. Events - platform-agnostic post-action runnables

Instead of Bukkit events, observe the teleport lifecycle through the platform-agnostic runnable lists on TeleportPipelineTask (and RTPTeleportCancel). These fire on every platform:

TeleportPipelineTask.teleportPostActions.add(task -> onPostTeleport(task));

Available lists: setupPreActions / setupPostActions, loadPreActions / loadPostActions, teleportPreActions / teleportPostActions, cleanupPreActions / cleanupPostActions, and RTPTeleportCancel.postActions.

!!! note "These callbacks may run off the main thread" Bounce any platform-facing work onto the appropriate thread via RTP.scheduler.

4. Reload hook - Configs.onReload(Runnable)

When operators run /rtp reload, LeafRTP drops its parsers and replays the hook list. Re-register your parser inside the callback so your config survives the reload:

Configs.onReload(() -> RTP.configs.putParser(buildParser()));

Bonus: binding a combat checker

LeafRTP's optional combat gate asks one question - "is this player in combat right now?" - through the single-binding PvPCombatStateRegistry. If your combat plugin is not one of the bundled integrations, bind your own authority from onLoad():

// Single-binding: the last bind wins and replaces the native tracker (and any bundled adapter).
RTPAPI.hooks().pvpCombatState().bind(uuid -> myCombatPlugin.isTagged(uuid));

Provider is a functional interface - boolean isInCombat(UUID player). true = in combat (teleport refused/delayed per safety.yml), false = free to teleport. Offline/unknown UUIDs should return false. Call RTPAPI.hooks().pvpCombatState().clear() from onUnload() so your provider does not outlive your addon.


Build

From the repository root:

.\gradlew :addons:LeafRTPCountdownAddon:build

The jar lands in addons/LeafRTPCountdownAddon/build/libs/. For where to put it and how to confirm it loaded, see Addon loading.


Safety checklist for your own addon

  • A META-INF/services/io.github.dailystruggle.rtp.api.addon.RTPAddon entry names your RTPAddon.
  • onLoad() registers a parser and a Configs.onReload callback for it.
  • onUnload() releases anything onLoad() allocated.
  • Long-running work is scheduled via RTP.scheduler, never run on the main thread.
  • Any allocated chunk ticket or pipeline task is released on every exit path (see MemoryTracker).
  • Failures go through RTP.log(Level.WARNING, msg, t) - never printStackTrace().

See also

Clone this wiki locally