Skip to content

ADR-062 — Biome-probability weighting for location selection

Status: Accepted Date: 2026-06-10

Context

A competitor (EzRTP) advertises "rare-biome optimization (weighted search + hotspot tracking)" - actively steering teleports toward scarce biomes (mushroom fields, cherry grove, ...) rather than rerolling until one happens to appear. During a feature-gap comparison this surfaced as something LeafRTP does not do.

Two distinct capabilities must not be conflated:

  • Bad-sector avoidance (already shipped): the persistent spatial memory (ADR-001 spiral + the .scan bad-location bitmap) records sectors that failed safety checks so the spiral selector skips known-bad ground. This is avoidance - it shrinks the candidate space away from unsafe coordinates.
  • Biome steering (not shipped): biasing selection toward a requested or rare target biome. This is the EzRTP feature.

The accuracy footing matters here and is a genuine LeafRTP advantage. Plugins that resolve biome via the live generator/noise-map lookup (World#getBiome without forcing a populated chunk) can return the wrong biome on a world pregenerated elsewhere or migrated across a Minecraft version, where Mojang's seed-based biome assignment has drifted from what is written to disk. LeafRTP already reads biome data from the populated .mca palette through the off-tick Anvil pre-filter (ADR-016), so any weighting built on top of LeafRTP's biome data stays authoritative on exactly the worlds where the noise-map approach is wrong.

/rtp biome:<x> already exists as a hard filter (reject candidates whose biome != target). What is missing is a soft, weighted draw that biases the selection distribution toward one or more biomes without an unbounded reroll loop, and the per-region biome occupancy data needed to make that draw cheap.

Decision

Accepted and delivered in phases. Phase 1 (equal-probability draw), Phase 2 (configurable per-biome weights), and Phase 3's registry-aware gray-space steering (point 5) are implemented. The Phase 3 cost-tiered biome-sampling SPI (noise-sample / Anvil / generate) is scaffolded with safe defaults; the per-version noise-sample fast path itself remains D-005 gated Future Work.

Add an optional biome-probability weighting layer over the existing, Anvil-sourced per-region biome occupancy data:

  1. Biome occupancy data (existing). The per-region spatial memory already records, per recorded run, which biome that run yielded (the biomeRecall machinery: MemoryShape.getBiomeKeys/getBiomePrefixSums), sourced from the Anvil palette read that already happens during /rtp scan and pipeline verification (ADR-016) - no new chunk loads and no new storage are introduced by this ADR. The weighting layer is built on top of this pre-existing data.
  2. Weighted draw - Phase 1 (implemented): equal-probability per biome. A single advanced/biomes.yml toggle, biomeWeighted (default false, effective only when biomeRecall is active and a non-default biome set is requested), changes a biome-filtered recall draw so each requested biome is selected with equal probability, rather than in proportion to how many recorded runs it occupies. This counteracts run-count dominance: a common biome's thousands of runs no longer drown out a rare requested biome (mushroom fields, cherry grove). The draw picks a requested biome uniformly, then a run within it weighted by run width (keeping placement spatially uniform inside the chosen biome), then a uniform offset within the run. Selection stays bounded - a finite weighted pick over a pre-mapped set, no unbounded reroll.
  3. Weighted draw - Phase 2 (implemented): configurable per-biome probabilities. A second advanced/biomes.yml knob, the biomeWeights map (biome name -> non-negative relative weight, default the single no-op plains: 1.0 example), lets operators express explicit, non-equal per-biome weights. When biomeWeighted is on and the map is non-empty, the weighted draw picks the target biome in proportion to these weights instead of equally; biomes absent from the map use weight 1.0, a weight of 0.0 suppresses a biome, and an all-zero (or empty) weight set falls back to the equal-probability pick. Phase 1's equal-weight draw is the degenerate (all-equal) case of this more general design. Weights are read once per /rtp invocation (PregenState), so the hot draw stays allocation-light and bounded.
  4. Graceful fallback. With biomeWeighted disabled the draw is identical to the legacy uniform-over-runs recall draw; with no recall data yet (cold region) behavior is identical to today's uniform bounded spiral. Steering only has an effect once /rtp scan (or organic traffic) has populated enough biome-recall data.
  5. Registry-aware gray-space steering - Phase 3 (implemented). Phases 1 and 2 draw only over biomes already present in recall memory, which has two limitations that Phase 3 addresses: (a) a requested biome that the world can produce but has not been recorded yet was implicitly treated as weight 0 (presumed absent from incomplete scan data), and (b) once a rare biome has a single recorded run, the width-weighted within-biome draw funnels every weighted teleport onto that one coordinate - and a recall run is only a biome match, not a safety-vetted destination (final safety is still re-verified downstream per S-001). Phase 3 reframes the draw around the world's full biome registry (RTPServerAccessor.getBiomes(world), resolved once per /rtp in PregenState). Area not yet recorded is treated as gray space in which any registered biome of that world may appear, so a registered-but-unrecorded requested biome stays reachable through bounded spiral exploration rather than being silently excluded; only a biome absent from the registry is a true 0. Concretely, each recorded biome defers a run-count-proportional share of its weight to gray space (PregenTask.grayFraction: an unrecorded biome is fully gray, a single-run biome defers most of its weight, a biome with >= GRAY_SPACE_MIN_RUNS distinct runs is fully recall-steered), and with probability equal to the gray-space share of total weight (PregenTask.graySpaceProbability) the draw explores a fresh bounded-spiral position whose biome is confirmed downstream instead of drawing from recall. A higher configured weight on a thinly-recorded biome therefore defers more to exploration, so a weight cannot amplify single-run clustering. Forced recall (biomeRecallForced) opts out of exploration (it must draw from memory). To answer "what biome is at (x,z)?" cheaply where possible, a cost-tiered sampling SPI is scaffolded (BiomeSampleCapability + RTPServerAccessor.biomeSampleCapability / sampleBiome, default GENERATE_REQUIRED / null): (i) noise-map sample - cheapest, no chunk I/O, valid only on a generator we can sample deterministically (see the vanilla-detection note in Future Work); (ii) Anvil .mca read - the current path (ADR-016), authoritative for already-generated chunks on any generator (custom namespaces included); (iii) chunk generation - worst case, already incurred by the L2 (cold/unkept) cache fill and the periodic random sampler, and never run synchronously on the main thread (S-005). The implemented gray-space draw relies on tier (iii) plus the existing downstream pipeline biome verification; tiers (i)/(ii) as a pre-filter are the remaining gated Future Work.

Bounded-algorithm and S-005 invariants are preserved: the draw operates over pre-mapped, Anvil-sourced runs and never triggers a synchronous chunk load to discover a biome. Phase 3's gray-space and generation tiers reuse the existing async cache-fill / sampler paths and never add a main-thread chunk load.

Future Work

  • Tiered biome-sampling SPI noise-sample path (Phase 3 follow-up). The capability classification and sampleBiome entry point exist (BiomeSampleCapability + the default RTPServerAccessor.biomeSampleCapability / sampleBiome methods), but every platform currently returns the conservative GENERATE_REQUIRED / null default, so the gray-space draw uses bounded-spiral exploration + downstream verification rather than a cheap pre-filter. Wiring an adapter to detect a deterministic noise biome source and answer sampleBiome from it needs version-specific net.minecraft.* access and therefore cannot live in rtp-core/rtp-api (no platform imports) - it routes through the adapter and returns the capability enum so core branches without knowing how. This crosses the D-005 propose-before-implementation threshold; a dedicated proposal/ADR precedes that code.
  • Vanilla detection. "Can we noise-sample?" is not simply "no plugin generator": a datapack/custom dimension can install a non-noise biome source. The real test is whether the live biome source is a known deterministic noise source; resolve it once per world and cache it (like the registry set and biomeWeights are resolved once per /rtp in PregenState).
  • Version targeting. Phase 3's per-version noise-sampling wiring targets MC 1.21 as the minimum (the floor for the current best "modded" reference, the latest All The Mods modpack); MC 26+ is the forward-looking baseline. Pre-1.21 runtimes fall back to the Anvil + recall + gray-space-via-generation tiers and gain no noise-sample fast path.

Alternatives Considered

Alternative Why Rejected
Do nothing; rely on the existing /rtp biome:<x> hard filter Hard filtering can degrade to many rerolls when the target biome is rare, which is exactly the bounded-latency problem LeafRTP exists to avoid. A weighted draw over pre-mapped sectors stays bounded.
Copy EzRTP's live World#getBiome weighted search Inaccurate on pregenerated / version-migrated worlds (noise-map drift), and re-introduces on-demand biome lookups. Contradicts the Anvil-first accuracy guarantee and risks S-005.
Add named "triangle"/"diamond" shapes and other surface-level EzRTP parity items in the same change Out of scope and declined separately - a triangle is a 3-vertex Polygon and a diamond a rotated square; no new capability.

Consequences

  • Positive: Matches the competitor's rare-biome feature while staying bounded and accurate, and reuses the existing Anvil-sourced biome-recall data (no new storage in Phase 1); the marketing story ("biome targeting that's correct on pregenerated and upgraded worlds, where noise-map plugins land you in the wrong biome") is defensible because it rests on the existing Anvil read.
  • Negative / Trade-offs: Biome-recall data must be populated (scan or traffic) before steering has any effect, so the knobs silently no-op on a cold region or when biomeRecall / a non-default biome set is not in play - an operator enabling only biomeWeighted sees no change until those preconditions hold. The biomeWeights map is gated by the biomeWeighted master toggle (the map alone does nothing while biomeWeighted is false). The knob name biomeWeighted denotes "equalize across biomes" (the inverse of the legacy width-proportional draw), which is easy to misread as "weight by occupancy"; the config comment and PregenState.biomeWeighted Javadoc carry the clarification. A dedicated per-region/menu surface for the weights (rather than the single global advanced/biomes.yml map) remains a possible future refinement.
  • Clustering is mitigated by Phase 3's gray-space steering. A registered-but-unrecorded requested biome is no longer an implicit weight 0 (it stays reachable via bounded-spiral exploration), and a thinly-recorded biome defers a run-count-proportional share of its weight to exploration so a single recorded run can no longer funnel every weighted teleport onto one coordinate. The exploratory candidate's biome is confirmed by the existing downstream pipeline verification (a recall run is a biome match, not a vetted destination). A reasonably complete /rtp scan still improves accuracy and reduces wasted exploratory attempts, and the cheap noise-sample pre-filter (Future Work) would tighten exploration further on samplable worlds.

References

  • ADR-001 - bounded Archimedean-spiral selection.
  • ADR-016 - Anvil .mca pre-filter (biome source of truth).
  • ADR-034 - memory-shape catalog (where a future per-region weighting surface could live).
  • RTPServerAccessor.getBiomes(RTPWorld) / RTPAPI.getBiomes(world) - the world biome registry that Phase 3 gray-space steering consults; AnvilChunkView.getBiomeAt / getBiomesPresent - the existing Anvil sampling tier.
  • rtp-core/.../commands/RTPCmd.java - existing /rtp biome:<x> hard-filter path.
  • advanced/biomes.yml biomeWeighted toggle and biomeWeights map; PregenTask.drawWeightedBiome (equal-probability and double[]-weighted overloads), PregenState.biomeWeighted / PregenState.biomeWeights - Phase 1 + Phase 2 draws, covered by BiomeWeightedDrawTest. The shipped biomeWeights map enumerates the full vanilla biome set (overworld/nether/end) at the no-op weight 1.0 so it doubles as an easy-to-edit, easy-to-update reference list (an all-1.0 map is behaviourally identical to the prior single-plains default); operators bias a biome by changing its number, suppress it with 0.0, or append a modded biome id, and append newly-added vanilla biomes as Minecraft versions ship them.
  • BiomeSampleCapability enum; RTPServerAccessor.biomeSampleCapability / sampleBiome default SPI methods; PregenState.worldBiomeRegistry / PregenState.biomeSampleCapability; PregenTask.grayFraction / PregenTask.graySpaceProbability / GRAY_SPACE_MIN_RUNS - Phase 3 registry-aware gray-space steering, covered by BiomeWeightedDrawTest.