Skip to content

Architecture

Leaf26 edited this page Jun 17, 2026 · 1 revision

Architecture

This page is the mental model. Before you write an addon, it helps to see what the engine is made of, where your code attaches, and how a teleport actually flows through it. Every diagram below is a real boundary in the codebase, not a marketing picture.

!!! note "Why this matters for addon authors" LeafRTP is structurally different from a classic random-teleport plugin. Legacy plugins reroll random coordinates synchronously on the main thread until one happens to be safe - an unbounded loop that stalls the server. LeafRTP instead pre-generates and validates locations off-tick, selects them with a bounded O(log n) lookup over an Archimedean-spiral mapping, and remembers bad sectors across restarts. You extend that machine by composing its seams - you never re-implement the safety it guarantees.


1. The big picture: modules and the two tiers

LeafRTP is a multi-module build. The two modules you compile against (rtp-api, and occasionally rtp-core) are platform-neutral; everything Bukkit/Paper/Folia/Fabric-specific lives in a platform adapter that you never touch.

graph TD
    subgraph contract["Contract tier (depend on this)"]
        api[rtp-api<br/>stable interfaces + models]
        cmd[commands-api]
        fx[effects-api]
        maps[maps-api]
        metrics[metrics-api]
    end

    subgraph impl["Implementation tier (derive only when needed)"]
        core[rtp-core<br/>regions, queues, spiral math,<br/>MemoryTracker, pipeline]
    end

    subgraph platform["Platform adapters (never import from these)"]
        bukkit[rtp-bukkit]
        paper[rtp-paper]
        folia[rtp-folia]
        fabric[rtp-fabric]
        neoforge[rtp-neoforge]
    end

    entry[rtp-plugin / mod entry point]
    yours[Your addon]

    api --> core
    cmd --> core
    fx --> core
    maps --> core
    metrics --> core
    core --> entry
    bukkit --> entry
    paper --> entry
    folia --> entry
    fabric -.-> core
    neoforge -.-> core

    yours -->|compile against| api
    yours -.->|reach in when you<br/>need to| core
Loading

!!! tip "Pick the lowest tier that does the job - but nothing is off-limits" Most addons never need more than the contract tier (rtp-api + the sibling *-api modules): menus, triggers, metrics exporters, and effects all live there, and adding a new destination geometry (RTP.addShape) or vertical placement (RTP.addVerticalAdjustor) is the common reason to pull in rtp-core. But the two-tier split (ADR-051) is a stability promise, not a fence: rtp-api is the surface we keep stable for you, while rtp-core is everything else - regions, queues, the pipeline, MemoryTracker. If you are smart enough to drive a core internal, it is fair game; you simply trade the stability guarantee for the extra reach, and you still owe the engine its safety rules (S-001..S-007, schedule through RTP.scheduler). Prefer the lowest tier that does the job, then go deeper when you have to.

!!! warning "Going deep is a trade, not a free lunch" rtp-core symbols can change between releases without notice (that is precisely what the contract tier shields you from). When you bind to a core internal, pin the version you built against and re-test on upgrades. See API stability.

!!! warning "The dependency rule is enforced" rtp-core and rtp-api must never import platform classes (org.bukkit.*, Fabric, etc.). ArchUnit tests fail the build if they do. Your addon should follow the same discipline: depend on rtp-api, and route anything platform-specific through the accessors the engine hands you (RTP.scheduler, RTP.serverAccessor).

For the full module breakdown in prose, see Addon development and the canonical docs/dev/ARCHITECTURE.md.


2. Where your addon plugs in

Each extension surface attaches at a different point of the engine. This is the same set of seams catalogued on Extending RTP, drawn as attachment points rather than a table.

graph LR
    player([Player / trigger])

    subgraph engine["LeafRTP engine"]
        sel[Location selector<br/>spiral + shapes + vert]
        queue[Pre-generation queue]
        pipe[Teleport pipeline<br/>validates + teleports]
        cfg[ConfigParser]
        sched[RTP.scheduler]
        net[NetworkTransport SPI]
    end

    menu[[Menu / GUI addon]]
    subcmd[[Sub-command addon]]
    verifier[[Verifier hook]]
    shape[[Custom Shape / VertAdjustor]]
    effect[[effects-api effect]]
    exporter[[Metrics exporter]]
    xserver[[Cross-server menu]]

    player --> menu --> pipe
    player --> subcmd --> pipe
    verifier --> sel
    shape --> sel
    effect -. warmup window .-> pipe
    exporter -. reads .-> queue
    xserver --> net
    menu -. reads status .-> queue
Loading
Attachment point What you build Tier Detail page
In front of teleport(...) menu / GUI, NPC / sign / item trigger contract Building a menu, Common RTP recipes
Inside the selector verifier (claim skip, "avoid water"), new Shape, new VerticalAdjustor contract / impl Hooks and verifiers, Shapes, Vertical adjustors
The command tree /rtp <verb> contract Sub-commands
The warmup window particle / bossbar / "rift" effect contract Writing an effect
Reads off the queue/metrics dashboards, Prometheus/InfluxDB exporter contract Metrics
NetworkTransport proxy / cross-server destination rows proxy SPI Cross-server RTP for addons

3. The teleport pipeline

When a player asks for a destination, almost all the cost has already been paid by the background queue. The fast path is a dequeue, not a search.

flowchart TD
    start([Player runs /rtp or addon calls teleport]) --> perm{Permission OK?}
    perm -- no --> deny[Send no-permission message, stop]
    perm -- yes --> econ{Economy OK?}
    econ -- no --> funds[Send insufficient-funds message, stop]
    econ -- yes --> dq[Dequeue a pre-generated location]
    dq --> ready{Queue has a ready location?}
    ready -- "yes (normal, instant)" --> tp[Teleport player]
    ready -- "no (cold start / scan not run)" --> gen[Generate on demand<br/>async chunk load, 1-2 ticks] --> tp
    tp --> invuln[Apply invulnerability timer] --> refill[Background task refills queue async]
Loading

!!! warning "Never a silent no-op (REQ-RTP-S-004)" Every branch ends in either a teleport or an explicit message. When you call RTPAPI.teleport(...) from an addon you get a typed RTPResult back (success / queued / a reason) - it is never a silent failure. Surface that result to the player; do not swallow it.

The validation each candidate passes through before it ever reaches the queue is shape -> chunk -> vert -> biome -> safety (the TeleportPipelineTask). That is the work an addon never has to redo.


4. The cache tiers (why the second teleport is instant)

Pre-generated locations sit in a layered cache. Reads fall through from hottest to coldest; the background task and /rtp scan push verified locations upward.

flowchart LR
    poll([poll for a location]) --> L1
    L1["L1 - kept cache<br/>chunks loaded, ready to serve"] -- empty --> L2
    L2["L2 - cold cache<br/>verified, chunks released"] -- empty --> L3
    L3["L3 - backlog cache<br/>unverified FIFO, optional"] -- empty --> ondemand["generate on demand"]
    scan[/"rtp scan"/] --> L3
    bg[background QueueTask] --> L1 & L2
Loading
Tier Code symbol State
L1 (hot / "kept") keptLocations Verified, chunks force-loaded with keep(true) - what /rtp normally serves
L2 (cold / "unkept") unkeptLocations Verified, chunks released; re-loaded on use
L3 (backlog) backlogLocations Optional unverified FIFO upstream of L2; promoted as the anvil pre-filter verifies bins

!!! note "This is the bring-up sequence" A freshly started server has empty caches, so the first teleport may pause to generate. The recommended setup order is install -> configure region geometry -> /rtp scan start region=<name> for each reasonably-sized region, which warms the caches before players arrive. See Scan and spatial memory.

Bad sectors are remembered across restarts in a small spatial-memory database, so the queue never starts truly cold after the first run.


5. Threading: schedule through RTP.scheduler, never raw threads

The single rule that keeps an addon safe on every platform - especially Folia, where touching the wrong region's data throws ThreadAccessException - is to route all periodic/async work through RTP.scheduler. The adapter is the only layer that knows what kind of thread the work lands on.

flowchart TD
    addon[Your addon] --> sched[RTP.scheduler<br/>RTPScheduler SPI]
    sched --> bukkit[Bukkit/Paper:<br/>Bukkit scheduler + async pool]
    sched --> folia[Folia:<br/>region / entity / global schedulers]
    sched --> fabric[Fabric/NeoForge:<br/>server-thread executor]
Loading

!!! warning "No raw Executors or new Thread(...) in backend addons" A hand-rolled thread bypasses Folia region ownership, MemoryTracker accounting, and the shutdown drain, and leaks across /reload. Convert a period in milliseconds to ticks (Math.max(1L, ms / 50L)) and use RTP.scheduler.runTaskTimerAsynchronously(...). See Addon development.


Canonical engine diagrams (go deeper)

The diagrams above are the addon-author's mental model. When you need the exact per-subsystem flow - every state, branch, and cleanup edge - the engine ships a set of normative Mermaid charts under docs/architecture/, each paired with a walkthrough in docs/dev/CODE_TOUR.md.

Diagram Covers
01 - Teleport execution pipeline End-to-end lifecycle of one /rtp: cache-vs-queue-vs-unqueued branch, async SETUP/LOAD, region-thread safety eval, entity-scheduler teleport, guaranteed cleanup
02 - Budgeted cache generator The background queue-refill loop that keeps each region's cache warm
03 - Chunk ticket lifecycle How chunk tickets are issued and released (S-002)
04 - Active GC sweep The MemoryTracker periodic reaper
05 - Scan task crawler The /rtp scan region safety pre-scanner
06 - Plugin setup lifecycle Startup / init ordering (when your onLoad() runs)
07 - /rtp command region selection How a region/world is resolved before the pipeline runs
08 - Location selection per attempt Accept/reject of one candidate (x, y, z) - where verifiers and shapes act
09 - Configuration load and reload ConfigParser load / /rtp reload
10 - Shutdown and flush lifecycle Drain / flush on disable
11 - Configuration write and persist How config writes are persisted
12 - Network model Multi-server / multi-proxy topology, cross-server /rtp lifecycle, reservation-token state machine

!!! tip "Map the addon seams to these charts" A verifier or custom Shape acts inside diagram 08; a menu/trigger sits in front of diagram 01; an effect runs on the warmup window of diagram 01; a metrics exporter reads the cache filled by diagram 02; a cross-server menu rides diagram 12.


Where to go next

Clone this wiki locally