Port authored Age features and Star Fissures to 26.3 beta
Build / build (push) Canceled after 0s

Advance to 1.0.1.54-beta.1 after player validation. Port material, vegetation, lake, terrain-volume, crystal, Dense Ores and fissure features with saved plans. Update beta documentation and release notes.

Refs #98, #104
This commit is contained in:
dnorberg committed 2026-11-02 01:51:20 -05:00
1 parent d75154fa9b
commit c223f3fdca
37 files changed
+2559 -193

No files matched your search

@@ -8,6 +8,8 @@ import net.dnorberg.rivencraft.age.AgeCatalogSync;
import net.dnorberg.rivencraft.block.BuddingCrystalBlock;
import net.dnorberg.rivencraft.block.PortBookBinderBlock;
import net.dnorberg.rivencraft.block.InkMixerBlock;
import net.dnorberg.rivencraft.block.StarFissureBlock;
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity;
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
@@ -26,6 +28,7 @@ import net.dnorberg.rivencraft.writing.LinkingBookRecipe;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
@@ -54,6 +57,11 @@ public final class Rivencraft {
public static final Logger LOGGER = LogUtils.getLogger();
private static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MOD_ID);
private static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MOD_ID);
private static final DeferredRegister<SoundEvent> SOUNDS =
DeferredRegister.create(Registries.SOUND_EVENT, MOD_ID);
public static final Supplier<SoundEvent> LINK_FISSURE = SOUNDS.register(
"linking.link-fissure", () -> SoundEvent.createVariableRangeEvent(
Identifier.fromNamespaceAndPath(MOD_ID, "linking.link-fissure")));
private static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES =
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, MOD_ID);
private static final DeferredRegister<MenuType<?>> MENUS =
@@ -94,6 +102,12 @@ public final class Rivencraft {
public static final DeferredBlock<Block> CRYSTAL_BLOCK = BLOCKS.registerBlock("crystal_block",
Block::new, () -> BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK));
public static final DeferredBlock<StarFissureBlock> STAR_FISSURE =
BLOCKS.registerBlock("star_fissure", StarFissureBlock::new,
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.END_PORTAL).lightLevel(state -> 6));
public static final Supplier<BlockEntityType<StarFissureBlockEntity>>
STAR_FISSURE_BLOCK_ENTITY = BLOCK_ENTITY_TYPES.register("star_fissure", () -> new BlockEntityType<>(
StarFissureBlockEntity::new, STAR_FISSURE.get()));
public static final DeferredBlock<PortBookBinderBlock> BOOK_BINDER = BLOCKS.registerBlock(
"book_binder", PortBookBinderBlock::new,
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
@@ -146,6 +160,7 @@ public final class Rivencraft {
public Rivencraft(IEventBus modEventBus) {
BLOCKS.register(modEventBus);
SOUNDS.register(modEventBus);
ITEMS.register(modEventBus);
BLOCK_ENTITY_TYPES.register(modEventBus);
MENUS.register(modEventBus);
@@ -26,7 +26,15 @@ final class AgeLevelIndex extends SavedData {
Codec.STRING.xmap(PortAgeTerrain::valueOf, PortAgeTerrain::name))
.optionalFieldOf("terrain", Map.of()).forGetter(index -> index.terrain),
Codec.unboundedMap(Identifier.CODEC, PortRockPlan.CODEC).optionalFieldOf("rocks", Map.of())
.forGetter(index -> index.rocks)
.forGetter(index -> index.rocks),
Codec.unboundedMap(Identifier.CODEC, PortVegetationPlan.CODEC)
.optionalFieldOf("vegetation", Map.of()).forGetter(index -> index.vegetation),
Codec.unboundedMap(Identifier.CODEC, Codec.intRange(1, 32))
.optionalFieldOf("dungeons", Map.of()).forGetter(index -> index.dungeons),
Codec.unboundedMap(Identifier.CODEC, PortLakePlan.CODEC)
.optionalFieldOf("lakes", Map.of()).forGetter(index -> index.lakes),
Codec.unboundedMap(Identifier.CODEC, PortTerrainFeaturePlan.CODEC)
.optionalFieldOf("terrain_features", Map.of()).forGetter(index -> index.terrainFeatures)
).apply(instance, AgeLevelIndex::new));
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
@@ -41,6 +49,10 @@ final class AgeLevelIndex extends SavedData {
private final Map<Identifier, PortBiomePlan> biomePlans;
private final Map<Identifier, PortAgeTerrain> terrain;
private final Map<Identifier, PortRockPlan> rocks;
private final Map<Identifier, PortVegetationPlan> vegetation;
private final Map<Identifier, Integer> dungeons;
private final Map<Identifier, PortLakePlan> lakes;
private final Map<Identifier, PortTerrainFeaturePlan> terrainFeatures;
private AgeLevelIndex() {
this.ages = new LinkedHashMap<>();
@@ -49,19 +61,31 @@ final class AgeLevelIndex extends SavedData {
this.biomePlans = new LinkedHashMap<>();
this.terrain = new LinkedHashMap<>();
this.rocks = new LinkedHashMap<>();
this.vegetation = new LinkedHashMap<>();
this.dungeons = new LinkedHashMap<>();
this.lakes = new LinkedHashMap<>();
this.terrainFeatures = 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, PortRockPlan> rocks) {
Map<Identifier, PortRockPlan> rocks,
Map<Identifier, PortVegetationPlan> vegetation,
Map<Identifier, Integer> dungeons,
Map<Identifier, PortLakePlan> lakes,
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures) {
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);
this.vegetation = new LinkedHashMap<>(vegetation);
this.dungeons = new LinkedHashMap<>(dungeons);
this.lakes = new LinkedHashMap<>(lakes);
this.terrainFeatures = new LinkedHashMap<>(terrainFeatures);
}
Map<Identifier, Long> entries() {
@@ -88,9 +112,14 @@ final class AgeLevelIndex extends SavedData {
}
PortRockPlan rocks(Identifier id) { return rocks.get(id); }
PortVegetationPlan vegetation(Identifier id) { return vegetation.get(id); }
int dungeons(Identifier id) { return dungeons.getOrDefault(id, 0); }
PortLakePlan lakes(Identifier id) { return lakes.get(id); }
PortTerrainFeaturePlan terrainFeatures(Identifier id) { return terrainFeatures.get(id); }
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain,
PortRockPlan rockPlan) {
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan, int dungeonCount,
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
if (contains(id)) {
throw new IllegalArgumentException("Age already exists: " + id);
}
@@ -98,6 +127,10 @@ final class AgeLevelIndex extends SavedData {
if (biomePlan != null) biomePlans.put(id, biomePlan);
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
if (rockPlan != null) rocks.put(id, rockPlan);
if (vegetationPlan != null) vegetation.put(id, vegetationPlan);
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
if (lakePlan != null) lakes.put(id, lakePlan);
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
setDirty();
}
@@ -107,17 +140,27 @@ final class AgeLevelIndex extends SavedData {
biomePlans.remove(id);
terrain.remove(id);
rocks.remove(id);
vegetation.remove(id);
dungeons.remove(id);
lakes.remove(id);
terrainFeatures.remove(id);
retired.add(id);
setDirty();
}
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
PortAgeTerrain ageTerrain, PortRockPlan rockPlan) {
PortAgeTerrain ageTerrain, PortRockPlan rockPlan,
PortVegetationPlan vegetationPlan, int dungeonCount,
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
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);
if (vegetationPlan != null) vegetation.put(id, vegetationPlan);
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
if (lakePlan != null) lakes.put(id, lakePlan);
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
setDirty();
}
@@ -18,6 +18,8 @@ import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.Prediction;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.block.Blocks;
import net.neoforged.neoforge.event.RegisterCommandsEvent;
import net.neoforged.fml.ModList;
import java.util.Map;
@@ -130,6 +132,51 @@ public final class PortAgeCommands {
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.FLAT,
null, new PortRockPlan(Identifier.withDefaultNamespace("gold_block")))))))
.then(Commands.literal("create_rocks_multi")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
fourGoldRocks(LongArgumentType.getLong(context, "seed")))))))
.then(Commands.literal("create_rocks_random")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(
java.util.List.of(Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "feature/rocks")),
LongArgumentType.getLong(context, "seed")))))))
.then(Commands.literal("create_vegetation")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
null, testVegetation())))))
.then(Commands.literal("create_dungeons")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
null, null, null, testDungeons())))))
.then(Commands.literal("create_lakes")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
null, null, null, 0, testLakes(
LongArgumentType.getLong(context, "seed"), false))))))
.then(Commands.literal("create_lakes_random")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
null, null, null, 0, testLakes(
LongArgumentType.getLong(context, "seed"), true))))))
.then(Commands.literal("create_islands")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(context -> createIslandAge(context, false))))))
.then(Commands.literal("create_islands_random")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(context -> createIslandAge(context, true))))))
.then(Commands.literal("list")
.executes(context -> {
var ages = RuntimeAgeBackend.savedAges(context.getSource().getServer());
@@ -236,6 +283,18 @@ public final class PortAgeCommands {
.then(Commands.literal("rocks_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeRocks)))
.then(Commands.literal("vegetation_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeVegetation)))
.then(Commands.literal("dungeons_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeDungeons)))
.then(Commands.literal("lakes_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeLakes)))
.then(Commands.literal("islands_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeIslands)))
.then(Commands.literal("biome")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("x", IntegerArgumentType.integer())
@@ -255,30 +314,229 @@ public final class PortAgeCommands {
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)) {
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)
|| features.rocks() == null) {
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());
var materials = features.rocks().materials().stream()
.map(net.minecraft.core.registries.BuiltInRegistries.BLOCK::getValue).toList();
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++;
var state = level.getBlockState(new net.minecraft.core.BlockPos(x, y, z));
if (materials.stream().anyMatch(state::is)) count++;
}
int placed = count;
context.getSource().sendSuccess(() -> Component.literal(
"Rocks probe " + id + " material=" + rocks.rocks().material() + " count=" + placed), false);
"Rocks probe " + id + " materials=" + features.rocks().materials() + " count=" + placed), false);
return placed;
}
private static int probeVegetation(CommandContext<CommandSourceStack> 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 flowers = 0;
int logs = 0;
for (int x = 0; x < 64; x++) for (int z = 0; z < 64; z++) {
for (int y = 4; y < 32; y++) {
var state = level.getBlockState(new net.minecraft.core.BlockPos(x, y, z));
if (state.is(BlockTags.FLOWERS)) flowers++;
if (state.is(BlockTags.LOGS)) logs++;
}
}
int flowerCount = flowers;
int logCount = logs;
context.getSource().sendSuccess(() -> Component.literal(
"Vegetation probe " + id + " flowers=" + flowerCount + " logs=" + logCount), false);
return flowerCount + logCount;
}
private static PortVegetationPlan testVegetation() {
java.util.List<Identifier> pages = new java.util.ArrayList<>();
for (int i = 0; i < 10; i++) {
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/flowers"));
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/trees"));
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/huge_trees"));
}
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.vegetationPlan(pages);
}
private static int testDungeons() {
Identifier page = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/dungeons");
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.dungeonCount(
java.util.Collections.nCopies(8, page));
}
private static int probeDungeons(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 PortFeatureChunkGenerator features)) {
context.getSource().sendFailure(Component.literal("Age has no loaded feature generator: " + id));
return 0;
}
int count = features.dungeons();
int spawners = 0;
for (int chunkX = 0; chunkX < 4; chunkX++) for (int chunkZ = 0; chunkZ < 4; chunkZ++) {
var chunk = level.getChunk(chunkX, chunkZ);
for (var pos : chunk.getBlockEntitiesPos()) {
if (chunk.getBlockState(pos).is(Blocks.SPAWNER)) spawners++;
}
}
int placed = spawners;
context.getSource().sendSuccess(() -> Component.literal(
"Dungeons probe " + id + " pages=" + count + " spawners=" + placed), false);
return placed;
}
private static PortLakePlan testLakes(long seed, boolean randomFluids) {
java.util.List<Identifier> pages = new java.util.ArrayList<>();
for (int i = 0; i < 8; i++) {
for (String kind : java.util.List.of("surface_lakes", "deep_lakes", "lava_lakes")) {
if (!randomFluids) pages.add(net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
Identifier.withDefaultNamespace("water")));
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/" + kind));
}
}
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.lakePlan(pages, seed);
}
private static int probeLakes(CommandContext<CommandSourceStack> 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;
}
PortLakePlan lakes = level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features
? features.lakes() : null;
// Finish neighboring features before counting, since lake bowls can
// cross a chunk boundary. Include underground lakes in the probe.
for (int x = -1; x <= 4; x++) for (int z = -1; z <= 4; z++) level.getChunk(x, z);
int[] fluids = new int[2];
for (int x = 0; x < 4; x++) for (int z = 0; z < 4; z++) {
level.getChunk(x, z).findBlocks(state -> state.is(Blocks.WATER) || state.is(Blocks.LAVA),
(pos, state) -> fluids[state.is(Blocks.WATER) ? 0 : 1]++);
}
int waterCount = fluids[0];
int lavaCount = fluids[1];
context.getSource().sendSuccess(() -> Component.literal(
"Lakes probe " + id + " pages=" + (lakes == null ? 0 : lakes.choices().size())
+ " fluids=" + (lakes == null ? java.util.List.of() : lakes.choices().stream()
.map(PortLakePlan.Choice::fluid).distinct().toList())
+ " plan=" + (lakes == null ? 0 : lakes.choices().hashCode())
+ " water=" + waterCount + " lava=" + lavaCount), false);
return waterCount + lavaCount;
}
private static int createIslandAge(CommandContext<CommandSourceStack> context, boolean randomMaterials) {
PortAgeTerrain terrain;
try {
terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException exception) {
context.getSource().sendFailure(Component.literal("Unknown island test terrain"));
return 0;
}
java.util.List<Identifier> pages = new java.util.ArrayList<>();
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT)));
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome/forest"));
for (int i = 0; i < 4; i++) {
if (!randomMaterials) pages.add(net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
Identifier.withDefaultNamespace("gold_block")));
pages.add(PortTerrainFeaturePlan.FLOATING_ISLANDS);
}
try {
long seed = LongArgumentType.getLong(context, "seed");
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
pages, context.getSource().getServer().registryAccess());
var features = net.dnorberg.rivencraft.writing.PortAuthoredWriting.terrainFeaturePlan(pages, seed);
return createAge(context, terrain, biomes, null, null, 0, null, features);
} catch (IllegalArgumentException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int probeIslands(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 PortFeatureChunkGenerator features)
|| features.terrainFeatures() == null) {
context.getSource().sendFailure(Component.literal("Age has no loaded island generator: " + id));
return 0;
}
var choices = features.terrainFeatures().choices();
var materials = choices.stream().map(PortTerrainFeaturePlan.Choice::material).distinct().toList();
var blocks = materials.stream().map(net.minecraft.core.registries.BuiltInRegistries.BLOCK::getValue).toList();
for (int x = -1; x <= 4; x++) for (int z = -1; z <= 4; z++) level.getChunk(x, z);
int[] count = {0};
long[] fingerprint = {0};
for (int x = 0; x < 4; x++) for (int z = 0; z < 4; z++) {
level.getChunk(x, z).findBlocks(state -> !state.isAir(), (pos, state) -> {
if (pos.getY() < 128 || pos.getY() >= 240) return;
if (blocks.stream().anyMatch(state::is)) count[0]++;
fingerprint[0] += pos.asLong() * 0x9E3779B97F4A7C15L ^ state.toString().hashCode();
});
}
context.getSource().sendSuccess(() -> Component.literal(
"Islands probe " + id + " pages=" + choices.size() + " materials=" + materials
+ " count=" + count[0] + " fingerprint=" + fingerprint[0]), false);
return count[0];
}
/** Exercise the actual ordered writing parser, not only the generator codec. */
private static PortRockPlan fourGoldRocks(long seed) {
Identifier block = net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
Identifier.withDefaultNamespace("gold_block"));
Identifier feature = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/rocks");
java.util.List<Identifier> pages = new java.util.ArrayList<>();
for (int i = 0; i < 4; i++) {
pages.add(block);
pages.add(feature);
}
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(pages, seed);
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan, PortRockPlan rockPlan) {
return createAge(context, terrain, biomePlan, rockPlan, null);
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan) {
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, 0);
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons) {
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, null);
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons, PortLakePlan lakes) {
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, lakes, null);
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures) {
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, rockPlan);
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan,
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
@@ -25,8 +25,13 @@ public final class PortAgeTravel {
if (!AgeCatalogSync.ready(player, id)) {
return "Age catalog not acknowledged by client; remaining in source dimension";
}
BlockPos landing = findLanding(target);
boolean fissure = target.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features
&& features.terrainFeatures() != null && features.terrainFeatures().starFissures();
BlockPos landing = fissure ? PortStarFissureFeature.savedSpawn(target) : null;
if (landing != null) landing = findFootedDestination(target, landing).orElse(null);
if (landing == null) landing = findLanding(target);
if (landing == null) return "No safe test landing available; remaining in source dimension";
if (fissure) PortStarFissureFeature.generateOnce(target, landing, isCaveTerrain(target));
boolean moved = player.teleportTo(target, landing.getX() + 0.5, landing.getY(), landing.getZ() + 0.5,
Set.of(), player.getYRot(), player.getXRot(), true);
return moved ? null : "Teleportation failed; remaining in source dimension";
@@ -34,12 +39,8 @@ public final class PortAgeTravel {
private static BlockPos findLanding(ServerLevel target) {
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)
.value().noiseSettings())) {
if (generator instanceof PortFeatureChunkGenerator features) generator = features.base();
if (isCaveTerrain(target)) {
// The cave roof is not a valid arrival surface. Search down from
// Legacy's Y=64 start, then construct the normal safety platform
// at that height only when no footed opening can be found.
@@ -88,6 +89,16 @@ public final class PortAgeTravel {
return makePlatform(target, new BlockPos(0, y, 0));
}
private static boolean isCaveTerrain(ServerLevel target) {
var generator = target.getChunkSource().getGenerator();
if (generator instanceof PortFeatureChunkGenerator features) generator = features.base();
return 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)
.value().noiseSettings());
}
private static BlockPos makePlatform(ServerLevel target, BlockPos foot) {
if (foot.getY() <= target.getMinY() || foot.getY() + 2 >= target.getMaxY()) return null;
// A carved pocket inside a flooded cave immediately refills. Refuse
@@ -112,7 +123,36 @@ public final class PortAgeTravel {
return target.getBlockState(floor).isFaceSturdy(target, floor, Direction.UP)
&& target.getFluidState(foot).isEmpty()
&& target.getFluidState(foot.above()).isEmpty()
&& target.getBlockState(foot).getCollisionShape(target, foot).isEmpty()
&& target.getBlockState(foot.above()).getCollisionShape(target, foot.above()).isEmpty();
&& passable(target, foot) && passable(target, foot.above());
}
private static boolean passable(ServerLevel level, BlockPos pos) {
var state = level.getBlockState(pos);
return state.getCollisionShape(level, pos).isEmpty() || state.is(net.minecraft.tags.BlockTags.LEAVES);
}
/** Same local-then-surface safe-return search used by 1.21.1 fissures. */
public static java.util.Optional<BlockPos> findFootedDestination(ServerLevel level, BlockPos origin) {
var local = BlockPos.betweenClosedStream(origin.offset(-4, -2, -4), origin.offset(4, 2, 4))
.map(BlockPos::immutable).sorted(java.util.Comparator.comparingDouble(pos -> pos.distSqr(origin)))
.filter(pos -> level.isInWorldBounds(pos) && level.getWorldBorder().isWithinBounds(pos))
.filter(pos -> hasFooting(level, pos)).findFirst();
if (local.isPresent()) return local;
for (int radius = 1; radius <= 64; radius++) {
final int ring = radius;
var found = BlockPos.betweenClosedStream(origin.offset(-ring, 0, -ring), origin.offset(ring, 0, ring))
.filter(pos -> Math.max(Math.abs(pos.getX() - origin.getX()), Math.abs(pos.getZ() - origin.getZ())) == ring)
.map(BlockPos::immutable)
.flatMap(pos -> {
BlockPos surface = new BlockPos(pos.getX(),
level.getHeight(Heightmap.Types.MOTION_BLOCKING, pos.getX(), pos.getZ()), pos.getZ());
return java.util.stream.Stream.of(surface, surface.above(), surface.below(),
surface.above(2), surface.below(2));
})
.filter(pos -> level.isInWorldBounds(pos) && level.getWorldBorder().isWithinBounds(pos))
.filter(pos -> hasFooting(level, pos)).findFirst();
if (found.isPresent()) return found;
}
return java.util.Optional.empty();
}
}
@@ -0,0 +1,78 @@
package net.dnorberg.rivencraft.age;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.AmethystClusterBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.Heightmap;
/** 26.3 port of the 1.21.1 surface crystal mound with budding hearts and seeded buds. */
final class PortCrystalFormationsFeature {
private static final long SALT = 0x4352595354414C53L;
private static final int CHANCE = 15;
private PortCrystalFormationsFeature() {}
record Site(int x, int z, long seed) {}
static Site site(long ageSeed, ChunkPos chunk, int occurrence) {
long seed = mix(ageSeed ^ SALT ^ (long) chunk.x() * 341873128712L
^ (long) chunk.z() * 132897987541L ^ (long) occurrence * 0x9E3779B97F4A7C15L);
RandomSource random = RandomSource.create(seed);
if (random.nextInt(CHANCE) != 0) return null;
return new Site(chunk.getMinBlockX() + 5 + random.nextInt(6),
chunk.getMinBlockZ() + 5 + random.nextInt(6), seed);
}
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, int occurrence) {
Site site = site(ageSeed, chunk.getPos(), occurrence);
if (site == null) return;
RandomSource random = RandomSource.create(site.seed() ^ 0x42554444494E474CL);
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, site.x(), site.z());
if (y <= level.getMinY() + 1 || y + 4 >= level.getMaxY()) return;
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
int x = site.x() + dx;
int z = site.z() + dz;
if (Math.abs(level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - y) > 1) return;
BlockState floor = level.getBlockState(pos.set(x, y - 1, z));
if (!floor.isFaceSturdy(level, pos, Direction.UP) || !floor.getFluidState().isEmpty()
|| floor.hasBlockEntity()) return;
for (int h = 0; h < 4; h++) {
BlockState existing = level.getBlockState(pos.set(x, y + h, z));
if (!existing.isAir() && !existing.canBeReplaced()) return;
}
}
}
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
boolean center = dx == 0 && dz == 0;
int height = center ? 3 : random.nextInt(3) == 0 ? 2 : 1;
for (int h = 0; h < height; h++) {
boolean budding = h == height - 1 && (center || random.nextBoolean());
level.setBlock(pos.set(site.x() + dx, y + h, site.z() + dz),
budding ? Rivencraft.BUDDING_CRYSTAL.get().defaultBlockState()
: Rivencraft.CRYSTAL_BLOCK.get().defaultBlockState(), 2);
}
if (level.getBlockState(pos.set(site.x() + dx, y + height - 1, site.z() + dz))
.is(Rivencraft.BUDDING_CRYSTAL) && random.nextBoolean()) {
level.setBlock(pos.set(site.x() + dx, y + height, site.z() + dz),
Rivencraft.SMALL_CRYSTAL_BUD.get().defaultBlockState()
.setValue(AmethystClusterBlock.FACING, Direction.UP), 2);
}
}
}
}
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
}
@@ -0,0 +1,90 @@
package net.dnorberg.rivencraft.age;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.tags.TagKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
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.levelgen.feature.AbstractOreFeature;
import net.minecraft.world.level.levelgen.placement.FeaturePlacer;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
import net.neoforged.neoforge.common.Tags;
/** Replays each eligible biome ore placement once per page, matching Rivencraft 1.21.1. */
public final class PortDenseOres {
public static final TagKey<PlacedFeature> EXTRA_ORES = TagKey.create(Registries.PLACED_FEATURE,
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "dense_ores"));
private PortDenseOres() {}
public static boolean hasOres(Holder<Biome> biome) {
return oreFeatures(biome).anyMatch(PortDenseOres::isOre);
}
/** Modern ore features carry their targets directly rather than an OreConfiguration. */
public static boolean isOre(Holder<PlacedFeature> holder) {
if (holder.is(EXTRA_ORES)) return true;
if (!(holder.value().feature().value() instanceof AbstractOreFeature ore)) return false;
return ore.targetStates().stream().anyMatch(target -> isOreBlock(target.state()));
}
private static boolean isOreBlock(BlockState state) {
if (state.is(Tags.BlockItems.ORES.block())) return true;
String path = BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath();
return path.endsWith("_ore") || path.contains("_ore_");
}
private static java.util.stream.Stream<Holder<PlacedFeature>> oreFeatures(Holder<Biome> biome) {
return biome.value().getGenerationSettings().features().stream().flatMap(HolderSet::stream);
}
public static void place(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator generator,
long ageSeed, int count) {
if (count == 0) return;
Set<Holder<Biome>> biomes = new HashSet<>();
ChunkPos.rangeClosed(chunk.getPos(), 1).forEach(pos -> {
for (var section : level.getChunk(pos.x(), pos.z()).getSections())
section.getBiomes().getAll(biomes::add);
});
biomes.retainAll(generator.getBiomeSource().possibleBiomes());
// Collect once per chunk, not once per repeated page. Stable registry-ID
// order and per-occurrence seeds retain the 1.21.1 placement sequence.
Map<Identifier, PlacedFeature> ores = new TreeMap<>();
for (Holder<Biome> biome : biomes) {
oreFeatures(biome).filter(PortDenseOres::isOre).forEach(feature ->
feature.unwrapKey().ifPresent(key -> ores.putIfAbsent(key.identifier(), feature.value())));
}
BlockPos origin = new BlockPos(chunk.getPos().getMinBlockX(), level.getMinY(),
chunk.getPos().getMinBlockZ());
FeaturePlacer placer = new FeaturePlacer(level, generator);
for (int occurrence = 0; occurrence < count; occurrence++) {
for (var entry : ores.entrySet()) {
long seed = mix(ageSeed ^ (long) origin.getX() * 341873128712L
^ (long) origin.getZ() * 132897987541L
^ (long) entry.getKey().hashCode() * 0x9E3779B97F4A7C15L
^ (long) occurrence * 0xD1B54A32D192ED03L);
placer.placeWithBiomeCheck(entry.getValue(), RandomSource.create(seed), origin);
}
}
}
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
}
@@ -0,0 +1,269 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.writing.PortRegistryPages;
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.core.registries.Registries;
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.tags.TagKey;
import net.minecraft.tags.BiomeTags;
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.blockpredicates.BlockPredicate;
import net.minecraft.world.level.levelgen.feature.BlockBlobFeature;
import net.minecraft.world.level.levelgen.feature.LakeFeature;
import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider;
import net.minecraft.world.level.levelgen.placement.BiomeFilter;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
import net.minecraft.world.level.levelgen.placement.RarityFilter;
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;
import net.minecraft.world.level.block.Block;
/** Delegates terrain generation and applies frozen Age-scoped features. */
public final class PortFeatureChunkGenerator extends ChunkGenerator {
private static final ResourceKey<PlacedFeature> FOREST_ROCK = ResourceKey.create(
Registries.PLACED_FEATURE, Identifier.withDefaultNamespace("forest_rock"));
private static final ResourceKey<PlacedFeature> DUNGEONS = ResourceKey.create(
Registries.PLACED_FEATURE, Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "dungeons"));
private static final TagKey<Block> ROCK_GROUND = TagKey.create(
Registries.BLOCK, Identifier.withDefaultNamespace("forest_rock_can_place_on"));
public static final MapCodec<PortFeatureChunkGenerator> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
ChunkGenerator.CODEC.fieldOf("base").forGetter(generator -> generator.base),
PortRockPlan.CODEC.optionalFieldOf("rocks").forGetter(generator -> Optional.ofNullable(generator.rocks)),
PortVegetationPlan.CODEC.optionalFieldOf("vegetation")
.forGetter(generator -> Optional.ofNullable(generator.vegetation)),
com.mojang.serialization.Codec.intRange(0, 32).optionalFieldOf("dungeons", 0)
.forGetter(generator -> generator.dungeons),
PortLakePlan.CODEC.optionalFieldOf("lakes")
.forGetter(generator -> Optional.ofNullable(generator.lakes)),
PortTerrainFeaturePlan.CODEC.optionalFieldOf("terrain_features")
.forGetter(generator -> Optional.ofNullable(generator.terrainFeatures)),
com.mojang.serialization.Codec.LONG.fieldOf("age_seed").forGetter(generator -> generator.ageSeed)
).apply(instance, (base, rocks, vegetation, dungeons, lakes, terrainFeatures, seed) ->
new PortFeatureChunkGenerator(base, rocks.orElse(null), vegetation.orElse(null),
dungeons, lakes.orElse(null), terrainFeatures.orElse(null), seed)));
private final ChunkGenerator base;
private final PortRockPlan rocks;
private final PortVegetationPlan vegetation;
private final int dungeons;
private final PortLakePlan lakes;
private final PortTerrainFeaturePlan terrainFeatures;
private final long ageSeed;
public PortFeatureChunkGenerator(ChunkGenerator base, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures, long ageSeed) {
super(base.getBiomeSource(), base::getBiomeGenerationSettings);
this.base = base;
this.rocks = rocks;
this.vegetation = vegetation;
this.dungeons = dungeons;
this.lakes = lakes;
this.terrainFeatures = terrainFeatures;
this.ageSeed = ageSeed;
}
public ChunkGenerator base() { return base; }
public PortRockPlan rocks() { return rocks; }
public PortVegetationPlan vegetation() { return vegetation; }
public int dungeons() { return dungeons; }
public PortLakePlan lakes() { return lakes; }
public PortTerrainFeaturePlan terrainFeatures() { return terrainFeatures; }
@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) {
CompletableFuture<ChunkAccess> built = base.buildTerrain(
chunk, blender, random, structures, biomes, carverBiomeRegion, possibleBiomes);
if (terrainFeatures == null || !terrainFeatures.skylands() && terrainFeatures.choices().isEmpty()) return built;
return built.thenApply(result -> {
// Match 1.21.1: filter the base terrain once, then add the authored
// solid volumes, irrespective of the Skylands page's written position.
if (terrainFeatures.skylands()) PortSkylandsFeature.filter(result, ageSeed);
java.util.Map<Identifier, Integer> occurrences = new java.util.HashMap<>();
for (PortTerrainFeaturePlan.Choice choice : terrainFeatures.choices()) {
if (PortTerrainFeaturePlan.isSurface(choice.page())) continue;
int occurrence = occurrences.merge(choice.page(), 1, Integer::sum) - 1;
long featureSeed = ageSeed ^ (long) occurrence * 0x9E3779B97F4A7C15L;
BlockState material = BuiltInRegistries.BLOCK.getValue(choice.material()).defaultBlockState();
if (choice.page().equals(PortTerrainFeaturePlan.FLOATING_ISLANDS))
PortFloatingIslandFeature.generate(result, featureSeed, material);
else if (choice.page().equals(PortTerrainFeaturePlan.TENDRILS))
PortTendrilFeature.generate(result, featureSeed, material);
else if (choice.page().equals(PortTerrainFeaturePlan.SPHERES))
PortSpheresFeature.generate(result, featureSeed, material);
else throw new IllegalStateException("Unsupported terrain feature: " + choice.page());
}
return result;
});
}
@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);
if (terrainFeatures != null) {
PortDenseOres.place(level, chunk, this, ageSeed, terrainFeatures.denseOres());
int spikeOccurrence = 0;
int obeliskOccurrence = 0;
int crystalOccurrence = 0;
for (PortTerrainFeaturePlan.Choice choice : terrainFeatures.choices()) {
if (!PortTerrainFeaturePlan.isSurface(choice.page())) continue;
BlockState material = BuiltInRegistries.BLOCK.getValue(choice.material()).defaultBlockState();
if (choice.page().equals(PortTerrainFeaturePlan.SPIKES))
PortSpikesFeature.generate(level, chunk, ageSeed, material, spikeOccurrence++);
else if (choice.page().equals(PortTerrainFeaturePlan.OBELISKS))
PortObelisksFeature.generate(level, chunk, ageSeed, material, obeliskOccurrence++);
else if (choice.page().equals(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS))
PortCrystalFormationsFeature.generate(level, chunk, ageSeed, crystalOccurrence++);
else throw new IllegalStateException("Unsupported surface feature: " + choice.page());
}
}
ChunkPos chunkPos = chunk.getPos();
long chunkSeed = ageSeed ^ (long) chunkPos.x() * 341873128712L
^ (long) chunkPos.z() * 132897987541L;
BlockPos origin = new BlockPos(chunkPos.getMinBlockX(), level.getMinY(), chunkPos.getMinBlockZ());
if (rocks != null) {
PlacedFeature vanilla = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
.getOrThrow(FOREST_ROCK).value();
var placement = vanilla.placement().stream()
.filter(modifier -> !(modifier instanceof BiomeFilter)).toList();
for (int occurrence = 0; occurrence < rocks.materials().size(); occurrence++) {
RandomSource random = RandomSource.create(chunkSeed
^ (long) occurrence * 0xD1B54A32D192ED03L);
BlockState material = BuiltInRegistries.BLOCK.getValue(
rocks.materials().get(occurrence)).defaultBlockState();
PlacedFeature authored = new PlacedFeature(Holder.direct(new BlockBlobFeature(
material, BlockPredicate.matchesTag(ROCK_GROUND))), placement);
authored.place(level, this, random, origin);
}
}
if (dungeons > 0) {
PlacedFeature authored = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
.getOrThrow(DUNGEONS).value();
for (int occurrence = 0; occurrence < dungeons; occurrence++) {
RandomSource random = RandomSource.create(chunkSeed
^ 0x44554E47454F4E53L ^ (long) occurrence * 0xD1B54A32D192ED03L);
authored.place(level, this, random, origin);
}
}
if (lakes != null) {
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
java.util.Map<PortLakePlan.Choice, Integer> occurrences = new java.util.HashMap<>();
for (PortLakePlan.Choice choice : lakes.choices()) {
PlacedFeature vanilla = registry.getOrThrow(ResourceKey.create(
Registries.PLACED_FEATURE, PortLakePlan.placedFeature(choice.page()))).value();
if (!(vanilla.feature().value() instanceof LakeFeature lake))
throw new IllegalStateException("Lake placement no longer references a LakeFeature: " + choice.page());
BlockState fluid = BuiltInRegistries.BLOCK.getValue(choice.fluid()).defaultBlockState();
LakeFeature feature = new LakeFeature(BlockStateProvider.holderOf(fluid), lake.barrier(),
lake.canPlaceFeature(), lake.canReplaceWithAirOrFluid(), lake.canReplaceWithBarrier());
PlacedFeature authored = new PlacedFeature(Holder.direct(feature), vanilla.placement().stream()
.filter(modifier -> !(modifier instanceof BiomeFilter))
.map(modifier -> !PortLakePlan.isDeep(choice.page()) && modifier instanceof RarityFilter
? RarityFilter.onAverageOnceEvery(12) : modifier)
.toList());
int occurrence = occurrences.merge(choice, 1, Integer::sum) - 1;
RandomSource random = RandomSource.create(chunkSeed
^ (long) choice.hashCode() * 0x9E3779B97F4A7C15L
^ (long) occurrence * 0xD1B54A32D192ED03L);
authored.place(level, this, random, origin);
}
}
if (vegetation != null) {
int surface = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 8);
Holder<Biome> biome = level.getBiome(new BlockPos(
chunkPos.getMinBlockX() + 8, surface, chunkPos.getMinBlockZ() + 8));
if (biome.is(BiomeTags.IS_END) || PortRegistryPages.isCaveBiome(biome)) return;
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
for (int occurrence = 0; occurrence < vegetation.pages().size(); occurrence++) {
Identifier page = vegetation.pages().get(occurrence);
Identifier placedId = PortVegetationPlan.placedFeature(page);
PlacedFeature vanilla = registry.getOrThrow(
ResourceKey.create(Registries.PLACED_FEATURE, placedId)).value();
PlacedFeature authored = new PlacedFeature(vanilla.feature(), vanilla.placement().stream()
.filter(modifier -> !(modifier instanceof BiomeFilter)).toList());
RandomSource random = RandomSource.create(chunkSeed
^ (long) page.hashCode() * 0x9E3779B97F4A7C15L
^ (long) occurrence * 0xD1B54A32D192ED03L);
authored.place(level, this, random, origin);
}
}
}
}
@@ -0,0 +1,83 @@
package net.dnorberg.rivencraft.age;
import net.minecraft.core.BlockPos;
import net.minecraft.core.QuartPos;
import net.minecraft.tags.BiomeTags;
import net.minecraft.util.RandomSource;
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.levelgen.Heightmap;
import net.dnorberg.rivencraft.writing.PortRegistryPages;
/** 1.21.1 floating-island shape adapted to the 26.3 chunk terrain stage. */
final class PortFloatingIslandFeature {
private static final long SALT = 0x464C4F4154494E47L;
private static final int SOURCE_RANGE = 3;
private static final int CHANCE = 128;
private PortFloatingIslandFeature() {}
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
var target = chunk.getPos();
for (int sourceX = target.x() - SOURCE_RANGE; sourceX <= target.x() + SOURCE_RANGE; sourceX++) {
for (int sourceZ = target.z() - SOURCE_RANGE; sourceZ <= target.z() + SOURCE_RANGE; sourceZ++) {
RandomSource random = RandomSource.create(mix(ageSeed ^ SALT
^ (long) sourceX * 341873128712L ^ (long) sourceZ * 132897987541L));
if (random.nextInt(CHANCE) != 0) continue;
int centerX = sourceX * 16 + random.nextInt(16);
int centerZ = sourceZ * 16 + random.nextInt(16);
int centerY = Math.min(chunk.getMaxY() - 30, 168 + random.nextInt(48));
if (centerY < chunk.getMinY() + 24) continue;
int radiusX = 24 + random.nextInt(18);
int radiusZ = 24 + random.nextInt(18);
fill(chunk, ageSeed, material, centerX, centerY, centerZ, radiusX, radiusZ);
}
}
}
private static void fill(ChunkAccess chunk, long ageSeed, BlockState material,
int centerX, int centerY, int centerZ, int radiusX, int radiusZ) {
int originX = chunk.getPos().getMinBlockX();
int originZ = chunk.getPos().getMinBlockZ();
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
for (int localX = 0; localX < 16; localX++) {
int worldX = originX + localX;
double dx = (worldX - centerX) / (double) radiusX;
for (int localZ = 0; localZ < 16; localZ++) {
int worldZ = originZ + localZ;
double dz = (worldZ - centerZ) / (double) radiusZ;
double distance = dx * dx + dz * dz;
if (distance >= 1.0) continue;
long detail = mix(ageSeed ^ (long) centerX * 341873128712L
^ (long) centerZ * 132897987541L ^ (long) worldX * 42317861L
^ (long) worldZ * 374761393L);
int top = centerY + (int) Math.round(3 * (1 - distance)) + Math.floorMod(detail, 3) - 1;
int depth = Math.max(1, (int) Math.round(3 + 17 * Math.pow(1 - distance, 1.5)));
// Biomes are already populated by createBiomes in 26.3, so
// sampling this chunk preserves its authored biome distribution.
var biome = chunk.getNoiseBiome(QuartPos.fromBlock(worldX), QuartPos.fromBlock(top),
QuartPos.fromBlock(worldZ));
BlockState soil = biome.is(BiomeTags.IS_END) || PortRegistryPages.isCaveBiome(biome)
? material : Blocks.DIRT.defaultBlockState();
BlockState cap = soil == material ? material : Blocks.GRASS_BLOCK.defaultBlockState();
for (int y = Math.max(top - depth, chunk.getMinY()); y <= top; y++) {
pos.set(worldX, y, worldZ);
if (!chunk.getBlockState(pos).isAir()) continue;
BlockState state = y == top ? cap : y >= top - 3 ? soil : material;
chunk.setBlockState(pos, state);
surface.update(localX, y, localZ, state);
floor.update(localX, y, localZ, state);
}
}
}
}
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
}
@@ -0,0 +1,46 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.List;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.resources.Identifier;
/** Ordered lake pages with fluid choices frozen when the Age is bound. */
public record PortLakePlan(List<Choice> choices) {
public static final Codec<PortLakePlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Choice.CODEC.listOf().fieldOf("choices").forGetter(PortLakePlan::choices)
).apply(instance, PortLakePlan::new));
public record Choice(Identifier page, Identifier fluid) {
public static final Codec<Choice> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Identifier.CODEC.fieldOf("page").forGetter(Choice::page),
Identifier.CODEC.fieldOf("fluid").forGetter(Choice::fluid)
).apply(instance, Choice::new));
public Choice {
if (!isLake(page) || fluid == null) throw new IllegalArgumentException("Invalid lake choice");
}
}
public PortLakePlan {
choices = List.copyOf(choices);
if (choices.isEmpty() || choices.size() > 32)
throw new IllegalArgumentException("Lake plan must contain 1–32 choices");
}
public static boolean isLake(Identifier page) {
if (page == null || !page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
return switch (page.getPath()) {
case "feature/surface_lakes", "feature/deep_lakes", "feature/lava_lakes" -> true;
default -> false;
};
}
public static boolean isDeep(Identifier page) { return page.getPath().equals("feature/deep_lakes"); }
public static Identifier placedFeature(Identifier page) {
if (!isLake(page)) throw new IllegalArgumentException("Unsupported lake page " + page);
return Identifier.withDefaultNamespace(isDeep(page) ? "lake_lava_underground" : "lake_lava_surface");
}
}
@@ -0,0 +1,91 @@
package net.dnorberg.rivencraft.age;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.Heightmap;
/** 26.3 port of Rivencraft 1.21.1's Legacy-style 4×4 plinth and 2×12×2 shaft. */
final class PortObelisksFeature {
private static final long SALT = 0x4F42454C49534B53L;
private static final int CHANCE = 128;
private static final int BASE = 4;
private static final int HEIGHT = 12;
private PortObelisksFeature() {}
record Site(int x, int z) {}
static Site site(long ageSeed, ChunkPos chunk, int occurrence) {
RandomSource random = RandomSource.create(mix(ageSeed ^ SALT
^ (long) chunk.x() * 341873128712L ^ (long) chunk.z() * 132897987541L
^ (long) occurrence * 0x9E3779B97F4A7C15L));
if (random.nextInt(CHANCE) != 0) return null;
return new Site(chunk.getMinBlockX() + 4 + random.nextInt(8),
chunk.getMinBlockZ() + 4 + random.nextInt(8));
}
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, BlockState material, int occurrence) {
Site site = site(ageSeed, chunk.getPos(), occurrence);
if (site == null) return;
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
int minSurface = Integer.MAX_VALUE;
int maxSurface = Integer.MIN_VALUE;
int[][] surface = new int[BASE][BASE];
for (int dx = 0; dx < BASE; dx++) {
for (int dz = 0; dz < BASE; dz++) {
int x = site.x() + dx;
int z = site.z() + dz;
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
if (y <= level.getMinY() + 1) return;
BlockState ground = level.getBlockState(pos.set(x, y - 1, z));
if (!ground.isFaceSturdy(level, pos, Direction.UP)
|| !ground.getFluidState().isEmpty() || ground.hasBlockEntity()) return;
surface[dx][dz] = y;
minSurface = Math.min(minSurface, y);
maxSurface = Math.max(maxSurface, y);
}
}
if (maxSurface - minSurface > 2 || maxSurface + HEIGHT >= level.getMaxY()) return;
for (int dx = 0; dx < BASE; dx++) {
for (int dz = 0; dz < BASE; dz++) {
for (int y = surface[dx][dz]; y <= maxSurface; y++) {
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
}
}
}
for (int dx = 1; dx <= 2; dx++) {
for (int dz = 1; dz <= 2; dz++) {
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++) {
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
}
}
}
for (int dx = 0; dx < BASE; dx++) {
for (int dz = 0; dz < BASE; dz++) {
for (int y = surface[dx][dz]; y <= maxSurface; y++)
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
}
}
for (int dx = 1; dx <= 2; dx++) {
for (int dz = 1; dz <= 2; dz++) {
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++)
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
}
}
}
private static boolean open(BlockState state) {
return state.getFluidState().isEmpty() && (state.isAir() || state.canBeReplaced());
}
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
}
@@ -1,134 +0,0 @@
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);
}
}
}
@@ -2,11 +2,23 @@ package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.List;
import java.util.Optional;
import net.minecraft.resources.Identifier;
/** First frozen authored feature: one Rocks page with an explicit block material. */
public record PortRockPlan(Identifier material) {
/** Frozen, ordered Rocks materials. Old single-material beta Ages remain readable. */
public record PortRockPlan(List<Identifier> materials) {
public static final Codec<PortRockPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Identifier.CODEC.fieldOf("material").forGetter(PortRockPlan::material)
).apply(instance, PortRockPlan::new));
Identifier.CODEC.optionalFieldOf("material").forGetter(plan -> Optional.empty()),
Identifier.CODEC.listOf().optionalFieldOf("materials", List.of()).forGetter(PortRockPlan::materials)
).apply(instance, (old, current) -> new PortRockPlan(current.isEmpty()
? old.map(List::of).orElse(List.of()) : current)));
public PortRockPlan {
materials = List.copyOf(materials);
if (materials.isEmpty() || materials.size() > 32)
throw new IllegalArgumentException("Rocks plan must contain 1–32 materials");
}
public PortRockPlan(Identifier material) { this(List.of(material)); }
}
@@ -0,0 +1,87 @@
package net.dnorberg.rivencraft.age;
import net.minecraft.core.BlockPos;
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.levelgen.Heightmap;
/**
* 26.3 port of Rivencraft's subtractive Skylands filter, adapted from
* Mystcraft Legacy's SymbolSkylands (LGPL-3.0-or-later). Keeps upper solid
* terrain above a seven-octave cutoff near Y=76 and removes liquids.
*/
final class PortSkylandsFeature {
private static final long SALT = 0x534B594C414E4453L;
private static final int BASE_CUTOFF = 76;
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
private PortSkylandsFeature() {}
static void filter(ChunkAccess chunk, long ageSeed) {
int originX = chunk.getPos().getMinBlockX();
int originZ = chunk.getPos().getMinBlockZ();
int minY = chunk.getMinY();
int maxY = chunk.getMaxY();
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
int cutoff = cutoff(ageSeed, originX + localX, originZ + localZ);
for (int y = minY; y < maxY; y++) {
pos.set(originX + localX, y, originZ + localZ);
BlockState state = chunk.getBlockState(pos);
if (!removes(state, y, cutoff)) continue;
chunk.setBlockState(pos, AIR);
surface.update(localX, y, localZ, AIR);
floor.update(localX, y, localZ, AIR);
}
}
}
}
static boolean removes(BlockState state, int y, int cutoff) {
return !state.isAir() && (y <= cutoff || state.liquid());
}
static int cutoff(long ageSeed, int x, int z) {
double variation = 0.0D;
int cellSize = 128;
double amplitude = 12.0D;
for (int octave = 0; octave < 7; octave++) {
variation += valueNoise(ageSeed ^ SALT ^ (long) octave * 0x9E3779B97F4A7C15L,
x, z, cellSize) * amplitude;
cellSize /= 2;
amplitude /= 2.0D;
}
return BASE_CUTOFF + (int) Math.round(variation);
}
private static double valueNoise(long seed, int x, int z, int cellSize) {
int cellX = Math.floorDiv(x, cellSize);
int cellZ = Math.floorDiv(z, cellSize);
double u = smooth((x - cellX * cellSize) / (double) cellSize);
double v = smooth((z - cellZ * cellSize) / (double) cellSize);
double north = lerp(value(seed, cellX, cellZ), value(seed, cellX + 1, cellZ), u);
double south = lerp(value(seed, cellX, cellZ + 1), value(seed, cellX + 1, cellZ + 1), u);
return lerp(north, south, v);
}
private static double value(long seed, int x, int z) {
long hash = mix(seed ^ (long) x * 0xD1B54A32D192ED03L ^ (long) z * 0xABC98388FB8FAC03L);
return (hash >>> 11) * 0x1.0p-52 - 1.0D;
}
private static double smooth(double value) { return value * value * (3.0D - 2.0D * value); }
private static double lerp(double first, double second, double amount) {
return first + (second - first) * amount;
}
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
}
@@ -0,0 +1,122 @@
package net.dnorberg.rivencraft.age;
import java.util.Random;
import net.minecraft.core.BlockPos;
import net.minecraft.util.Mth;
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.levelgen.Heightmap;
/**
* 26.3 adaptation of Rivencraft 1.21.1's solid sphere nodes, derived from
* Mystcraft Legacy's MapGenSpheresMyst (Mystcraft contributors,
* LGPL-3.0-or-later, commit 9bc8ddc). Writes stay within the target chunk.
*/
final class PortSpheresFeature {
private static final int SOURCE_RANGE = 8;
private static final long SALT = 0x53504845524553L;
private PortSpheresFeature() {}
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
var target = chunk.getPos();
Random master = new Random(ageSeed ^ SALT);
long xSeed = master.nextLong();
long zSeed = master.nextLong();
for (int sx = target.x() - SOURCE_RANGE; sx <= target.x() + SOURCE_RANGE; sx++) {
for (int sz = target.z() - SOURCE_RANGE; sz <= target.z() + SOURCE_RANGE; sz++) {
Random source = new Random((long) sx * xSeed ^ (long) sz * zSeed ^ ageSeed ^ SALT);
if (source.nextFloat() > 0.05F) continue;
double x = sx * 16 + source.nextInt(16);
double y = source.nextInt(source.nextInt(192) + 1) + 32;
double z = sz * 16 + source.nextInt(16);
trace(chunk, material, source.nextLong(), x, y, z,
1.0F + source.nextFloat() * 4.0F, 0, 0, -1, -1);
}
}
}
private static void trace(ChunkAccess chunk, BlockState material, long seed,
double x, double y, double z, float radius, float yaw, float pitch,
int step, int length) {
Random random = new Random(seed);
if (length <= 0) {
int maximum = SOURCE_RANGE * 16 - 16;
length = maximum - random.nextInt(maximum / 4);
}
boolean singleNode = step == -1;
if (singleNode) step = length / 2;
int splitAt = random.nextInt(length / 2) + length / 4;
boolean slowPitch = random.nextInt(6) == 0;
float yawDrift = 0;
float pitchDrift = 0;
double middleX = chunk.getPos().getMinBlockX() + 8;
double middleZ = chunk.getPos().getMinBlockZ() + 8;
for (; step < length; step++) {
double nodeRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
float horizontal = Mth.cos(pitch);
x += Mth.cos(yaw) * horizontal;
y += Mth.sin(pitch);
z += Mth.sin(yaw) * horizontal;
pitch *= slowPitch ? 0.92F : 0.7F;
pitch += pitchDrift * 0.1F;
yaw += yawDrift * 0.1F;
pitchDrift = pitchDrift * 0.9F
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
yawDrift = yawDrift * 0.75F
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
if (!singleNode && step == splitAt && radius > 1.0F) {
trace(chunk, material, random.nextLong(), x, y, z,
random.nextFloat() * 0.5F + 0.5F, yaw - Mth.HALF_PI, pitch / 3, step, length);
trace(chunk, material, random.nextLong(), x, y, z,
random.nextFloat() * 0.5F + 0.5F, yaw + Mth.HALF_PI, pitch / 3, step, length);
return;
}
if (!singleNode && random.nextInt(4) == 0) continue;
double dx = x - middleX;
double dz = z - middleZ;
double remaining = length - step;
double reach = radius + 18;
if (dx * dx + dz * dz - remaining * remaining > reach * reach) return;
if (Math.abs(dx) <= 16 + nodeRadius * 2 && Math.abs(dz) <= 16 + nodeRadius * 2)
fillNode(chunk, material, x, y, z, nodeRadius);
if (singleNode) break;
}
}
private static void fillNode(ChunkAccess chunk, BlockState material,
double centerX, double centerY, double centerZ, double radius) {
int originX = chunk.getPos().getMinBlockX();
int originZ = chunk.getPos().getMinBlockZ();
int minX = Math.max(0, Mth.floor(centerX - radius) - originX - 1);
int maxX = Math.min(16, Mth.floor(centerX + radius) - originX + 2);
int minZ = Math.max(0, Mth.floor(centerZ - radius) - originZ - 1);
int maxZ = Math.min(16, Mth.floor(centerZ + radius) - originZ + 2);
int minY = Math.max(chunk.getMinY() + 1, Mth.floor(centerY - radius) - 1);
int maxY = Math.min(chunk.getMaxY(), Mth.floor(centerY + radius) + 2);
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
for (int worldY = minY; worldY < maxY; worldY++) {
double dy = (worldY + 0.5D - centerY) / radius;
for (int localZ = minZ; localZ < maxZ; localZ++) {
double dz = (originZ + localZ + 0.5D - centerZ) / radius;
for (int localX = minX; localX < maxX; localX++) {
double dx = (originX + localX + 0.5D - centerX) / radius;
if (!insideSphere(dx, dy, dz)) continue;
pos.set(originX + localX, worldY, originZ + localZ);
BlockState previous = chunk.getBlockState(pos);
if (previous == material || previous.is(Blocks.BEDROCK)) continue;
chunk.setBlockState(pos, material);
surface.update(localX, worldY, localZ, material);
floor.update(localX, worldY, localZ, material);
}
}
}
}
static boolean insideSphere(double dx, double dy, double dz) {
return dx * dx + dy * dy + dz * dz < 1.0D;
}
}
@@ -0,0 +1,56 @@
package net.dnorberg.rivencraft.age;
import net.minecraft.core.BlockPos;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.Heightmap;
/**
* 26.3 port of Rivencraft 1.21.1's rare surface spikes, adapted from
* Mystcraft Legacy's WorldGenSpikesAdv (Mystcraft contributors,
* LGPL-3.0-or-later, commit 9bc8ddc). The footprint stays within one chunk.
*/
final class PortSpikesFeature {
private static final long SALT = 0x5350494B4553L;
private PortSpikesFeature() {}
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, BlockState material, int occurrence) {
int originX = chunk.getPos().getMinBlockX();
int originZ = chunk.getPos().getMinBlockZ();
RandomSource random = RandomSource.create(ageSeed ^ SALT
^ (long) originX * 341873128712L ^ (long) originZ * 132897987541L
^ (long) occurrence * 0x9E3779B97F4A7C15L);
if (random.nextInt(18) != 0) return;
int x = originX + 4 + random.nextInt(8);
int z = originZ + 4 + random.nextInt(8);
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
int height = 6 + random.nextInt(32);
int width = 1 + random.nextInt(4);
if (y < level.getMinY() + 1 || y + height >= level.getMaxY()) return;
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
if (!level.isEmptyBlock(pos.set(x, y, z))) return;
for (int px = x - width; px <= x + width; px++) {
for (int pz = z - width; pz <= z + width; pz++) {
if (!insideFootprint(px - x, pz - z, width)) continue;
if (!level.getBlockState(pos.set(px, y - 1, pz)).isSolid()) return;
}
}
for (int px = x - width; px <= x + width; px++) {
for (int pz = z - width; pz <= z + width; pz++) {
if (!insideFootprint(px - x, pz - z, width)) continue;
int top = y + 1 + random.nextInt(random.nextInt(height) + 1);
for (int py = y; py < top; py++) {
pos.set(px, py, pz);
if (level.isEmptyBlock(pos)) level.setBlock(pos, material, 2);
}
}
}
}
static boolean insideFootprint(int dx, int dz, int width) {
return dx * dx + dz * dz <= width * width + 1;
}
}
@@ -0,0 +1,107 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.saveddata.SavedData;
import net.minecraft.world.level.saveddata.SavedDataType;
/**
* Ports Rivencraft 1.21.1's Legacy fissure footprint/carving.
* Derived from Mystcraft's WorldGenMystStarFissure, LGPL-3.0,
* copyright XCompWiz and Mystcraft contributors.
*/
public final class PortStarFissureFeature {
private static final long SALT = 0x5354415246495353L;
private PortStarFissureFeature() {}
/** Per-Age data prevents revisits/restarts from carving the same fissure again. */
private static final class Arrival extends SavedData {
private static final Codec<Arrival> CODEC = BlockPos.CODEC.optionalFieldOf("spawn").codec()
.xmap(Arrival::new, data -> Optional.ofNullable(data.spawn));
private static final SavedDataType<Arrival> TYPE = new SavedDataType<>(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "star_fissure_arrival"),
() -> new Arrival(Optional.empty()), CODEC);
private BlockPos spawn;
private Arrival(Optional<BlockPos> spawn) { this.spawn = spawn.orElse(null); }
}
public static BlockPos savedSpawn(ServerLevel level) {
return level.getDataStorage().computeIfAbsent(Arrival.TYPE).spawn;
}
public static void generateOnce(ServerLevel level, BlockPos spawn, boolean caveTerrain) {
Arrival data = level.getDataStorage().computeIfAbsent(Arrival.TYPE);
if (data.spawn != null) return;
generate(level, level.getSeed(), spawn, caveTerrain);
data.spawn = spawn.immutable();
data.setDirty();
}
/** Legacy chooses 8–23 blocks after the spawn chunk, then grows along +Z. */
public static List<BlockPos> footprint(long ageSeed, BlockPos spawn) {
long chunkX = spawn.getX() >> 4;
long chunkZ = spawn.getZ() >> 4;
Random random = new Random(ageSeed ^ SALT ^ chunkX ^ (chunkZ << 32));
int originX = ((int) chunkX << 4) + random.nextInt(16) + 8;
int originZ = ((int) chunkZ << 4) + random.nextInt(16) + 8;
int length = random.nextInt(8) + 10;
int[][] intervals = new int[length][2];
List<BlockPos> columns = new ArrayList<>();
addRow(columns, originX, originZ, 0, 0, 0);
for (int row = 1; row < length; row++) {
int range = length + Math.min(row, length - row) + 1;
int right = (random.nextInt(range) - (range >> 1) + intervals[row - 1][1]) / 2;
int left = (random.nextInt(range) - (range >> 1) + intervals[row - 1][0]) / 2;
if (left > right) {
int swap = left;
left = right;
right = swap;
}
left = Math.min(left, intervals[row - 1][1]);
right = Math.max(right, intervals[row - 1][0]);
intervals[row][0] = left;
intervals[row][1] = right;
addRow(columns, originX, originZ, row, left, right);
}
return List.copyOf(columns);
}
private static void addRow(List<BlockPos> columns, int originX, int originZ, int row, int left, int right) {
for (int x = left; x <= right; x++) {
// Only X/Z are used; the real floor is the dimension's minimum Y.
columns.add(new BlockPos(originX + x, 0, originZ + row * 2));
columns.add(new BlockPos(originX + x, 0, originZ + row * 2 + 1));
}
}
private static void generate(ServerLevel level, long ageSeed, BlockPos spawn, boolean caveTerrain) {
int floorY = level.getMinY();
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
for (BlockPos column : footprint(ageSeed, spawn)) {
int top = level.getHeight(Heightmap.Types.WORLD_SURFACE, column.getX(), column.getZ());
if (caveTerrain) {
for (int y = Math.max(spawn.getY(), floorY + 1); y < top; y++) {
if (level.getBlockState(mutable.set(column.getX(), y, column.getZ())).is(Blocks.BEDROCK)) {
top = y;
break;
}
}
}
for (int y = floorY + 1; y < top; y++)
level.setBlock(mutable.set(column.getX(), y, column.getZ()), Blocks.AIR.defaultBlockState(), 2);
level.setBlock(mutable.set(column.getX(), floorY, column.getZ()),
Rivencraft.STAR_FISSURE.get().defaultBlockState(), 2);
}
}
}
@@ -0,0 +1,142 @@
package net.dnorberg.rivencraft.age;
import java.util.Random;
import net.minecraft.core.BlockPos;
import net.minecraft.util.Mth;
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.levelgen.Heightmap;
/**
* 26.3 adaptation of Rivencraft 1.21.1's winding solid volumes, originally
* adapted from Mystcraft Legacy's SymbolTendrils and MapGenCavesMyst (LGPL-3.0-or-later).
* Each chunk reconstructs the neighboring source paths to preserve continuous borders.
*/
final class PortTendrilFeature {
private static final long SALT = 0x54454E4452494C53L;
private static final int SOURCE_RANGE = 8;
private static final int SOURCE_RATE = 15;
private static final int SOURCE_SIZE = 18;
private PortTendrilFeature() {}
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
var target = chunk.getPos();
Random master = new Random(ageSeed ^ SALT);
long xSeed = master.nextLong();
long zSeed = master.nextLong();
for (int sourceX = target.x() - SOURCE_RANGE; sourceX <= target.x() + SOURCE_RANGE; sourceX++) {
for (int sourceZ = target.z() - SOURCE_RANGE; sourceZ <= target.z() + SOURCE_RANGE; sourceZ++) {
Random source = new Random((long) sourceX * xSeed ^ (long) sourceZ * zSeed ^ ageSeed ^ SALT);
int nodes = source.nextInt(source.nextInt(source.nextInt(SOURCE_SIZE) + 1) + 1);
if (source.nextInt(SOURCE_RATE) != 0) continue;
for (int node = 0; node < nodes; node++) {
double x = sourceX * 16 + source.nextInt(16);
double y = source.nextInt(source.nextInt(120) + 8);
double z = sourceZ * 16 + source.nextInt(16);
int branches = 1;
if (source.nextInt(4) == 0) {
trace(chunk, material, source.nextLong(), x, y, z,
1.0F + source.nextFloat() * 6.0F, 0.0F, 0.0F, -1, -1, 0.5D);
branches += source.nextInt(4);
}
for (int branch = 0; branch < branches; branch++) {
float yaw = source.nextFloat() * Mth.TWO_PI;
float pitch = (source.nextFloat() - 0.5F) / 4.0F;
float radius = source.nextFloat() * 2.0F + source.nextFloat();
if (source.nextInt(10) == 0)
radius *= source.nextFloat() * source.nextFloat() * 3.0F + 1.0F;
trace(chunk, material, source.nextLong(), x, y, z,
radius, yaw, pitch, 0, 0, 1.0D);
}
}
}
}
}
private static void trace(ChunkAccess chunk, BlockState material, long seed,
double x, double y, double z, float radius, float yaw, float pitch,
int step, int length, double verticalScale) {
Random random = new Random(seed);
if (length <= 0) {
int maximum = SOURCE_RANGE * 16 - 16;
length = maximum - random.nextInt(maximum / 4);
}
boolean singleNode = step == -1;
if (singleNode) step = length / 2;
int splitAt = random.nextInt(length / 2) + length / 4;
boolean slowPitch = random.nextInt(6) == 0;
float yawDrift = 0.0F;
float pitchDrift = 0.0F;
double middleX = chunk.getPos().getMinBlockX() + 8;
double middleZ = chunk.getPos().getMinBlockZ() + 8;
for (; step < length; step++) {
double horizontalRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
double verticalRadius = horizontalRadius * verticalScale;
float horizontalMotion = Mth.cos(pitch);
x += Mth.cos(yaw) * horizontalMotion;
y += Mth.sin(pitch);
z += Mth.sin(yaw) * horizontalMotion;
pitch *= slowPitch ? 0.92F : 0.7F;
pitch += pitchDrift * 0.1F;
yaw += yawDrift * 0.1F;
pitchDrift = pitchDrift * 0.9F
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
yawDrift = yawDrift * 0.75F
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
if (!singleNode && step == splitAt && radius > 1.0F) {
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
yaw - Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
yaw + Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
return;
}
if (!singleNode && random.nextInt(4) == 0) continue;
double distanceX = x - middleX;
double distanceZ = z - middleZ;
double remaining = length - step;
double reach = radius + 18.0D;
if (distanceX * distanceX + distanceZ * distanceZ - remaining * remaining > reach * reach) return;
if (x >= middleX - 16.0D - horizontalRadius * 2.0D
&& x <= middleX + 16.0D + horizontalRadius * 2.0D
&& z >= middleZ - 16.0D - horizontalRadius * 2.0D
&& z <= middleZ + 16.0D + horizontalRadius * 2.0D) {
fillNode(chunk, material, x, y, z, horizontalRadius, verticalRadius);
}
if (singleNode) break;
}
}
private static void fillNode(ChunkAccess chunk, BlockState material, double centerX, double centerY,
double centerZ, double horizontalRadius, double verticalRadius) {
int originX = chunk.getPos().getMinBlockX();
int originZ = chunk.getPos().getMinBlockZ();
int minX = Math.max(0, Mth.floor(centerX - horizontalRadius) - originX - 1);
int maxX = Math.min(16, Mth.floor(centerX + horizontalRadius) - originX + 2);
int minZ = Math.max(0, Mth.floor(centerZ - horizontalRadius) - originZ - 1);
int maxZ = Math.min(16, Mth.floor(centerZ + horizontalRadius) - originZ + 2);
int minY = Math.max(chunk.getMinY() + 1, Mth.floor(centerY - verticalRadius) - 1);
int maxY = Math.min(chunk.getMaxY(), Mth.floor(centerY + verticalRadius) + 2);
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
for (int worldY = minY; worldY < maxY; worldY++) {
double dy = (worldY + 0.5D - centerY) / verticalRadius;
if (dy <= -0.7D || dy * dy >= 1.0D) continue;
for (int localZ = minZ; localZ < maxZ; localZ++) {
double dz = (originZ + localZ + 0.5D - centerZ) / horizontalRadius;
for (int localX = minX; localX < maxX; localX++) {
double dx = (originX + localX + 0.5D - centerX) / horizontalRadius;
if (dx * dx + dy * dy + dz * dz >= 1.0D) continue;
pos.set(originX + localX, worldY, originZ + localZ);
BlockState previous = chunk.getBlockState(pos);
if (previous == material || previous.is(Blocks.BEDROCK)) continue;
chunk.setBlockState(pos, material);
surface.update(localX, worldY, localZ, material);
floor.update(localX, worldY, localZ, material);
}
}
}
}
}
@@ -0,0 +1,79 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.List;
import java.util.HashMap;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.resources.Identifier;
/** Frozen terrain filters and ordered solid/surface features for an Age. */
public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres, boolean starFissures) {
public static final Identifier FLOATING_ISLANDS = Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "feature/floating_islands");
public static final Identifier TENDRILS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/tendrils");
public static final Identifier SKYLANDS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/skylands");
public static final Identifier SPHERES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/spheres");
public static final Identifier SPIKES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/spikes");
public static final Identifier OBELISKS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/obelisks");
public static final Identifier CRYSTAL_FORMATIONS = Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "feature/crystal_formations");
public static final Identifier CRYSTAL_MATERIAL = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "crystal_block");
public static final Identifier DENSE_ORES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/dense_ores");
public static final Identifier STAR_FISSURES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/star_fissures");
public static final int MAX_PER_FEATURE = 32;
public static final Codec<PortTerrainFeaturePlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Choice.CODEC.listOf().fieldOf("choices").forGetter(PortTerrainFeaturePlan::choices),
Codec.BOOL.optionalFieldOf("skylands", false).forGetter(PortTerrainFeaturePlan::skylands),
Codec.intRange(0, MAX_PER_FEATURE).optionalFieldOf("dense_ores", 0)
.forGetter(PortTerrainFeaturePlan::denseOres),
Codec.BOOL.optionalFieldOf("star_fissures", false).forGetter(PortTerrainFeaturePlan::starFissures)
).apply(instance, PortTerrainFeaturePlan::new));
public record Choice(Identifier page, Identifier material) {
public static final Codec<Choice> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Identifier.CODEC.fieldOf("page").forGetter(Choice::page),
Identifier.CODEC.fieldOf("material").forGetter(Choice::material)
).apply(instance, Choice::new));
public Choice {
if (!isSupported(page) || material == null)
throw new IllegalArgumentException("Invalid terrain feature choice");
if (CRYSTAL_FORMATIONS.equals(page) && !CRYSTAL_MATERIAL.equals(material))
throw new IllegalArgumentException("Crystal formations use Rivencraft crystal, not a Block modifier");
}
}
public PortTerrainFeaturePlan {
choices = List.copyOf(choices);
if (denseOres < 0 || denseOres > MAX_PER_FEATURE)
throw new IllegalArgumentException("Dense Ores permits at most 32 occurrences");
if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures)
throw new IllegalArgumentException("Terrain feature plan must not be empty");
var counts = new HashMap<Identifier, Integer>();
for (Choice choice : choices) {
if (counts.merge(choice.page(), 1, Integer::sum) > MAX_PER_FEATURE)
throw new IllegalArgumentException("Terrain feature plan permits at most 32 occurrences per feature");
}
}
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres) {
this(choices, skylands, denseOres, false);
}
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands) { this(choices, skylands, 0, false); }
public PortTerrainFeaturePlan(List<Choice> choices) { this(choices, false, 0, false); }
public static boolean isSupported(Identifier page) {
return FLOATING_ISLANDS.equals(page) || TENDRILS.equals(page) || SPHERES.equals(page) || isSurface(page);
}
public static boolean acceptsMaterial(Identifier page) {
return isSupported(page) && !CRYSTAL_FORMATIONS.equals(page);
}
public static boolean isSurface(Identifier page) {
return SPIKES.equals(page) || OBELISKS.equals(page) || CRYSTAL_FORMATIONS.equals(page);
}
}
@@ -0,0 +1,38 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.List;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.resources.Identifier;
/** Ordered, frozen authored vegetation pages for surface Ages. */
public record PortVegetationPlan(List<Identifier> pages) {
public static final Identifier FLOWERS = page("flowers");
public static final Identifier TREES = page("trees");
public static final Identifier HUGE_TREES = page("huge_trees");
public static final Codec<PortVegetationPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Identifier.CODEC.listOf().fieldOf("pages").forGetter(PortVegetationPlan::pages)
).apply(instance, PortVegetationPlan::new));
public PortVegetationPlan {
pages = List.copyOf(pages);
if (pages.isEmpty() || pages.size() > 32 || pages.stream().anyMatch(page -> !isVegetation(page)))
throw new IllegalArgumentException("Vegetation plan requires 1–32 supported pages");
}
public static boolean isVegetation(Identifier page) {
return page.equals(FLOWERS) || page.equals(TREES) || page.equals(HUGE_TREES);
}
public static Identifier placedFeature(Identifier page) {
if (page.equals(FLOWERS)) return Identifier.withDefaultNamespace("flower_default");
if (page.equals(TREES)) return Identifier.withDefaultNamespace("trees_plains");
if (page.equals(HUGE_TREES)) return Identifier.withDefaultNamespace("trees_old_growth_spruce_taiga");
throw new IllegalArgumentException("Unsupported vegetation page " + page);
}
private static Identifier page(String name) {
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/" + name);
}
}
@@ -57,7 +57,8 @@ 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.rocks(age.getKey()));
index.terrain(age.getKey()), index.rocks(age.getKey()), index.vegetation(age.getKey()),
index.dungeons(age.getKey()), index.lakes(age.getKey()), index.terrainFeatures(age.getKey()));
} catch (RuntimeException exception) {
Rivencraft.LOGGER.error("Could not restore Age {}. Its saved index remains intact.", age.getKey(), exception);
}
@@ -80,12 +81,37 @@ public final class RuntimeAgeBackend {
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks) {
return create(server, id, seed, biomePlan, terrain, rocks, null);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation) {
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, 0);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons) {
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, null);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes) {
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, null);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures) {
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, rocks);
index.add(id, seed, biomePlan, terrain, rocks);
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
// 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.
@@ -106,7 +132,8 @@ public final class RuntimeAgeBackend {
checkServerThread(server);
long seed = requireSeed(server, id);
ServerLevel level = load(server, id, seed, index(server).biomePlan(id), index(server).terrain(id),
index(server).rocks(id));
index(server).rocks(id), index(server).vegetation(id), index(server).dungeons(id), index(server).lakes(id),
index(server).terrainFeatures(id));
AgeCatalogSync.onCatalogChanged(server);
return level;
}
@@ -119,18 +146,22 @@ public final class RuntimeAgeBackend {
PortBiomePlan biomePlan = index.biomePlan(id);
PortAgeTerrain terrain = index.terrain(id);
PortRockPlan rocks = index.rocks(id);
PortVegetationPlan vegetation = index.vegetation(id);
int dungeons = index.dungeons(id);
PortLakePlan lakes = index.lakes(id);
PortTerrainFeaturePlan terrainFeatures = index.terrainFeatures(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, rocks);
if (loaded) load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
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, rocks);
load(server, id, previousSeed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
AgeCatalogSync.onCatalogChanged(server);
} catch (RuntimeException rollbackFailure) {
exception.addSuppressed(rollbackFailure);
@@ -149,6 +180,10 @@ public final class RuntimeAgeBackend {
PortBiomePlan biomePlan = index.biomePlan(id);
PortAgeTerrain terrain = index.terrain(id);
PortRockPlan rocks = index.rocks(id);
PortVegetationPlan vegetation = index.vegetation(id);
int dungeons = index.dungeons(id);
PortLakePlan lakes = index.lakes(id);
PortTerrainFeaturePlan terrainFeatures = index.terrainFeatures(id);
boolean loaded = server.getLevel(dimensionKey(id)) != null;
ArchivePaths paths = archivePaths(server, name);
if (loaded) unloadLoaded(server, id);
@@ -167,14 +202,15 @@ public final class RuntimeAgeBackend {
}
} catch (IOException | RuntimeException exception) {
try {
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks);
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures);
server.overworld().getDataStorage().saveAndJoin();
} catch (RuntimeException rollbackFailure) {
exception.addSuppressed(rollbackFailure);
}
if (loaded) {
try {
load(server, id, seed, biomePlan, terrain, rocks);
load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
} catch (RuntimeException rollbackFailure) {
exception.addSuppressed(rollbackFailure);
}
@@ -290,7 +326,9 @@ public final class RuntimeAgeBackend {
}
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks) {
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures) {
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
@@ -361,7 +399,8 @@ public final class RuntimeAgeBackend {
};
generator = new NoiseBasedChunkGenerator(source, settings);
}
if (rocks != null) generator = new PortRockChunkGenerator(generator, rocks, seed);
if (rocks != null || vegetation != null || dungeons > 0 || lakes != null || terrainFeatures != null)
generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, 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,
@@ -0,0 +1,61 @@
package net.dnorberg.rivencraft.block;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.InsideBlockEffectApplier;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EndPortalBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.portal.TeleportTransition;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
/** Non-obtainable starfield portal returning entities to the shared Overworld spawn. */
public final class StarFissureBlock extends EndPortalBlock {
private static final VoxelShape SHAPE = Block.box(0, 0, 0, 16, 1.6, 16);
public StarFissureBlock(BlockBehaviour.Properties properties) { super(properties); }
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new StarFissureBlockEntity(pos, state);
}
@Override
public void animateTick(BlockState state, Level level, BlockPos pos, net.minecraft.util.RandomSource random) {
// Legacy fissures have a starfield, not vanilla End portal smoke.
}
@Override
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
return SHAPE;
}
@Override
protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity,
InsideBlockEffectApplier effects, boolean precise) {
// Do not inherit End credits or End-dimension travel from the vanilla block.
if (entity.canUsePortal(false)) entity.setAsInsidePortal(this, pos);
}
@Override
public TeleportTransition getPortalDestination(ServerLevel level, Entity entity, BlockPos pos) {
if (level.dimension() == Level.OVERWORLD) return null;
ServerLevel overworld = level.getServer().overworld();
if (overworld == null) return null;
BlockPos spawn = overworld.getRespawnData().pos();
BlockPos landing = net.dnorberg.rivencraft.age.PortAgeTravel.findFootedDestination(overworld, spawn).orElse(spawn);
level.playSound(null, pos, Rivencraft.LINK_FISSURE.get(), SoundSource.BLOCKS, 1.0F, 1.0F);
return new TeleportTransition(overworld, Vec3.atBottomCenterOf(landing), entity.getDeltaMovement(),
entity.getYRot(), entity.getXRot(), TeleportTransition.PLACE_PORTAL_TICKET);
}
}
@@ -0,0 +1,12 @@
package net.dnorberg.rivencraft.block.entity;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.TheEndPortalBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
public final class StarFissureBlockEntity extends TheEndPortalBlockEntity {
public StarFissureBlockEntity(BlockPos pos, BlockState state) {
super(Rivencraft.STAR_FISSURE_BLOCK_ENTITY.get(), pos, state);
}
}
@@ -14,6 +14,12 @@ import net.minecraft.server.packs.resources.ResourceManagerReloadListener;
public final class PortBookBinderClientEvents {
private PortBookBinderClientEvents() {}
@SubscribeEvent
public static void registerBlockEntityRenderers(
net.neoforged.neoforge.client.event.EntityRenderersEvent.RegisterRenderers event) {
event.registerBlockEntityRenderer(Rivencraft.STAR_FISSURE_BLOCK_ENTITY.get(), context -> new StarFissureRenderer());
}
@SubscribeEvent
public static void registerScreens(RegisterMenuScreensEvent event) {
event.register(Rivencraft.BOOK_BINDER_MENU.get(), PortBookBinderScreen::new);
@@ -0,0 +1,34 @@
package net.dnorberg.rivencraft.client;
import com.mojang.blaze3d.vertex.PoseStack;
import java.util.List;
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.client.renderer.blockentity.AbstractEndPortalRenderer;
import net.minecraft.client.renderer.blockentity.state.EndPortalRenderState;
import net.minecraft.client.renderer.rendertype.RenderTypes;
import net.minecraft.client.renderer.state.level.CameraRenderState;
import net.minecraft.core.Direction;
/** Retains 1.21.1's UP-at-zero/DOWN-at-one planes with the modern starfield renderer. */
public final class StarFissureRenderer extends AbstractEndPortalRenderer<StarFissureBlockEntity, EndPortalRenderState> {
@Override
public EndPortalRenderState createRenderState() { return new EndPortalRenderState(); }
@Override
public void submit(EndPortalRenderState state, PoseStack pose, SubmitNodeCollector collector,
CameraRenderState camera) {
if (state.facesToShow.contains(Direction.UP)) {
pose.pushPose();
pose.translate(0, -1, 0);
submitCube(List.of(Direction.UP), RenderTypes.endPortal(), pose, collector);
pose.popPose();
}
if (state.facesToShow.contains(Direction.DOWN)) {
pose.pushPose();
pose.translate(0, 1, 0);
submitCube(List.of(Direction.DOWN), RenderTypes.endPortal(), pose, collector);
pose.popPose();
}
}
}
@@ -135,7 +135,11 @@ public final class PortBookNetworking {
biomePlan = random.biomes();
}
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
PortAuthoredWriting.rockPlan(pages));
PortAuthoredWriting.rockPlan(pages, seed),
PortAuthoredWriting.vegetationPlan(pages),
PortAuthoredWriting.dungeonCount(pages),
PortAuthoredWriting.lakePlan(pages, seed),
PortAuthoredWriting.terrainFeaturePlan(pages, seed));
} catch (IllegalArgumentException | IllegalStateException exception) {
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
return null;
@@ -3,7 +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.dnorberg.rivencraft.age.PortFeatureChunkGenerator;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.neoforged.bus.api.IEventBus;
@@ -16,7 +16,8 @@ public final class PortChunkGenerators {
static {
GENERATORS.register("age_flat", () -> PortFlatChunkGenerator.CODEC);
GENERATORS.register("age_rocks", () -> PortRockChunkGenerator.CODEC);
// Keep the beta codec ID so previously saved Rocks Ages remain readable.
GENERATORS.register("age_rocks", () -> PortFeatureChunkGenerator.CODEC);
}
private PortChunkGenerators() {}
@@ -2,18 +2,30 @@ package net.dnorberg.rivencraft.writing;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Random;
import java.util.HashMap;
import java.util.Map;
import net.dnorberg.rivencraft.age.PortBiomePlan;
import net.dnorberg.rivencraft.age.PortAgeTerrain;
import net.dnorberg.rivencraft.age.PortRockPlan;
import net.dnorberg.rivencraft.age.PortVegetationPlan;
import net.dnorberg.rivencraft.age.PortLakePlan;
import net.dnorberg.rivencraft.age.PortTerrainFeaturePlan;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.tags.BiomeTags;
/** Authored noise terrain and biome distribution supported by the 26.3 backend. */
public final class PortAuthoredWriting {
private static final int MAX_AUTHORED_BIOMES = 252;
private static final int MAX_ROCKS = 32;
private static final int MAX_VEGETATION = 32;
private static final int MAX_DUNGEONS = 32;
private static final int MAX_LAKES = 32;
private static final Identifier STANDARD_TERRAIN = Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "terrain/standard");
private static final Identifier AMPLIFIED_TERRAIN = Identifier.fromNamespaceAndPath(
@@ -28,6 +40,8 @@ public final class PortAuthoredWriting {
Rivencraft.MOD_ID, "terrain/cave");
private static final Identifier ROCKS = Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "feature/rocks");
private static final Identifier DUNGEONS = Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "feature/dungeons");
private PortAuthoredWriting() {}
@@ -36,19 +50,51 @@ public final class PortAuthoredWriting {
boolean terrain = false;
int biomes = 0;
int controllers = 0;
boolean material = false;
boolean rocks = false;
String pendingMaterial = null;
int rocks = 0;
int vegetation = 0;
int dungeons = 0;
int lakes = 0;
int skylands = 0;
int denseOres = 0;
int starFissures = 0;
Map<Identifier, Integer> terrainFeatures = new HashMap<>();
for (Identifier page : pages) {
if (pendingMaterial != null && !(pendingMaterial.equals("rivencraft:block")
&& (page.equals(ROCKS) || PortTerrainFeaturePlan.acceptsMaterial(page))
|| pendingMaterial.equals("rivencraft:fluid") && PortLakePlan.isLake(page))) return false;
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
} else if (isBlockPage(page) && pendingMaterial == null
&& PortSymbolPages.find(page) != null
&& PortSymbolPages.find(page).category().equals("rivencraft:block")) {
material = true;
} else if (page.equals(ROCKS) && material && !rocks) {
rocks = true;
&& (PortSymbolPages.find(page).category().equals("rivencraft:block")
|| PortSymbolPages.find(page).category().equals("rivencraft:fluid"))) {
pendingMaterial = PortSymbolPages.find(page).category();
} else if (page.equals(ROCKS) && rocks < MAX_ROCKS) {
pendingMaterial = null;
rocks++;
} else if (PortLakePlan.isLake(page) && lakes < MAX_LAKES) {
pendingMaterial = null;
lakes++;
} else if (PortTerrainFeaturePlan.isSupported(page)
&& terrainFeatures.getOrDefault(page, 0) < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
pendingMaterial = null;
terrainFeatures.merge(page, 1, Integer::sum);
} else if (page.equals(PortTerrainFeaturePlan.SKYLANDS)
&& skylands < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
skylands++;
} else if (page.equals(PortTerrainFeaturePlan.DENSE_ORES)
&& denseOres < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
denseOres++;
} else if (page.equals(PortTerrainFeaturePlan.STAR_FISSURES)
&& starFissures < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
starFissures++;
} else if (PortVegetationPlan.isVegetation(page) && vegetation < MAX_VEGETATION) {
vegetation++;
} else if (page.equals(DUNGEONS) && dungeons < MAX_DUNGEONS) {
dungeons++;
} else if (page.getNamespace().equals(Rivencraft.MOD_ID)
&& page.getPath().startsWith("biome/") && biomes < MAX_AUTHORED_BIOMES
&& PortSymbolPages.find(page) != null
@@ -57,7 +103,14 @@ public final class PortAuthoredWriting {
biomes++;
} else return false;
}
if (material != rocks || rocks && (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
if (pendingMaterial != null || (rocks > 0 || vegetation > 0)
&& (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
if ((dungeons > 0 || lakes > 0) && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN)
|| pages.contains(CAVE_TERRAIN))) return false;
if (denseOres > 0 && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
if (pages.contains(PortTerrainFeaturePlan.FLOATING_ISLANDS) && pages.contains(CAVE_TERRAIN)) return false;
if (pages.stream().anyMatch(PortTerrainFeaturePlan::isSurface)
&& (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();
@@ -107,11 +160,94 @@ public final class PortAuthoredWriting {
return PortAgeTerrain.STANDARD;
}
public static PortRockPlan rockPlan(List<Identifier> pages) {
public static PortRockPlan rockPlan(List<Identifier> pages, long seed) {
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;
List<Identifier> materials = solidMaterials(pages, seed, ROCKS);
return materials.isEmpty() ? null : new PortRockPlan(materials);
}
public static PortTerrainFeaturePlan terrainFeaturePlan(List<Identifier> pages, long seed) {
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
Map<Identifier, List<Identifier>> materials = new HashMap<>();
Map<Identifier, Integer> occurrences = new HashMap<>();
List<PortTerrainFeaturePlan.Choice> choices = new ArrayList<>();
for (Identifier page : pages) {
if (!PortTerrainFeaturePlan.isSupported(page)) continue;
if (page.equals(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS)) {
choices.add(new PortTerrainFeaturePlan.Choice(page, PortTerrainFeaturePlan.CRYSTAL_MATERIAL));
continue;
}
List<Identifier> selected = materials.computeIfAbsent(page, feature -> solidMaterials(pages, seed, feature));
int occurrence = occurrences.merge(page, 1, Integer::sum) - 1;
choices.add(new PortTerrainFeaturePlan.Choice(page, selected.get(occurrence)));
}
boolean skylands = pages.contains(PortTerrainFeaturePlan.SKYLANDS);
int denseOres = (int) pages.stream().filter(PortTerrainFeaturePlan.DENSE_ORES::equals).count();
boolean starFissures = pages.contains(PortTerrainFeaturePlan.STAR_FISSURES);
return choices.isEmpty() && !skylands && denseOres == 0 && !starFissures ? null
: new PortTerrainFeaturePlan(choices, skylands, denseOres, starFissures);
}
private static List<Identifier> solidMaterials(List<Identifier> pages, long seed, Identifier feature) {
if (!pages.contains(feature)) return List.of();
List<Identifier> randomPool = new ArrayList<>(PortRegistryPages.solidMaterialIds());
if (randomPool.isEmpty()) throw new IllegalStateException("No full-block materials are registered");
Collections.shuffle(randomPool, new Random(mix(seed ^ (long) feature.hashCode() * 0x9E3779B97F4A7C15L)));
List<Identifier> materials = new ArrayList<>();
Identifier pending = null;
int randomIndex = 0;
for (Identifier page : pages) {
if (isBlockPage(page)) pending = PortRegistryPages.blockId(page);
else if (page.equals(feature)) {
materials.add(pending != null ? pending : randomPool.get(randomIndex++ % randomPool.size()));
pending = null;
} else pending = null;
}
return List.copyOf(materials);
}
public static PortLakePlan lakePlan(List<Identifier> pages, long seed) {
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
if (pages.stream().noneMatch(PortLakePlan::isLake)) return null;
List<Identifier> fluidPool = PortRegistryPages.fluidMaterialIds();
if (fluidPool.isEmpty()) throw new IllegalStateException("No source-fluid materials are registered");
Map<Identifier, List<Identifier>> orders = new HashMap<>();
Map<Identifier, Integer> occurrences = new HashMap<>();
List<PortLakePlan.Choice> choices = new ArrayList<>();
Identifier pending = null;
for (Identifier page : pages) {
if (isBlockPage(page)) pending = PortRegistryPages.blockId(page);
else if (PortLakePlan.isLake(page)) {
int occurrence = occurrences.merge(page, 1, Integer::sum) - 1;
List<Identifier> fluids = orders.computeIfAbsent(page, id -> {
List<Identifier> shuffled = new ArrayList<>(fluidPool);
Collections.shuffle(shuffled, new Random(mix(seed ^ (long) id.hashCode()
* 0x9E3779B97F4A7C15L)));
return shuffled;
});
choices.add(new PortLakePlan.Choice(page,
pending != null ? pending : fluids.get(occurrence % fluids.size())));
pending = null;
} else pending = null;
}
return new PortLakePlan(choices);
}
public static PortVegetationPlan vegetationPlan(List<Identifier> pages) {
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
List<Identifier> vegetation = pages.stream().filter(PortVegetationPlan::isVegetation).toList();
return vegetation.isEmpty() ? null : new PortVegetationPlan(vegetation);
}
public static int dungeonCount(List<Identifier> pages) {
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
return (int) pages.stream().filter(DUNGEONS::equals).count();
}
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
/** Resolves all authored biomes from the active registry before creating a level. */
@@ -121,8 +257,15 @@ public final class PortAuthoredWriting {
List<Identifier> biomes = new ArrayList<>();
PortBiomePlan.Distribution distribution = null;
boolean caveTerrain = pages.contains(CAVE_TERRAIN);
boolean hasSurfaceBiome = false;
boolean hasOreBiome = false;
for (Identifier page : pages) {
if (isTerrain(page) || page.equals(ROCKS) || isBlockPage(page)) continue;
if (isTerrain(page) || page.equals(ROCKS) || page.equals(DUNGEONS)
|| page.equals(PortTerrainFeaturePlan.SKYLANDS)
|| page.equals(PortTerrainFeaturePlan.DENSE_ORES)
|| page.equals(PortTerrainFeaturePlan.STAR_FISSURES)
|| PortLakePlan.isLake(page) || PortTerrainFeaturePlan.isSupported(page) || isBlockPage(page)
|| PortVegetationPlan.isVegetation(page)) continue;
PortBiomePlan.Distribution selected = controller(page);
if (selected != null) { distribution = selected; continue; }
Identifier biome = PortRegistryPages.biomeId(page);
@@ -131,8 +274,16 @@ public final class PortAuthoredWriting {
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + biome));
if (caveTerrain && !PortRegistryPages.isCaveBiome(holder))
throw new IllegalArgumentException("Cave terrain requires a Cave or Nether biome: " + biome);
if (!PortRegistryPages.isCaveBiome(holder) && !holder.is(BiomeTags.IS_END))
hasSurfaceBiome = true;
if (net.dnorberg.rivencraft.age.PortDenseOres.hasOres(holder)) hasOreBiome = true;
biomes.add(biome);
}
if (pages.stream().anyMatch(PortVegetationPlan::isVegetation)
&& !biomes.isEmpty() && !hasSurfaceBiome)
throw new IllegalArgumentException("Vegetation pages need at least one surface Overworld biome");
if (pages.contains(PortTerrainFeaturePlan.DENSE_ORES) && !biomes.isEmpty() && !hasOreBiome)
throw new IllegalArgumentException("Dense Ores needs at least one biome with registered ore placements");
if (distribution == PortBiomePlan.Distribution.NATIVE)
return new PortBiomePlan(distribution, List.of());
if (biomes.isEmpty()) return null;
@@ -76,13 +76,27 @@ public final class PortRegistryPages {
public static List<ItemStack> materialPages(boolean fluids) {
String category = fluids ? "rivencraft:fluid" : "rivencraft:block";
return BuiltInRegistries.BLOCK.keySet().stream().sorted()
.filter(id -> category.equals(materialCategory(BuiltInRegistries.BLOCK.getValue(id))))
return materialIds(category).stream()
.map(id -> PageItem.createSymbolPage(blockPageId(id))).toList();
}
public static boolean isCaveBiome(net.minecraft.core.Holder.Reference<net.minecraft.world.level.biome.Biome> holder) {
String path = holder.key().identifier().getPath();
/** The same full-block pool used by Creative pages, in stable registry-ID order. */
public static List<Identifier> solidMaterialIds() {
return materialIds("rivencraft:block");
}
public static List<Identifier> fluidMaterialIds() {
return materialIds("rivencraft:fluid");
}
private static List<Identifier> materialIds(String category) {
return BuiltInRegistries.BLOCK.keySet().stream().sorted()
.filter(id -> category.equals(materialCategory(BuiltInRegistries.BLOCK.getValue(id))))
.toList();
}
public static boolean isCaveBiome(net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome> holder) {
String path = holder.unwrapKey().map(key -> key.identifier().getPath()).orElse("");
return holder.is(CAVE_BIOMES) || holder.is(BiomeTags.IS_NETHER)
|| path.equals("deep_dark") || path.endsWith("_caves");
}
@@ -0,0 +1,5 @@
{
"linking.link-fissure": {
"sounds": [{"name": "rivencraft:linking/link-fissure"}]
}
}
@@ -0,0 +1,15 @@
{
"feature": "minecraft:monster_room",
"placement": [
{ "type": "minecraft:count", "count": 8 },
{ "type": "minecraft:in_square" },
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:uniform",
"min_inclusive": { "above_bottom": 1 },
"max_inclusive": { "below_top": 1 }
}
}
]
}