Port seeded Age completion and Accelerated pages to 26.3 beta
Build / build (push) Waiting to run

This commit is contained in:
dnorberg committed 2026-11-05 15:19:11 -05:00
1 parent 40770841ee
commit f13bedd26f
20 files changed
+976 -63

No files matched your search

+1 -1
View File
@@ -20,7 +20,7 @@ feature completion, and random structure selection. Nether/End noise-biome
materials now follow their native rules. The beta does not yet provide full
1.21.1 gameplay or optional-integration parity.
See the [26.3 beta guide](ports/mc263/README.md) and
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.93-beta.1.md).
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.98-beta.1.md).
Keep the builds and their saves separate, and back up worlds before updating.
## Getting started
+149
View File
@@ -10,6 +10,17 @@ tracks issues #97–#103 under parent #96.
## Version boundary
Local slice `1.0.1.98-beta.1` ports the 1.21.1 Accelerated environment page:
stacking extra random block ticks in already-loaded player-nearby chunks,
eight-chunk/16,384-attempt default budgets, fair rotating section allocation,
and an Age-local saved page count. The 26.3 LOCAL config holds these limits.
Entities, machines and scheduled ticks are unaffected. Existing Ages remain
unaccelerated; random Age inclusion and instability scoring are not added.
The suspected host-seed biome zoom mismatch was checked against NeoForge's
patched ServerLevel constructor: seedOverride already replaces it correctly,
so the backend seed path was left unchanged. Native Cave policy issue #111
remains separate future work for both Minecraft tracks.
Minecraft 26.3/NeoForge uses Java 25, while Rivencraft 1.21.1 uses Java 21.
Build and publish separate artifacts; do not widen the Minecraft dependency
range of the 1.21.1 JAR. The official [26.3 release notes](https://www.minecraft.net/en-us/article/minecraft-java-edition-26-3),
@@ -1801,6 +1812,144 @@ fresh chunks after full restart again used Nylium/End Stone. No full feature
or entity chunk scans were run. The server was stopped and temporary world
deleted. Nothing was committed, pushed or published.
## Structure-aware biome completion
`1.0.1.94-beta.1` adapts root `AgeBiomeSelection.completeStructureBiomes` for
explicit non-Native biome controllers. It reads compatible biome IDs from
the active registered structure sets and shares this lookup with random
structure eligibility, avoiding two different compatibility definitions.
Repeated structure pages produce one requirement; Dungeons are excluded.
Single with no supplied biome chooses from the intersection of all structure
requirements and terrain-eligible biomes. No intersection leaves ordinary
seeded filling intact. An explicit Single biome is never rewritten.
Multi-biome completion adds one seeded compatible biome for each unmet
requirement where a terrain-eligible candidate exists, before ordinary minimum
filling. Root's salts, requirement order and shuffled candidate selection are
preserved. Existing biome order and duplicate weights remain, but a structure
can intentionally extend a pool beyond the usual minimum. Native and the
port's no-controller fallback remain unchanged. Impossible requirements do
not become new writing rejections: authored structure overrides still apply.
No saved format, network protocol or generated chunks are changed.
The completed-biome fixture accepts an optional structure page and checks
32 seeds for compatible Single/Cave Fortress choices, common Village/Stronghold
requirements, explicit End Single precedence, Native behavior, multi-biome
prefix preservation and avoiding unnecessary additions for already-compatible
or repeated structure pages. Existing biome plan codec checks remain in place.
The .94 build and disposable server checks passed. Village Single and Tiny
fixtures included compatible Plains; Cave Fortress Single selected Crimson
Forest. The 32-seed authoring assertions and the preexisting no-structure
completion checks passed. Saved generator pools survived unload/reload,
terrain reseed and full restart. The server was stopped and its temporary
world deleted. No structure-placement chunk scan was needed. Client book
authoring remains a manual check; no commit, push or publication occurred.
## Omitted biome-controller completion
`1.0.1.95-beta.1` ports the no-controller branch of root `AgeBiomeSelection`
and the first `AgeTraits.select` draw. The rank table and seed stream are
shared with `PortRandomAgeSelection` rather than duplicated. The production
seeded biome compiler uses that default when there is no explicit controller;
the registry-only diagnostic overload retains its exact older defaults.
Explicit controllers are unchanged. Native rolls with authored biomes, or
Single rolls with more than one biome page, fall back by authored count to
Single/Tiled/Medium. Other multi-biome rolls retain their distribution and
ordinary minimum-only filling, plus .94's structure requirements.
Void's omitted controller remains Single for empty pools and count-based
for supplied biome pages. Cave's omitted Native roll follows the port's
existing page-free seeded Single workaround; a native multi-cave source is
still not implemented. Explicit Native is not silently converted. No other
terrain, weather, lighting, color or body seed stream changes. Saved plans
remain frozen through restart and terrain reseed; old Ages are not rewritten.
The completed-biome diagnostic accepts `auto` and checks 128 seeds for all
nine distribution results, independence from unrelated weather pages,
authored prefix preservation, one/two-biome fallbacks, Void behavior and
structure-aware completion. Existing explicit-controller checks still run.
The .95 build and disposable-server checks passed. The expanded 128-seed
matrix covered all distributions, authored one/two-biome rules and Cave pools.
Created automatic Standard, Cave, Void and Cave Fortress fixtures, plus the
unchanged full-random command and explicit Single/Native Cave controls.
Generator pools survived unload/reload, reseed and full restart. A second seed
produced Small regions with three biomes, while explicit Native Cave retained
its Lush Caves source. No chunk-generation scan was needed. The server was
stopped and its temporary world deleted. Client authoring remains a manual
check; nothing was committed, pushed or published.
## Omitted terrain completion
`1.0.1.96-beta.1` adapts root `AgeRuntimeProfiles.selectForWriting`. A partly
authored book without terrain starts with the shared ranked terrain draw;
if unsupported, compatible profiles in stable ID order are indexed using
`floorMod(Long.hashCode(seed), count)`. Explicit terrain is never substituted.
Unlike root's feature-only candidate test, the port additionally preflights
registry-backed biome restrictions to avoid selecting an unloadable Cave
combination for supplied surface biomes. This is a deliberate compatibility
guard while the broader writing/instability recovery remains unported.
Binder availability checks now consider whether any supported terrain can
host an omitted-terrain book instead of assuming Standard. The server resolves
the actual terrain and biome plan once, then passes the resolved internal page
view to material/feature compilers. The inferred terrain is appended internally,
not inserted before original pages or written back to the book. Original
indices and ordered modifiers remain intact. Environment controllers use the
selected terrain, while sky colors and celestials still compile from original
pages. Page-free random books retain their original path and seed streams.
The chosen terrain and generator plans use existing saved fields, so no
network protocol change or retroactive terrain reroll is introduced.
The fixture checks 64 seeds for terrain coverage, deterministic selection,
original page prefixes, Cave-only Fortress support, implied-fluid constraints,
surface-biome compatibility, explicit terrain precedence and Binder
availability. Generator placement still uses the previously ported routines.
The .96 build and disposable server checks passed. The 64-seed assertions
covered all six terrains, deterministic fallback, page-prefix preservation,
explicit Flat, surface-biome protection and Cave-only Fortress availability.
Created Sky-only, Fortress-only, Water-only, Plains-only, explicit Flat and
full-random fixtures. A Water page retained Water as its implied lake fluid;
Fortress-only selected Cave with eligible biomes. Plans survived unload/reload,
reseed and full restart. The server was stopped and its temporary world
deleted. No chunk scan was needed; client Binder checks remain manual.
Nothing was committed, pushed or published.
## Native Cave regions
`1.0.1.97-beta.1` ports root `AgeBiomeSelection`'s full Native Cave pool and
`AgeDimensionService.createBiomeSource`'s Native-to-Tiny mapping. New Native
Cave plans retain distribution NATIVE with a saved recognized Cave/Nether
list, and the backend uses `PortScaledBiomeSource` at scale 0 (or Fixed for
a one-biome pool). Explicit Native biome pages remain prohibited. Both authored
and page-free/omitted-controller paths use the same eligibility query; the
temporary Native-to-Single Cave conversions are removed.
Native pool data is permitted only for Cave terrain at backend validation.
Other Native terrain sources are unchanged. Empty older Native Cave records
still load the old Lush source; old Single choices also remain frozen. New
Native pools are saved through the existing biome-plan codec without a new
network protocol, but earlier betas cannot decode populated Native pools, so
downgrading those saves is not supported. The current pool is passed to random
structure eligibility rather than incorrectly assuming Lush Caves.
The fixture verifies registry-derived pool ordering, old empty Native codec
defaults, the actual generator source, deterministic Tiny-region equivalence,
horizontal region variation and sampled pool membership without loading
chunks. The omitted-controller matrix now expects Native Cave to remain
Native with its full pool. This does not implement modded surface rules.
The .97 build and disposable-server checks passed: explicit and omitted
Native Cave, the full-random Native Cave path (seed 114), Native + Fortresses,
explicit Single and the legacy Lush-only control. The vanilla registry pool
contained nine biomes, including Sulfur Caves and all five Nether biomes;
the actual Native source passed Tiny-equivalence and sample-membership checks.
Pools survived unload/reload, reseed and full restart. The server was stopped
and its temporary world deleted. No chunk scan was needed. Client exploration
remains a manual check; nothing was committed, pushed or published.
## Verification of this first slice
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
+123 -1
View File
@@ -9,7 +9,7 @@ independent Age weather and lighting, multiple authored or seeded celestial
bodies and eclipses, biome/material completion, and random features/structures.
Instability, crystal portals, discovery/stations, writing recovery and optional
integration parity remain unfinished. The latest published beta is
[1.0.1.93-beta.1](RELEASE_NOTES_1.0.1.93-beta.1.md).
[1.0.1.98-beta.1](RELEASE_NOTES_1.0.1.98-beta.1.md).
Use a test world and keep a backup; do not
substitute this JAR for the 1.21.1 mod or import 1.21.1 saves.
@@ -1291,6 +1291,128 @@ and `age material_probe <name> [chunk_x chunk_z]`. The latter examines one
terrain-stage chunk (default 0,0) without feature, lighting or entity generation. Nothing is committed
or published without explicit request.
The local `1.0.1.94-beta.1` slice ports **structure-aware completion of explicit
biome distributions** from 1.21.1. Single Biome without a supplied biome now
prefers a biome compatible with every authored structure page, when a common
candidate exists. Cave + Single + Nether Fortresses therefore selects an
eligible Cave/Nether biome rather than an unrelated cave biome. If there is
no common candidate, ordinary seeded filling and authored structure overrides
remain available, as in root.
For multi-biome controllers, each uncovered structure requirement can add one
compatible biome from the terrain's available pool. This is an intentional
exception to ordinary minimum-only filling: an already-full pool may gain
another biome for its structure pages. Supplied biomes, their order and
duplicate weights remain intact. A supplied Single Biome is never replaced,
Native remains unchanged, and Dungeons impose no biome requirement. The
no-controller fallback remains unchanged in this slice.
Test Single Biome + Villages without biome pages, Cave + Single Biome + Nether
Fortresses, and a multi-biome book with structures. Compare an explicit End
Single Biome + Villages to confirm that authored override still works.
`age create_completed_biomes <name> <seed> <terrain> <distribution> [structure-page-id]`
now accepts an optional structure fixture. Existing Ages are not rerolled;
restart/reseed retains completed pools. Nothing is committed or published
without explicit request.
The local `1.0.1.95-beta.1` slice seed-completes **omitted biome controllers**
in partly authored books, using 1.21.1's first AgeTraits distribution draw.
Native, Single and Medium are common; the other supported distributions are
rarer. Explicit controllers still win. Supplied biomes are never discarded:
a Native roll with authored biomes falls back by count, as does a Single roll
with multiple biome pages. Other seeded multi-biome controllers are retained,
so a lone supplied biome may receive a second biome to meet the minimum.
Void without biome pages still uses The Void; supplied Void biome pages keep
their existing count-based behavior. An omitted controller that rolls Native
on Cave terrain uses the same seeded Single fallback already used by random
Ages, since the port has no native multi-cave source. Explicit Native Cave
behavior remains unchanged. Structure-aware completion from .94 applies to
the resulting nonnative controller. Full Link-Panel-only random generation
retains its existing selector and seeds.
Test new terrain-only books without biome controllers, one/two supplied biome
pages without a controller, and Cave + Nether Fortresses without a biome
controller. Compare an explicit Single Biome book and restart/revisit.
`age create_completed_biomes <name> <seed> <terrain> auto [structure-page-id]`
provides an omitted-controller fixture. Existing Ages are not rerolled.
Nothing is committed or published without explicit request.
The local `1.0.1.96-beta.1` slice seed-completes **omitted terrain pages** in
partly authored books. It starts with the same ranked terrain draw as random
Ages, following 1.21.1's `selectForWriting` model. If that terrain cannot host
the supplied features/materials, a deterministic fallback chooses a compatible
terrain in stable profile order. The port also preflights its current biome
restrictions, avoiding an automatic Cave choice for supplied surface biomes.
Explicit terrain still wins and incompatible explicit combinations remain
rejected. Missing-terrain Fortress books can now bind using Cave terrain;
unpaired fluids use supported surface noise terrain. No extra random features
are added to partly authored books. The original book page list stays intact;
the chosen terrain is appended only to internal generation input so material
and color/controller indices do not shift. Sky/color/celestial compilation
still reads the original pages. Fully random Link-Panel-only Ages retain their
existing selector. Already-created Ages do not change terrain.
Test several new books with Sky or a feature but no terrain page, Nether
Fortresses alone, and an unpaired Water page. Compare an explicit Flat book
and a Plains biome page without terrain; the latter must not choose Cave.
`age create_auto_terrain <name> <seed> <page-id>` exercises the production
selector for one-page fixtures. Restart and revisit to confirm persistence.
Nothing is committed or published without explicit request.
The local `1.0.1.97-beta.1` slice ports **Native Cave biome distribution** from
1.21.1. A new Cave + Native book now freezes the full recognized Cave/Nether
biome pool and distributes it with the existing seeded Tiny-region sampler,
instead of forcing Lush Caves. An omitted controller or random Cave Age that
rolls Native now uses that same pool rather than converting to Single.
Random structures inspect the saved pool, so eligible Nether structures are
no longer hidden behind an assumed Lush-only source.
Existing Single Cave Ages remain Single. Old empty Native Cave records keep
their Lush fallback; they are not silently expanded on load. Other Native
terrains remain unchanged, and explicit Native still rejects biome pages.
Pools persist across restart/reseed; reseeding can still change future region
layout. Modded biome surface-rule integration remains separate pending work.
Back up before updating and do not downgrade worlds containing new Native
Cave pools to earlier beta builds.
Test a **new** Cave + Native Biomes book without individual biome pages,
explore multiple regions, then restart/revisit. Compare an explicit Single
Cave book and an existing old Cave Age. The existing
`age create_completed_biomes <name> <seed> cave native` fixture also checks
the native source against Tiny's sampler without generating chunks.
Nothing is committed or published without explicit request.
### Accelerated random block ticks
The local `1.0.1.98-beta.1` slice ports the **Accelerated** environment page
from 1.21.1. Repeated pages stack: each adds three extra random positions per
eligible block section per tick. Only already-loaded chunks within one chunk
of players are candidates; no extra chunks are loaded. Defaults match 1.21.1:
at most eight candidate chunks and 16,384 attempted positions per Age tick,
shared across sections with rotating selection. These limits live in
`config/rivencraft-local.toml` under `accelerated_age`; setting
`max_random_tick_attempts_per_tick` to zero disables the extra ticks.
This affects random-ticking blocks such as crops and saplings, not entities,
machines, scheduled block ticks, or the day/night clock. The authored page
count persists with the Age across unload/reload, reseeding and restart.
Existing Ages default to zero; pageless random Ages do not gain Accelerated
automatically. Instability costs remain part of the pending instability port.
Test two otherwise comparable new Ages, one with a single Accelerated page
and one with repeated pages. Plant crops/saplings with suitable light and
space, remain nearby, and compare growth. Revisit after restart. The operator
command `/rivencraft_port age acceleration <name>` reports the saved count
and current budgets. Disposable-server fixture
`age create_accelerated <name> <seed> <pages>` checks authoring and bounded
attempt arithmetic and creates a Flat Age; it does not prove live growth.
Build and disposable dedicated-server checks passed for counts 0, 1 and 32,
overflow/disabled budget arithmetic, unload/reload, reseed and full restart.
No player was connected for the server checks, so live growth remains an
in-game acceptance check. The test server was stopped and its world removed.
From the repository root:
```sh
@@ -0,0 +1,63 @@
# Rivencraft 26.3 beta 1.0.1.98-beta.1
Collects the player-validated port slices .94–.98 since 1.0.1.93-beta.1.
This beta is not yet feature-complete with Minecraft 1.21.1.
## Seeded terrain and biome completion
- Books without a Terrain page now choose terrain using the 1.21.1 weighted
selector. Incompatible choices are replaced with a seeded compatible terrain;
explicit Terrain pages retain their authored behavior.
- Omitted biome controllers now use the seeded weighted distribution selector.
Supplied biome pages are retained, with missing minimum slots completed.
Void without biome pages still uses The Void. Link-Panel-only random Ages
retain their existing selection path.
- Missing nonnative biome pools account for authored structures. Single chooses
a terrain-compatible biome satisfying all structure requirements where one
exists. Multiple-biome distributions add compatible biomes for unmet structure
requirements before completing their ordinary minimum. Explicit Single
choices are never replaced.
- New Native Cave Ages freeze the full recognized Cave/Nether biome pool and
distribute it using seeded Tiny regions, matching the 1.21.1 implementation.
This includes recognized modded cave biomes. Existing empty Native Cave
profiles retain their previous Lush Caves fallback; explicit Single is unchanged.
- Removing Nether-style biomes from the default Native Cave pool is separate
planned work for both versions in issue #111, not part of this release.
## Accelerated pages
- Ported the Accelerated environment page. Repeated pages stack, each adding
three extra random positions per eligible block section per tick near players.
- Defaults match 1.21.1: at most eight already-loaded candidate chunks and
16,384 extra random-tick attempts per Age tick, shared fairly across sections.
No extra chunks are force-loaded.
- Limits are configurable in `config/rivencraft-local.toml`, under
`accelerated_age`. A zero attempt budget disables the extra ticks.
- Page counts persist across unload/reload, reseeding and restart. Existing
Ages default to zero. Pageless random Ages do not gain Accelerated automatically.
- This currently affects random-ticking blocks such as crops and saplings.
Machine/block-entity acceleration is deferred for both versions under issue
#112; entities and scheduled block ticks are also unaffected. Instability
scoring remains part of the unfinished instability port.
- Operator command `/rivencraft_port age acceleration <name>` reports the
saved page count and current budgets.
## Compatibility and validation
- Requires Minecraft **26.3**, NeoForge **26.3.0.99**, and **Java 25**. Dynamic
Dimensions and Bad Packets are not required. Minecraft 1.21.1 is unchanged.
- Catalog protocol remains **10**. Use matching client/server builds.
- Player in-game checks passed. Builds and targeted disposable-server checks
covered seeded completion, Native Cave sampling, tick-budget arithmetic,
unload/reload, reseeding and restart. Test servers were stopped and their
worlds removed. This was not a full automated GameTest run.
- Back up worlds before updating. New populated Native Cave profiles cannot
be read by earlier beta builds; do not downgrade those saves. Keep Minecraft
1.21.1 and 26.3 saves separate.
- Full writing recovery, discovery/stations, instability, crystal portals and
optional integration parity remain unfinished. Random-page eligibility config
and modded biome surface-rule integration also remain pending. Creative page
availability does not imply full support.
Report issues and help improve the wiki at
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
+1 -1
View File
@@ -7,4 +7,4 @@ neo_version=26.3.0.99
mod_id=rivencraft
mod_name=Rivencraft
mod_license=LGPL-3.0-or-later
mod_version=1.0.1.93-beta.1
mod_version=1.0.1.98-beta.1
+2 -1
View File
@@ -85,10 +85,11 @@
"symbol.rivencraft.feature.abandoned_camps": "Abandoned Camps",
"symbol.rivencraft.feature.abandoned_camps.description": "Allow abandoned camps on surface noise terrain, including outside their usual biomes.",
"symbol.rivencraft.block.generated.description": "Modify the next compatible feature. Without a following feature, choose a compatible feature from the Age seed.",
"symbol.rivencraft.biome.native.description": "Use the terrain's native biome source; do not include Biome pages.",
"symbol.rivencraft.biome.native.description": "Use the terrain's default biome source. Cave uses Tiny regions from recognized Cave/Nether biomes. Do not include Biome pages.",
"symbol.rivencraft.feature.dungeons.description": "Add monster-room dungeon attempts throughout surface noise or Cave terrain, including Nether cave biomes. Not Flat or Void.",
"symbol.rivencraft.biome.single.description": "Use one biome across the Age. If omitted, choose an eligible biome from the Age seed.",
"symbol.rivencraft.terrain.flat.description": "A level, biome-decorated landscape with its foundation at Y=0.",
"symbol.rivencraft.environment.accelerated.description": "Adds extra random block ticks near players in this Age. Repeated pages stack within server tick budgets; does not speed up entities or machines.",
"symbol.rivencraft.feature.floating_islands.description": "Add high floating islands alongside noise, Flat, or Void terrain; not Cave terrain.",
"symbol.rivencraft.feature.caves.description": "Add extra cave carving to noise terrain, including Cave; not Flat or Void.",
"symbol.rivencraft.feature.ravines.description": "Add canyon carving to noise terrain; not Flat or Void. Skips Nether biomes.",
@@ -0,0 +1,23 @@
package net.dnorberg.rivencraft;
import net.neoforged.neoforge.common.ModConfigSpec;
/** Server-side limits shared with the 1.21.1 Accelerated implementation. */
public final class PortConfig {
public static final ModConfigSpec SPEC;
public static final ModConfigSpec.IntValue ACCELERATED_MAX_CHUNKS;
public static final ModConfigSpec.IntValue ACCELERATED_MAX_ATTEMPTS;
static {
var builder = new ModConfigSpec.Builder();
builder.push("accelerated_age");
ACCELERATED_MAX_CHUNKS = builder.comment("Maximum already-loaded player-nearby chunks given extra random ticks per Age tick.")
.defineInRange("max_loaded_chunks", 8, 1, 64);
ACCELERATED_MAX_ATTEMPTS = builder.comment("Maximum extra random-tick positions sampled per Age tick, shared fairly across loaded sections; zero disables Accelerated.")
.defineInRange("max_random_tick_attempts_per_tick", 16384, 0, 65536);
builder.pop();
SPEC = builder.build();
}
private PortConfig() {}
}
@@ -166,7 +166,8 @@ public final class Rivencraft {
public static final Supplier<RecipeSerializer<LinkingBookRecipe>> LINKING_BOOK_RECIPE =
RECIPE_SERIALIZERS.register("linking_book", () -> LinkingBookRecipe.SERIALIZER);
public Rivencraft(IEventBus modEventBus) {
public Rivencraft(IEventBus modEventBus, net.neoforged.fml.ModContainer container) {
container.registerConfig(net.neoforged.fml.config.ModConfig.Type.LOCAL, PortConfig.SPEC);
BLOCKS.register(modEventBus);
SOUNDS.register(modEventBus);
ITEMS.register(modEventBus);
@@ -179,6 +180,7 @@ public final class Rivencraft {
PortChunkGenerators.register(modEventBus);
NeoForge.EVENT_BUS.addListener(RuntimeAgeBackend::onLevelLoad);
NeoForge.EVENT_BUS.addListener(net.dnorberg.rivencraft.age.PortAgeWeather::onLevelTickPre);
NeoForge.EVENT_BUS.addListener(net.dnorberg.rivencraft.age.PortAcceleratedTicks::onLevelTickPost);
NeoForge.EVENT_BUS.addListener(PortAgeCommands::register);
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin);
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogout);
@@ -0,0 +1,119 @@
package net.dnorberg.rivencraft.age;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import net.dnorberg.rivencraft.PortConfig;
import net.minecraft.resources.Identifier;
import net.neoforged.neoforge.event.tick.LevelTickEvent;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.util.RandomSource;
/** Extra random block ticks from authored Accelerated pages, bounded to loaded player chunks. */
public final class PortAcceleratedTicks {
public static final Identifier PAGE = Identifier.fromNamespaceAndPath("rivencraft", "environment/accelerated");
private PortAcceleratedTicks() {}
public static int pageCount(List<Identifier> pages) {
return (int) pages.stream().filter(PAGE::equals).count();
}
public static void configure(ServerLevel level, int pages) {
if (!(level instanceof PortAgeLevel)) throw new IllegalArgumentException("Not a Rivencraft Age");
level.getDataStorage().computeIfAbsent(PortAccelerationSettings.TYPE).setPages(pages);
level.getDataStorage().saveAndJoin();
}
public static int pages(ServerLevel level) {
return level.getDataStorage().computeIfAbsent(PortAccelerationSettings.TYPE).pages();
}
public static void onLevelTickPost(LevelTickEvent.Post event) {
if (event.getLevel() instanceof PortAgeLevel level) tick(level);
}
static void tick(ServerLevel level) {
int maximumAttempts = PortConfig.ACCELERATED_MAX_ATTEMPTS.get();
if (maximumAttempts == 0) return;
int pages = pages(level);
if (pages == 0) return;
List<ChunkPos> chunks = loadedPlayerChunks(level);
if (chunks.isEmpty()) return;
int maximumChunks = Math.min(chunks.size(), PortConfig.ACCELERATED_MAX_CHUNKS.get());
int first = Math.floorMod(level.getServer().getTickCount(), chunks.size());
List<TickSection> eligible = new ArrayList<>();
for (int offset = 0; offset < maximumChunks; offset++) {
ChunkPos pos = chunks.get((first + offset) % chunks.size());
LevelChunk chunk = level.getChunkSource().getChunkNow(pos.x(), pos.z());
if (chunk == null) continue;
LevelChunkSection[] sections = chunk.getSections();
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
LevelChunkSection section = sections[sectionIndex];
if (!section.isRandomlyTickingBlocks()) continue;
int baseY = SectionPos.sectionToBlockCoord(chunk.getSectionYFromSectionIndex(sectionIndex));
eligible.add(new TickSection(pos, baseY, section));
}
}
if (eligible.isEmpty()) return;
// Legacy attempts three positions per random-ticking section per page. Divide
// the bounded total between sections before ticking any one of them, so a
// lower section cannot exhaust the entire budget ahead of surface crops.
int total = attemptBudget(pages, eligible.size(), maximumAttempts);
int perSection = total / eligible.size();
int remainder = total % eligible.size();
int firstSection = Math.floorMod(level.getServer().getTickCount(), eligible.size());
RandomSource random = level.getRandom();
for (int offset = 0; offset < eligible.size(); offset++) {
TickSection candidate = eligible.get((firstSection + offset) % eligible.size());
int samples = perSection + (offset < remainder ? 1 : 0);
for (int sample = 0; sample < samples; sample++) {
int x = random.nextInt(16);
int y = random.nextInt(16);
int z = random.nextInt(16);
BlockState state = candidate.section().getBlockState(x, y, z);
if (state.isRandomlyTicking()) {
state.randomTick(level, new BlockPos(candidate.chunk().getMinBlockX() + x,
candidate.baseY() + y, candidate.chunk().getMinBlockZ() + z), random);
}
}
}
}
static int attemptBudget(int pages, int sections, int maximumAttempts) {
if (pages < 0 || sections < 0 || maximumAttempts < 0)
throw new IllegalArgumentException("Negative accelerated tick budget input");
if (sections == 0) return 0;
long perSection = 3L * pages;
return perSection > maximumAttempts / (long) sections
? maximumAttempts : (int) (perSection * sections);
}
private static List<ChunkPos> loadedPlayerChunks(ServerLevel level) {
Set<ChunkPos> chunks = new LinkedHashSet<>();
for (var player : level.players()) {
ChunkPos center = player.chunkPosition();
for (int x = -1; x <= 1; x++) {
for (int z = -1; z <= 1; z++) {
int chunkX = center.x() + x;
int chunkZ = center.z() + z;
if (level.getChunkSource().hasChunk(chunkX, chunkZ)) {
chunks.add(new ChunkPos(chunkX, chunkZ));
}
}
}
}
return new ArrayList<>(chunks);
}
private record TickSection(ChunkPos chunk, int baseY, LevelChunkSection section) {}
}
@@ -0,0 +1,24 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.saveddata.SavedData;
import net.minecraft.world.level.saveddata.SavedDataType;
/** Age-local authored page count, retained across unload, restart and reseeding. */
final class PortAccelerationSettings extends SavedData {
private static final Codec<PortAccelerationSettings> CODEC = Codec.intRange(0, Integer.MAX_VALUE)
.fieldOf("pages").codec().xmap(PortAccelerationSettings::new, settings -> settings.pages);
static final SavedDataType<PortAccelerationSettings> TYPE = new SavedDataType<>(
Identifier.fromNamespaceAndPath("rivencraft", "acceleration"),
() -> new PortAccelerationSettings(0), CODEC);
private int pages;
private PortAccelerationSettings(int pages) { this.pages = pages; }
int pages() { return pages; }
void setPages(int pages) {
if (pages < 0) throw new IllegalArgumentException("Negative Accelerated page count");
this.pages = pages;
setDirty();
}
}
@@ -69,6 +69,19 @@ public final class PortAgeCommands {
return 1;
})))
.then(Commands.literal("age")
.then(Commands.literal("create_accelerated")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("pages", IntegerArgumentType.integer(0, 256))
.executes(PortAgeCommands::createAccelerated)))))
.then(Commands.literal("acceleration")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::acceleration)))
.then(Commands.literal("create_auto_terrain")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("page", IdentifierArgument.id())
.executes(PortAgeCommands::createAutomaticTerrain)))))
.then(Commands.literal("create_biome_surface")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
@@ -108,7 +121,10 @@ public final class PortAgeCommands {
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("terrain", StringArgumentType.word())
.then(Commands.argument("distribution", StringArgumentType.word())
.executes(PortAgeCommands::createCompletedBiomes))))))
.executes(context -> createCompletedBiomes(context, null))
.then(Commands.argument("structure", IdentifierArgument.id())
.executes(context -> createCompletedBiomes(context,
IdentifierArgument.getId(context, "structure")))))))))
.then(Commands.literal("biome_pool")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeBiomePool)))
@@ -692,6 +708,63 @@ public final class PortAgeCommands {
return 1;
}
private static int createAutomaticTerrain(CommandContext<CommandSourceStack> context) {
try {
var server = context.getSource().getServer();
verifyAutomaticTerrain(server);
long seed = LongArgumentType.getLong(context, "seed");
var source = java.util.List.of(IdentifierArgument.getId(context, "page"));
var selected = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server, source, seed);
var pages = selected.generationPages();
int created = createAge(context, selected.terrain(), selected.biomes(),
net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(pages, seed),
net.dnorberg.rivencraft.writing.PortAuthoredWriting.vegetationPlan(pages),
net.dnorberg.rivencraft.writing.PortAuthoredWriting.dungeonCount(pages),
net.dnorberg.rivencraft.writing.PortAuthoredWriting.lakePlan(pages, seed),
net.dnorberg.rivencraft.writing.PortAuthoredWriting.terrainFeaturePlan(pages, seed));
if (created != 0) context.getSource().sendSuccess(() -> Component.literal("Automatic terrain: " + selected), false);
return created;
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static void verifyAutomaticTerrain(net.minecraft.server.MinecraftServer server) {
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
var sky = java.util.List.of(symbol.apply("sky/overworld"));
var water = net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(Identifier.withDefaultNamespace("water"));
var plains = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("plains"));
var seen = java.util.EnumSet.noneOf(PortAgeTerrain.class);
for (long seed = 0; seed < 64; seed++) {
var selected = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server, sky, seed);
seen.add(selected.terrain());
if (selected.terrain() != PortRandomAgeSelection.terrain(seed)
|| !selected.generationPages().subList(0, sky.size()).equals(sky)
|| !selected.equals(net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server, sky, seed)))
throw new IllegalStateException("Automatic terrain seed/determinism/page-prefix check failed");
var fortress = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server,
java.util.List.of(symbol.apply("feature/nether_fortresses")), seed);
if (fortress.terrain() != PortAgeTerrain.CAVE)
throw new IllegalStateException("Omitted terrain cannot host a Cave-only structure");
var wet = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server, java.util.List.of(water), seed);
if (wet.terrain() == PortAgeTerrain.FLAT || wet.terrain() == PortAgeTerrain.VOID || wet.terrain() == PortAgeTerrain.CAVE)
throw new IllegalStateException("Automatic terrain cannot host its implied lake");
var biome = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server, java.util.List.of(plains), seed);
if (biome.terrain() == PortAgeTerrain.CAVE || !biome.biomes().biomes().contains(Identifier.withDefaultNamespace("plains")))
throw new IllegalStateException("Automatic terrain discarded a supplied surface biome");
var explicit = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server,
java.util.List.of(symbol.apply("terrain/flat"), sky.getFirst()), seed);
if (explicit.terrain() != PortAgeTerrain.FLAT || explicit.generationPages().size() != 2)
throw new IllegalStateException("Explicit terrain lost precedence");
}
if (seen.size() != PortAgeTerrain.values().length
|| !net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(java.util.List.of(symbol.apply("feature/nether_fortresses")))
|| net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(java.util.List.of(
symbol.apply("terrain/flat"), symbol.apply("feature/dungeons"))))
throw new IllegalStateException("Automatic terrain availability/coverage failed");
}
private static int createBiomeSurface(CommandContext<CommandSourceStack> context) {
try {
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
@@ -946,16 +1019,24 @@ public final class PortAgeCommands {
return 1;
}
private static int createCompletedBiomes(CommandContext<CommandSourceStack> context) {
private static int createCompletedBiomes(CommandContext<CommandSourceStack> context, Identifier structure) {
try {
var server = context.getSource().getServer();
long seed = LongArgumentType.getLong(context, "seed");
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var distribution = PortBiomePlan.Distribution.valueOf(
StringArgumentType.getString(context, "distribution").toUpperCase(Locale.ROOT));
String distributionName = StringArgumentType.getString(context, "distribution");
var distribution = distributionName.equalsIgnoreCase("auto") ? null
: PortBiomePlan.Distribution.valueOf(distributionName.toUpperCase(Locale.ROOT));
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
var pages = java.util.List.of(symbol.apply("terrain/" + terrain.name().toLowerCase(Locale.ROOT)),
symbol.apply("biome/" + distribution.name().toLowerCase(Locale.ROOT)));
var pages = new java.util.ArrayList<Identifier>();
pages.add(symbol.apply("terrain/" + terrain.name().toLowerCase(Locale.ROOT)));
if (distribution != null) pages.add(symbol.apply("biome/" + distribution.name().toLowerCase(Locale.ROOT)));
else verifyAutomaticBiomes(server);
if (structure != null) {
if (!PortStructurePages.supports(structure)) throw new IllegalArgumentException("Unsupported structure page " + structure);
pages.add(structure);
verifyStructureBiomes(server);
}
var plan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server, seed);
// Controller-only and partial pools complete; already-sufficient pools never gain extras.
var plains = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("plains"));
@@ -980,7 +1061,10 @@ public final class PortAgeCommands {
var encoded = PortBiomePlan.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, plan).getOrThrow();
if (!plan.equals(PortBiomePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
throw new IllegalStateException("Completed biome plan codec mismatch");
int created = createAge(context, terrain, plan);
var features = net.dnorberg.rivencraft.writing.PortAuthoredWriting.terrainFeaturePlan(pages, seed);
int created = createAge(context, terrain, plan, null, null, 0, null, features);
if (created != 0 && terrain == PortAgeTerrain.CAVE && plan.distribution() == PortBiomePlan.Distribution.NATIVE)
verifyNativeCave(server, ageId(context), seed, plan);
if (created != 0) context.getSource().sendSuccess(() -> Component.literal("Completed biome plan: " + plan), false);
return created == 0 ? 0 : probeBiomePool(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
@@ -989,6 +1073,121 @@ public final class PortAgeCommands {
}
}
private static void verifyAutomaticBiomes(net.minecraft.server.MinecraftServer server) {
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
var standard = symbol.apply("terrain/standard");
var end = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("the_end"));
var forest = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("forest"));
var seen = java.util.EnumSet.noneOf(PortBiomePlan.Distribution.class);
var caveIds = java.util.Set.copyOf(PortRandomAgeSelection.eligibleBiomes(server, PortAgeTerrain.CAVE));
for (long seed = 0; seed < 128; seed++) {
var expected = PortRandomAgeSelection.distribution(seed);
var selected = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(standard), server, seed);
seen.add(selected.distribution());
if (selected.distribution() != expected || !selected.equals(
net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
java.util.List.of(standard, symbol.apply("weather/off")), server, seed)))
throw new IllegalStateException("Missing biome controller is not seeded/independent");
var authored = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(standard, end, forest), server, seed);
if (authored.biomes().size() != 2 || !authored.biomes().equals(java.util.List.of(
Identifier.withDefaultNamespace("the_end"), Identifier.withDefaultNamespace("forest")))
|| authored.distribution() != (expected == PortBiomePlan.Distribution.NATIVE
|| expected == PortBiomePlan.Distribution.SINGLE ? PortBiomePlan.Distribution.TILED : expected))
throw new IllegalStateException("Automatic controller discarded or extended authored biomes");
var one = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(standard, end), server, seed);
if (!one.biomes().getFirst().equals(Identifier.withDefaultNamespace("the_end"))
|| one.distribution() != (expected == PortBiomePlan.Distribution.NATIVE ? PortBiomePlan.Distribution.SINGLE : expected)
|| one.biomes().size() != (one.distribution() == PortBiomePlan.Distribution.SINGLE ? 1 : 2))
throw new IllegalStateException("Automatic controller lost a supplied biome");
var cave = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
java.util.List.of(symbol.apply("terrain/cave")), server, seed);
if (cave.distribution() != expected || !caveIds.containsAll(cave.biomes())
|| expected == PortBiomePlan.Distribution.NATIVE && !java.util.Set.copyOf(cave.biomes()).equals(caveIds))
throw new IllegalStateException("Automatic Cave controller/biome pool is invalid");
var emptyVoid = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
java.util.List.of(symbol.apply("terrain/void")), server, seed);
var authoredVoid = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
java.util.List.of(symbol.apply("terrain/void"), end), server, seed);
if (emptyVoid.distribution() != PortBiomePlan.Distribution.SINGLE
|| !emptyVoid.biomes().equals(java.util.List.of(Identifier.withDefaultNamespace("the_void")))
|| !authoredVoid.biomes().equals(java.util.List.of(Identifier.withDefaultNamespace("the_end"))))
throw new IllegalStateException("Automatic Void controller changed biome fallback");
var village = symbol.apply("feature/villages");
var withStructure = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(standard, village), server, seed);
if (withStructure.distribution() != PortBiomePlan.Distribution.NATIVE
&& withStructure.biomes().stream().noneMatch(PortStructurePages.compatibleBiomes(village, server.registryAccess())::contains))
throw new IllegalStateException("Automatic controller skipped structure-aware completion");
}
if (seen.size() != PortBiomePlan.Distribution.values().length)
throw new IllegalStateException("Automatic biome controller fixture lacks distribution coverage");
}
private static void verifyNativeCave(net.minecraft.server.MinecraftServer server, Identifier age, long seed, PortBiomePlan plan) {
var expected = PortRandomAgeSelection.caveBiomes(server.registryAccess());
if (!plan.biomes().equals(expected)) throw new IllegalStateException("Native Cave did not freeze the recognized biome pool");
var old = PortBiomePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE,
com.google.gson.JsonParser.parseString("{\"distribution\":\"NATIVE\",\"biomes\":[]}")).getOrThrow();
if (!old.biomes().isEmpty() || old.distribution() != PortBiomePlan.Distribution.NATIVE)
throw new IllegalStateException("Old Native Cave plan was rewritten");
var level = server.getLevel(ResourceKey.create(Registries.DIMENSION, age));
var source = level.getChunkSource().getGenerator().getBiomeSource();
if (expected.size() < 2) return;
if (!(source instanceof PortScaledBiomeSource scaled)) throw new IllegalStateException("Native Cave is not using region distribution");
var registry = server.registryAccess().lookupOrThrow(Registries.BIOME);
var holders = plan.biomes().stream().map(id -> (net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome>)
registry.getOrThrow(ResourceKey.create(Registries.BIOME, id))).toList();
var tiny = new PortScaledBiomeSource(net.minecraft.core.HolderSet.direct(holders), seed, 0);
var sampled = new java.util.HashSet<Identifier>();
for (int x = -64; x <= 64; x += 4) for (int z = -64; z <= 64; z += 4) {
var biome = scaled.getNoiseBiome(x, 0, z);
if (!biome.equals(tiny.getNoiseBiome(x, 0, z)) || !biome.equals(scaled.getNoiseBiome(x, 16, z)))
throw new IllegalStateException("Native Cave does not match root's Tiny-region sampler");
sampled.add(biome.unwrapKey().orElseThrow().identifier());
}
if (!java.util.Set.copyOf(expected).containsAll(sampled) || sampled.size() < 2)
throw new IllegalStateException("Native Cave sampler did not distribute its recognized biome pool");
}
private static void verifyStructureBiomes(net.minecraft.server.MinecraftServer server) {
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
java.util.function.Function<String, Identifier> biome = name ->
net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace(name));
var village = symbol.apply("feature/villages");
var fortress = symbol.apply("feature/nether_fortresses");
for (long seed = 0; seed < 32; seed++) {
var singlePages = java.util.List.of(symbol.apply("biome/single"), village);
var single = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(singlePages, server, seed);
var cave = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
java.util.List.of(symbol.apply("terrain/cave"), symbol.apply("biome/single"), fortress), server, seed);
if (!PortStructurePages.compatibleBiomes(village, server.registryAccess()).contains(single.biomes().getFirst())
|| !single.equals(net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(singlePages, server, seed))
|| !PortStructurePages.compatibleBiomes(fortress, server.registryAccess()).contains(cave.biomes().getFirst()))
throw new IllegalStateException("Structure-aware Single completion failed");
var stronghold = symbol.apply("feature/strongholds");
var shared = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(
symbol.apply("biome/single"), village, stronghold), server, seed);
if (!PortStructurePages.compatibleBiomes(village, server.registryAccess()).contains(shared.biomes().getFirst())
|| !PortStructurePages.compatibleBiomes(stronghold, server.registryAccess()).contains(shared.biomes().getFirst()))
throw new IllegalStateException("Single completion did not satisfy common structure requirements");
var explicit = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
java.util.List.of(symbol.apply("biome/single"), biome.apply("the_end"), village), server, seed);
var nativePlan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
java.util.List.of(symbol.apply("biome/native"), village), server, seed);
if (!explicit.biomes().equals(java.util.List.of(Identifier.withDefaultNamespace("the_end")))
|| nativePlan.distribution() != PortBiomePlan.Distribution.NATIVE || !nativePlan.biomes().isEmpty())
throw new IllegalStateException("Explicit Single/Native biome precedence changed");
var multiPages = java.util.List.of(symbol.apply("biome/tiny"), biome.apply("forest"), biome.apply("the_end"), village);
var multi = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(multiPages, server, seed);
if (multi.biomes().size() != 3 || !multi.biomes().subList(0, 2).equals(java.util.List.of(
Identifier.withDefaultNamespace("forest"), Identifier.withDefaultNamespace("the_end")))
|| !PortStructurePages.compatibleBiomes(village, server.registryAccess()).contains(multi.biomes().get(2)))
throw new IllegalStateException("Structure biome completion discarded authored choices");
var full = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(
symbol.apply("biome/tiny"), biome.apply("plains"), biome.apply("forest"), village, village), server, seed);
if (full.biomes().size() != 2) throw new IllegalStateException("Compatible/repeated structures added unnecessary biomes");
}
}
private static int probeBiomePool(CommandContext<CommandSourceStack> context) {
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
if (level == null) {
@@ -1947,6 +2146,40 @@ public final class PortAgeCommands {
return 1;
}
private static int createAccelerated(CommandContext<CommandSourceStack> context) {
int count = IntegerArgumentType.getInteger(context, "pages");
var pages = new java.util.ArrayList<Identifier>();
pages.add(Identifier.fromNamespaceAndPath("rivencraft", "terrain/flat"));
for (int i = 0; i < count; i++) pages.add(PortAcceleratedTicks.PAGE);
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages)
|| !net.dnorberg.rivencraft.writing.PortAuthoredWriting.isPageImplemented(PortAcceleratedTicks.PAGE)
|| PortAcceleratedTicks.pageCount(pages) != count
|| PortAcceleratedTicks.attemptBudget(1, 8, 16384) != 24
|| PortAcceleratedTicks.attemptBudget(32, 8, 16384) != 768
|| PortAcceleratedTicks.attemptBudget(Integer.MAX_VALUE, Integer.MAX_VALUE, 16384) != 16384
|| PortAcceleratedTicks.attemptBudget(32, 8, 0) != 0)
throw new IllegalStateException("Accelerated writing/budget regression");
ServerLevel level = RuntimeAgeBackend.create(context.getSource().getServer(), ageId(context),
LongArgumentType.getLong(context, "seed"), null, PortAgeTerrain.FLAT);
PortAcceleratedTicks.configure(level, count);
return acceleration(context);
}
private static int acceleration(CommandContext<CommandSourceStack> context) {
ServerLevel level = context.getSource().getServer().getLevel(
ResourceKey.create(Registries.DIMENSION, ageId(context)));
if (level == null) {
context.getSource().sendFailure(Component.literal("Age not loaded: " + ageId(context)));
return 0;
}
context.getSource().sendSuccess(() -> Component.literal(ageId(context) + " Accelerated pages="
+ PortAcceleratedTicks.pages(level) + "; extra attempts/eligible section="
+ 3L * PortAcceleratedTicks.pages(level)
+ "; max loaded chunks=" + net.dnorberg.rivencraft.PortConfig.ACCELERATED_MAX_CHUNKS.get()
+ "; max attempts/tick=" + net.dnorberg.rivencraft.PortConfig.ACCELERATED_MAX_ATTEMPTS.get()), false);
return 1;
}
private static int sampleBiome(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
@@ -5,7 +5,7 @@ import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.List;
import net.minecraft.resources.Identifier;
/** Frozen authored biome choice; repeated entries retain their selection weight. */
/** Frozen biome choice; Native may carry an explicit Cave pool. Repeats retain weight. */
public record PortBiomePlan(Distribution distribution, List<Identifier> biomes) {
public enum Distribution { NATIVE, SINGLE, TILED, GRID, TINY, SMALL, MEDIUM, LARGE, HUGE }
@@ -17,8 +17,7 @@ public record PortBiomePlan(Distribution distribution, List<Identifier> biomes)
public PortBiomePlan {
biomes = List.copyOf(biomes);
if (distribution == Distribution.NATIVE && !biomes.isEmpty()
|| distribution == Distribution.SINGLE && biomes.size() != 1
if (distribution == null || distribution == Distribution.SINGLE && biomes.size() != 1
|| distribution != Distribution.NATIVE && distribution != Distribution.SINGLE && biomes.size() < 2)
throw new IllegalArgumentException("Invalid authored biome distribution");
}
@@ -19,30 +19,18 @@ public final class PortRandomAgeSelection {
public static Selection select(MinecraftServer server, long seed) {
// Match the 1.21.1 Legacy-rank choices: Standard and Flat are common;
// the other terrain families are rarer. The sorted order is deliberate.
PortAgeTerrain terrain = weighted(new Random(seed ^ 0x5445525241494E4CL),
List.of(PortAgeTerrain.AMPLIFIED, PortAgeTerrain.CAVE, PortAgeTerrain.FLAT,
PortAgeTerrain.ISLAND, PortAgeTerrain.STANDARD, PortAgeTerrain.VOID),
new int[]{1, 1, 5, 1, 6, 1});
PortAgeTerrain terrain = terrain(seed);
if (terrain == PortAgeTerrain.VOID)
return new Selection(terrain, new PortBiomePlan(PortBiomePlan.Distribution.SINGLE,
List.of(Identifier.withDefaultNamespace("the_void"))));
PortBiomePlan.Distribution distribution = weighted(new Random(seed ^ 0x524956454E435241L),
List.of(PortBiomePlan.Distribution.NATIVE, PortBiomePlan.Distribution.SINGLE,
PortBiomePlan.Distribution.MEDIUM, PortBiomePlan.Distribution.TILED,
PortBiomePlan.Distribution.GRID, PortBiomePlan.Distribution.TINY,
PortBiomePlan.Distribution.SMALL, PortBiomePlan.Distribution.LARGE,
PortBiomePlan.Distribution.HUGE),
new int[]{3, 3, 3, 1, 1, 1, 1, 1, 1});
PortBiomePlan.Distribution distribution = distribution(seed);
List<Identifier> eligible = eligibleBiomes(server, terrain);
if (eligible.size() < 2 && distribution != PortBiomePlan.Distribution.NATIVE)
distribution = PortBiomePlan.Distribution.SINGLE;
// 26.3 has no native multi-cave biome source; choose a seeded Cave
// biome rather than silently using Lush Caves for every random Age.
if (terrain == PortAgeTerrain.CAVE && distribution == PortBiomePlan.Distribution.NATIVE)
distribution = PortBiomePlan.Distribution.SINGLE;
if (distribution == PortBiomePlan.Distribution.NATIVE)
return new Selection(terrain, new PortBiomePlan(distribution, List.of()));
return new Selection(terrain, new PortBiomePlan(distribution,
terrain == PortAgeTerrain.CAVE ? eligible : List.of()));
int count = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
if (count > 1) {
@@ -54,14 +42,30 @@ public final class PortRandomAgeSelection {
return new Selection(terrain, new PortBiomePlan(distribution, shuffled.subList(0, count)));
}
public static List<PortAgeTerrain> terrains() {
return List.of(PortAgeTerrain.AMPLIFIED, PortAgeTerrain.CAVE, PortAgeTerrain.FLAT,
PortAgeTerrain.ISLAND, PortAgeTerrain.STANDARD, PortAgeTerrain.VOID);
}
public static PortAgeTerrain terrain(long seed) {
return weighted(new Random(seed ^ 0x5445525241494E4CL), terrains(), new int[]{1, 1, 5, 1, 6, 1});
}
/** First AgeTraits draw in 1.21.1; shared by random and partly authored Ages. */
public static PortBiomePlan.Distribution distribution(long seed) {
return weighted(new Random(seed ^ 0x524956454E435241L),
List.of(PortBiomePlan.Distribution.NATIVE, PortBiomePlan.Distribution.SINGLE,
PortBiomePlan.Distribution.MEDIUM, PortBiomePlan.Distribution.TILED,
PortBiomePlan.Distribution.GRID, PortBiomePlan.Distribution.TINY,
PortBiomePlan.Distribution.SMALL, PortBiomePlan.Distribution.LARGE,
PortBiomePlan.Distribution.HUGE),
new int[]{3, 3, 3, 1, 1, 1, 1, 1, 1});
}
public static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
if (terrain == PortAgeTerrain.VOID) return List.of(Identifier.withDefaultNamespace("the_void"));
if (terrain == PortAgeTerrain.CAVE) {
List<Identifier> caves = server.registryAccess().lookupOrThrow(Registries.BIOME).listElements()
.filter(PortRegistryPages::isCaveBiome)
.map(holder -> holder.key().identifier())
.sorted().toList();
return caves.isEmpty() ? List.of(Identifier.withDefaultNamespace("lush_caves")) : caves;
return caveBiomes(server.registryAccess());
}
List<Identifier> surface = server.overworld().getChunkSource().getGenerator().getBiomeSource()
.possibleBiomes().stream().flatMap(holder -> holder.unwrapKey().stream())
@@ -69,6 +73,12 @@ public final class PortRandomAgeSelection {
return surface.isEmpty() ? List.of(Identifier.withDefaultNamespace("plains")) : surface;
}
public static List<Identifier> caveBiomes(net.minecraft.core.HolderLookup.Provider registries) {
var caves = registries.lookupOrThrow(Registries.BIOME).listElements()
.filter(PortRegistryPages::isCaveBiome).map(holder -> holder.key().identifier()).sorted().toList();
return caves.isEmpty() ? List.of(Identifier.withDefaultNamespace("lush_caves")) : caves;
}
static <T> T weighted(Random random, List<T> choices, int[] weights) {
int total = 0;
for (int weight : weights) total += weight;
@@ -47,7 +47,7 @@ public final class PortRandomStructureSelection {
}
private static Set<Identifier> actualBiomes(MinecraftServer server, PortAgeTerrain terrain, PortBiomePlan plan) {
if (plan != null && plan.distribution() != PortBiomePlan.Distribution.NATIVE)
if (plan != null && !plan.biomes().isEmpty())
return Set.copyOf(plan.biomes());
if (terrain == PortAgeTerrain.FLAT) return Set.of(Identifier.withDefaultNamespace("plains"));
if (terrain == PortAgeTerrain.VOID) return Set.of(Identifier.withDefaultNamespace("the_void"));
@@ -95,11 +95,22 @@ public final class PortStructurePages {
var set = registries.lookupOrThrow(Registries.STRUCTURE_SET)
.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page)));
if (set.isEmpty()) return false;
var compatible = set.get().value().structures().stream()
var compatible = compatibleBiomes(set.get().value());
return compatible.isEmpty() || biomes.stream().anyMatch(compatible::contains);
}
public static Set<Identifier> compatibleBiomes(Identifier page, HolderLookup.Provider registries) {
if (!supports(page)) return Set.of();
return registries.lookupOrThrow(Registries.STRUCTURE_SET)
.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page)))
.map(holder -> compatibleBiomes(holder.value())).orElse(Set.of());
}
private static Set<Identifier> compatibleBiomes(StructureSet set) {
return set.structures().stream()
.flatMap(entry -> entry.structure().value().biomes().stream())
.flatMap(holder -> holder.unwrapKey().stream()).map(key -> key.identifier())
.collect(java.util.stream.Collectors.toSet());
return compatible.isEmpty() || biomes.stream().anyMatch(compatible::contains);
.collect(java.util.stream.Collectors.toUnmodifiableSet());
}
private static Map<Identifier, Integer> enabled(List<Identifier> pages) {
@@ -366,15 +366,14 @@ public final class RuntimeAgeBackend {
}
private static BiomeSource authoredBiomeSource(MinecraftServer server, PortBiomePlan plan, long seed) {
if (plan.distribution() == PortBiomePlan.Distribution.NATIVE)
throw new IllegalArgumentException("Native biome distribution has no authored source");
var registry = server.registryAccess().lookupOrThrow(Registries.BIOME);
List<Holder<Biome>> biomes = plan.biomes().stream().map(id ->
(Holder<Biome>) registry.get(ResourceKey.create(Registries.BIOME, id))
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + id)))
.toList();
return switch (plan.distribution()) {
case NATIVE -> throw new IllegalStateException("Native is not an override");
case NATIVE -> biomes.size() == 1 ? new FixedBiomeSource(biomes.getFirst())
: new PortScaledBiomeSource(HolderSet.direct(biomes), seed, 0);
case SINGLE -> new FixedBiomeSource(biomes.getFirst());
case TILED, GRID -> new CheckerboardColumnBiomeSource(HolderSet.direct(biomes),
plan.distribution() == PortBiomePlan.Distribution.GRID ? 0 : 4);
@@ -399,9 +398,12 @@ public final class RuntimeAgeBackend {
PortAgeColors colors, PortDayCycle dayCycle) {
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
if (biomePlan != null && biomePlan.distribution() == PortBiomePlan.Distribution.NATIVE
&& !biomePlan.biomes().isEmpty() && terrain != PortAgeTerrain.CAVE)
throw new IllegalArgumentException("Only Native Cave terrain may carry an explicit native biome pool");
// Noise terrains reuse the Overworld biome source unless a
// non-Native authored distribution replaces it.
// non-Native authored distribution or a frozen Native Cave pool replaces it.
// LevelStem.seedOverride controls chunk random state and getSeed().
LevelStem overworldStem = server.registryAccess().lookupOrThrow(Registries.LEVEL_STEM)
.getValue(LevelStem.OVERWORLD);
@@ -453,7 +455,8 @@ public final class RuntimeAgeBackend {
throw new IllegalArgumentException("Cave terrain requires a Cave or Nether biome: " + biomeId);
}
}
if (biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE) {
if (biomePlan != null && (biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE
|| terrain == PortAgeTerrain.CAVE && !biomePlan.biomes().isEmpty())) {
source = authoredBiomeSource(server, biomePlan, seed);
}
var settings = switch (terrain) {
@@ -110,8 +110,20 @@ public final class PortBookNetworking {
if (title == null || title.isBlank() || title.length() > MAX_BOOK_TITLE_LENGTH) return null;
long seed = player.getRandom().nextLong();
net.dnorberg.rivencraft.age.PortBiomePlan biomePlan;
PortAgeTerrain terrain;
List<Identifier> generationPages;
try {
biomePlan = PortAuthoredWriting.biomePlan(pages, player.level().getServer(), seed);
if (pages.isEmpty()) {
var random = PortRandomAgeSelection.select(player.level().getServer(), seed);
terrain = random.terrain();
biomePlan = random.biomes();
generationPages = pages;
} else {
var selected = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(player.level().getServer(), pages, seed);
terrain = selected.terrain();
biomePlan = selected.biomes();
generationPages = selected.generationPages();
}
} catch (IllegalArgumentException exception) {
player.sendOverlayMessage(Component.literal("Age writing unavailable: " + exception.getMessage()));
return null;
@@ -128,25 +140,21 @@ public final class PortBookNetworking {
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
}
try {
PortAgeTerrain terrain = PortAuthoredWriting.terrain(pages);
if (pages.isEmpty()) {
PortRandomAgeSelection.Selection random = PortRandomAgeSelection.select(server, seed);
terrain = random.terrain();
biomePlan = random.biomes();
}
var environment = net.dnorberg.rivencraft.age.PortEnvironmentSelection.resolve(pages, seed, terrain);
var randomFeatures = pages.isEmpty()
? net.dnorberg.rivencraft.age.PortRandomFeatureSelection.select(server, seed, terrain, biomePlan) : null;
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
randomFeatures == null ? PortAuthoredWriting.rockPlan(pages, seed) : randomFeatures.rocks(),
PortAuthoredWriting.vegetationPlan(pages),
randomFeatures == null ? PortAuthoredWriting.dungeonCount(pages) : randomFeatures.dungeons(),
randomFeatures == null ? PortAuthoredWriting.lakePlan(pages, seed) : randomFeatures.lakes(),
randomFeatures == null ? PortAuthoredWriting.terrainFeaturePlan(pages, seed) : randomFeatures.terrain(),
var level = RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
randomFeatures == null ? PortAuthoredWriting.rockPlan(generationPages, seed) : randomFeatures.rocks(),
PortAuthoredWriting.vegetationPlan(generationPages),
randomFeatures == null ? PortAuthoredWriting.dungeonCount(generationPages) : randomFeatures.dungeons(),
randomFeatures == null ? PortAuthoredWriting.lakePlan(generationPages, seed) : randomFeatures.lakes(),
randomFeatures == null ? PortAuthoredWriting.terrainFeaturePlan(generationPages, seed) : randomFeatures.terrain(),
environment.weather(), environment.lighting(), environment.sky(),
net.dnorberg.rivencraft.age.PortAgeColors.resolve(pages, seed),
net.dnorberg.rivencraft.age.PortDayCycle.resolve(pages, seed,
net.dnorberg.rivencraft.age.PortDayCycle.nativeTicks(server.overworld())));
net.dnorberg.rivencraft.age.PortAcceleratedTicks.configure(level,
net.dnorberg.rivencraft.age.PortAcceleratedTicks.pageCount(pages));
} catch (IllegalArgumentException | IllegalStateException exception) {
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
return null;
@@ -0,0 +1,47 @@
package net.dnorberg.rivencraft.writing;
import java.util.ArrayList;
import java.util.List;
import net.dnorberg.rivencraft.age.PortAgeTerrain;
import net.dnorberg.rivencraft.age.PortBiomePlan;
import net.dnorberg.rivencraft.age.PortRandomAgeSelection;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
/** Root AgeRuntimeProfiles.selectForWriting, limited to currently supported port combinations. */
public final class PortAuthoredTerrainSelection {
private PortAuthoredTerrainSelection() {}
public record Selection(PortAgeTerrain terrain, List<Identifier> generationPages, PortBiomePlan biomes) {
public Selection { generationPages = List.copyOf(generationPages); }
}
public static Selection select(MinecraftServer server, List<Identifier> pages, long seed) {
var explicit = PortAuthoredWriting.explicitTerrain(pages);
if (explicit != null) return compile(server, pages, seed, explicit);
var preferred = PortRandomAgeSelection.terrain(seed);
try {
return compile(server, pages, seed, preferred);
} catch (IllegalArgumentException ignored) {
// Match root's stable profile-ID fallback rather than rerolling the seed.
}
var compatible = new ArrayList<Selection>();
for (var terrain : PortRandomAgeSelection.terrains()) {
if (terrain == preferred) continue;
try { compatible.add(compile(server, pages, seed, terrain)); }
catch (IllegalArgumentException ignored) { }
}
if (compatible.isEmpty()) throw new IllegalArgumentException("No supported terrain can host this book's pages");
return compatible.get(Math.floorMod(Long.hashCode(seed), compatible.size()));
}
private static Selection compile(MinecraftServer server, List<Identifier> pages, long seed, PortAgeTerrain terrain) {
if (!PortAuthoredWriting.supportsOnTerrain(pages, terrain, seed))
throw new IllegalArgumentException("Unsupported pages for " + terrain + " terrain");
var resolved = PortAuthoredWriting.withTerrain(pages, terrain);
// Also preflight registry-backed biome restrictions, so an automatic Cave
// choice cannot turn supplied surface biome pages into an unusable book.
var biomes = PortAuthoredWriting.biomePlan(resolved, server, seed);
return new Selection(terrain, resolved, biomes);
}
}
@@ -57,6 +57,7 @@ public final class PortAuthoredWriting {
if (entry == null) return false;
if (net.dnorberg.rivencraft.age.PortAgeColors.isModifier(page)
|| net.dnorberg.rivencraft.age.PortDayCycle.supports(page)) return true;
if (page.equals(net.dnorberg.rivencraft.age.PortAcceleratedTicks.PAGE)) return true;
if (isTerrain(page) || controller(page) != null || PortWeatherMode.forPage(page) != null
|| PortLighting.forPage(page) != null || PortSkyStyle.forPage(page) != null) return true;
if (entry.category().equals("rivencraft:biome") && PortRegistryPages.biomeId(page) != null) return true;
@@ -72,11 +73,35 @@ public final class PortAuthoredWriting {
/** A page never disappears into an Age unless this port can apply its effect. */
public static boolean supports(List<Identifier> pages) {
if (pages.stream().noneMatch(PortAuthoredWriting::isTerrain))
return net.dnorberg.rivencraft.age.PortRandomAgeSelection.terrains().stream()
.anyMatch(terrain -> supportsOnTerrain(pages, terrain, 0));
try {
return supportsExplicit(PortMaterialCompletion.complete(pages, 0));
} catch (IllegalArgumentException exception) { return false; }
}
static boolean supportsOnTerrain(List<Identifier> pages, PortAgeTerrain terrain, long seed) {
try {
return supportsExplicit(PortMaterialCompletion.complete(withTerrain(pages, terrain), seed));
} catch (IllegalArgumentException exception) { return false; }
}
static List<Identifier> withTerrain(List<Identifier> pages, PortAgeTerrain terrain) {
if (pages.stream().anyMatch(PortAuthoredWriting::isTerrain)) return List.copyOf(pages);
var resolved = new ArrayList<>(pages);
// Append internally so original material/color/controller indices stay stable.
resolved.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
"terrain/" + terrain.name().toLowerCase(java.util.Locale.ROOT)));
return List.copyOf(resolved);
}
static PortAgeTerrain explicitTerrain(List<Identifier> pages) {
return pages.stream().filter(PortAuthoredWriting::isTerrain).findFirst()
.map(page -> PortAgeTerrain.valueOf(page.getPath().substring(8).toUpperCase(java.util.Locale.ROOT)))
.orElse(null);
}
private static boolean supportsExplicit(List<Identifier> pages) {
if (!net.dnorberg.rivencraft.age.PortAgeColors.validNumbers(pages)) return false;
if (!net.dnorberg.rivencraft.age.PortDayCycle.validWriting(pages)) return false;
@@ -104,6 +129,8 @@ public final class PortAuthoredWriting {
if (net.dnorberg.rivencraft.age.PortAgeColors.isModifier(page)
|| net.dnorberg.rivencraft.age.PortDayCycle.supports(page)) {
// Shared colors target Sky, Ambient Light Color or Normal Time; trailing colors tint the sky.
} else if (page.equals(net.dnorberg.rivencraft.age.PortAcceleratedTicks.PAGE)) {
// Every Accelerated page adds random-tick attempts; it is not a controller.
} else if (isTerrain(page) && !terrain) {
terrain = true;
} else if (PortSkyStyle.forPage(page) != null && sky++ == 0) {
@@ -321,17 +348,18 @@ public final class PortAuthoredWriting {
/** Resolves all authored biomes from the active registry before creating a level. */
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) {
return biomePlan(pages, registries, 0, null);
return biomePlan(pages, registries, 0, null, null);
}
/** Creation-only completion; the saved plan never rerolls on load or reseed. */
public static PortBiomePlan biomePlan(List<Identifier> pages, net.minecraft.server.MinecraftServer server, long seed) {
return biomePlan(pages, server.registryAccess(), seed,
net.dnorberg.rivencraft.age.PortRandomAgeSelection.eligibleBiomes(server, terrain(pages)));
net.dnorberg.rivencraft.age.PortRandomAgeSelection.eligibleBiomes(server, terrain(pages)),
net.dnorberg.rivencraft.age.PortRandomAgeSelection.distribution(seed));
}
private static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries,
long seed, List<Identifier> available) {
long seed, List<Identifier> available, PortBiomePlan.Distribution fallback) {
if (!supports(pages)) throw new IllegalArgumentException(
"Unsupported terrain or biome-page combination in this port slice");
List<Identifier> biomes = new ArrayList<>();
@@ -364,8 +392,22 @@ public final class PortAuthoredWriting {
throw new IllegalArgumentException("Vegetation pages need at least one surface Overworld biome");
if (pages.contains(PortTerrainFeaturePlan.DENSE_ORES) && !biomes.isEmpty() && !hasOreBiome)
throw new IllegalArgumentException("Dense Ores needs at least one biome with registered ore placements");
if (distribution == PortBiomePlan.Distribution.NATIVE)
return new PortBiomePlan(distribution, List.of());
if (distribution == null && fallback != null) {
distribution = fallback;
// Root's authored-pool fallback never discards supplied biome pages.
if (pages.contains(VOID_TERRAIN) && biomes.isEmpty()) distribution = PortBiomePlan.Distribution.SINGLE;
if (!biomes.isEmpty() && (pages.contains(VOID_TERRAIN)
|| distribution == PortBiomePlan.Distribution.NATIVE
|| distribution == PortBiomePlan.Distribution.SINGLE && biomes.size() > 1))
distribution = biomes.size() == 1 ? PortBiomePlan.Distribution.SINGLE : biomes.size() == 2
? PortBiomePlan.Distribution.TILED : PortBiomePlan.Distribution.MEDIUM;
}
if (distribution == PortBiomePlan.Distribution.NATIVE) {
var nativePool = caveTerrain
? (available != null ? available : net.dnorberg.rivencraft.age.PortRandomAgeSelection.caveBiomes(registries))
: List.<Identifier>of();
return new PortBiomePlan(distribution, nativePool);
}
if (distribution != null && available != null) {
int minimum = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
int target = minimum;
@@ -374,6 +416,9 @@ public final class PortAuthoredWriting {
while (target < available.size() && random.nextInt(3) != 0) target++;
}
var distinct = new java.util.HashSet<>(biomes);
PortStructureBiomeCompletion.complete(pages, registries, available, !biomes.isEmpty(),
distribution, biomes, distinct, seed);
target = Math.max(target, distinct.size());
// Void may retain authored weather/season biomes, but must not gain
// an unrequested The Void biome just to fill a multi-biome controller.
var filler = new ArrayList<>(pages.contains(VOID_TERRAIN) && !biomes.isEmpty() ? List.<Identifier>of() : available);
@@ -0,0 +1,54 @@
package net.dnorberg.rivencraft.writing;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.Set;
import net.dnorberg.rivencraft.age.PortBiomePlan;
import net.dnorberg.rivencraft.age.PortStructurePages;
import net.minecraft.core.HolderLookup;
import net.minecraft.resources.Identifier;
/** Root AgeBiomeSelection.completeStructureBiomes, before ordinary minimum filling. */
final class PortStructureBiomeCompletion {
private PortStructureBiomeCompletion() {}
static void complete(List<Identifier> pages, HolderLookup.Provider registries, List<Identifier> available,
boolean hasAuthoredBiomes, PortBiomePlan.Distribution distribution,
List<Identifier> selected, Set<Identifier> distinct, long seed) {
var requirements = pages.stream().filter(PortStructurePages::supports).distinct()
.map(page -> PortStructurePages.compatibleBiomes(page, registries))
.filter(requirement -> !requirement.isEmpty()).toList();
if (requirements.isEmpty()) return;
if (distribution == PortBiomePlan.Distribution.SINGLE) {
if (hasAuthoredBiomes) return;
var candidates = available.stream()
.filter(candidate -> requirements.stream().allMatch(requirement -> requirement.contains(candidate)))
.toList();
if (candidates.isEmpty()) return;
Identifier biome = choose(candidates, seed, 0x5354525543544249L);
selected.clear();
distinct.clear();
selected.add(biome);
distinct.add(biome);
return;
}
for (int index = 0; index < requirements.size(); index++) {
var requirement = requirements.get(index);
if (selected.stream().anyMatch(requirement::contains)) continue;
var candidates = available.stream().filter(requirement::contains)
.filter(candidate -> !distinct.contains(candidate)).toList();
if (candidates.isEmpty()) continue;
Identifier biome = choose(candidates, seed, 0x5354525543544249L + index);
selected.add(biome);
distinct.add(biome);
}
}
private static Identifier choose(List<Identifier> candidates, long seed, long salt) {
var shuffled = new ArrayList<>(candidates);
Collections.shuffle(shuffled, new Random(seed ^ salt));
return shuffled.getFirst();
}
}