[3.2.0] - 2026-06-21
The primary change is baking-in of example addons such that servers should have what they need and remove/upgrade those for better. For example, the "gui" fills itself automatically based on a "center-out" design and permission checks, where a user might instead want a gui to follow a specified format instead so would be able to replace the gui as a modular component. The addons are examples of how to extend the plugin.
Plus a few fixes for 26.x on fabric and neoforge.
Upgrade note (default behavior change). This release bundles and self-extracts the
LeafRTPGuiAddonon first run, and that addon binds the bare/rtp(no arguments) root action (ADR-056). On servers that have a GUI renderer available, a player typing bare/rtpnow opens the destination-picker menu instead of teleporting immediately; the classic instant teleport is still reachable via the menu and as a fallback when no renderer is installed or the player is offline. To restore the old behavior, delete the extractedaddons/LeafRTPGuiAddon.jar(theaddons/folder then exists, so it is never re-extracted) or unbind the root action. The minor version bump (3.1.x->3.2.0) reflects this operator-visible default change to the headline command; the publicrtp-api/SPI surface is unchanged.
Added
-
The bundled
LeafRTPCountdownAddonnow shows its delay and queue countdowns as an on-screen boss-bar instead of chat spam, via a new per-player progress-bar API.RTPPlayergains two default methods,showProgressBar(id, title, progress)andclearProgressBar(id), that render a personal, single-player boss-bar (distinct from the existing server-wide, permission-scopedRTPServerAccessor.updateProgressBarssurface). The Bukkit adapter (BukkitRTPPlayer) implements them over BukkitBossBars keyed per player and bar id; other platforms inherit a graceful no-op default. The countdown addon drives both the "Teleporting in N..." delay countdown and the "you are #N in the queue" / "you're up!" queue countdown through this API, replacing one bar in place each second rather than flooding chat. -
Addons can now react when an admin applies a config prefab. A new public
rtp-apievent surface (RTPAPI.onPrefabApplied(Consumer<PrefabAppliedEvent>), returning anAutoCloseablefor unregistration) fires a fire-and-forget, post-apply notification after/rtp admin prefab ... confirmwrites the affected config files and reloads. The event carries the prefab id, the applying caller's UUID, the written file ids, a per-file list ofPrefabChange(keyPath / oldValue / newValue), and whether the reload succeeded. The dispatcher (PrefabEventDispatcher) mirrors the existing player-lifecycle hook pattern: aCopyOnWriteArrayListof subscribers with per-subscriber exception isolation so one faulty addon cannot break delivery to the others. It is created eagerly, so an addon may subscribe before core finishes loading. Covered by two newPrefabCommandTestcases. -
Loaded chunks now opportunistically register their biome into every in-range region's spatial memory, and this is on by default.
performance.yml#checkOnChunkLoads(now defaulting totrue, wasfalse) gates a new on-load harvest: whenever the server loads a chunk for any reason, the chunk's center-column biome is recorded into theMemoryShapeof each configured region whose range contains that column, feeding biome recall / biome-weighted selection without any extra chunk I/O. A new always-registeredOnChunkLoadlistener (Bukkit/Paper/Folia) forwards loaded chunks toSelectionAPI.observeChunkLoad, which reads only already-resident chunk data (S-005 safe) and gates O(1) on the config flag, so the cost when disabled is a single boolean check. Positions inside a region's center-radius hole are skipped (no valid location there). Covered bySelectionAPIObserveChunkLoadTest(3 cases). The new default value is mirrored across every shipped locale. -
The GUI destination-picker addon now ships inside the RTP jar and demos itself out of the box, and gains a proper
/rtp guicommand. TheLeafRTPGuiAddonmulti-loader jar is bundled inside both the Pro and lite RTP jars (underbundled-addons/) and self-extracted into<pluginDir>/addons/on first run only - when theaddons/folder does not yet exist (AddonRegistry.extractBundledAddons) - so a casual admin gets the flashy chest menu the moment the jar is dropped in, with no second download. Turning it off is just deleting the extracted jar (the folder then exists, so it is never re-extracted) or editing the addon's config. A new/rtp guisubcommand (permissionrtp.gui) lives inrtp-coreand opens whatever menu an addon bound to the bare/rtproot action (ADR-056), registered on the Bukkit, Fabric, and NeoForge command trees so it works on every platform and however the addon was loaded - replacing the old Bukkit-plugin.yml/rtpguicommand, which never appeared when the addon was loaded from theaddons/folder (itsJavaPluginentry point does not run there). The Bukkit chest renderer is now also published via aMenuRendererMETA-INF/servicesdescriptor so it registers on theServiceLoaderload path (addons-folder / bundled), where the entry point is skipped. Covered byAddonRegistryTest(2 new cases). -
The
lockAfterUsesusage cap now survives a restart, reaching parity with the already-persisted cooldown (ADR-068). The BetterRTPLockAfterrolling-window counter previously lived only in memory and was lost on every reload/restart, so a player at their cap got a fresh allowance whenever the server bounced. A newTeleportLimitStoreseam (rtp-core) now fronts the usage-cap guards (/rtpcommand and the addon-facingRTPAPI.teleport) and the success counter; itsLocalTeleportLimitStorewrite-through-persists each player's{count, start}window to the configured database (the file-backedYamlFileDatabasein the lite assembly, so no driver dependency) and lazily reloads it on first use, hydrating the window on restart. The cooldown anchor is unchanged (it already persists). This is the local-durability axis of ADR-068; the cross-server (proxy/transport) axes are tracked as follow-up work. Covered byTeleportLimitStoreTest(4 cases). -
The RTP-lite assembly variant now ships the full base
safety.ymlinstead of a trimmed lite-specific copy (ADR-024 2026-06-20 amendment). The lite-specificsafety.ymloverlay is removed, so lite inherits the base config verbatim - including both the vanilla block-tag grammar (#minecraft:<tag>) and state predicates from ADR-017, which are now sufficiently stable for lite. No driver change was needed (theSafetyTokenParser/SafetyCompilationCache/SafetyTokenExpanderalready ship in the lite jar); only thertp-tagsmodule (tags/,tagsRefresh.yml) remains a full-edition feature.
Changed
-
The
LINEARvertical adjustor now defaults to middle-out search (direction=2) in every world, not just the nether/end. World generation changed such that a center-out vertical scan is the better general default, so the globalLINEARdefaultdirectionmoved from0(bottom-up) to2(middle-out) - the same setting already seeded for_nether/_the_endregions. The dimension-specific direction override is removed fromNetherEndConfigAmender(its nethermaxY<=128clamp andrequireSkyLight=falseseeding are unchanged), since middle-out is now the baseline. Existing region configs that setdirectionexplicitly are unaffected. -
The shipped default overworld region now uses the
LINEARvertical adjustor with middle-out search (direction: 2) and requires sky light (vert.requireSkyLight: true) so/rtplands players on open-sky surfaces.regions/default.ymlswitchesvert.namefromJUMP(which has no scan-direction concept) toLINEARwithdirection: 2, matching the middle-out search already used for nether/end, and flipsrequireSkyLightfromfalsetotrue. Nether/end regions created from this default still seedrequireSkyLight: falseautomatically viaNetherEndConfigAmender(those dimensions have no sky light), so cross-dimension behavior is unchanged. Existing region configs that set these explicitly are unaffected. -
Claim-plugin integrations moved out of
rtp-plugininto the new bundledLeafRTPClaimAddon(ADR-069, superseding ADR-019). The 12 claim/protection checkers (WorldGuard, GriefDefender, GriefPrevention, Towny Advanced, SaberFactions, FactionsBridge, Lands, RedProtect, Residence, CrashClaim, HuskClaims, KingdomsX), theIntegrationsKeysenum, andintegrations.ymlleft the core plugin for a single Bukkit-only addon module that registers its verifiers through the public hook facade (ADR-026) exactly as before (REQ-RTP-S-003 unchanged) and self-gates onPlatformFamily.BUKKIT. The addon jar is bundled inside both the Pro and lite RTP jars (underbundled-addons/) and self-extracted into<pluginDir>/addons/on first run, so claim-aware teleport still works out of the box with no extra download. The claimcompileOnlydependencies and their dedicated soft-dep Maven repositories are removed fromrtp-plugin/build.gradle. Behaviour change for operators: the per-localeintegrations.ymltranslations that previously shipped in the core plugin are dropped (the addon ships an Englishintegrations.yml); re-localizing the addon config is tracked follow-up work.
Fixed
-
requireSkyLight: trueno longer lands players inside caves. The live full-load vertical-adjustor path (LinearAdjustor/JumpAdjustor) gated sky access on the chunk's stored sky-light nibble (getSkyLight(y+1) > 7), which is unreliable on freshly-generated or unticked chunks - the server frequently reports a stale full15before relighting, so a roofed cave column passed the gate. Both adjustors now derive the gate from block data instead (the samecomputeColumnSkyFloorapproach the anvil probe path already used): a candidate is accepted only when its head cell is strictly above the highest non-air block in that column, so any overhang, cave ceiling, or structure roof rejects it deterministically regardless of stored lighting. Covered by a newLinearAdjustorTestregression case (roofed cave with a stale full sky-light is rejected). -
The bare
/rtpGUI (bound via the ADR-056 root-action hook, e.g.LeafRTPGuiAddon) now opens on Fabric and NeoForge instead of silently running a classic teleport. The root-action consult lived only in the BukkitString[]-argsonCommandpath; Fabric (RTPCmdFabricRoot.onCommand) and NeoForge dispatch a bare/rtpstraight throughRTPCmd.compute(...), which had no root-action check, so a bound GUI never opened on those platforms. The hook is now also evaluated at the head ofcompute(...)for a bare invocation (no args), mirroring the Bukkit path and releasing theprocessingPlayerslock when the action handles the command; the BukkitString[]path still short-circuits beforecompute(...)when it handles the command, so the action never double-fires. Covered by two newReqApiF006RootActionTestcases exercising thecompute()entry point. -
Fabric on the deobfuscated MC 26.x runtime no longer crashes with
NoClassDefFoundError: net/minecraft/class_3222the moment a/rtpeffect (sound / particle / potion) fires. The common effects resolve the player throughHandleRegistry.wrapPlayer, which on Fabric routed to the sharedeffectsapi.fabric.FabricHandles. That provider lives in the Loom intermediary-remapped:effects-apimodule, so its bytecode names intermediary aliases (net.minecraft.server.level.ServerPlayer->class_3222) that exist on a 1.20/1.21 Fabric runtime but not on the deobf 26.x runtime, crashing the server tick thread. The deobf carrier now ships its own Mojmap-nativeeffectsapi.fabric_unobf.FabricHandlesUnobf(compiled with nomappingsstep, so it names Mojang types directly and absorbs 26.x API drift:(ServerLevel) player.level(), record-shapeGameProfile#name()with fallback, the 2-booleansendParticlesoverload, reflective/packet teleport), andFabricEffectsInitializer.registerAll()registers it instead of the intermediaryFabricHandles. The Bukkit/Paper/Folia and 1.20/1.21 Fabric paths are unchanged. -
Rebuilt jars now actually pick up changes to the Fabric/NeoForge Mojmap "unobf" carriers (e.g. the
FabricHandlesUnobffix above) instead of silently re-shipping stale carrier bytecode. The unobf carriers are appended into the Loom-remapped jar by theremapJar/remapLiteJardoLastmerge step, but those tasks only declared adependsOnon the carrier jars - the carriers are NOT part of the task'sinputFile(which isshadowJar/shadowLiteJar, where the bundled:effects-api/rtp-fabric-commonare the intermediary-remapped variants). So a carrier-only source change left the shadow jar's bytes identical,remapJarstayedUP-TO-DATE, itsdoLastnever re-ran, and the shipped jar kept the previously-merged carrier classes. The carrier configurations are now declared as taskinputs.files(...), so any carrier change invalidates the remap+merge and the corrected bytecode lands in the jar. -
Fabric (and NeoForge) effects wiring no longer fails to link with
NoClassDefFoundError: org/bukkit/entity/Player. The platform-neutraleffectsapi.common.effects.PotionEffectcarried a Bukkit/Folia entity-thread dispatch seam in a static lambda whose functional-interface (EntityDispatcher) descriptor namedorg.bukkit.entity.Player/org.bukkit.plugin.Plugin. Resolving that descriptor forced eager loading oforg.bukkit.entity.Playerthe instant the class was instantiated, soFabricEffectsInitializer.registerAll()(new PotionEffect(...)) threw on Fabric/NeoForge where Bukkit is absent - which also masked the real effects setup and let the obf-carrier fallback surface its ownclass_7923link failure. The seam (EntityDispatcher,entityDispatcher,applyOnEntityThread) was moved verbatim to the new Bukkit-onlyeffectsapi.bukkit.BukkitPotionDispatch; the common effect now links on every platform and applies potions through the platform-neutralPlayerHandle#applyPotionEffect. Covered by the relocatedFoliaDispatchTestandEffectsApiCommonNoPlatformImportsTest. -
NeoForge 26.1 server no longer crashes with a 60s ServerWatchdog timeout when
/rtploads a chunk.V26_1_R1NeoForgeVersionAdapter#requestFullChunkAsyncreflectively invoked vanilla's publicServerChunkCache#getChunkFuture(..., create=true)on the server tick thread (the live-load path dispatches it viaMinecraftServer#execute). On the main thread that public entry point parks the tick thread onmainThreadProcessor.managedBlock(...)until generation finishes - a synchronous main-thread chunk load (S-005) that tripped the watchdog and crashed the server. The reflective resolver now prefers vanilla's non-blocking innergetChunkFutureMainThread(same signature), which returns the pending future without blocking while the chunk system settles it off-thread;create=truestill self-issues the generation ticket so the holder reaches FULL status. -
NeoForge 26.1 no longer floods the console with
[ScanTask] INSTANT BYPASS: Chunk manager returned a null chunkand starves the location cache.V26_1_R1NeoForgeVersionAdapter#requestFullChunkAsyncinvoked vanilla'sgetChunkFuture(..., create=true)without first applying a load-ticket, so on this runtime the chunk holder never reached FULL status and the future resolved with noChunkAccess- every full-load scan of an ungenerated chunk bypassed instead of generating, spamming the warning and leaving the kept cache underfilled. The adapter now applies a dedicated, non-persistent (S-002), self-expiring temporary load-ticket around the generation request (added on the tick thread, released on completion), mirroring the 1.21.1 carrier which already did this, so the chunk generates and the bypass flood stops. -
Fabric on the deobfuscated MC 26.x runtime now generates chunks for
/rtpinstead of flooding[ScanTask] INSTANT BYPASS: Chunk manager returned a null chunkand starving the cache. Thertp-fabric-v26_1_R1/rtp-fabric-v26_2_R1carriers never overroderequestFullChunkAsync, so the unobf chunk path (FabricRTPWorldUnobf.loadLiveChunk) fell through to the throwinggetChunkFullstub default and no chunk ever generated. Both carriers now implementrequestFullChunkAsyncthe same way as the NeoForge 26.1 carrier (the same deobf MC 26.x runtime): they apply a dedicated, non-persistent (S-002), self-expiring temporary load-ticket via the typedServerChunkCache#addTicketWithRadius/#removeTicketWithRadiuspair, then invoke the publicgetChunkFuture(..., create=true)off the server tick thread so vanilla takes its non-blockingsupplyAsync(mainThreadProcessor)branch and the normal tick pumps generation to completion - the chunk reaches FULL status, the bypass flood stops, and the kept cache fills. -
Fabric on the deobfuscated MC 26.x runtime now actually generates chunks for
/rtp(every attempt was failing withnullChunk/asyncLoadNull). The runtime world is not the commonFabricRTPWorld/FabricRTPWorldUnobfbut the per-versionV26_1_R1FabricRTPWorld/V26_2_R1FabricRTPWorld(constructed viaFabricVersionAdapter#createWorld), which carry their owngetChunkAt. That path invoked the publicgetChunkFuture(..., create=true)on the server tick thread with no load-ticket, so on the deobf MC 26.x chunk system the future resolved with noChunkAccessand every attempt was attributednullChunk/asyncLoadNull. Both per-version worlds now mirror the NeoForge 26.1 fix: a dedicated, non-persistent (S-002), self-expiring temporary load-ticket is applied on the tick thread, the publicgetChunkFutureis invoked off the tick thread (where vanilla takes its non-blockingsupplyAsync(mainThreadProcessor)branch), and the method resolver prefers the publicgetChunkFutureover the innergetChunkFutureMainThreadso the off-thread self-pump branch is taken. The temporary ticket is released once the generation future settles. -
NeoForge 26.1 no longer spams
NoSuchMethodErrorevery tick while rendering a/rtpscan progress bar.NeoForgeProgressBars(in the version-agnosticrtp-neoforge-common, typechecked against the MC 1.21.x mappings) hard-linked theServerBossEvent(Component, BossBarColor, BossBarOverlay)constructor, whose signature drifted on MC 26.1, so every progress-bar update threwNoSuchMethodErrorfrom the scheduler tick. The construction site now resolves a matching publicServerBossEventconstructor by reflection once (cached), accepting any shape that exposes aComponent,BossBarColorandBossBarOverlayparameter and filling any extra parameters with sensible defaults (a freshUUIDper bar,0f,false, ornull); if no usable constructor exists the bar is skipped rather than crashing the tick. -
Heap pressure now actively reclaims memory instead of only pausing cache fill, so a small-heap server no longer stays parked above
maxHeapPercentwith the "pausing background cache generation" warning repeating forever. TheHeapPressureMonitorgate paused cold->hot promotion under pressure, which stopped the retained set from growing but never shrank it - each kept location pins a chunk via itsChunkReservation, so heap usage stayed pinned at the threshold indefinitely.Region.executenow runs an active shed step while under pressure: it drains a bounded number of kept locations per pulse (shedHotCacheUnderPressure), closes each chunk reservation so the JVM can reclaim the chunk, and returns the bare coordinates to the ticketless cold buffer (unkeptLocations) via the newLockFreeLocationBuffer.offerSilently- both buffers key the same DB row, so the move issues zero writes. A small reserve of hot locations is left intact so waiting players keep being served while the heap recovers. Covered by a newLockFreeLocationBufferTestcase. -
The multi-world prefab now also repoints each world's
worlds/<world>.ymlat its synthesised region, so the per-world regions are actually used. Applying themulti-worldprefab synthesised oneregions/<world>.ymlper loaded world (cloningregions/default.ymlwithworld: "<world>") but left everyworlds/<world>.ymlstill pointing itsregionfield at the shareddefaultregion, so the new per-world regions were inert.PrefabApplier.apply(..., worldNames)now emits a sparseworlds/<world>.ymloverlay settingregion: "<world>"for each synthesised world (the default-mapped overworld, which keeps thedefaultregion, is left untouched), andPrefabApplyCmdseeds each world's authoritative currentregionfrom the in-memoryRTP.configsworld parser (every loaded world has one by definition, even when noworlds/<world>.ymlfile exists on disk) so the diff reports the true delta (e.g.defaultto<world>). The merge is idempotent (a world already pointing at its own region yields no diff) and preserves any other keys in the world file. Covered by a newMultiWorldExpanderTestcase. -
The multi-world prefab now repairs the cloned vertical adjustor for nether/end worlds, so
/rtpinto a synthesised_netherregion no longer fails every attempt withvert/no-stand-y. The prefab clones the overworldregions/defaulttemplate (sky-light-requiredLINEARvert reaching the build limit) verbatim into each world; in the nether (solid bedrock ceiling, no sky light) and the end (no sky light) that vert rejects every candidate column, so the teleport burned all 32 attempts and timed out. The repair now runs through the canonicalNetherEndConfigAmender(the same helper/rtp config regionuses) at apply time, reading the liveregions/defaultvert as the template and the destination world's real height bounds: it dropsrequireSkyLightfor both*_netherand*_the_endand clampsmaxYbelow the nether ceiling.MultiWorldExpanderstays a pure transform and accepts the repair as an injectedRegionOverlayAmender, so the dimension rules have a single definition and the apply preview diff already reflects the repaired vert (overworld and unrecognised worlds are untouched). Covered by newMultiWorldExpanderTestandPrefabCommandTestcases. -
Prefab config writes now go through the live config system (
rtp.configs) instead of bespoke disk IO, andConfigParser.save()gained automatic.bakrotation. Backup write/rotate/restore/prune logic was extracted into a sharedConfigBackupshelper;ConfigParser.save()now snapshots the current file as a rotating<name>.bak.<ts>sibling before overwriting (retention via the newConfigParser.bakRetention, best-effort so a backup failure never blocks the save).PrefabConfirmCmdwrites each file through the matching liveConfigParser(set on the cached config +save()), creating brand-new per-world region files via the same region-creation path the config command uses (MultiConfigParser.addParser, which clonesregions/default.ymlwith its comments). The previous bespokePrefabDiskIO.writeWithBackuppath (and its comment-template seeding overload) is retained only as a fallback when the live config system is unavailable. This also fixes the prior loss ofregions/default.ymlcomments in synthesised per-world region files. Covered by the existingPrefabDiskIOTest/PrefabCommandTestsuites plus theconfigurationtests. -
Fabric on a late 1.21.x runtime (e.g. 1.21.11) no longer crashes the server tick with
NoSuchMethodErroronServerLevel.sendParticlesthe moment a/rtpparticle effect fires. The intermediary-remapped obf carriereffectsapi.fabric.FabricHandles.PlayerWrapper.spawnParticlehard-linked the single-booleansendParticles(ServerPlayer, ParticleOptions, boolean, double, double, double, int, double, double, double, double)overload, which exists on 1.20.x/early 1.21.x but was replaced on later 1.21.x by a two-boolean (longDistance,overrideLimiter) overload, so the older descriptor failed to link at runtime and crashed the tick thread. The carrier now tries the single-boolean overload first and, onNoSuchMethodError, falls back to the two-boolean overload located reflectively by its parameter shape (a name lookup is unreliable because Loom remaps the Mojmap name to its intermediary alias). The deobf MC 26.x carrier (FabricHandlesUnobf) already used the two-boolean overload and is unchanged.