ADR-056 - Pluggable bare-/rtp root action¶
Status: Accepted Date: 2026-05-30
Context¶
Server owners want a bare /rtp (no arguments) to open a destination menu (e.g. the GUI addon) instead of immediately teleporting, while keeping every subcommand (/rtp admin, /rtp config, ...) working and keeping the classic teleport reachable. Competing plugins (BetterRTP + its companion "Rtp GUI") achieve this by having a separate GUI plugin claim /rtp and shell out to the underlying teleport command; the menu is a pure presentation layer.
RTP already exposes a stable, addon-facing surface (RTPAPI, RTPHooks) and a single platform-neutral command tree (commands-api) dispatched identically on Bukkit-family adapters and via the Brigadier bridge. A bare /rtp runs the command root's own onCommand; subcommands resolve to child nodes before that no-arg branch. This ADR records the design (Rule D-005) for letting an addon override only the bare-root behaviour.
Scope note: renaming/aliasing the command label itself (rtp -> tpr) is a separate, deferred task and is not covered here.
Decision¶
Introduce an optional, single-binding root-action hook on the same replaceable-provider pattern as the biome/anvil/PvP seams:
rtp-apiSPI.RootActionRegistry(single-bindingbind/current/clear, mirroringPvPCombatStateRegistry) with a functionalAction#run(UUID, Consumer<String>)returningboolean handled, reachable viaRTPAPI.hooks().rootAction().- No new configuration. The behaviour is supplied entirely by the registered action; there is no
default-actionenum or config map. When no action is bound (current() == null) a bare/rtpperforms the classic random teleport, so stock behaviour is unchanged. - Core dispatch (
rtp-core).RTPCmd.onCommand(...)consults the registry only on the!hasSubCommand(bare-root) branch, before the teleport-specific guards. A handled action (return true) suppresses the classic teleport and bypasses cooldown /processingPlayersregistration - exactly as a subcommand does, because a menu open is not a teleport. An action that returnsfalsedefers to the classic teleport. - Platform-neutral. Because the lookup lives in the shared command root, it resolves identically for the Bukkit executor and the Brigadier bridge (Fabric, planned Velocity); no platform-adapter change is required.
- Reusing classic behaviour. An action that wants the original teleport for some inputs/player-state can return
false, or callRTPAPI.teleport(uuid, target)itself (which re-applies every safety/permission check) and returntrue.
Alternatives Considered¶
| Alternative | Why Rejected |
|---|---|
default-action enum + config map (label -> action-id) |
The owner explicitly rejected new config; the registered action alone determines behaviour. Label renaming is a separate, deferred concern. |
| Last-registered-wins multi-action registry | Unnecessary now (addon-only consumer); a single override slot is deterministic and load-order-independent. |
Addon claims /rtp via Bukkit CommandMap (BetterRTP-GUI approach) |
Fights registration order with core's plugin.yml command, is Bukkit-only, and risks reload leaks; the in-tree hook is platform-neutral and safe. |
Hard-code a built-in menu action |
The menu/GUI is addon-supplied; core stays ignorant of GUI specifics. |
Consequences¶
- Positive: One small, replaceable seam consistent with the existing hook architecture; subcommands provably untouched (resolved before the branch); platform-neutral; off by default so no behaviour change; safe failure mode.
- Negative / Trade-offs: Adds one read on the bare-
/rtphot path (a single volatile-slot lookup, negligible). The menu itself is addon-supplied - a bare install without an addon sees no behaviour change. An eventual addon-loader / built-in menu action can layer on later without changing this SPI.
References¶
- ADR-026 - external hook API surface, EXTERNAL_HOOKS.md
PvPCombatStateRegistry/AnvilPrefilterRegistry(the single-binding precedent), REQ-RTP-S-004, REQ-API-F-006- Code:
rtp-api/.../hooks/RootActionRegistry.java;rtp-core/.../common/hooks/DefaultRTPHooks.java;rtp-core/.../common/commands/RTPCmd.java - Test:
rtp-core/.../common/commands/ReqApiF006RootActionTest.java