Welcome to the Hangar Open Beta. Please report any issue you encounter on GitHub!
Avatar for leaf26

Deterministic-Time Random Teleportation [Paper, Folia, Bukkit, Fabric, Neoforge, Velocity]

Report LeafRTP?

Release
3.1.2

leaf26 released LeafRTP version 3.1.2 on June 13, 2026

(since 3.1.1)

Added

  • Cross-server /rtp on the DB-free proxy-direct tier, as a drop-in alternative to Redis/SQL for data management (rtp-proxy-ADR-019, refining rtp-proxy-ADR-018). Building on the proxy-direct region discovery (rtp-proxy-ADR-017), a cross-server /rtp <server>:<region> issued on a lobby now moves the player and runs /rtp on the destination backend with no Redis/SQL. proxy-direct is simply "another data-management transport like redis/sql": the proxy's own in-memory NetworkTransport + NetworkRequestQueue are the shared store, and a backend reaches them over the existing player-independent proxy-direct socket via a thin RPC. The wire carries one opcode per backend-invoked SPI method (OP_HEARTBEAT, OP_FLUSH_PENDING, OP_POLL_STATUS, OP_CANCEL, OP_FIND_RESERVATION, OP_REDEEM, OP_LIST_ACTIVE), HMAC-signed when RTP_NET_SECRET is set; proxy-local methods (claim/dequeueReady/reaper) never leave the proxy. As a result enrolment flows through the standard NetworkEnrolmentBuffer -> NetworkRequestQueue.flushPending, the proxy's existing TransportRequestTriggerSource + dispatcher relocate the player via createConnectionRequest, and arrival redeem runs through the standard JoinTriggerSource (findReservation/redeem) - the same code the Redis/SQL tiers use, with no behavioural branch outside the binding factory. ProxyDirectNetworkBinding is now a full RPC NetworkTransport, plus a new ProxyDirectNetworkRequestQueue (rtp-core); the proxy ProxyDirectListener dispatches each RPC onto the proxy's store. Reservation tokens are retained this pass (slimming them is a future cleanup). Covered by ProxyDirectNetworkBindingTest (5) and ProxyDirectListenerTest (2).

  • Per-server /rtp permission gate for networked backends. A qualified cross-server target /rtp <server>:<region> is now gated by rtp.servers.<server> (the destination backend) in addition to the existing rtp.regions.<region> (the region), mirroring the local rtp.regions.x model so operators can allow/deny teleport per backend. Enforced in the RegionParameter validator, which also filters tab-completion, so a forged argument cannot bypass the suggestion filter. rtp.servers.* defaults to op (and is a child of rtp.*) in both the full and lite plugin.yml.

  • proxy-direct network transport: player-independent cross-server region discovery on the DB-free edition (rtp-proxy-ADR-017). A new transport.type: proxy-direct lets a backend open its OWN outbound TCP connection to the proxy companion (configured transport.proxies: [host:port, ...], the same mirrorable list on every backend) and publish its real region list at startup with NO player online, so cross-server /rtp region=<server>:<region> tab-completion converges with zero players on any backend and no per-server operator config - which plugin messaging cannot do (it is gated on a player connection, so a player-empty backend has no proxy connection). The Velocity proxy opens a matching TCP listener (transport.direct.enabled: true, default port 25599) that ingests backend pushes into and serves the merged availability snapshot from the same store the rtp:net plugin-message companion uses; frames are HMAC-SHA-256 signed when network.secretEnv (RTP_NET_SECRET) is set (rtp-proxy-ADR-010), unsigned otherwise. No Redis/SQL required. The backend dial is driven by the existing heartbeat tick (RTP.scheduler, no raw backend thread); the tier is non-durable (region discovery only - durable cross-server reservations still need the SQL/Redis tier). The proxy availability store no longer fabricates an assumed-default region row for a server that has not actually reported. Covered by ProxyDirectNetworkBindingTest (4), ProxyDirectListenerTest (1), and VelocityProxyAvailabilityCacheTest.

  • /rtp info biomes biome-occupancy leaderboard. A new /rtp info subcommand reporting which biomes online players actually spend their time in, ranked by share - useful for world designers gauging where players congregate (distinct from teleport landings, which mirror biome surface area). A new in-memory BiomeActivityTracker (RTP.biomeActivity, rtp-core) accumulates per-biome occupancy; the platform sampler periodically enumerates online players and BiomeActivityTracker.sample(...) dispatches each per-player biome read to that player's owning thread via RTP.scheduler.runTaskForPlayer (Folia-correct: the read lands on the entity's region thread), reporting back through independent per-player futures so one stalled region never blocks the sweep. Wired on the Bukkit/Paper/Folia plugin (~30s sampler pulse); counts are in-memory only and reset on restart/reload, so behaviour is identical in the full and lite editions. Gated by the existing rtp.info permission. New infoBiomeActivityHeader / infoBiomeActivityRow / infoBiomeActivityEmpty message keys mirrored across every shipped locale. Covered by BiomeActivityTrackerTest (3). Fabric/NeoForge samplers are a tracked follow-up.

  • spark profiler is now an optional source for /rtp info TPS/MSPT metrics (metrics-api-ADR-001; ADR-026 soft-hook). When the spark plugin is installed on a Bukkit/Paper/Spigot/Folia backend, RTP now merges spark's richer per-window TPS (1m/5m/15m) and mean MSPT over the native binding, instead of reading only the coarse Server#getTPS() / Server#getAverageTickTime(). spark is a pure soft-dependency reached entirely by reflection (no compile dependency on me.lucko:spark-api); softdepend: [spark] in plugin.yml only constrains load order so the spark API is ready when queried. New SparkMetricsBinding (per-field merge over a native delegate - player count / soft cap / chunk backlog / db latency / Folia regions still come from the native binding) and ReflectiveSparkStats (self-disables permanently on a ClassNotFoundException / NoClassDefFoundError such as a trimmed lite jar, and returns the native value per-getter on a transient pre-init / not-yet-sampled state so the merge self-heals once spark warms up), wired by MetricsBindingDispatcher#wrapWithSparkIfPresent on the Paper, Spigot, and Folia paths. On Folia, spark wraps FoliaMetricsBinding so its real per-tick MSPT is surfaced (Folia's native per-region sampler can only derive MSPT from the inter-tick interval, ~50 ms on a healthy server, so the visualizer / /rtp info previously showed a flat, incorrect MSPT) while the per-region foliaRegions() samples still come from the native binding. Documented in EXTERNAL_HOOKS.md. Covered by SparkMetricsBindingTest (5) and MetricsBindingDispatcherTest.

  • performance.yml#biomeWeights ships the full vanilla biome set pre-listed at the no-op weight 1.0 (ADR-062). The map previously shipped a single plains: 1.0 example; it now enumerates every vanilla overworld/nether/end biome (grouped by dimension) at 1.0, so the knob doubles as an easy-to-edit reference - change a number to bias a biome, set 0.0 to suppress it, or append a modded biome id, without having to know each biome's exact name first. Behaviour is unchanged: an all-1.0 map is identical to the prior default (unlisted biomes already defaulted to weight 1.0), and weighting only takes effect under biomeWeighted: true. When a future Minecraft version adds a biome, append it here to keep the list current. Mirrored across every shipped locale and the rtp-lite assembly.

  • Registry-aware gray-space steering for biome weighting (ADR-062 Phase 3) (ADR-062). The biomeWeighted / biomeWeights draw no longer treats a requested biome that has not yet been scanned as an implicit weight 0, and a rare biome with a single recorded run no longer funnels every weighted teleport onto one coordinate. The weighted recall draw now consults the world's biome registry (RTPServerAccessor.getBiomes(world), resolved once per /rtp in PregenState): a requested biome the world can produce but that recall memory has not recorded stays reachable as "gray space" via bounded-spiral exploration (its biome confirmed by the existing downstream pipeline verification), and only a biome absent from the registry is a true 0. Each recorded biome also defers a run-count-proportional share of its weight to exploration (PregenTask.grayFraction: unrecorded -> fully gray, a single recorded run -> mostly gray, >= GRAY_SPACE_MIN_RUNS distinct runs -> fully recall-steered), and the draw explores with probability equal to the gray-space share of total weight (PregenTask.graySpaceProbability), so a higher configured weight on a thinly-recorded biome defers more to exploration and can never amplify single-run clustering. Forced recall (biomeRecallForced) opts out. Stays bounded and S-005-safe (no synchronous chunk load to discover a biome). A new cost-tiered biome-sampling SPI is scaffolded for a future cheap pre-filter (BiomeSampleCapability enum + default RTPServerAccessor.biomeSampleCapability / sampleBiome methods, currently GENERATE_REQUIRED / null on every platform); the per-version noise-sample fast path remains D-005-gated follow-up work. Covered by BiomeWeightedDrawTest (8 cases).

  • Biome-probability weighting for biome-filtered selection (ADR-062). New performance.yml knob biomeWeighted (default false, requires biomeRecall). When enabled, a biome-filtered /rtp (or background pre-fill) that requests several biomes steers toward each requested biome with equal probability instead of in proportion to how many recorded location runs it occupies, so a rare biome (mushroom fields, cherry grove) is no longer drowned out by a common biome's many runs. The draw operates over the existing Anvil-sourced per-region biome occupancy map (MemoryShape getBiomeKeys/getBiomePrefixSums): a new PregenTask.drawWeightedBiome(...) picks the target biome uniformly among those present in spatial memory, then a run within it weighted by run width (keeping placement spatially uniform inside the chosen biome), then a uniform offset. It stays bounded (a finite weighted pick over a pre-mapped set, no reroll loop, no chunk I/O) and falls back to the legacy uniform-over-runs draw when disabled and to the plain bounded spiral when no occupancy data exists yet. Accuracy footing is the existing Anvil read, so steering stays correct on worlds pregenerated elsewhere or migrated across a Minecraft version (where a noise-map getBiome lookup would land in the wrong biome). A companion biomeWeights map knob lets operators set explicit, non-equal per-biome weights: when biomeWeighted is on and the map is non-empty, the target biome is picked in proportion to the configured weights instead of equally (biomes absent from the map use weight 1.0, 0.0 suppresses a biome, and an all-zero/empty set falls back to the equal-probability pick); the shipped plains: 1.0 entry is a no-op example, and the equal-probability draw is just the all-equal degenerate case. Weights are parsed once per /rtp invocation (PregenState), keeping the hot draw allocation-light. Mirrored across every shipped locale's performance.yml. Covered by BiomeWeightedDrawTest (6 cases).

  • /rtp version (alias /rtp about) descriptive subcommand. A new lightweight subcommand that reports the running RTP plugin version, the platform brand, and the host server version. Unlike /rtp info, it captures no live MetricsSnapshot and reads no runtime counters, so it is a purely descriptive identity readout rather than realtime telemetry. Registered on the Bukkit/Paper/Folia, Fabric, and NeoForge command roots (the alias about resolves to the same command). Gated by a new rtp.version permission (default true, also a child of rtp.*). Covered by VersionCmdTest.

  • Basic Folia support in the free build (ADR-024 2026-06-10 amendment). The free (lite) build now runs on Folia instead of failing to enable: the lite plugin.yml sets folia-supported: true, and a new correctness-first FoliaAwareScheduler (in the lite-bundled rtp-paper-common) routes work through paper-api's regionized scheduler statics (getGlobalRegionScheduler / getRegionScheduler / getAsyncScheduler) and the per-entity scheduler, while BukkitRTPPlayer.setLocation performs the teleport via Entity#teleportAsync (reflectively, since a cross-region sync teleport() throws on Folia; failures are logged per REQ-RTP-S-004). BukkitServerProvider.resolveServerModel prefers the tuned folia_v* adapter when present (Pro) and falls back to the Paper accessor + FoliaAwareScheduler otherwise (lite). The tuned, throughput-optimized rtp-folia adapter remains a (Pro) early-access feature and is still excluded from the lite jar, so no PolyForm-licensed source enters the MIT lite binary (ADR-061).

Fixed

  • /rtp visualization/map charts no longer crash on the free (lite) build running on Folia. Map chart delivery dropped the chart's FILLED_MAP item via World#dropItem on the thread that ran MapDispatch, which on Folia is the global region thread - it does not own the player's region, so Folia threw NullPointerException: ... ServerLevel.getCurrentWorldData() is null and the chart was never delivered. The region-thread hop is now performed once at the call site: MapDispatch.paint dispatches binding.deliverTo(...) through RTP.scheduler.runTask(viewerLocation, ...) (resolved via RTP.serverAccessor.getPlayer(viewer).getLocation()), so on Folia the drop lands on the region thread owning the viewer's chunk and on Paper/Spigot it runs inline on the main thread as before. The platform map bindings (BukkitMapBinding, FoliaMapBinding) no longer reimplement an EntityScheduler hop - deliverTo simply drops on the (now correct) calling thread, and the S-004 failure-to-mapUnavailable path is evaluated inside the scheduled runnable. Covered by MapDispatchTest, BukkitMapBindingTest, and FoliaMapBindingTest.

  • Metrics now install on the free (lite) build running on Folia, so bStats / /rtp info report sampled data instead of UNSAMPLED. On a Folia server the lite jar's MetricsBindingDispatcher.install() tried to load the Pro-only FoliaMetricsBinding (the whole rtp/folia/** adapter is excluded from the MIT lite jar, ADR-061), caught the ClassNotFoundException, and aborted the metrics install with a startup WARNING + stack trace - leaving the binding at NOOP. The dispatcher now resolves each platform binding through a tryInstallBinding(...) helper that returns gracefully (logging at FINE, no stack trace) when a binding class is absent, falling through Folia -> Paper (Bukkit#getTPS works on Folia) -> the raw Bukkit sampler. Lite-on-Folia therefore installs PaperMetricsBinding and metrics work out of the box. Covered by MetricsBindingDispatcherTest.

  • (Pro) Folia TPS/MSPT metrics no longer go UNSAMPLED on an idle server. FoliaMetricsBinding's scalar TPS/MSPT clock was fed only by FoliaRegionProcessor, which runs only while a region has queued /rtp tasks; with no teleport traffic no samples were recorded and the 60s idle-eviction left /rtp info and bStats reporting UNSAMPLED. The binding now also drives a steady, traffic-independent heartbeat sampler scheduled once per server tick on the Folia global region scheduler (RTP.scheduler.runTaskTimer), folded into the scalar aggregation (but not into the per-region foliaRegions() list), and self-terminating on hot-reload/uninstall. Per-region attribution from FoliaRegionProcessor is unchanged. Covered by a new FoliaMetricsBindingTest case.

  • Relative coordinate tokens (/rtp centerx=~ centerz=~) now resolve to the player's position instead of a garbage center. The Bukkit CoordinateParameter advertises the vanilla /spreadplayers-style ~ / -~ tokens in tab-completion, but the per-call shape-override path in RTPCmd.compute parsed override values with Long.parseLong -> Double.parseDouble -> Boolean.valueOf; a ~ value fell through to Boolean.valueOf("~") == false, so centerx/centerz silently became the boolean false (an effective 0 center) rather than the caller's coordinate. A new RTPCmd.resolveRelativeCoordinate(token, base) helper now resolves ~, ~<n> (e.g. ~-50), -~, and -~<n> against the teleporting player's current X/Z before the generic coercion, with absolute (tilde-free) tokens unchanged and a malformed offset falling back to the player's coordinate. Covered by RelativeCoordinateResolutionTest (6 cases).

  • Fabric and NeoForge no longer suggest /rtp subcommands and parameters the caller lacks permission for (commands-api-ADR-001; REQ-API-F-007). The Brigadier bridge's greedy args-slot suggestion provider (BrigadierCommandAdapter.argsSuggestionsFor) emitted every subcommand name and every paramName= key regardless of the caller's permissions, so on the Brigadier-dispatched platforms (Fabric, NeoForge) tab-completion advertised commands the player could not run - diverging from the Bukkit TreeCommand.onTabComplete contract, which filters by permission. The provider now gates both subcommand-literal and paramName= suggestions through BrigadierBridgeContext.permissionCheck() (empty/null permission stays open; a throwing predicate denies and logs, mirroring applyRequires). Sub-command literal nodes were already requirement-gated via requires(...) and filtered out of the client command-tree packet; this closes the dynamic-suggestion leak. Covered by BrigadierSuggestionPermissionTest (4 cases).

Changed

  • Released jars trimmed of dead weight. Both shadowJar (Pro) and shadowLiteJar (lite) now exclude the compile-only IntelliJ annotation classes (org/jetbrains/annotations/**, org/intellij/lang/annotations/**, CLASS retention, no runtime use) and the orphan maven metadata (META-INF/maven/**, pom/properties never read at runtime) that leaked in transitively from shaded dependencies. The Pro jar additionally stops bundling the contributor-facing docs/architecture/** tree (per-attempt pipeline internals, chunk-ticket lifecycle, network model, etc.) - it remains available in the public repository - while the operator-facing docs/admin/** set (Quick Start, Configuration, Commands, Hazards, Runbook, proxies) is unchanged. No code, dependency, or public-API change: shadowJar's minimize() was deliberately not adopted because the heavy shaded libs are reflectively driven (Jedis pub/sub, lz4 variant selection) or relocated into our own namespace and the RTP modules expose an addon-facing API plus descriptor-loaded platform entry points that static reachability analysis cannot see.

  • /rtp info trimmed to stay compact before any metrics are sampled. The live-health block previously printed a wall of n/a rows (TPS, server MSPT, average pipeline latency, the P50..P99 percentile line, and database latency) on servers where no live MetricsBinding had sampled yet. InfoCmd now suppresses each of those rows until its underlying value exists: the TPS and MSPT rows render only once a live sample is recorded, the average-latency and percentile rows render only once at least one teleport pipeline has completed, and the database-latency row renders only once a query has run. The always-available rows (JVM heap, queue depth, pending teleports, generation outcomes) are unchanged. Backed by four new read-only predicates on PlaceholderProvider (hasLiveTps, hasLiveMspt, pipelineSampleCount, hasDatabaseLatency) that read the snapshot sentinels (NaN TPS/MSPT, negative database latency, zero pipeline samples). The admin-only DRM footer was condensed from up to six separate lines into a single DRM: <source> id=<id> nonce=<nonce> line (BuiltByBit builds append bbb=<resourceId>@<timestamp>); the DRM strings stay hardcoded in core rather than moving into the player-editable messages.yml. Covered by InfoCmdTest.

  • Per-batch [TRACE] ScanTask diag line demoted from INFO to FINE. The scan diagnostic split (probeNull / adjustNull / cacheHitLoad / genscan*) had been logged at INFO, printing a line every few seconds to the default console for the full duration of a world scan. It now logs at FINE, so it stays available when an operator raises the log level to debug a scan but no longer spams a normally-configured server. No behaviour change beyond log verbosity.

Information

Published onJune 13, 2026
Downloads4

Platforms

Paper Paper (1.20-26.1.2)
Velocity Velocity (3.3-3.5)