Port seeded Age completion and Accelerated pages to 26.3 beta
Build / build (push) Canceled after 0s

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

@@ -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();
}
}