Loading an Addon¶
How RTP discovers, loads, and unloads a platform-agnostic addon. This is the deployment
counterpart to the authoring guide in addons/LeafRTPCountdownAddon/README.md.
The mechanism is defined by ADR-057.
Since ADR-057, an addon is no longer 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 RTP can find it viajava.util.ServiceLoader.
This works identically on Bukkit / Spigot / Paper / Folia, Fabric, and (for the RTPAPI-only
surface) proxy JVMs, because ServiceLoader is pure JDK and requires no Bukkit plugin loader.
What RTP 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"; }
}
ServiceLoader instantiates the class reflectively, so it must have a public no-argument
constructor.
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. See the reference addon's descriptor at
addons/LeafRTPCountdownAddon/src/main/resources/META-INF/services/io.github.dailystruggle.rtp.api.addon.RTPAddon.
Getting the addon onto RTP's classpath¶
RTP discovers addons with ServiceLoader over the classloader that loaded rtp-core
(AddonRegistry.discover()), so the addon jar must be visible to that classloader. Pick the
path that matches your platform:
| Platform | How to load |
|---|---|
| Any backend platform | Drop the addon jar into the RTP-owned <pluginDir>/addons/ folder (plugins/RTP/addons/ on Bukkit/Spigot/Paper/Folia; the platform's RTP config directory on Fabric/NeoForge). RTP scans this folder at startup, loads each jar in a child classloader (parent = RTP's own classloader), and discovers the addon via ServiceLoader. This is the recommended path: the jar does not go in plugins/, where the server's plugin loader would reject it for lacking a plugin.yml. |
| Bukkit / Spigot / Paper / Folia | Alternatively, place the addon jar on the same classloader as the RTP plugin (bundled/shaded into the RTP distribution), or use a thin Bukkit JavaPlugin shim that calls RTP.addons.register(new MyAddon()) for back-compat. |
| Fabric | Ship the addon classes inside (or alongside) the RTP 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 RTP proxy plugin's classpath. Addons touching RTP.configs or world state are backend-only. |
The <pluginDir>/addons/ folder¶
At startup (after core init settles) RTP calls AddonRegistry.discoverFromDirectory(new File(pluginDir, "addons"))
in addition to the classpath discover(). 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.
ServiceLoader enumeration then registers every discovered RTPAddon. A missing or empty folder
is a silent no-op, so the folder is purely optional. The jar must still contain a valid
META-INF/services/io.github.dailystruggle.rtp.api.addon.RTPAddon descriptor (see above); a plain
plugin jar with only a plugin.yml is ignored.
Programmatic registration¶
A platform adapter (or a back-compat shim) that has 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¶
- 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.
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.
- No 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 a [ADDONS] warning. If you see neither line,
the jar is not on RTP's classpath or the META-INF/services descriptor is missing/misnamed.
See also¶
addons/LeafRTPCountdownAddon/README.md: authoring guide (the four interfaces an addon uses).- ADR-057: the platform-agnostic addon SPI decision.
EXTERNAL_HOOKS.md: safety verifier / economy / placeholder hooks (ADR-026).FOR_ADDON_DEVELOPERS.md: addon-developer entry point.