From d75154fa9b40776741da459b0e746faacfdd5311 Mon Sep 17 00:00:00 2001 From: Dustin Norberg Date: Sun, 1 Nov 2026 13:25:42 -0500 Subject: [PATCH] Port seeded random Ages and authored Rocks to 26.3 beta --- docs/MINECRAFT_26_3_PORT.md | 14 ++ ports/mc263/README.md | 17 +++ ports/mc263/RELEASE_NOTES_1.0.1.41-beta.1.md | 13 ++ ports/mc263/gradle.properties | 2 +- .../rivencraft/age/AgeLevelIndex.java | 20 ++- .../rivencraft/age/PortAgeCommands.java | 47 +++++- .../rivencraft/age/PortAgeTravel.java | 6 +- .../age/PortRandomAgeSelection.java | 81 +++++++++++ .../age/PortRockChunkGenerator.java | 134 ++++++++++++++++++ .../dnorberg/rivencraft/age/PortRockPlan.java | 12 ++ .../rivencraft/age/RuntimeAgeBackend.java | 27 ++-- .../network/PortBookNetworking.java | 13 +- .../registry/PortChunkGenerators.java | 2 + .../writing/PortAuthoredWriting.java | 25 +++- scripts/smoke_server_263.sh | 31 +++- 15 files changed, 423 insertions(+), 21 deletions(-) create mode 100644 ports/mc263/RELEASE_NOTES_1.0.1.41-beta.1.md create mode 100644 ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortRandomAgeSelection.java create mode 100644 ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortRockChunkGenerator.java create mode 100644 ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortRockPlan.java diff --git a/docs/MINECRAFT_26_3_PORT.md b/docs/MINECRAFT_26_3_PORT.md index b3f5261..59e2515 100644 --- a/docs/MINECRAFT_26_3_PORT.md +++ b/docs/MINECRAFT_26_3_PORT.md @@ -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 diff --git a/ports/mc263/README.md b/ports/mc263/README.md index 37755c6..c965024 100644 --- a/ports/mc263/README.md +++ b/ports/mc263/README.md @@ -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 ` 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 diff --git a/ports/mc263/RELEASE_NOTES_1.0.1.41-beta.1.md b/ports/mc263/RELEASE_NOTES_1.0.1.41-beta.1.md new file mode 100644 index 0000000..ec8b381 --- /dev/null +++ b/ports/mc263/RELEASE_NOTES_1.0.1.41-beta.1.md @@ -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. diff --git a/ports/mc263/gradle.properties b/ports/mc263/gradle.properties index 9c70723..7bafdde 100644 --- a/ports/mc263/gradle.properties +++ b/ports/mc263/gradle.properties @@ -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 diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeLevelIndex.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeLevelIndex.java index 0adefec..f253a7d 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeLevelIndex.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeLevelIndex.java @@ -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 TYPE = new SavedDataType<>( @@ -38,6 +40,7 @@ final class AgeLevelIndex extends SavedData { private final Map biomeOverrides; private final Map biomePlans; private final Map terrain; + private final Map 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 ages, List retired, Map biomeOverrides, Map biomePlans, - Map terrain) { + Map terrain, + Map 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 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(); } diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeCommands.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeCommands.java index 47576d9..53d7056 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeCommands.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeCommands.java @@ -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 context, PortAgeTerrain terrain, PortBiomePlan biomePlan) { + return createAge(context, terrain, biomePlan, null); + } + + private static int probeRocks(CommandContext 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 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; diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeTravel.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeTravel.java index 84d77b2..39e091a 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeTravel.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeTravel.java @@ -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; diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortRandomAgeSelection.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortRandomAgeSelection.java new file mode 100644 index 0000000..1614bd4 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortRandomAgeSelection.java @@ -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 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 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 eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) { + if (terrain == PortAgeTerrain.CAVE) { + List 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 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 weighted(Random random, List 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"); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortRockChunkGenerator.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortRockChunkGenerator.java new file mode 100644 index 0000000..715ef8d --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortRockChunkGenerator.java @@ -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 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 codec() { return CODEC; } + @Override public void validate() { base.validate(); } + @Override public ChunkPos getOrigin(RandomState random) { return base.getOrigin(random); } + @Override public java.util.Optional getTypeNameForDataFixer() { return base.getTypeNameForDataFixer(); } + @Override public CompletableFuture createBiomes(RandomState random, Blender blender, + StructureManager structures, ChunkAccess chunk) { + return base.createBiomes(random, blender, structures, chunk); + } + @Override public ChunkGeneratorStructureState createState(HolderLookup 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 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> findNearestMapStructure(ServerLevel level, + HolderSet 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 buildTerrain(ChunkAccess chunk, Blender blender, + RandomState random, StructureManager structures, BiomeManager biomes, + WorldGenRegion carverBiomeRegion, Set> 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 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); + } + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortRockPlan.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortRockPlan.java new file mode 100644 index 0000000..d3b9771 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortRockPlan.java @@ -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 CODEC = RecordCodecBuilder.create(instance -> instance.group( + Identifier.CODEC.fieldOf("material").forGetter(PortRockPlan::material) + ).apply(instance, PortRockPlan::new)); +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/RuntimeAgeBackend.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/RuntimeAgeBackend.java index f2f1501..bd11eb8 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/RuntimeAgeBackend.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/RuntimeAgeBackend.java @@ -57,7 +57,7 @@ public final class RuntimeAgeBackend { for (Map.Entry 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 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, diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/PortBookNetworking.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/PortBookNetworking.java index ed0799b..97e4ca1 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/PortBookNetworking.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/PortBookNetworking.java @@ -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; diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/registry/PortChunkGenerators.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/registry/PortChunkGenerators.java index 63721d8..22b8f7c 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/registry/PortChunkGenerators.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/registry/PortChunkGenerators.java @@ -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() {} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/PortAuthoredWriting.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/PortAuthoredWriting.java index 88aae28..6d87847 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/PortAuthoredWriting.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/PortAuthoredWriting.java @@ -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 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 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); diff --git a/scripts/smoke_server_263.sh b/scripts/smoke_server_263.sh index 538b71a..6bb7359 100755 --- a/scripts/smoke_server_263.sh +++ b/scripts/smoke_server_263.sh @@ -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'