ADR-051 - Two-Tier API Model: Thin rtp-api Contract + rtp-core Extension API¶
Status: Accepted Date: 2026-05-29
Context¶
Developer-UX work (improving the experience of plugin authors who build against RTP) surfaced a recurring request: a developer should be able to derive a new shape from an existing one, the same way the project itself derives Polygon and the other built-ins from MemoryShape / Shape. The natural assumption was that this belongs in rtp-api, the module addon authors are told to compile against.
Investigation showed that the extensible selection types cannot be reduced to a thin contract:
Shape<E>andVerticalAdjustor<E>bothextend FactoryValue<E>.FactoryValuecouples to the configuration substrate (RtpYamlConfig) and toRTPfor logging and plugin-directory access.MemoryShape(~1200 lines) is bound to file/DB save-load, biome bookkeeping, and theFactoryregistry, which in turn depends onConfigParser(the whole config subsystem).
Forcing these into rtp-api would drag Factory, ConfigParser, and the configuration subsystem upward until rtp-api became rtp-core under a different name, violating REQ-API-NF-002 ("API interfaces shall not expose internal implementation specifics of rtp-core").
The key realization: there are two distinct kinds of "API" that had been conflated.
- A contract API - thin, stable, semver-pinned, dependency-light, publishable - for the common case (do an RTP, read hooks, by-world queries). This is
rtp-api. - An implementation/extension API - the platform-independent engine itself, with no Bukkit/Fabric imports - for authors who need to subclass the real, heavyweight base classes. This is
rtp-core.
Deriving a new shape is fundamentally an implementation-tier (rtp-core) extension, not a contract-tier one. rtp-core already exposes the typed, type-safe registration entry points RTP.addShape(Shape<?>) and RTP.addVerticalAdjustor(VerticalAdjustor<?>); the missing piece was recognizing and documenting rtp-core as a first-class extension surface, and removing the untyped Object-based shim that pretended rtp-api owned shape registration.
Decision¶
- Adopt an explicit two-tier API model.
rtp-apiis the thin contract surface: teleport (RTPAPI.teleport), hooks (RTPAPI.hooks()), by-world queries, shared platform-agnostic models. It stays dependency-light and is the publish target for the common consumer.-
rtp-coreis the implementation-extension API: authors who derive a customShape/VerticalAdjustorcompile againstrtp-coreand subclass the concrete base classes (Shape,MemoryShape,VerticalAdjustor), registering them through the typedRTP.addShape(Shape)/RTP.addVerticalAdjustor(VerticalAdjustor). -
Remove the untyped registration shim from
rtp-api.RTPAPI.addShape(Object),RTPAPI.addVerticalAdjustor(Object), and their backingshapeAdder/vertAdderdelegate fields are deleted. They were untyped (no compile-time guidance) and only existed becausertp-apicannot see theShapetype without a dependency cycle. Internal/built-in registrations (RTPstatic init,ChunkyChecker,ChunkyRTPShape,ChunkyBorderChecker) now call the typedRTP.addShape(...)directly. The artifact is unpublished (nomaven-publishonrtp-apiyet), so this is a clean, non-deprecated removal. -
Extract
yaml-api. The in-house, zero-dependency YAML substrate (ADR-025), previously inrtp-core'sio.github.dailystruggle.rtp.common.configuration.yamlpackage, is extracted into a new pure-Javayaml-apimodule. The package name is preserved verbatim so no import site across the monorepo changes. This decouples a genuinely reusable parser and is a prerequisite for any future lift ofFactoryValueinto a higher tier. -
rtp-apimay depend on sibling APIs.rtp-apinow depends (api) oncommands-apiandyaml-api. Both are pure-Java and depend on nothing else in the project graph, so the dependency DAG stays acyclic:commands-api,yaml-api<-rtp-api<-rtp-core. -
Requirements updated. REQ-API-F-001 / REQ-API-F-002 are reworded so that custom shape / vertical-adjustor registration is an implementation-extension-tier (
rtp-core) capability, consistent with REQ-API-NF-002. The contract surface (rtp-api) no longer claims to own shape registration.
Alternatives Considered¶
| Alternative | Why Rejected |
|---|---|
Thin marker interfaces (RTPShape/RTPVerticalAdjustor) in rtp-api, core types implements them |
Gives compile-time typing but does not let an author derive a new shape from an existing one (the actual request) - they would only see the marker's tiny surface, not MemoryShape's reusable machinery. Rejected by the project owner. |
Physically move FactoryValue + Shape + VerticalAdjustor + MemoryShape + concrete shapes into rtp-api |
Requires also lifting Factory + ConfigParser + the configuration subsystem (transitive coupling), which inverts the architecture and makes rtp-api indistinguishable from rtp-core. Violates REQ-API-NF-002. |
Keep the untyped RTPAPI.addShape(Object) shim as a deprecated convenience |
Preserves the exact untyped delegate the developer-UX work set out to eliminate; the typed RTP.addShape(Shape) already exists and is strictly better. Artifact is unpublished, so no compatibility cost to removing now. |
Consequences¶
- Positive:
- Authors can derive a new shape (e.g.
extends MemoryShape) with full type safety, exactly like the built-inPolygon, by compiling againstrtp-core- the platform-independent engine has no Bukkit/Fabric imports, so this is safe. - The untyped
Objectregistration shim is gone; the only registration path is the typedRTP.addShape(Shape)/RTP.addVerticalAdjustor(VerticalAdjustor). yaml-apiis a clean, reusable, zero-dependency module;rtp-apino longer (transitively) pretends to own the config substrate.-
The contract vs extension distinction is documented, so future "should this go in rtp-api?" questions have a decision rule: contract =
rtp-api, heavyweight extension =rtp-core. -
Negative / Trade-offs:
- Shape/vert authors take a dependency on
rtp-core, which is heavier and changes more often thanrtp-api. This is inherent to the task (the base classes are heavy) and is now an explicit, documented choice rather than an accident. rtp-api->commands-apiis a new (additive) dependency edge. It is acyclic andcommands-apiis platform-neutral, but it does widen thertp-apitransitive surface; consumers who only want the contract now also resolvecommands-api+yaml-api.- Publishing
rtp-coreas a consumable extension artifact (with a documented compatibility posture) is follow-up work, not landed here.
References¶
rtp-api/REQUIREMENTS.md- REQ-API-F-001 / F-002 (reworded), REQ-API-NF-002 (decoupling).- [ADR-025] - the in-house zero-dependency YAML substrate now hosted by
yaml-api. - ADR-026 - the behavior-modification hook facade (
RTPHooks), which remains the contract-tier extension seam. docs/dev/EXTERNAL_HOOKS.md,docs/dev/DESIGN.md(rtp-api Implementation Notes),docs/admin/HAZARDS.md(H-009),docs/admin/RUNBOOK.md- updated to the typedRTP.addShape/RTP.addVerticalAdjustorentry points.RTP.addShape(Shape)/RTP.addVerticalAdjustor(VerticalAdjustor)inrtp-core/.../common/RTP.java- the extension-tier registration entry points.- Phase 1 of the same developer-UX initiative:
RTPAPI.teleport(...)first-class contract-tier entry point.