Skip to content

3.2.0

Latest

Choose a tag to compare

@DailyStruggle DailyStruggle released this 22 Jun 16:07

[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 LeafRTPGuiAddon on 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 /rtp now 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 extracted addons/LeafRTPGuiAddon.jar (the addons/ 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 public rtp-api/SPI surface is unchanged.

Added

  • The bundled LeafRTPCountdownAddon now shows its delay and queue countdowns as an on-screen boss-bar instead of chat spam, via a new per-player progress-bar API. RTPPlayer gains two default methods, showProgressBar(id, title, progress) and clearProgressBar(id), that render a personal, single-player boss-bar (distinct from the existing server-wide, permission-scoped RTPServerAccessor.updateProgressBars surface). The Bukkit adapter (BukkitRTPPlayer) implements them over Bukkit BossBars 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-api event surface (RTPAPI.onPrefabApplied(Consumer<PrefabAppliedEvent>), returning an AutoCloseable for unregistration) fires a fire-and-forget, post-apply notification after /rtp admin prefab ... confirm writes 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 of PrefabChange (keyPath / oldValue / newValue), and whether the reload succeeded. The dispatcher (PrefabEventDispatcher) mirrors the existing player-lifecycle hook pattern: a CopyOnWriteArrayList of 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 new PrefabCommandTest cases.

  • 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 to true, was false) gates a new on-load harvest: whenever the server loads a chunk for any reason, the chunk's center-column biome is recorded into the MemoryShape of each configured region whose range contains that column, feeding biome recall / biome-weighted selection without any extra chunk I/O. A new always-registered OnChunkLoad listener (Bukkit/Paper/Folia) forwards loaded chunks to SelectionAPI.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 by SelectionAPIObserveChunkLoadTest (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 gui command. The LeafRTPGuiAddon multi-loader jar is bundled inside both the Pro and lite RTP jars (under bundled-addons/) and self-extracted into <pluginDir>/addons/ on first run only - when the addons/ 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 gui subcommand (permission rtp.gui) lives in rtp-core and opens whatever menu an addon bound to the bare /rtp root 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 /rtpgui command, which never appeared when the addon was loaded from the addons/ folder (its JavaPlugin entry point does not run there). The Bukkit chest renderer is now also published via a MenuRenderer META-INF/services descriptor so it registers on the ServiceLoader load path (addons-folder / bundled), where the entry point is skipped. Covered by AddonRegistryTest (2 new cases).

  • The lockAfterUses usage cap now survives a restart, reaching parity with the already-persisted cooldown (ADR-068). The BetterRTP LockAfter rolling-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 new TeleportLimitStore seam (rtp-core) now fronts the usage-cap guards (/rtp command and the addon-facing RTPAPI.teleport) and the success counter; its LocalTeleportLimitStore write-through-persists each player's {count, start} window to the configured database (the file-backed YamlFileDatabase in 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 by TeleportLimitStoreTest (4 cases).

  • The RTP-lite assembly variant now ships the full base safety.yml instead of a trimmed lite-specific copy (ADR-024 2026-06-20 amendment). The lite-specific safety.yml overlay 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 (the SafetyTokenParser / SafetyCompilationCache / SafetyTokenExpander already ship in the lite jar); only the rtp-tags module (tags/, tagsRefresh.yml) remains a full-edition feature.

Changed

  • The LINEAR vertical 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 global LINEAR default direction moved from 0 (bottom-up) to 2 (middle-out) - the same setting already seeded for _nether/_the_end regions. The dimension-specific direction override is removed from NetherEndConfigAmender (its nether maxY<=128 clamp and requireSkyLight=false seeding are unchanged), since middle-out is now the baseline. Existing region configs that set direction explicitly are unaffected.

  • The shipped default overworld region now uses the LINEAR vertical adjustor with middle-out search (direction: 2) and requires sky light (vert.requireSkyLight: true) so /rtp lands players on open-sky surfaces. regions/default.yml switches vert.name from JUMP (which has no scan-direction concept) to LINEAR with direction: 2, matching the middle-out search already used for nether/end, and flips requireSkyLight from false to true. Nether/end regions created from this default still seed requireSkyLight: false automatically via NetherEndConfigAmender (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-plugin into the new bundled LeafRTPClaimAddon (ADR-069, superseding ADR-019). The 12 claim/protection checkers (WorldGuard, GriefDefender, GriefPrevention, Towny Advanced, SaberFactions, FactionsBridge, Lands, RedProtect, Residence, CrashClaim, HuskClaims, KingdomsX), the IntegrationsKeys enum, and integrations.yml left 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 on PlatformFamily.BUKKIT. The addon jar is bundled inside both the Pro and lite RTP jars (under bundled-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 claim compileOnly dependencies and their dedicated soft-dep Maven repositories are removed from rtp-plugin/build.gradle. Behaviour change for operators: the per-locale integrations.yml translations that previously shipped in the core plugin are dropped (the addon ships an English integrations.yml); re-localizing the addon config is tracked follow-up work.

Fixed

  • requireSkyLight: true no 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 full 15 before relighting, so a roofed cave column passed the gate. Both adjustors now derive the gate from block data instead (the same computeColumnSkyFloor approach 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 new LinearAdjustorTest regression case (roofed cave with a stale full sky-light is rejected).

  • The bare /rtp GUI (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 Bukkit String[]-args onCommand path; Fabric (RTPCmdFabricRoot.onCommand) and NeoForge dispatch a bare /rtp straight through RTPCmd.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 of compute(...) for a bare invocation (no args), mirroring the Bukkit path and releasing the processingPlayers lock when the action handles the command; the Bukkit String[] path still short-circuits before compute(...) when it handles the command, so the action never double-fires. Covered by two new ReqApiF006RootActionTest cases exercising the compute() entry point.

  • Fabric on the deobfuscated MC 26.x runtime no longer crashes with NoClassDefFoundError: net/minecraft/class_3222 the moment a /rtp effect (sound / particle / potion) fires. The common effects resolve the player through HandleRegistry.wrapPlayer, which on Fabric routed to the shared effectsapi.fabric.FabricHandles. That provider lives in the Loom intermediary-remapped :effects-api module, 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-native effectsapi.fabric_unobf.FabricHandlesUnobf (compiled with no mappings step, so it names Mojang types directly and absorbs 26.x API drift: (ServerLevel) player.level(), record-shape GameProfile#name() with fallback, the 2-boolean sendParticles overload, reflective/packet teleport), and FabricEffectsInitializer.registerAll() registers it instead of the intermediary FabricHandles. 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 FabricHandlesUnobf fix above) instead of silently re-shipping stale carrier bytecode. The unobf carriers are appended into the Loom-remapped jar by the remapJar / remapLiteJar doLast merge step, but those tasks only declared a dependsOn on the carrier jars - the carriers are NOT part of the task's inputFile (which is shadowJar/shadowLiteJar, where the bundled :effects-api / rtp-fabric-common are the intermediary-remapped variants). So a carrier-only source change left the shadow jar's bytes identical, remapJar stayed UP-TO-DATE, its doLast never re-ran, and the shipped jar kept the previously-merged carrier classes. The carrier configurations are now declared as task inputs.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-neutral effectsapi.common.effects.PotionEffect carried a Bukkit/Folia entity-thread dispatch seam in a static lambda whose functional-interface (EntityDispatcher) descriptor named org.bukkit.entity.Player / org.bukkit.plugin.Plugin. Resolving that descriptor forced eager loading of org.bukkit.entity.Player the instant the class was instantiated, so FabricEffectsInitializer.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 own class_7923 link failure. The seam (EntityDispatcher, entityDispatcher, applyOnEntityThread) was moved verbatim to the new Bukkit-only effectsapi.bukkit.BukkitPotionDispatch; the common effect now links on every platform and applies potions through the platform-neutral PlayerHandle#applyPotionEffect. Covered by the relocated FoliaDispatchTest and EffectsApiCommonNoPlatformImportsTest.

  • NeoForge 26.1 server no longer crashes with a 60s ServerWatchdog timeout when /rtp loads a chunk. V26_1_R1NeoForgeVersionAdapter#requestFullChunkAsync reflectively invoked vanilla's public ServerChunkCache#getChunkFuture(..., create=true) on the server tick thread (the live-load path dispatches it via MinecraftServer#execute). On the main thread that public entry point parks the tick thread on mainThreadProcessor.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 inner getChunkFutureMainThread (same signature), which returns the pending future without blocking while the chunk system settles it off-thread; create=true still 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 chunk and starves the location cache. V26_1_R1NeoForgeVersionAdapter#requestFullChunkAsync invoked vanilla's getChunkFuture(..., create=true) without first applying a load-ticket, so on this runtime the chunk holder never reached FULL status and the future resolved with no ChunkAccess - 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 /rtp instead of flooding [ScanTask] INSTANT BYPASS: Chunk manager returned a null chunk and starving the cache. The rtp-fabric-v26_1_R1 / rtp-fabric-v26_2_R1 carriers never overrode requestFullChunkAsync, so the unobf chunk path (FabricRTPWorldUnobf.loadLiveChunk) fell through to the throwing getChunkFull stub default and no chunk ever generated. Both carriers now implement requestFullChunkAsync the 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 typed ServerChunkCache#addTicketWithRadius / #removeTicketWithRadius pair, then invoke the public getChunkFuture(..., create=true) off the server tick thread so vanilla takes its non-blocking supplyAsync(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 with nullChunk/asyncLoadNull). The runtime world is not the common FabricRTPWorld / FabricRTPWorldUnobf but the per-version V26_1_R1FabricRTPWorld / V26_2_R1FabricRTPWorld (constructed via FabricVersionAdapter#createWorld), which carry their own getChunkAt. That path invoked the public getChunkFuture(..., create=true) on the server tick thread with no load-ticket, so on the deobf MC 26.x chunk system the future resolved with no ChunkAccess and every attempt was attributed nullChunk/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 public getChunkFuture is invoked off the tick thread (where vanilla takes its non-blocking supplyAsync(mainThreadProcessor) branch), and the method resolver prefers the public getChunkFuture over the inner getChunkFutureMainThread so the off-thread self-pump branch is taken. The temporary ticket is released once the generation future settles.

  • NeoForge 26.1 no longer spams NoSuchMethodError every tick while rendering a /rtp scan progress bar. NeoForgeProgressBars (in the version-agnostic rtp-neoforge-common, typechecked against the MC 1.21.x mappings) hard-linked the ServerBossEvent(Component, BossBarColor, BossBarOverlay) constructor, whose signature drifted on MC 26.1, so every progress-bar update threw NoSuchMethodError from the scheduler tick. The construction site now resolves a matching public ServerBossEvent constructor by reflection once (cached), accepting any shape that exposes a Component, BossBarColor and BossBarOverlay parameter and filling any extra parameters with sensible defaults (a fresh UUID per bar, 0f, false, or null); 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 maxHeapPercent with the "pausing background cache generation" warning repeating forever. The HeapPressureMonitor gate paused cold->hot promotion under pressure, which stopped the retained set from growing but never shrank it - each kept location pins a chunk via its ChunkReservation, so heap usage stayed pinned at the threshold indefinitely. Region.execute now 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 new LockFreeLocationBuffer.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 new LockFreeLocationBufferTest case.

  • The multi-world prefab now also repoints each world's worlds/<world>.yml at its synthesised region, so the per-world regions are actually used. Applying the multi-world prefab synthesised one regions/<world>.yml per loaded world (cloning regions/default.yml with world: "<world>") but left every worlds/<world>.yml still pointing its region field at the shared default region, so the new per-world regions were inert. PrefabApplier.apply(..., worldNames) now emits a sparse worlds/<world>.yml overlay setting region: "<world>" for each synthesised world (the default-mapped overworld, which keeps the default region, is left untouched), and PrefabApplyCmd seeds each world's authoritative current region from the in-memory RTP.configs world parser (every loaded world has one by definition, even when no worlds/<world>.yml file exists on disk) so the diff reports the true delta (e.g. default to <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 new MultiWorldExpanderTest case.

  • The multi-world prefab now repairs the cloned vertical adjustor for nether/end worlds, so /rtp into a synthesised _nether region no longer fails every attempt with vert/no-stand-y. The prefab clones the overworld regions/default template (sky-light-required LINEAR vert 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 canonical NetherEndConfigAmender (the same helper /rtp config region uses) at apply time, reading the live regions/default vert as the template and the destination world's real height bounds: it drops requireSkyLight for both *_nether and *_the_end and clamps maxY below the nether ceiling. MultiWorldExpander stays a pure transform and accepts the repair as an injected RegionOverlayAmender, 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 new MultiWorldExpanderTest and PrefabCommandTest cases.

  • Prefab config writes now go through the live config system (rtp.configs) instead of bespoke disk IO, and ConfigParser.save() gained automatic .bak rotation. Backup write/rotate/restore/prune logic was extracted into a shared ConfigBackups helper; ConfigParser.save() now snapshots the current file as a rotating <name>.bak.<ts> sibling before overwriting (retention via the new ConfigParser.bakRetention, best-effort so a backup failure never blocks the save). PrefabConfirmCmd writes each file through the matching live ConfigParser (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 clones regions/default.yml with its comments). The previous bespoke PrefabDiskIO.writeWithBackup path (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 of regions/default.yml comments in synthesised per-world region files. Covered by the existing PrefabDiskIOTest/PrefabCommandTest suites plus the configuration tests.

  • Fabric on a late 1.21.x runtime (e.g. 1.21.11) no longer crashes the server tick with NoSuchMethodError on ServerLevel.sendParticles the moment a /rtp particle effect fires. The intermediary-remapped obf carrier effectsapi.fabric.FabricHandles.PlayerWrapper.spawnParticle hard-linked the single-boolean sendParticles(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, on NoSuchMethodError, 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.