diff --git a/docs/MINECRAFT_26_3_PORT.md b/docs/MINECRAFT_26_3_PORT.md index f40f437..5815d87 100644 --- a/docs/MINECRAFT_26_3_PORT.md +++ b/docs/MINECRAFT_26_3_PORT.md @@ -480,6 +480,21 @@ symbol-page data and authoring pipeline, not just drawing more placeholders. The local `1.0.1.15-alpha.1` follow-up changes the missing-panel cue to the Link Panel's 16×16 item icon while preserving its red pulse and tooltip. +The local `1.0.1.33-beta.1` slice adds Flat and Void terrain to authored Binder +books. Flat uses vanilla superflat layers and can use one authored Biome; Void +generates no terrain but can use one authored Biome for environmental behavior. +Without a Biome page, Void uses `minecraft:the_void`. Books with multiple Biome +pages and Flat/Void terrain remain unassembled until their custom distribution +generator is ported. The dedicated-server smoke creates, samples, and reloads +both terrain families in a disposable world. + +The local `1.0.1.34-beta.1` follow-up moves Flat ground to Y=0 and targets its +surface for travel. `1.0.1.35-beta.1` enables the selected biome's decoration +on Flat. `1.0.1.36-beta.1` ports a multi-biome Flat/Void chunk generator and +accepts the existing authored distribution pages for those terrains. New Flat +chunks can vary vegetation by biome; Void keeps biome climate without adding +a floor or decorations. These are beta-port slices, not full 1.21.1 parity. + ## 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 b001a69..ccb0c32 100644 --- a/ports/mc263/README.md +++ b/ports/mc263/README.md @@ -3,8 +3,9 @@ This is an **experimental beta port** for issues #97–#104, not gameplay parity with the Minecraft 1.21.1 release. The 26.3 backend persists separate Ages and their seeds. Books, the Book Binder, and the Ink Mixer work, and the Binder can -author Standard, Amplified, or Island terrain with supported biome-distribution -pages. Other terrain families, feature/material effects, portals, and much of +author Standard, Amplified, Island, Flat, or Void terrain with supported +biome-distribution pages. Other terrain families, feature/material effects, +portals, and much of the 1.21.1 gameplay remain unported. Use a test world and keep a backup; do not substitute this JAR for the 1.21.1 mod or import 1.21.1 saves. @@ -153,6 +154,29 @@ terrain; and saved terrain/biome plans across reload. A Binder or Ink Mixer beside another block no longer hides that block's faces. Unsupported authoring still stays in the Binder rather than silently producing an incorrect Age. +The local `1.0.1.33-beta.1` slice adds Flat and Void terrain to Binder +authoring. Flat uses vanilla's superflat layers; Void has no generated floor +and uses the selected Biome for its environmental rules. Each accepts zero or +one authored Biome page. Unsupported multi-biome Flat/Void books remain in the +Binder until their custom generator is ported. The operator probe can create +test Ages with `rivencraft_port age create flat|void`. + +The local `1.0.1.34-beta.1` follow-up starts Flat's solid layers at Y=0 +(bedrock at 0, dirt at 1–2, grass at 3) and lands players at the ground surface +instead of creating an elevated fallback platform. This affects newly +generated Flat chunks; previously generated Age chunks are not rewritten. + +The local `1.0.1.35-beta.1` follow-up enables biome decoration on newly +generated Flat chunks, allowing the selected biome's trees, vegetation, and +other placed features to appear on the Flat surface. Void remains undecorated. + +The local `1.0.1.36-beta.1` slice accepts multiple Biome pages with Flat and +Void terrain. It ports the 1.21.1 authored-biome Flat generator to 26.3, so +Tiled, Grid, and scaled regions affect climate in both terrain types and +surface decoration in Flat. Void continues to generate no floor or biome +decoration. The operator-only `age create_multi` probe creates two-biome test +Ages for the dedicated-server smoke test. + From the repository root: ```sh diff --git a/ports/mc263/RELEASE_NOTES_1.0.1.36-beta.1.md b/ports/mc263/RELEASE_NOTES_1.0.1.36-beta.1.md new file mode 100644 index 0000000..f2ab468 --- /dev/null +++ b/ports/mc263/RELEASE_NOTES_1.0.1.36-beta.1.md @@ -0,0 +1,12 @@ +# Rivencraft 26.3 beta 1.0.1.36-beta.1 + +This beta adds Flat and Void terrain to authored Ages on Minecraft 26.3. + +- Bind Descriptive Books with Flat or Void terrain, including multiple Biome pages and supported biome-distribution pages. +- Flat ground starts at Y=0; travel lands on its surface rather than on an elevated fallback platform. +- Flat Ages generate the selected biomes' surface decorations, including trees and vegetation. Void Ages remain empty while retaining authored biome climate. +- Expanded dedicated-server checks for Flat and Void terrain, biome distribution, and persistence after restart. + +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 de0e5b9..d1b5cf3 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.32-beta.1 +mod_version=1.0.1.36-beta.1 diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/Rivencraft.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/Rivencraft.java index 9a83455..00c319d 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/Rivencraft.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/Rivencraft.java @@ -21,6 +21,7 @@ import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.network.PortBookNetworking; import net.dnorberg.rivencraft.registry.ModCreativeTabs; import net.dnorberg.rivencraft.registry.PortBiomeSources; +import net.dnorberg.rivencraft.registry.PortChunkGenerators; import net.dnorberg.rivencraft.writing.LinkingBookRecipe; import net.minecraft.core.component.DataComponentType; import net.minecraft.core.registries.Registries; @@ -152,6 +153,7 @@ public final class Rivencraft { RECIPE_SERIALIZERS.register(modEventBus); ModCreativeTabs.register(modEventBus); PortBiomeSources.register(modEventBus); + PortChunkGenerators.register(modEventBus); NeoForge.EVENT_BUS.addListener(RuntimeAgeBackend::onLevelLoad); NeoForge.EVENT_BUS.addListener(PortAgeCommands::register); NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin); 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 44fc710..02b43a3 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 @@ -3,9 +3,11 @@ package net.dnorberg.rivencraft.age; import com.mojang.brigadier.arguments.LongArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.item.LinkBookTarget; import net.minecraft.commands.Commands; +import net.minecraft.commands.CommandSourceStack; import net.minecraft.core.registries.Registries; import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; @@ -20,6 +22,7 @@ import net.neoforged.neoforge.event.RegisterCommandsEvent; import net.neoforged.fml.ModList; import java.util.Map; import java.util.TreeMap; +import java.util.Locale; /** Operator-only backend and synchronized-travel probes, not normal book behavior. */ public final class PortAgeCommands { @@ -66,23 +69,44 @@ public final class PortAgeCommands { .then(Commands.literal("create") .then(Commands.argument("name", StringArgumentType.word()) .then(Commands.argument("seed", LongArgumentType.longArg()) - .executes(context -> { - 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); - } catch (IllegalArgumentException | IllegalStateException exception) { - context.getSource().sendFailure(Component.literal(exception.getMessage())); - return 0; - } - context.getSource().sendSuccess(() -> Component.literal( - "Created " + id + " with seed " + level.getSeed()), true); - return 1; - })))) + .executes(context -> createAge(context, PortAgeTerrain.STANDARD)) + .then(Commands.argument("terrain", StringArgumentType.word()) + .executes(context -> { + PortAgeTerrain terrain; + try { + terrain = PortAgeTerrain.valueOf(StringArgumentType.getString( + context, "terrain").toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException exception) { + context.getSource().sendFailure(Component.literal( + "Unknown terrain; use standard, amplified, island, flat, or void")); + return 0; + } + return createAge(context, terrain); + }))))) + .then(Commands.literal("create_multi") + .then(Commands.argument("name", StringArgumentType.word()) + .then(Commands.argument("seed", LongArgumentType.longArg()) + .then(Commands.argument("terrain", StringArgumentType.word()) + .executes(context -> { + PortAgeTerrain terrain; + try { + terrain = PortAgeTerrain.valueOf(StringArgumentType.getString( + context, "terrain").toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException exception) { + context.getSource().sendFailure(Component.literal( + "Unknown terrain; use flat or void")); + return 0; + } + if (terrain != PortAgeTerrain.FLAT && terrain != PortAgeTerrain.VOID) { + context.getSource().sendFailure(Component.literal( + "Multi-biome probe supports flat or void only")); + return 0; + } + return createAge(context, terrain, new PortBiomePlan( + PortBiomePlan.Distribution.TILED, java.util.List.of( + Identifier.withDefaultNamespace("plains"), + Identifier.withDefaultNamespace("forest")))); + }))))) .then(Commands.literal("list") .executes(context -> { var ages = RuntimeAgeBackend.savedAges(context.getSource().getServer()); @@ -185,7 +209,33 @@ public final class PortAgeCommands { .executes(context -> sampleAge(context, 0)) .then(Commands.argument("chunkOffset", IntegerArgumentType.integer(0)) .executes(context -> sampleAge(context, - IntegerArgumentType.getInteger(context, "chunkOffset")))))))); + IntegerArgumentType.getInteger(context, "chunkOffset")))))) + .then(Commands.literal("biome") + .then(Commands.argument("name", StringArgumentType.word()) + .then(Commands.argument("x", IntegerArgumentType.integer()) + .then(Commands.argument("z", IntegerArgumentType.integer()) + .executes(PortAgeCommands::sampleBiome))))))); + } + + private static int createAge(CommandContext context, PortAgeTerrain terrain) { + return createAge(context, terrain, null); + } + + private static int createAge(CommandContext context, + PortAgeTerrain terrain, PortBiomePlan biomePlan) { + 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); + } catch (IllegalArgumentException | IllegalStateException exception) { + context.getSource().sendFailure(Component.literal(exception.getMessage())); + return 0; + } + context.getSource().sendSuccess(() -> Component.literal( + "Created " + id + " with seed " + level.getSeed() + " terrain=" + terrain), true); + return 1; } private static int sampleAge(com.mojang.brigadier.context.CommandContext context, @@ -213,6 +263,23 @@ public final class PortAgeCommands { return 1; } + private static int sampleBiome(CommandContext context) { + Identifier id = ageId(context); + ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id)); + if (level == null) { + context.getSource().sendFailure(Component.literal("Age not loaded: " + id)); + return 0; + } + int x = IntegerArgumentType.getInteger(context, "x"); + int z = IntegerArgumentType.getInteger(context, "z"); + level.getChunk(x >> 4, z >> 4); + Identifier biome = level.getBiome(new net.minecraft.core.BlockPos(x, 4, z)) + .unwrapKey().orElseThrow().identifier(); + context.getSource().sendSuccess(() -> Component.literal( + id + " biome at " + x + "," + z + "=" + biome), false); + return 1; + } + private static Identifier ageId(com.mojang.brigadier.context.CommandContext context) { return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + StringArgumentType.getString(context, "name")); diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeTerrain.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeTerrain.java index 99f5826..2aec3ae 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeTerrain.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeTerrain.java @@ -4,5 +4,7 @@ package net.dnorberg.rivencraft.age; public enum PortAgeTerrain { STANDARD, AMPLIFIED, - ISLAND + ISLAND, + FLAT, + VOID } 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 2e60b20..2be87a2 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 @@ -11,6 +11,8 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.FlatLevelSource; +import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; /** Shared, server-authoritative landing for the 26.3 book and operator probe. */ public final class PortAgeTravel { @@ -31,16 +33,26 @@ public final class PortAgeTravel { } private static BlockPos findLanding(ServerLevel target) { + // 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()) { + case FlatLevelSource flat -> flat.settings(); + case PortFlatChunkGenerator flat -> flat.settings(); + default -> null; + }; + if (flatSettings != null && !flatSettings.getLayersInfo().isEmpty()) { + int surfaceY = target.getMinY() + flatSettings.getLayersInfo().stream() + .mapToInt(layer -> layer.getHeight()).sum(); + BlockPos surface = new BlockPos(0, surfaceY, 0); + if (hasFooting(target, surface)) return surface; + } for (int radius = 0; radius <= 16; radius++) { for (int x = -radius; x <= radius; x++) { for (int z = -radius; z <= radius; z++) { if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue; int y = target.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z); BlockPos foot = new BlockPos(x, y, z); - BlockPos floor = foot.below(); - if (target.getBlockState(floor).isFaceSturdy(target, floor, Direction.UP) - && target.getBlockState(foot).getCollisionShape(target, foot).isEmpty() - && target.getBlockState(foot.above()).getCollisionShape(target, foot.above()).isEmpty()) { + if (hasFooting(target, foot)) { return foot; } } @@ -60,4 +72,11 @@ public final class PortAgeTravel { target.setBlock(foot.above(), Blocks.AIR.defaultBlockState(), 3); return foot; } + + private static boolean hasFooting(ServerLevel target, BlockPos foot) { + BlockPos floor = foot.below(); + return target.getBlockState(floor).isFaceSturdy(target, floor, Direction.UP) + && target.getBlockState(foot).getCollisionShape(target, foot).isEmpty() + && target.getBlockState(foot.above()).getCollisionShape(target, foot.above()).isEmpty(); + } } diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortFlatChunkGenerator.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortFlatChunkGenerator.java new file mode 100644 index 0000000..bec4c45 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortFlatChunkGenerator.java @@ -0,0 +1,142 @@ +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 java.util.stream.Stream; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.HolderSet; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.NoiseColumn; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeGenerationSettings; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +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.GenerationStep; +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.flat.FlatLevelGeneratorSettings; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.neoforged.neoforge.common.util.MemoizedFunction; +import org.jspecify.annotations.Nullable; + +/** Flat layers with the authored biome distribution used by 1.21.1 Ages. */ +public final class PortFlatChunkGenerator extends ChunkGenerator { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( + BiomeSource.CODEC.fieldOf("biome_source").forGetter(generator -> generator.biomeSource), + FlatLevelGeneratorSettings.CODEC.fieldOf("settings").forGetter(PortFlatChunkGenerator::settings) + ).apply(instance, PortFlatChunkGenerator::new)); + + private final FlatLevelGeneratorSettings settings; + private final MemoizedFunction, BiomeGenerationSettings> generationSettingsGetter; + + public PortFlatChunkGenerator(BiomeSource biomeSource, FlatLevelGeneratorSettings settings) { + super(biomeSource, this.generationSettingsGetter = MemoizedFunction.of( + biome -> generationSettings(settings, biome))); + this.settings = settings; + } + + public FlatLevelGeneratorSettings settings() { + return settings; + } + + private static BiomeGenerationSettings generationSettings(FlatLevelGeneratorSettings settings, Holder biome) { + // Void biomes still control climate, but must not place terrain features. + if (settings.getLayersInfo().isEmpty()) return BiomeGenerationSettings.EMPTY; + if (biome.equals(settings.getBiome())) return settings.adjustGenerationSettings(biome); + BiomeGenerationSettings.PlainBuilder builder = new BiomeGenerationSettings.PlainBuilder(); + List> features = biome.value().getGenerationSettings().features(); + for (int step = 0; step < features.size(); step++) { + if (step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES.ordinal() + || step == GenerationStep.Decoration.SURFACE_STRUCTURES.ordinal()) continue; + for (Holder feature : features.get(step)) builder.addFeature(step, feature); + } + return builder.build(); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + public ChunkGeneratorStructureState createState(HolderLookup structures, + RandomState random, long seed) { + Stream> entries = settings.structureOverrides() + .map(HolderSet::stream) + .orElseGet(() -> structures.listElements().map(holder -> (Holder) holder)); + return ChunkGeneratorStructureState.createForFlat(random, seed, getOrigin(random), biomeSource, entries); + } + + @Override + public int getSpawnHeight(LevelHeightAccessor height) { + return height.getMinY() + Math.min(height.getHeight(), settings.getLayers().size()); + } + + @Override + public CompletableFuture buildTerrain(ChunkAccess chunk, Blender blender, + RandomState random, StructureManager structures, BiomeManager biomes, + @Nullable WorldGenRegion carverBiomeRegion, Set> possibleBiomes) { + List layers = settings.getLayers(); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG); + Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG); + for (int layer = 0; layer < Math.min(chunk.getHeight(), layers.size()); layer++) { + BlockState state = layers.get(layer); + if (state == null) continue; + int y = chunk.getMinY() + layer; + for (int x = 0; x < 16; x++) for (int z = 0; z < 16; z++) { + chunk.setBlockState(pos.set(x, y, z), state); + floor.update(x, y, z, state); + surface.update(x, y, z, state); + } + } + return CompletableFuture.completedFuture(chunk); + } + + @Override + public int getBaseHeight(int x, int z, Heightmap.Types type, + LevelHeightAccessor height, RandomState random) { + List layers = settings.getLayers(); + for (int layer = Math.min(layers.size() - 1, height.getHeight() - 1); layer >= 0; layer--) { + BlockState state = layers.get(layer); + if (state != null && type.isOpaque().test(state)) return height.getMinY() + layer + 1; + } + return height.getMinY(); + } + + @Override + public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor height, RandomState random) { + return new NoiseColumn(height.getMinY(), settings.getLayers().stream().limit(height.getHeight()) + .map(state -> state == null ? Blocks.AIR.defaultBlockState() : state) + .toArray(BlockState[]::new)); + } + + @Override public void addDebugScreenInfo(List info, RandomState random, + BlockPos pos, SamplerContext context) { } + @Override public void spawnOriginalMobs(WorldGenRegion level) { } + @Override public int getMinY() { return 0; } + @Override public int getGenDepth() { return 384; } + @Override public int getSeaLevel() { return -63; } + + @Override + public void refreshFeaturesPerStep() { + super.refreshFeaturesPerStep(); + generationSettingsGetter.clear(); + settings.updateLayers(); + } +} 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 0f7e0bd..ea455fa 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 @@ -8,6 +8,7 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.OptionalLong; import net.dnorberg.rivencraft.Rivencraft; import net.minecraft.core.registries.Registries; @@ -20,12 +21,18 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.Level; import net.minecraft.world.level.TicketStorage; import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.biome.BiomeSource; import net.minecraft.world.level.biome.FixedBiomeSource; import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; import net.minecraft.world.level.biome.CheckerboardColumnBiomeSource; import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.levelgen.FlatLevelSource; import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; import net.minecraft.world.level.levelgen.NoiseGeneratorSettings; +import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; +import net.minecraft.world.level.levelgen.flat.FlatLayerInfo; +import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.storage.DerivedLevelData; import net.minecraft.world.level.storage.LevelResource; import net.neoforged.neoforge.common.NeoForge; @@ -254,6 +261,25 @@ public final class RuntimeAgeBackend { private record ArchivePaths(Path live, Path archive) { } + 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> biomes = plan.biomes().stream().map(id -> + (Holder) 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 SINGLE -> new FixedBiomeSource(biomes.getFirst()); + case TILED, GRID -> new CheckerboardColumnBiomeSource(HolderSet.direct(biomes), + plan.distribution() == PortBiomePlan.Distribution.GRID ? 0 : 4); + case TINY, SMALL, MEDIUM, LARGE, HUGE -> new PortScaledBiomeSource( + HolderSet.direct(biomes), seed, plan.distribution().ordinal() - + PortBiomePlan.Distribution.TINY.ordinal()); + }; + } + private static ServerLevel load(MinecraftServer server, Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain terrain) { ResourceKey key = ResourceKey.create(Registries.DIMENSION, id); @@ -266,26 +292,42 @@ public final class RuntimeAgeBackend { .getValue(LevelStem.OVERWORLD); if (overworldStem == null) throw new IllegalStateException("Missing Overworld LevelStem"); var generator = overworldStem.generator(); - if ((biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE) + if (terrain == PortAgeTerrain.FLAT || terrain == PortAgeTerrain.VOID) { + var biomes = server.registryAccess().lookupOrThrow(Registries.BIOME); + Holder biome = biomes.getOrThrow(terrain == PortAgeTerrain.VOID + ? Biomes.THE_VOID : Biomes.PLAINS); + if (biomePlan != null && !biomePlan.biomes().isEmpty()) { + Identifier biomeId = biomePlan.biomes().getFirst(); + biome = biomes.get(ResourceKey.create(Registries.BIOME, biomeId)) + .orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + biomeId)); + } + FlatLevelGeneratorSettings settings; + if (terrain == PortAgeTerrain.FLAT) { + settings = FlatLevelGeneratorSettings.getDefault(biomes, + server.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET), + server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)); + // Vanilla superflat begins at the modern minimum build height. + // Pad the column with air so the first solid layer begins at Y=0. + int airBelowZero = Math.max(0, -server.overworld().getMinY()); + if (airBelowZero > 0) + settings.getLayersInfo().addFirst(new FlatLayerInfo(airBelowZero, Blocks.AIR)); + settings = settings.withBiomeAndLayers(settings.getLayersInfo(), + settings.structureOverrides(), biome); + settings.setDecoration(); + } else { + settings = new FlatLevelGeneratorSettings(Optional.empty(), biome, List.of()); + settings.updateLayers(); + } + generator = biomePlan != null && biomePlan.biomes().size() > 1 + ? new PortFlatChunkGenerator(authoredBiomeSource(server, biomePlan, seed), settings) + : new FlatLevelSource(settings); + } else if ((biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE) || terrain != PortAgeTerrain.STANDARD) { if (!(generator instanceof NoiseBasedChunkGenerator noise)) throw new IllegalStateException("Overworld generator cannot accept authored noise settings"); var source = generator.getBiomeSource(); if (biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE) { - var registry = server.registryAccess().lookupOrThrow(Registries.BIOME); - List> biomes = biomePlan.biomes().stream().map(biome -> - (Holder) registry.get(ResourceKey.create(Registries.BIOME, biome)) - .orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + biome))) - .toList(); - source = switch (biomePlan.distribution()) { - case NATIVE -> throw new IllegalStateException("Native is not an override"); - case SINGLE -> new FixedBiomeSource(biomes.getFirst()); - case TILED, GRID -> new CheckerboardColumnBiomeSource(HolderSet.direct(biomes), - biomePlan.distribution() == PortBiomePlan.Distribution.GRID ? 0 : 4); - case TINY, SMALL, MEDIUM, LARGE, HUGE -> new PortScaledBiomeSource( - HolderSet.direct(biomes), seed, biomePlan.distribution().ordinal() - - PortBiomePlan.Distribution.TINY.ordinal()); - }; + source = authoredBiomeSource(server, biomePlan, seed); } var settings = switch (terrain) { case STANDARD -> noise.generatorSettings(); @@ -293,6 +335,7 @@ public final class RuntimeAgeBackend { .getOrThrow(NoiseGeneratorSettings.AMPLIFIED); case ISLAND -> server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS) .getOrThrow(NoiseGeneratorSettings.FLOATING_ISLANDS); + case FLAT, VOID -> throw new IllegalStateException("Flat terrain should use FlatLevelSource"); }; generator = new NoiseBasedChunkGenerator(source, settings); } 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 new file mode 100644 index 0000000..63721d8 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/registry/PortChunkGenerators.java @@ -0,0 +1,25 @@ +package net.dnorberg.rivencraft.registry; + +import com.mojang.serialization.MapCodec; +import net.dnorberg.rivencraft.Rivencraft; +import net.dnorberg.rivencraft.age.PortFlatChunkGenerator; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.registries.DeferredRegister; + +/** Codec for the authored Flat/Void biome source and layer combination. */ +public final class PortChunkGenerators { + private static final DeferredRegister> GENERATORS = + DeferredRegister.create(Registries.CHUNK_GENERATOR, Rivencraft.MOD_ID); + + static { + GENERATORS.register("age_flat", () -> PortFlatChunkGenerator.CODEC); + } + + private PortChunkGenerators() {} + + public static void register(IEventBus bus) { + GENERATORS.register(bus); + } +} 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 e6b3ff1..bd71b11 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 @@ -19,6 +19,10 @@ public final class PortAuthoredWriting { Rivencraft.MOD_ID, "terrain/amplified"); private static final Identifier ISLAND_TERRAIN = Identifier.fromNamespaceAndPath( Rivencraft.MOD_ID, "terrain/island"); + private static final Identifier FLAT_TERRAIN = Identifier.fromNamespaceAndPath( + Rivencraft.MOD_ID, "terrain/flat"); + private static final Identifier VOID_TERRAIN = Identifier.fromNamespaceAndPath( + Rivencraft.MOD_ID, "terrain/void"); private PortAuthoredWriting() {} @@ -28,8 +32,7 @@ public final class PortAuthoredWriting { int biomes = 0; int controllers = 0; for (Identifier page : pages) { - if ((page.equals(STANDARD_TERRAIN) || page.equals(AMPLIFIED_TERRAIN) - || page.equals(ISLAND_TERRAIN)) && !terrain) { + if (isTerrain(page) && !terrain) { terrain = true; } else if (controller(page) != null && controllers++ == 0) { // An explicit controller needs an authored pool. @@ -44,15 +47,22 @@ public final class PortAuthoredWriting { if (controllers != 0) { PortBiomePlan.Distribution distribution = pages.stream().map(PortAuthoredWriting::controller) .filter(value -> value != null).findFirst().orElseThrow(); - return switch (distribution) { + boolean valid = switch (distribution) { case NATIVE -> biomes == 0; case SINGLE -> biomes == 1; default -> biomes >= 2; }; + return valid; } return true; } + private static boolean isTerrain(Identifier page) { + return page.equals(STANDARD_TERRAIN) || page.equals(AMPLIFIED_TERRAIN) + || page.equals(ISLAND_TERRAIN) || page.equals(FLAT_TERRAIN) + || page.equals(VOID_TERRAIN); + } + private static PortBiomePlan.Distribution controller(Identifier page) { if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null; return switch (page.getPath()) { @@ -73,18 +83,19 @@ public final class PortAuthoredWriting { if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages"); if (pages.contains(AMPLIFIED_TERRAIN)) return PortAgeTerrain.AMPLIFIED; if (pages.contains(ISLAND_TERRAIN)) return PortAgeTerrain.ISLAND; + if (pages.contains(FLAT_TERRAIN)) return PortAgeTerrain.FLAT; + if (pages.contains(VOID_TERRAIN)) return PortAgeTerrain.VOID; return PortAgeTerrain.STANDARD; } /** 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( - "Only Standard/Amplified/Island terrain with supported Biome pages is available in this port slice"); + "Unsupported terrain or biome-page combination in this port slice"); List biomes = new ArrayList<>(); PortBiomePlan.Distribution distribution = null; for (Identifier page : pages) { - if (page.equals(STANDARD_TERRAIN) || page.equals(AMPLIFIED_TERRAIN) - || page.equals(ISLAND_TERRAIN)) continue; + if (isTerrain(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 58b7575..d88659c 100755 --- a/scripts/smoke_server_263.sh +++ b/scripts/smoke_server_263.sh @@ -69,6 +69,10 @@ start_server first send_and_wait 'rivencraft_port compat' 'Registered biomes by namespace:' send_and_wait 'rivencraft_port age create smoke_alpha 123456789' 'Created rivencraft:age/smoke_alpha with seed 123456789' send_and_wait 'rivencraft_port age create smoke_beta 987654321' 'Created rivencraft:age/smoke_beta with seed 987654321' +send_and_wait 'rivencraft_port age create smoke_flat 24680 flat' 'Created rivencraft:age/smoke_flat with seed 24680 terrain=FLAT' +send_and_wait 'rivencraft_port age create smoke_void 13579 void' 'Created rivencraft:age/smoke_void with seed 13579 terrain=VOID' +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' # 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" @@ -78,26 +82,66 @@ index_file="$server_dir/world/dimensions/minecraft/overworld/data/rivencraft/run } gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_alpha' gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_beta' +gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_flat' +gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_void' +gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_flat_multi' +gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_void_multi' 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=' +send_and_wait 'rivencraft_port age sample smoke_void' 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain=' +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 '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_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' +send_and_wait 'execute in rivencraft:age/smoke_flat_multi if block 0 0 0 minecraft:bedrock run say Multi Flat bedrock starts at Y0' 'Multi Flat bedrock starts at Y0' +send_and_wait 'execute in rivencraft:age/smoke_void_multi if block 0 0 0 minecraft:air run say Multi Void remains empty' 'Multi Void remains empty' +# Check region interiors after loading the sampled chunks. +send_and_wait 'rivencraft_port age biome smoke_flat_multi 128 128' 'rivencraft:age/smoke_flat_multi biome at 128,128=minecraft:plains' +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' alpha_before=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//') 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=//') if [[ "$alpha_before" == "$beta_before" ]]; then printf 'Different Age seeds produced the same terrain probe: %s\n' "$alpha_before" >&2 exit 1 fi +if [[ "$flat_before" == "$void_before" ]]; then + printf 'Flat and Void Ages produced the same height probe: %s\n' "$flat_before" >&2 + exit 1 +fi stop_server start_server restart send_and_wait 'rivencraft_port age list' 'smoke_alpha' 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=' +send_and_wait 'rivencraft_port age sample smoke_void' 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain=' +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 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' alpha_after=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//') 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=//') [[ "$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 exit 1 } +[[ "$flat_before" == "$flat_after" && "$void_before" == "$void_after" ]] || { + printf 'Flat/Void terrain changed across restart: %s/%s -> %s/%s\n' \ + "$flat_before" "$void_before" "$flat_after" "$void_after" >&2 + exit 1 +} stop_server -printf '26.3 dedicated-server smoke passed: two distinct Ages persisted across restart.\n' +printf '26.3 dedicated-server smoke passed: Standard, Flat and Void Ages persisted across restart.\n'