Port seeded random Ages and authored Rocks to 26.3 beta
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-11-01 13:25:42 -05:00
1 parent 9ccec18c18
commit d75154fa9b
15 files changed
+423 -21

No files matched your search

+14
View File
@@ -508,6 +508,20 @@ versions.
The local `1.0.1.39-beta.1` follow-up makes the 26.3 landing search reject
fluid at feet and head height and prevents a submerged fallback platform.
The local `1.0.1.40-beta.1` slice gives pageless Descriptive Books a
seed-selected terrain and biome plan. It follows the 1.21.1 rank weights for
terrain and biome distribution, selects registered Cave/Nether biomes for Cave
Ages and overworld-source biomes for other non-Void terrains, and persists the
resolved choices. Other random Age traits remain a later port task.
The local `1.0.1.41-beta.1` slice adds the first Age-scoped feature wrapper to
the 26.3 generator. A Block page followed by Rocks now places clusters of the
chosen block in new Standard, Amplified, Island, or Flat terrain chunks. The
Age index persists that selection, and the wrapper delegates all vanilla
terrain, biome, and structure generation. Cave and Void are intentionally
excluded until their feature placement rules are ported. This is not yet the
full 1.21.1 feature grammar or instability system.
## Verification of this first slice
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
+17
View File
@@ -194,6 +194,23 @@ The local `1.0.1.39-beta.1` follow-up rejects fluid at both feet and head
height during safe-landing searches. It also refuses to carve the fallback
platform into submerged space if no dry footing can be found.
The `1.0.1.40-beta.1` port slice seeds terrain and biome selection for
Descriptive Books with no authored symbol pages. It uses the same terrain and
biome-controller rank weights as the 1.21.1 line, freezes the resulting plan
in the Age index, and accepts the result after restart. The operator-only
`age create_random <name> <seed>` probe exercises this path. Random feature,
sky, weather, and instability choices are not yet ported; this slice covers
terrain and biomes only.
The `1.0.1.41-beta.1` slice establishes an Age-scoped decoration wrapper
for 26.3 and accepts one Block material page immediately before a Rocks feature
page. Newly generated surface chunks place small rock clusters made from that
block without modifying the shared biome registry. The selected block and
generator survive a restart. Cave and Void terrain remain unsupported for this
first surface-feature path; unsupported combinations stay in the Binder.
Random feature selection, unpaired materials, duplicate feature pages, and
instability scoring remain future port steps.
From the repository root:
```sh
@@ -0,0 +1,13 @@
# Rivencraft 26.3 beta 1.0.1.41-beta.1
This beta expands Age generation on Minecraft 26.3.
- Descriptive Books with no authored symbol pages now choose a seeded random terrain and biome distribution. The chosen plan is saved with the Age and restored after restart.
- A Block material page followed by a Rocks feature page now generates small clusters of that block in newly generated surface chunks. This first feature path supports Standard, Amplified, Island, and Flat terrain; Cave and Void combinations remain unsupported.
- Expanded dedicated-server checks cover random Age plans and authored Rocks generation across restart.
The player verified the Rocks feature in game. Random feature selection, duplicate feature pages, unpaired materials, and instability scoring are not yet ported. Existing chunks are not regenerated.
This 26.3 port is still experimental and does not yet include all features of the separate Minecraft 1.21.1 version. Use a test world and keep a backup. Minecraft 1.21.1 saves are not supported by this beta.
Requires Minecraft 26.3, NeoForge 26.3.0.99 or a compatible 26.3 build, and Java 25.
+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.39-beta.1
mod_version=1.0.1.41-beta.1
@@ -24,7 +24,9 @@ final class AgeLevelIndex extends SavedData {
.forGetter(index -> index.biomePlans),
Codec.unboundedMap(Identifier.CODEC,
Codec.STRING.xmap(PortAgeTerrain::valueOf, PortAgeTerrain::name))
.optionalFieldOf("terrain", Map.of()).forGetter(index -> index.terrain)
.optionalFieldOf("terrain", Map.of()).forGetter(index -> index.terrain),
Codec.unboundedMap(Identifier.CODEC, PortRockPlan.CODEC).optionalFieldOf("rocks", Map.of())
.forGetter(index -> index.rocks)
).apply(instance, AgeLevelIndex::new));
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
@@ -38,6 +40,7 @@ final class AgeLevelIndex extends SavedData {
private final Map<Identifier, Identifier> biomeOverrides;
private final Map<Identifier, PortBiomePlan> biomePlans;
private final Map<Identifier, PortAgeTerrain> terrain;
private final Map<Identifier, PortRockPlan> rocks;
private AgeLevelIndex() {
this.ages = new LinkedHashMap<>();
@@ -45,17 +48,20 @@ final class AgeLevelIndex extends SavedData {
this.biomeOverrides = new LinkedHashMap<>();
this.biomePlans = new LinkedHashMap<>();
this.terrain = new LinkedHashMap<>();
this.rocks = new LinkedHashMap<>();
}
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
Map<Identifier, Identifier> biomeOverrides,
Map<Identifier, PortBiomePlan> biomePlans,
Map<Identifier, PortAgeTerrain> terrain) {
Map<Identifier, PortAgeTerrain> terrain,
Map<Identifier, PortRockPlan> rocks) {
this.ages = new LinkedHashMap<>(ages);
this.retired = new LinkedHashSet<>(retired);
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
this.biomePlans = new LinkedHashMap<>(biomePlans);
this.terrain = new LinkedHashMap<>(terrain);
this.rocks = new LinkedHashMap<>(rocks);
}
Map<Identifier, Long> entries() {
@@ -81,13 +87,17 @@ final class AgeLevelIndex extends SavedData {
return terrain.getOrDefault(id, PortAgeTerrain.STANDARD);
}
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain) {
PortRockPlan rocks(Identifier id) { return rocks.get(id); }
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain,
PortRockPlan rockPlan) {
if (contains(id)) {
throw new IllegalArgumentException("Age already exists: " + id);
}
ages.put(id, seed);
if (biomePlan != null) biomePlans.put(id, biomePlan);
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
if (rockPlan != null) rocks.put(id, rockPlan);
setDirty();
}
@@ -96,16 +106,18 @@ final class AgeLevelIndex extends SavedData {
biomeOverrides.remove(id);
biomePlans.remove(id);
terrain.remove(id);
rocks.remove(id);
retired.add(id);
setDirty();
}
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
PortAgeTerrain ageTerrain) {
PortAgeTerrain ageTerrain, PortRockPlan rockPlan) {
if (!retired.remove(id)) throw new IllegalStateException("Age is not retired: " + id);
ages.put(id, seed);
if (biomePlan != null) biomePlans.put(id, biomePlan);
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
if (rockPlan != null) rocks.put(id, rockPlan);
setDirty();
}
@@ -112,6 +112,24 @@ public final class PortAgeCommands {
: java.util.List.of(Identifier.withDefaultNamespace("plains"),
Identifier.withDefaultNamespace("forest"))));
})))))
.then(Commands.literal("create_random")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> {
long seed = LongArgumentType.getLong(context, "seed");
PortRandomAgeSelection.Selection selected = PortRandomAgeSelection.select(
context.getSource().getServer(), seed);
int created = createAge(context, selected.terrain(), selected.biomes());
if (created != 0) context.getSource().sendSuccess(() -> Component.literal(
"Random Age plan: terrain=" + selected.terrain()
+ " biomes=" + selected.biomes()), false);
return created;
}))))
.then(Commands.literal("create_rocks")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.FLAT,
null, new PortRockPlan(Identifier.withDefaultNamespace("gold_block")))))))
.then(Commands.literal("list")
.executes(context -> {
var ages = RuntimeAgeBackend.savedAges(context.getSource().getServer());
@@ -215,6 +233,9 @@ public final class PortAgeCommands {
.then(Commands.argument("chunkOffset", IntegerArgumentType.integer(0))
.executes(context -> sampleAge(context,
IntegerArgumentType.getInteger(context, "chunkOffset"))))))
.then(Commands.literal("rocks_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeRocks)))
.then(Commands.literal("biome")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("x", IntegerArgumentType.integer())
@@ -228,12 +249,36 @@ public final class PortAgeCommands {
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan) {
return createAge(context, terrain, biomePlan, null);
}
private static int probeRocks(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortRockChunkGenerator rocks)) {
context.getSource().sendFailure(Component.literal("Age has no loaded Rocks generator: " + id));
return 0;
}
var material = net.minecraft.core.registries.BuiltInRegistries.BLOCK.getValue(rocks.rocks().material());
int count = 0;
for (int x = 0; x < 64; x++) for (int z = 0; z < 64; z++) {
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1;
if (level.getBlockState(new net.minecraft.core.BlockPos(x, y, z)).is(material)) count++;
}
int placed = count;
context.getSource().sendSuccess(() -> Component.literal(
"Rocks probe " + id + " material=" + rocks.rocks().material() + " count=" + placed), false);
return placed;
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan, PortRockPlan rockPlan) {
String name = StringArgumentType.getString(context, "name");
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
long seed = LongArgumentType.getLong(context, "seed");
ServerLevel level;
try {
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan, terrain);
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan, terrain, rockPlan);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
@@ -33,7 +33,9 @@ public final class PortAgeTravel {
}
private static BlockPos findLanding(ServerLevel target) {
if (target.getChunkSource().getGenerator() instanceof net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator noise
net.minecraft.world.level.chunk.ChunkGenerator generator = target.getChunkSource().getGenerator();
if (generator instanceof PortRockChunkGenerator rocks) generator = rocks.base();
if (generator instanceof net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator noise
&& noise.generatorSettings().value().noiseSettings().equals(target.registryAccess()
.lookupOrThrow(Registries.NOISE_SETTINGS)
.getOrThrow(net.minecraft.world.level.levelgen.NoiseGeneratorSettings.CAVES)
@@ -56,7 +58,7 @@ public final class PortAgeTravel {
}
// A Flat Age has a known surface. Use it directly before a heightmap
// fallback can place a platform in the air above a low superflat world.
FlatLevelGeneratorSettings flatSettings = switch (target.getChunkSource().getGenerator()) {
FlatLevelGeneratorSettings flatSettings = switch (generator) {
case FlatLevelSource flat -> flat.settings();
case PortFlatChunkGenerator flat -> flat.settings();
default -> null;
@@ -0,0 +1,81 @@
package net.dnorberg.rivencraft.age;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import net.dnorberg.rivencraft.writing.PortRegistryPages;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
/** Seed-frozen terrain and biome choices for a book with no authored symbols. */
public final class PortRandomAgeSelection {
private PortRandomAgeSelection() {}
public record Selection(PortAgeTerrain terrain, PortBiomePlan biomes) {}
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});
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});
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()));
int count = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
if (count > 1) {
Random countRandom = new Random(seed ^ 0x42494F4D45434F55L);
while (count < eligible.size() && countRandom.nextInt(3) != 0) count++;
}
List<Identifier> shuffled = new ArrayList<>(eligible);
Collections.shuffle(shuffled, new Random(seed ^ 0x42494F4D45504F4FL));
return new Selection(terrain, new PortBiomePlan(distribution, shuffled.subList(0, count)));
}
private static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
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;
}
List<Identifier> surface = server.overworld().getChunkSource().getGenerator().getBiomeSource()
.possibleBiomes().stream().flatMap(holder -> holder.unwrapKey().stream())
.map(key -> key.identifier()).distinct().sorted(Comparator.naturalOrder()).toList();
return surface.isEmpty() ? List.of(Identifier.withDefaultNamespace("plains")) : surface;
}
private static <T> T weighted(Random random, List<T> choices, int[] weights) {
int total = 0;
for (int weight : weights) total += weight;
int ticket = random.nextInt(total);
for (int index = 0; index < choices.size(); index++) {
ticket -= weights[index];
if (ticket < 0) return choices.get(index);
}
throw new IllegalStateException("Random Age ticket was not consumed");
}
}
@@ -0,0 +1,134 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.blending.Blender;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.HolderSet;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
/** Delegates 26.3 terrain generation and adds one Age-scoped Rocks decoration. */
public final class PortRockChunkGenerator extends ChunkGenerator {
public static final MapCodec<PortRockChunkGenerator> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
ChunkGenerator.CODEC.fieldOf("base").forGetter(generator -> generator.base),
PortRockPlan.CODEC.fieldOf("rocks").forGetter(generator -> generator.rocks),
com.mojang.serialization.Codec.LONG.fieldOf("age_seed").forGetter(generator -> generator.ageSeed)
).apply(instance, PortRockChunkGenerator::new));
private final ChunkGenerator base;
private final PortRockPlan rocks;
private final long ageSeed;
public PortRockChunkGenerator(ChunkGenerator base, PortRockPlan rocks, long ageSeed) {
super(base.getBiomeSource(), base::getBiomeGenerationSettings);
this.base = base;
this.rocks = rocks;
this.ageSeed = ageSeed;
}
public ChunkGenerator base() { return base; }
public PortRockPlan rocks() { return rocks; }
@Override protected MapCodec<? extends ChunkGenerator> codec() { return CODEC; }
@Override public void validate() { base.validate(); }
@Override public ChunkPos getOrigin(RandomState random) { return base.getOrigin(random); }
@Override public java.util.Optional<Identifier> getTypeNameForDataFixer() { return base.getTypeNameForDataFixer(); }
@Override public CompletableFuture<ChunkAccess> createBiomes(RandomState random, Blender blender,
StructureManager structures, ChunkAccess chunk) {
return base.createBiomes(random, blender, structures, chunk);
}
@Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets,
RandomState random, long seed) {
return base.createState(sets, random, seed);
}
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
StructureManager structures, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> dimension) {
base.createStructures(registries, state, structures, chunk, templates, dimension);
}
@Override public void createReferences(WorldGenLevel level, StructureManager structures, ChunkAccess chunk) {
base.createReferences(level, structures, chunk);
}
@Override public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level,
HolderSet<Structure> targets, BlockPos origin, int radius, boolean skipKnown) {
return base.findNearestMapStructure(level, targets, origin, radius, skipKnown);
}
@Override public void spawnOriginalMobs(WorldGenRegion level) { base.spawnOriginalMobs(level); }
@Override public int getSpawnHeight(LevelHeightAccessor height) { return base.getSpawnHeight(height); }
@Override public int getGenDepth() { return base.getGenDepth(); }
@Override public int getSeaLevel() { return base.getSeaLevel(); }
@Override public int getMinY() { return base.getMinY(); }
@Override public CompletableFuture<ChunkAccess> buildTerrain(ChunkAccess chunk, Blender blender,
RandomState random, StructureManager structures, BiomeManager biomes,
WorldGenRegion carverBiomeRegion, Set<Holder<Biome>> possibleBiomes) {
return base.buildTerrain(chunk, blender, random, structures, biomes, carverBiomeRegion, possibleBiomes);
}
@Override public int getBaseHeight(int x, int z, Heightmap.Types type,
LevelHeightAccessor height, RandomState random) {
return base.getBaseHeight(x, z, type, height, random);
}
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor height, RandomState random) {
return base.getBaseColumn(x, z, height, random);
}
@Override public void addDebugScreenInfo(List<String> info, RandomState random,
BlockPos pos, SamplerContext context) {
base.addDebugScreenInfo(info, random, pos, context);
}
@Override public void refreshFeaturesPerStep() {
base.refreshFeaturesPerStep();
super.refreshFeaturesPerStep();
}
@Override public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk,
StructureManager structures) {
base.applyBiomeDecoration(level, chunk, structures);
ChunkPos chunkPos = chunk.getPos();
long seed = ageSeed ^ (long) chunkPos.x() * 341873128712L ^ (long) chunkPos.z() * 132897987541L;
RandomSource random = RandomSource.create(seed);
if (random.nextInt(5) != 0) return;
BlockState material = BuiltInRegistries.BLOCK.getValue(rocks.material()).defaultBlockState();
int centerX = chunkPos.getMinBlockX() + 2 + random.nextInt(12);
int centerZ = chunkPos.getMinBlockZ() + 2 + random.nextInt(12);
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) {
if (random.nextInt(4) == 0) continue;
int x = centerX + dx;
int z = centerZ + dz;
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
BlockPos place = new BlockPos(x, y, z);
if (level.getBlockState(place).isAir()
&& level.getFluidState(place).isEmpty()
&& level.getBlockState(place.below()).isFaceSturdy(level, place.below(), net.minecraft.core.Direction.UP))
level.setBlock(place, material, 2);
}
}
}
@@ -0,0 +1,12 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.resources.Identifier;
/** First frozen authored feature: one Rocks page with an explicit block material. */
public record PortRockPlan(Identifier material) {
public static final Codec<PortRockPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Identifier.CODEC.fieldOf("material").forGetter(PortRockPlan::material)
).apply(instance, PortRockPlan::new));
}
@@ -57,7 +57,7 @@ public final class RuntimeAgeBackend {
for (Map.Entry<Identifier, Long> age : index.entries().entrySet()) {
try {
load(server, age.getKey(), age.getValue(), index.biomePlan(age.getKey()),
index.terrain(age.getKey()));
index.terrain(age.getKey()), index.rocks(age.getKey()));
} catch (RuntimeException exception) {
Rivencraft.LOGGER.error("Could not restore Age {}. Its saved index remains intact.", age.getKey(), exception);
}
@@ -75,12 +75,17 @@ public final class RuntimeAgeBackend {
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
return create(server, id, seed, biomePlan, terrain, null);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks) {
checkServerThread(server);
ageName(id);
AgeLevelIndex index = index(server);
if (index.contains(id)) throw new IllegalArgumentException("Age already exists: " + id);
ServerLevel level = load(server, id, seed, biomePlan, terrain);
index.add(id, seed, biomePlan, terrain);
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks);
index.add(id, seed, biomePlan, terrain, rocks);
// A newly bound book may be used immediately. Persist its Age identity
// before publishing the catalog so a later crash cannot leave that
// book pointing at a world omitted from the on-disk index.
@@ -100,7 +105,8 @@ public final class RuntimeAgeBackend {
public static ServerLevel reload(MinecraftServer server, Identifier id) {
checkServerThread(server);
long seed = requireSeed(server, id);
ServerLevel level = load(server, id, seed, index(server).biomePlan(id), index(server).terrain(id));
ServerLevel level = load(server, id, seed, index(server).biomePlan(id), index(server).terrain(id),
index(server).rocks(id));
AgeCatalogSync.onCatalogChanged(server);
return level;
}
@@ -112,18 +118,19 @@ public final class RuntimeAgeBackend {
long previousSeed = requireSeed(server, id);
PortBiomePlan biomePlan = index.biomePlan(id);
PortAgeTerrain terrain = index.terrain(id);
PortRockPlan rocks = index.rocks(id);
if (previousSeed == seed) return;
boolean loaded = server.getLevel(dimensionKey(id)) != null;
if (loaded) unloadLoaded(server, id);
try {
if (loaded) load(server, id, seed, biomePlan, terrain);
if (loaded) load(server, id, seed, biomePlan, terrain, rocks);
index.reseed(id, seed);
server.overworld().getDataStorage().saveAndJoin();
if (loaded) AgeCatalogSync.onCatalogChanged(server);
} catch (RuntimeException exception) {
if (loaded) {
try {
load(server, id, previousSeed, biomePlan, terrain);
load(server, id, previousSeed, biomePlan, terrain, rocks);
AgeCatalogSync.onCatalogChanged(server);
} catch (RuntimeException rollbackFailure) {
exception.addSuppressed(rollbackFailure);
@@ -141,6 +148,7 @@ public final class RuntimeAgeBackend {
long seed = requireSeed(server, id);
PortBiomePlan biomePlan = index.biomePlan(id);
PortAgeTerrain terrain = index.terrain(id);
PortRockPlan rocks = index.rocks(id);
boolean loaded = server.getLevel(dimensionKey(id)) != null;
ArchivePaths paths = archivePaths(server, name);
if (loaded) unloadLoaded(server, id);
@@ -159,14 +167,14 @@ public final class RuntimeAgeBackend {
}
} catch (IOException | RuntimeException exception) {
try {
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain);
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks);
server.overworld().getDataStorage().saveAndJoin();
} catch (RuntimeException rollbackFailure) {
exception.addSuppressed(rollbackFailure);
}
if (loaded) {
try {
load(server, id, seed, biomePlan, terrain);
load(server, id, seed, biomePlan, terrain, rocks);
} catch (RuntimeException rollbackFailure) {
exception.addSuppressed(rollbackFailure);
}
@@ -282,7 +290,7 @@ public final class RuntimeAgeBackend {
}
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks) {
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
@@ -353,6 +361,7 @@ public final class RuntimeAgeBackend {
};
generator = new NoiseBasedChunkGenerator(source, settings);
}
if (rocks != null) generator = new PortRockChunkGenerator(generator, rocks, seed);
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed));
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
ServerLevel level = new ServerLevel(server, server.executor, server.storageSource,
@@ -2,7 +2,9 @@ package net.dnorberg.rivencraft.network;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.age.PortAgeTravel;
import net.dnorberg.rivencraft.age.PortAgeTerrain;
import net.dnorberg.rivencraft.age.RuntimeAgeBackend;
import net.dnorberg.rivencraft.age.PortRandomAgeSelection;
import net.dnorberg.rivencraft.item.LinkBookTarget;
import net.dnorberg.rivencraft.item.LinkProperties;
import net.dnorberg.rivencraft.item.LinkingBookItem;
@@ -125,8 +127,15 @@ public final class PortBookNetworking {
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
}
try {
RuntimeAgeBackend.create(server, id, player.getRandom().nextLong(), biomePlan,
PortAuthoredWriting.terrain(pages));
long seed = player.getRandom().nextLong();
PortAgeTerrain terrain = PortAuthoredWriting.terrain(pages);
if (pages.isEmpty()) {
PortRandomAgeSelection.Selection random = PortRandomAgeSelection.select(server, seed);
terrain = random.terrain();
biomePlan = random.biomes();
}
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
PortAuthoredWriting.rockPlan(pages));
} catch (IllegalArgumentException | IllegalStateException exception) {
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
return null;
@@ -3,6 +3,7 @@ package net.dnorberg.rivencraft.registry;
import com.mojang.serialization.MapCodec;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.age.PortFlatChunkGenerator;
import net.dnorberg.rivencraft.age.PortRockChunkGenerator;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.neoforged.bus.api.IEventBus;
@@ -15,6 +16,7 @@ public final class PortChunkGenerators {
static {
GENERATORS.register("age_flat", () -> PortFlatChunkGenerator.CODEC);
GENERATORS.register("age_rocks", () -> PortRockChunkGenerator.CODEC);
}
private PortChunkGenerators() {}
@@ -4,6 +4,7 @@ import java.util.List;
import java.util.ArrayList;
import net.dnorberg.rivencraft.age.PortBiomePlan;
import net.dnorberg.rivencraft.age.PortAgeTerrain;
import net.dnorberg.rivencraft.age.PortRockPlan;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.registries.Registries;
@@ -25,6 +26,8 @@ public final class PortAuthoredWriting {
Rivencraft.MOD_ID, "terrain/void");
private static final Identifier CAVE_TERRAIN = Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "terrain/cave");
private static final Identifier ROCKS = Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "feature/rocks");
private PortAuthoredWriting() {}
@@ -33,11 +36,19 @@ public final class PortAuthoredWriting {
boolean terrain = false;
int biomes = 0;
int controllers = 0;
boolean material = false;
boolean rocks = false;
for (Identifier page : pages) {
if (isTerrain(page) && !terrain) {
terrain = true;
} else if (controller(page) != null && controllers++ == 0) {
// An explicit controller needs an authored pool.
} else if (isBlockPage(page) && !material && !rocks
&& PortSymbolPages.find(page) != null
&& PortSymbolPages.find(page).category().equals("rivencraft:block")) {
material = true;
} else if (page.equals(ROCKS) && material && !rocks) {
rocks = true;
} else if (page.getNamespace().equals(Rivencraft.MOD_ID)
&& page.getPath().startsWith("biome/") && biomes < MAX_AUTHORED_BIOMES
&& PortSymbolPages.find(page) != null
@@ -46,6 +57,7 @@ public final class PortAuthoredWriting {
biomes++;
} else return false;
}
if (material != rocks || rocks && (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
if (controllers != 0) {
PortBiomePlan.Distribution distribution = pages.stream().map(PortAuthoredWriting::controller)
.filter(value -> value != null).findFirst().orElseThrow();
@@ -65,6 +77,10 @@ public final class PortAuthoredWriting {
|| page.equals(VOID_TERRAIN) || page.equals(CAVE_TERRAIN);
}
private static boolean isBlockPage(Identifier page) {
return page.getNamespace().equals(Rivencraft.MOD_ID) && page.getPath().startsWith("block/");
}
private static PortBiomePlan.Distribution controller(Identifier page) {
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
return switch (page.getPath()) {
@@ -91,6 +107,13 @@ public final class PortAuthoredWriting {
return PortAgeTerrain.STANDARD;
}
public static PortRockPlan rockPlan(List<Identifier> pages) {
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
return pages.contains(ROCKS) ? new PortRockPlan(pages.stream()
.filter(PortAuthoredWriting::isBlockPage).map(PortRegistryPages::blockId)
.findFirst().orElseThrow()) : null;
}
/** Resolves all authored biomes from the active registry before creating a level. */
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) {
if (!supports(pages)) throw new IllegalArgumentException(
@@ -99,7 +122,7 @@ public final class PortAuthoredWriting {
PortBiomePlan.Distribution distribution = null;
boolean caveTerrain = pages.contains(CAVE_TERRAIN);
for (Identifier page : pages) {
if (isTerrain(page)) continue;
if (isTerrain(page) || page.equals(ROCKS) || isBlockPage(page)) continue;
PortBiomePlan.Distribution selected = controller(page);
if (selected != null) { distribution = selected; continue; }
Identifier biome = PortRegistryPages.biomeId(page);
+30 -1
View File
@@ -75,6 +75,8 @@ send_and_wait 'rivencraft_port age create smoke_cave 112233 cave' 'Created riven
send_and_wait 'rivencraft_port age create_multi smoke_flat_multi 24681 flat' 'Created rivencraft:age/smoke_flat_multi with seed 24681 terrain=FLAT'
send_and_wait 'rivencraft_port age create_multi smoke_void_multi 13580 void' 'Created rivencraft:age/smoke_void_multi with seed 13580 terrain=VOID'
send_and_wait 'rivencraft_port age create_multi smoke_cave_multi 112234 cave' 'Created rivencraft:age/smoke_cave_multi with seed 112234 terrain=CAVE'
send_and_wait 'rivencraft_port age create_random smoke_random 889900' 'Random Age plan: terrain='
send_and_wait 'rivencraft_port age create_rocks smoke_rocks 445566' 'Created rivencraft:age/smoke_rocks with seed 445566 terrain=FLAT'
# The index must reach disk while the server is still running. A graceful
# shutdown would hide the interrupted-save regression this checks.
index_file="$server_dir/world/dimensions/minecraft/overworld/data/rivencraft/runtime_ages.dat"
@@ -90,6 +92,8 @@ gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_cave'
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_flat_multi'
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_void_multi'
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_cave_multi'
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_random'
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_rocks'
send_and_wait 'rivencraft_port age sample smoke_alpha' 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_beta' 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_flat' 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain='
@@ -98,12 +102,21 @@ send_and_wait 'rivencraft_port age sample smoke_cave' 'rivencraft:age/smoke_cave
send_and_wait 'rivencraft_port age sample smoke_flat_multi' 'rivencraft:age/smoke_flat_multi seed=24681 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_void_multi' 'rivencraft:age/smoke_void_multi seed=13580 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_cave_multi' 'rivencraft:age/smoke_cave_multi seed=112234 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_random' 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_rocks' 'rivencraft:age/smoke_rocks seed=445566 chunkOffset=0 terrain='
send_and_wait 'execute in rivencraft:age/smoke_flat run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_flat to be force loaded'
send_and_wait 'execute in rivencraft:age/smoke_void run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_void to be force loaded'
send_and_wait 'execute in rivencraft:age/smoke_flat_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_flat_multi to be force loaded'
send_and_wait 'execute in rivencraft:age/smoke_void_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_void_multi to be force loaded'
send_and_wait 'execute in rivencraft:age/smoke_cave run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_cave to be force loaded'
send_and_wait 'execute in rivencraft:age/smoke_cave_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_cave_multi to be force loaded'
send_and_wait 'execute in rivencraft:age/smoke_rocks run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_rocks to be force loaded'
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks' 'Rocks probe rivencraft:age/smoke_rocks material=minecraft:gold_block count='
rocks_before=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks material=minecraft:gold_block count=' "$log_file" | tail -1 | sed 's/.*count=//')
[[ "$rocks_before" =~ ^[1-9][0-9]*$ ]] || {
printf 'Authored Rocks placed no gold blocks: %s\n' "$rocks_before" >&2
exit 1
}
send_and_wait 'execute in rivencraft:age/smoke_flat if block 0 0 0 minecraft:bedrock run say Flat bedrock starts at Y0' 'Flat bedrock starts at Y0'
send_and_wait 'execute in rivencraft:age/smoke_flat if block 0 3 0 minecraft:grass_block run say Flat surface is Y3' 'Flat surface is Y3'
send_and_wait 'execute in rivencraft:age/smoke_void if block 0 0 0 minecraft:air run say Void remains empty' 'Void remains empty'
@@ -119,6 +132,7 @@ alpha_before=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0
beta_before=$(grep -F 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
flat_before=$(grep -F 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
void_before=$(grep -F 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
random_before=$(grep -F 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
if [[ "$alpha_before" == "$beta_before" ]]; then
printf 'Different Age seeds produced the same terrain probe: %s\n' "$alpha_before" >&2
exit 1
@@ -139,6 +153,10 @@ send_and_wait 'rivencraft_port age sample smoke_cave' 'rivencraft:age/smoke_cave
send_and_wait 'rivencraft_port age sample smoke_flat_multi' 'rivencraft:age/smoke_flat_multi seed=24681 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_void_multi' 'rivencraft:age/smoke_void_multi seed=13580 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_cave_multi' 'rivencraft:age/smoke_cave_multi seed=112234 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_random' 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_rocks' 'rivencraft:age/smoke_rocks seed=445566 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks' 'Rocks probe rivencraft:age/smoke_rocks material=minecraft:gold_block count='
rocks_after=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks material=minecraft:gold_block count=' "$log_file" | tail -1 | sed 's/.*count=//')
send_and_wait 'rivencraft_port age biome smoke_flat_multi 384 128' 'rivencraft:age/smoke_flat_multi biome at 384,128=minecraft:forest'
send_and_wait 'rivencraft_port age biome smoke_void_multi 384 128' 'rivencraft:age/smoke_void_multi biome at 384,128=minecraft:forest'
send_and_wait 'rivencraft_port age biome smoke_cave_multi 384 128' 'rivencraft:age/smoke_cave_multi biome at 384,128=minecraft:dripstone_caves'
@@ -146,6 +164,7 @@ alpha_after=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 t
beta_after=$(grep -F 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
flat_after=$(grep -F 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
void_after=$(grep -F 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
random_after=$(grep -F 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
[[ "$alpha_before" == "$alpha_after" && "$beta_before" == "$beta_after" ]] || {
printf 'Age terrain changed across restart: %s/%s -> %s/%s\n' \
"$alpha_before" "$beta_before" "$alpha_after" "$beta_after" >&2
@@ -156,5 +175,15 @@ void_after=$(grep -F 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain
"$flat_before" "$void_before" "$flat_after" "$void_after" >&2
exit 1
}
[[ "$random_before" == "$random_after" ]] || {
printf 'Random Age terrain changed across restart: %s -> %s\n' \
"$random_before" "$random_after" >&2
exit 1
}
[[ "$rocks_before" == "$rocks_after" ]] || {
printf 'Authored Rocks changed across restart: %s -> %s\n' \
"$rocks_before" "$rocks_after" >&2
exit 1
}
stop_server
printf '26.3 dedicated-server smoke passed: Standard, Flat, Void and Cave Ages persisted across restart.\n'
printf '26.3 dedicated-server smoke passed: Standard, Flat, Void, Cave, random and authored Rocks Ages persisted across restart.\n'