Skip to content

Addon Loading

Leaf26 edited this page Jun 17, 2026 · 3 revisions

How LeafRTP discovers, loads, and unloads a platform-agnostic addon. This is the deployment counterpart to Addon development (how to write one).

An addon is not a Bukkit JavaPlugin with a plugin.yml. It is a plain jar that:

  1. contains a class implementing io.github.dailystruggle.rtp.api.addon.RTPAddon, and
  2. declares that class in a META-INF/services descriptor so LeafRTP can find it via java.util.ServiceLoader.

Because ServiceLoader is pure JDK and needs no plugin loader, the same jar loads identically on Bukkit / Spigot / Paper / Folia, Fabric, and NeoForge (and, for the RTPAPI-only surface, proxy JVMs).


What LeafRTP looks for

1. The RTPAddon implementation

package com.example.myaddon;

import io.github.dailystruggle.rtp.api.addon.RTPAddon;

public final class MyAddon implements RTPAddon {
    public MyAddon() {}            // public no-arg constructor is REQUIRED (ServiceLoader)

    @Override public void onLoad()   { /* register parsers, verifiers, post-actions */ }
    @Override public void onUnload() { /* release tickets, cancel tasks, flush state */ }
    @Override public String name()   { return "MyAddon"; }
}

!!! warning "Public no-arg constructor is mandatory" ServiceLoader instantiates your class reflectively, so it must have a public no-argument constructor. A missing or private constructor means silent non-discovery.

2. The META-INF/services descriptor

The jar must contain a file named exactly:

META-INF/services/io.github.dailystruggle.rtp.api.addon.RTPAddon

whose contents are the fully-qualified name(s) of your implementation, one per line:

com.example.myaddon.MyAddon

In a Gradle/Maven project this file lives at src/main/resources/META-INF/services/io.github.dailystruggle.rtp.api.addon.RTPAddon and is packaged into the jar automatically.


Getting the addon onto LeafRTP's classpath

LeafRTP discovers addons with ServiceLoader over the classloader that loaded rtp-core, so the addon jar must be visible to that classloader. Pick the path that matches your platform:

Platform How to load
Any backend platform (recommended) Drop the addon jar into the LeafRTP-owned <pluginDir>/addons/ folder (plugins/RTP/addons/ on Bukkit / Spigot / Paper / Folia; the platform's RTP config directory on Fabric / NeoForge). LeafRTP scans this folder at startup, loads each jar in a child classloader (parent = LeafRTP's own classloader), and discovers the addon via ServiceLoader.
Bukkit / Spigot / Paper / Folia The server's plugins/ folder also works: a bare addon jar there is on a classloader LeafRTP can see, so ServiceLoader still discovers it. The server's own plugin loader simply ignores it (no plugin.yml), which is harmless. Use whichever location you find tidier.
Bukkit / Spigot / Paper / Folia Alternatively, bundle/shade the addon into the LeafRTP distribution, or use a thin Bukkit JavaPlugin shim that calls RTP.addons.register(new MyAddon()).
Fabric / NeoForge Ship the addon classes inside (or alongside) the LeafRTP mod jar so they share the mod classloader, or have a mod entrypoint call RTP.addons.register(new MyAddon()).
Proxy (Velocity / BungeeCord) Only addons that use the RTPAPI query/teleport surface are supported proxy-side: place the jar on the LeafRTP proxy plugin's classpath. Addons touching RTP.configs or world state are backend-only.

!!! tip "The <pluginDir>/addons/ folder" Every *.jar directly inside that folder is added to a child URLClassLoader whose parent is the classloader that loaded rtp-core, so the addon and the rtp-api/rtp-core types it references resolve to the same classes the running plugin uses. A missing or empty folder is a silent no-op, so the folder is purely optional - an addon dropped in the server's plugins/ folder is discovered too. A plain plugin jar with only a plugin.yml (and no META-INF/services descriptor) is ignored.

Programmatic registration

A platform adapter (or a back-compat shim) that already instantiated an addon can register it directly instead of relying on ServiceLoader:

RTP.addons.register(new MyAddon());

register(...) ignores null and duplicate instances. If core has already finished its load pass, a late registration is loaded eagerly so it is never silently dropped.


Lifecycle and timing

  1. Discovery + load. After rtp-core finishes initialising, a startup task runs RTP.addons.discover(), then RTP.addons.discoverFromDirectory(<pluginDir>/addons), then RTP.addons.loadAll(). loadAll() calls onLoad() exactly once per addon. Loading is deferred to a startup task so the RTPAPI delegates installed by the platform adapter (serverAccessor, hooks, the teleport delegate) are guaranteed non-null inside onLoad().
  2. Failure isolation. Each onLoad() / onUnload() is wrapped; a throwing addon is logged via RTP.log(Level.WARNING, ...) and cannot abort the load/unload of its peers.
  3. Unload. On RTP.stop() (server/plugin shutdown or /rtp reload teardown), RTP.addons.unloadAll() calls onUnload() on every addon and clears the registry. Release anything onLoad() allocated here.

!!! warning "Threading / safety rules for onLoad()" onLoad() runs on an RTP task thread, not the main thread. Inside it (and in any callback you register): do not block; never do synchronous chunk I/O on the main thread (S-005); never silently swallow a teleport failure (S-004) - log with RTP.log(Level.WARNING, msg, t); schedule platform-facing work through RTP.scheduler.


Verifying the addon loaded

On a successful load you will see in the server log:

[ADDONS] loaded addon: MyAddon

A discovery or instantiation failure surfaces as an [ADDONS] warning. If you see neither line, the jar is not on LeafRTP's classpath or the META-INF/services descriptor is missing or misnamed.


See also

Clone this wiki locally