-
Notifications
You must be signed in to change notification settings - Fork 10
Addon Loading
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:
- contains a class implementing
io.github.dailystruggle.rtp.api.addon.RTPAddon, and - declares that class in a
META-INF/servicesdescriptor so LeafRTP can find it viajava.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).
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.
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.
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.
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.
-
Discovery + load. After
rtp-corefinishes initialising, a startup task runsRTP.addons.discover(), thenRTP.addons.discoverFromDirectory(<pluginDir>/addons), thenRTP.addons.loadAll().loadAll()callsonLoad()exactly once per addon. Loading is deferred to a startup task so theRTPAPIdelegates installed by the platform adapter (serverAccessor,hooks, the teleport delegate) are guaranteed non-null insideonLoad(). -
Failure isolation. Each
onLoad()/onUnload()is wrapped; a throwing addon is logged viaRTP.log(Level.WARNING, ...)and cannot abort the load/unload of its peers. -
Unload. On
RTP.stop()(server/plugin shutdown or/rtp reloadteardown),RTP.addons.unloadAll()callsonUnload()on every addon and clears the registry. Release anythingonLoad()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.
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.
- Addon development - the authoring landing page and happy path.
- Example addon - a walkthrough of the four interfaces most addons use.
- Hooks and verifiers - the behavior-modification SPI catalog.