ADR-026 — Unified External-Hook API Surface (rtp-api/hooks)¶
Status: Accepted Date: 2026-05-01 Relates to: ADR-019 (claim integrations folded into plugin), ADR-013 (addons as external Gradle projects), REQ-API-F-001…F-003, REQ-RTP-S-003, REQ-RTP-S-006.
Context¶
RTP exposes several seams where third-party plugins or bundled soft-depend integrations modify its behavior:
- Region/location verifiers —
GlobalRegionVerifiers.addGlobalRegionVerifier(...)(rtp-core), used by every claim-plugin checker (Factions, GriefDefender, GriefPrevention, Lands, HuskTowns, RedProtect, TownyAdvanced, WorldGuard) and by example/addon code. - Economy —
RTP.economyfield bound byVaultCheckerfromrtp-plugin. - Placeholders —
PAPI_expansion(PlaceholderAPI) attaches resolvers to RTP state. - World border —
ChunkyBorderCheckerconsulted by shape/border code. - Anvil pre-filter cache — reflectively discovered via
Class.forName("io.github.dailystruggle.rtp.anvil.AnvilRegionByteCache")inScanTask. - Effects —
effects-apiextension points for particle/potion side effects (covered separately, see EXTERNAL_HOOKS.md → Effects and theeffects-apimodule docs).
Today, integrations couple directly to symbols in rtp-core (GlobalRegionVerifiers, RTP.economy) or rely on reflection (AnvilRegionByteCache). This violates the architecture rule that addons depend on rtp-api only (see ARCHITECTURE.md and ADR-013), and it leaves the surface undocumented for third-party authors.
Decision¶
- Add a single API package
io.github.dailystruggle.rtp.api.hooksinrtp-apicontaining a thin facadeRTPHooksexposed via static accessors onRTPAPI(RTPAPI.hooks()). The facade exposes register/unregister/list operations for each behavior-modification point listed above (except effects, which retain their own module). New seams in this ADR: RegionVerifierRegistry(sync + async predicates overRTPCoords).EconomyProviderRegistry(single-binding,RTPEconomy).PlaceholderProviderRegistry(named string resolvers(player, key) → String).WorldBorderProviderRegistry(predicate(world, x, z) → boolean inside).AnvilPrefilterRegistry(single-binding SPI replacing the reflective lookup inScanTask).- Backed by volatile delegate fields populated by
rtp-coreduringonEnable, matching the establishedRTPAPIpattern (shapeAdder,vertAdder,biomeProvider). Calling any registry method before core is loaded throwsIllegalStateException(REQ-RTP-S-006); never silently no-ops. - Backward compatibility shall be preserved.
GlobalRegionVerifiersretains its public static methods; they delegate into the new registry. Addons compiled against the old API continue to link and run unchanged.RTP.economycontinues to be the read path insidertp-core; the newEconomyProviderRegistryis the write path that integrations shall use going forward. rtp-pluginsoft-depend checkers shall be refactored to register throughRTPHooksinstead of callingrtp-coresymbols. Addons underaddons/(notablyRTP_ExampleAddon) shall be updated to demonstrate the new API.- Reflective lookup of
AnvilRegionByteCacheinScanTaskshall be replaced with a registry call. Thertp-anvilmodule registers itself viaRTPHooks.anvilPrefilter().bind(...)during its initialisation. The reflective code remains as a fallback for one release cycle, gated on registry not being bound, then removed. - A single canonical document
docs/dev/EXTERNAL_HOOKS.mdshall list every hook (file, API symbol, target plugin, behavior modified, threading rule, fallback), and shall be linked fromdocs/dev/INDEX.mdand.junie/AGENTS.mdRequired Reading.
Consequences¶
- Positive:
- Third-party integrations have a stable, discoverable,
rtp-api-only surface — nortp-coreimports required. - Reflection in
ScanTask(anvil discovery) is replaced by a typed SPI; failures become loud rather than silent. - Documentation invariant: every reflection/hook site in the codebase has a row in
EXTERNAL_HOOKS.md. New hooks shall be added there as part of the same change. - Test coverage gains: each registry has a
ReqApi*HookTestverifying registration, removal, ordering, and "absent target plugin" fallback. - Negative / Trade-offs:
- New public API surface in
rtp-apiis semver-locked. Mitigation: all registries accept functional interfaces only; no implementation classes leak through the API. - Two paths exist for region verifiers (legacy static + new registry) until the next major release. The legacy path is documented as
@deprecatedonly after at least one release in which both work; this ADR does not deprecate it. effects-apiis intentionally out of scope here; its hook surface is documented separately to avoid coupling two evolving subsystems in one ADR.
References¶
rtp-api/src/main/java/io/github/dailystruggle/rtp/api/hooks/(new)rtp-api/src/main/java/io/github/dailystruggle/rtp/api/RTPAPI.java(extended withhooks())rtp-core/src/main/java/io/github/dailystruggle/rtp/common/selection/region/GlobalRegionVerifiers.javartp-plugin/src/main/java/io/github/dailystruggle/rtp/bukkit/tools/softdepends/- ADR-019 (claim-plugin integrations folded into plugin)
- ADR-013 (addons as external Gradle projects)
docs/dev/EXTERNAL_HOOKS.md(new)- REQ-API-F-001/F-002/F-003, REQ-RTP-S-003, REQ-RTP-S-006 — see
docs/dev/TRACEABILITY.md.