diff --git a/docs/AGE_PROFILES.md b/docs/AGE_PROFILES.md index dbb0443..b151916 100644 --- a/docs/AGE_PROFILES.md +++ b/docs/AGE_PROFILES.md @@ -65,6 +65,31 @@ writing. Each unpaired material gets its own compatible feature, and repeated decorations use separate placement rolls. Repeated unmodified feature pages choose different compatible materials until their material pool is exhausted. +Structure pages are separate Feature symbols. Legacy's Dungeons, Mineshafts, +Nether Fortresses, Strongholds, and Villages have writable pages, as do modern +Trial Chambers, Bastion Remnants, Buried Treasure, Shipwrecks, Ocean Ruins, +Pillager Outposts, Ruined Portals, Nether Fossils, Ancient Cities, and Trail +Ruins. They use Minecraft's 1.21.1 structure templates and placement rules, +not a recreation of 1.12.2 structure geometry. A page enables only its own +structure set; absent pages do not inherit these structures from a biome. +Nether Fortresses and Bastions use separate sets instead of vanilla's shared +Nether-complexes set, so writing one does not enable the other. Dungeons use +the vanilla monster-room feature with eight attempts per chunk; unpaged Ages +do not inherit biome-default monster-room attempts. A page-free random Age +may choose zero to two distinct compatible structure types, frozen on creation. +An authored structure needs a compatible biome in the Age's selected pool. +Fortresses, Bastions, and Nether Fossils require Cave terrain plus a Nether +biome; surface-only structures reject Cave terrain. Void rejects all of these +structure pages. Villages and Strongholds can also be written into Flat Ages. + +For visual testing, write a Standard/Plains/Single Age with Mineshafts, +Villages, and Trial Chambers, then use `/locate structure` in that Age to find +each. An unwritten Pillager Outpost should not locate there. Test a Cave Age +with Nether Wastes/Single and either the Nether Fortresses or Bastion page: +only the selected type should locate. Ocean structures need their matching +ocean or beach biome page; Ancient Cities need Deep Dark. Check the new pages +in Rivencraft's Feature creative tab and JEI as well. + For an in-game visual check, bind Descriptive Books with a Link Panel, one of Standard, Amplified, Sky Islands, Flat, or Void terrain, and the Floating Islands feature page in that order. Fly through each Age at roughly Y=170–220 and diff --git a/docs/LEGACY_AGE_GRAMMAR_AUDIT.md b/docs/LEGACY_AGE_GRAMMAR_AUDIT.md index 955fdbf..2527f14 100644 --- a/docs/LEGACY_AGE_GRAMMAR_AUDIT.md +++ b/docs/LEGACY_AGE_GRAMMAR_AUDIT.md @@ -18,7 +18,7 @@ Legacy does **not** reject a book just because it omits terrain, biome, weather, | `WeatherNorm`, `Fast`, `Slow`, `Off`, `On`, `Cloudy`, `Rain`, `Snow`, `Storm` | `AgeTraits.Weather` has matching names except Legacy `On` maps to `ALWAYS` | Compile weather semantics separately from biome selection. `Snow` must remain deferred until optional season integration; do not silently substitute another weather mode. | | Suns, moons, starfields; angle, phase, period, color/gradient/sunset modifiers | One `celestialPeriod`, `celestialAngle`, `celestialPhase` on `AgeTraits` | Compile only the representable single-cycle subset first. Multiple celestial bodies, independent periods, custom stars, gradients and sunsets need a richer definition and renderer, not flattening into one value. | | Sky/fog/cloud/grass/foliage/water colors, natural variants, horizon, rainbow | Existing sky/dimension visuals and preview | Require an explicit modern client/biome-color design; metadata-only symbol pages must not pretend to change rendering. | -| Caves, ores, huge trees, lakes, floating islands, tendrils, skylands, spheres, spikes, crystals; vanilla villages, strongholds, mineshafts, dungeons, fortresses, obelisks, fissures | Current cave profile, vanilla data-pack generation, and authored Floating Islands/Tendrils/Skylands/Spheres/Spikes | Compile supported vanilla placements and port bespoke shapes explicitly. Remaining bespoke structures and polish stay in later issues. | +| Caves, ores, huge trees, lakes, floating islands, tendrils, skylands, spheres, spikes, crystals; vanilla villages, strongholds, mineshafts, dungeons, fortresses, obelisks, fissures | Current cave profile, authored Floating Islands/Tendrils/Skylands/Spheres/Spikes, and page-gated vanilla Dungeons/Mineshafts/Nether Fortresses/Strongholds/Villages | The five Legacy vanilla structure symbols now gate Minecraft 1.21.1 generation. Dungeons use monster-room placement; the others use vanilla structure sets. Bespoke obelisks, fissures, crystals, and exact 1.12 structure geometry remain separate work. | | `EnvAccel`, explosions, lightning, meteors, scorching; PvP and dummy suppression pages | No equivalent Age-definition effect | Defer environmental hazards/instability effects to milestone 6, PvP rule to a separately scoped server policy; retain authored IDs without fake behavior. | The Legacy symbol list and page names come from [ModSymbols](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/ModSymbols.java#L129-L240) and [SymbolRules](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/SymbolRules.java#L19-L137). Rivencraft's [catalog](../src/main/java/net/dnorberg/rivencraft/writing/SymbolCatalog.java) currently holds display metadata only; its built-in set is just standard terrain, native biomes, normal weather, normal lighting, normal time, and caves. [OrderedWriting](../src/main/java/net/dnorberg/rivencraft/writing/OrderedWriting.java) preserves page order, but no compiler yet connects it to [AgeTraits](../src/main/java/net/dnorberg/rivencraft/book/AgeTraits.java) or [Age profiles](AGE_PROFILES.md). diff --git a/gradle.properties b/gradle.properties index adf49ca..7532417 100644 --- a/gradle.properties +++ b/gradle.properties @@ -36,7 +36,7 @@ mod_name=Rivencraft # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=LGPL-3.0-or-later # The mod version. Rivencraft uses the x.y.z.b policy documented in README.md. -mod_version=0.4.14.0 +mod_version=0.4.15.0 immersive_portals_version=6.0.7 terrablender_version=4.1.0.8 serene_seasons_version_id=B6nTjh4b diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java b/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java index 23118be..2c2d2e7 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java @@ -329,7 +329,8 @@ public final class AgeDimensionService { Registries.NOISE_SETTINGS, profile.noiseSettings()); NoiseGeneratorSettings baseSettings = noiseSettings.getOrThrow(settingsKey).value(); Holder settings = Holder.direct(withBiomeMaterialRules(baseSettings)); - return features.isEmpty() ? new NoiseBasedChunkGenerator(biomeSource, settings) + return age.generationVersion() < AgeDefinition.CURRENT_GENERATION_VERSION + ? new NoiseBasedChunkGenerator(biomeSource, settings) : new AgeFeatureChunkGenerator(biomeSource, settings, features, age.seed()); } diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeFeatureSelection.java b/src/main/java/net/dnorberg/rivencraft/book/AgeFeatureSelection.java index 5741b09..ad2a24a 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgeFeatureSelection.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeFeatureSelection.java @@ -10,6 +10,7 @@ import java.util.Map; import java.util.Optional; import java.util.Random; import net.dnorberg.rivencraft.writing.BlockSymbolPages; +import net.dnorberg.rivencraft.writing.OrderedWriting; import net.dnorberg.rivencraft.writing.SymbolSemantics; import net.dnorberg.rivencraft.writing.WritingGrammar; import net.minecraft.core.Holder; @@ -49,6 +50,8 @@ public final class AgeFeatureSelection { .map(symbol -> symbol.semantics()).toList(), profile, ageSeed); for (Choice choice : choices) { String error = error(choice.id(), profile, registries); + if (error == null && AgeStructureSelection.feature(choice.id())) + error = AgeStructureSelection.error(choice.id(), profile, biomes, registries); if (error == null) error = materialError(choice); if (error != null) throw new IllegalArgumentException(error); if (surfaceVegetation(choice.id()) && biomes.biomes().stream() @@ -163,6 +166,9 @@ public final class AgeFeatureSelection { if (profile.terrain() == AgeRuntimeProfile.Terrain.NOISE && Math.floorMod(mix(ageSeed ^ RANDOM_SKYLANDS_SALT), 8) == 0) choices.add(new Choice(id("skylands"), Optional.empty())); + AgeBiomeSelection biomes = AgeBiomeSelection.compile(WritingGrammar.parse(OrderedWriting.EMPTY), + AgeTraits.select(ageSeed), profile, ageSeed, registries); + choices.addAll(AgeStructureSelection.randomChoices(ageSeed, profile, biomes, registries)); return List.copyOf(choices); } @@ -187,6 +193,8 @@ public final class AgeFeatureSelection { } public static String error(ResourceLocation choice, AgeRuntimeProfile profile, RegistryAccess registries) { + if (AgeStructureSelection.feature(choice)) + return AgeStructureSelection.error(choice, profile, null, registries); if (skylands(choice)) return null; if (tendrils(choice)) return null; if (spheres(choice)) return null; @@ -313,6 +321,7 @@ public final class AgeFeatureSelection { case "trees" -> ResourceLocation.withDefaultNamespace("trees_plains"); case "huge_trees" -> ResourceLocation.withDefaultNamespace("trees_old_growth_spruce_taiga"); case "rocks" -> ResourceLocation.withDefaultNamespace("forest_rock"); + case "dungeons" -> ResourceLocation.fromNamespaceAndPath("rivencraft", "dungeons"); case "surface_lakes", "lava_lakes" -> ResourceLocation.withDefaultNamespace("lake_lava_surface"); case "deep_lakes" -> ResourceLocation.withDefaultNamespace("lake_lava_underground"); default -> null; diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgePersistenceGameTests.java b/src/main/java/net/dnorberg/rivencraft/book/AgePersistenceGameTests.java index bfa9728..2d76122 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgePersistenceGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgePersistenceGameTests.java @@ -74,12 +74,16 @@ public final class AgePersistenceGameTests { helper.assertTrue(age.randomFeatures().stream().filter(choice -> AgeFeatureSelection.skylands(choice.id())).count() <= 1, "A random Age may roll at most one Skylands filter"); + helper.assertTrue(age.randomFeatures().stream().filter(choice -> + AgeStructureSelection.feature(choice.id())).count() <= 2, + "A random Age may roll at most two distinct structure features"); for (AgeFeatureSelection.Choice choice : age.randomFeatures()) { helper.assertTrue(AgeFeatureSelection.skylands(choice.id()) + || AgeStructureSelection.feature(choice.id()) ? choice.material().isEmpty() : choice.material().isPresent() && AgeFeatureSelection.materialError(choice) == null, - "Random material features, including Spheres and Spikes, need valid blocks or fluids; Skylands needs none"); + "Random material features need valid blocks or fluids; Skylands and structures need none"); } } AgeRegistrySavedData registry = AgeRegistrySavedData.get(helper.getLevel()); diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeStructureGameTests.java b/src/main/java/net/dnorberg/rivencraft/book/AgeStructureGameTests.java new file mode 100644 index 0000000..dddf71f --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeStructureGameTests.java @@ -0,0 +1,100 @@ +package net.dnorberg.rivencraft.book; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import net.dnorberg.rivencraft.Rivencraft; +import net.dnorberg.rivencraft.writing.OrderedWriting; +import net.dnorberg.rivencraft.writing.SymbolReference; +import net.dnorberg.rivencraft.writing.WritingEntry; +import net.minecraft.core.registries.Registries; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.levelgen.structure.BuiltinStructureSets; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; + +/** Authored structure pages gate their own structure sets without enabling unrelated structures. */ +@GameTestHolder(Rivencraft.MOD_ID) +@PrefixGameTestTemplate(false) +public final class AgeStructureGameTests { + private static final Map SETS = Map.ofEntries( + Map.entry("mineshafts", BuiltinStructureSets.MINESHAFTS.location()), + Map.entry("nether_fortresses", id("fortresses")), + Map.entry("strongholds", BuiltinStructureSets.STRONGHOLDS.location()), + Map.entry("villages", BuiltinStructureSets.VILLAGES.location()), + Map.entry("trial_chambers", BuiltinStructureSets.TRIAL_CHAMBERS.location()), + Map.entry("bastions", id("bastions")), + Map.entry("buried_treasure", BuiltinStructureSets.BURIED_TREASURES.location()), + Map.entry("shipwrecks", BuiltinStructureSets.SHIPWRECKS.location()), + Map.entry("ocean_ruins", BuiltinStructureSets.OCEAN_RUINS.location()), + Map.entry("pillager_outposts", BuiltinStructureSets.PILLAGER_OUTPOSTS.location()), + Map.entry("ruined_portals", BuiltinStructureSets.RUINED_PORTALS.location()), + Map.entry("nether_fossils", BuiltinStructureSets.NETHER_FOSSILS.location()), + Map.entry("ancient_cities", BuiltinStructureSets.ANCIENT_CITIES.location()), + Map.entry("trail_ruins", BuiltinStructureSets.TRAIL_RUINS.location())); + + private AgeStructureGameTests() {} + + @GameTest(template = "empty") + public static void structurePagesGateDistinctVanillaSets(GameTestHelper helper) { + var lookup = helper.getLevel().registryAccess().lookupOrThrow(Registries.STRUCTURE_SET); + for (var entry : SETS.entrySet()) { + var setKey = ResourceKey.create(Registries.STRUCTURE_SET, entry.getValue()); + helper.assertTrue(lookup.get(setKey).isPresent(), entry.getKey() + " needs a registered structure set"); + var filtered = AgeStructureSelection.filteredLookup(lookup, + List.of(new AgeFeatureSelection.Choice(id(entry.getKey()), Optional.empty()))); + helper.assertTrue(filtered.get(setKey).isPresent(), entry.getKey() + " must enable its structure set"); + helper.assertTrue(filtered.get(ResourceKey.create(Registries.STRUCTURE_SET, + BuiltinStructureSets.NETHER_COMPLEXES.location())).isEmpty(), + "Vanilla Nether complexes must stay disabled to keep fortresses and bastions separate"); + for (var other : SETS.entrySet()) { + if (other.getKey().equals(entry.getKey())) continue; + helper.assertTrue(filtered.get(ResourceKey.create(Registries.STRUCTURE_SET, + other.getValue())).isEmpty(), + entry.getKey() + " must not enable " + other.getKey()); + } + } + helper.assertTrue(AgeStructureSelection.filteredLookup(lookup, List.of()).listElements() + .noneMatch(holder -> SETS.containsValue(holder.key().location())), + "An Age without structure pages must not inherit these structures from vanilla biomes"); + helper.succeed(); + } + + @GameTest(template = "empty") + public static void authoredPagesPersistAndRejectIncompatibleTerrain(GameTestHelper helper) { + var registries = helper.getLevel().registryAccess(); + OrderedWriting written = new OrderedWriting(List.of(WritingEntry.linkPanel(), page("terrain/standard"), + page("biome/plains"), page("biome/single"), page("feature/mineshafts"), + page("feature/villages"), page("feature/trial_chambers"))); + AgeGenerationPlan plan = AgeGenerationPlan.compile(written, 149L, AgeGenerationProfiles.STANDARD, + AgeTraits.select(149L), registries); + List expected = AgeFeatureSelection.resolved(plan); + helper.assertValueEqual(expected.stream().map(AgeFeatureSelection.Choice::id).toList(), + List.of(id("mineshafts"), id("villages"), id("trial_chambers")), + "Authored structure pages must retain their order"); + helper.assertValueEqual(AgeFeatureSelection.resolved(AgeGenerationPlan.load(plan.save())), expected, + "Authored structure choices must survive an Age plan reload"); + var filtered = AgeStructureSelection.filteredLookup(registries.lookupOrThrow(Registries.STRUCTURE_SET), expected); + helper.assertTrue(filtered.get(ResourceKey.create(Registries.STRUCTURE_SET, + BuiltinStructureSets.VILLAGES.location())).isPresent(), "Village set must be enabled"); + helper.assertTrue(filtered.get(ResourceKey.create(Registries.STRUCTURE_SET, + BuiltinStructureSets.PILLAGER_OUTPOSTS.location())).isEmpty(), "Unwritten outposts must stay disabled"); + String error = AgeStructureSelection.error(id("bastions"), + AgeRuntimeProfiles.profiles().get(id("standard")), null, registries); + helper.assertTrue(error != null && error.contains("Cave terrain"), + "Bastion page must not silently run on Standard terrain"); + helper.succeed(); + } + + private static ResourceLocation id(String path) { + return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, path); + } + + private static WritingEntry page(String path) { + return WritingEntry.symbol(new SymbolReference(id(path))); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeStructureSelection.java b/src/main/java/net/dnorberg/rivencraft/book/AgeStructureSelection.java new file mode 100644 index 0000000..aa6cd7a --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeStructureSelection.java @@ -0,0 +1,169 @@ +package net.dnorberg.rivencraft.book; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.Set; +import java.util.stream.Stream; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.HolderSet; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.levelgen.structure.BuiltinStructureSets; +import net.minecraft.world.level.levelgen.structure.StructureSet; + +/** Frozen Age structure-page choices and their vanilla structure-set gate. */ +public final class AgeStructureSelection { + private static final ResourceLocation FORTRESSES = ResourceLocation.fromNamespaceAndPath("rivencraft", "fortresses"); + private static final ResourceLocation BASTIONS = ResourceLocation.fromNamespaceAndPath("rivencraft", "bastions"); + private static final List FEATURES = List.of( + id("dungeons"), id("mineshafts"), id("nether_fortresses"), id("strongholds"), + id("villages"), id("trial_chambers"), id("bastions"), id("buried_treasure"), + id("shipwrecks"), id("ocean_ruins"), id("pillager_outposts"), id("ruined_portals"), + id("nether_fossils"), id("ancient_cities"), id("trail_ruins")); + private static final Map STRUCTURE_SETS = Map.ofEntries( + Map.entry(id("mineshafts"), BuiltinStructureSets.MINESHAFTS.location()), + Map.entry(id("nether_fortresses"), FORTRESSES), + Map.entry(id("strongholds"), BuiltinStructureSets.STRONGHOLDS.location()), + Map.entry(id("villages"), BuiltinStructureSets.VILLAGES.location()), + Map.entry(id("trial_chambers"), BuiltinStructureSets.TRIAL_CHAMBERS.location()), + Map.entry(id("bastions"), BASTIONS), + Map.entry(id("buried_treasure"), BuiltinStructureSets.BURIED_TREASURES.location()), + Map.entry(id("shipwrecks"), BuiltinStructureSets.SHIPWRECKS.location()), + Map.entry(id("ocean_ruins"), BuiltinStructureSets.OCEAN_RUINS.location()), + Map.entry(id("pillager_outposts"), BuiltinStructureSets.PILLAGER_OUTPOSTS.location()), + Map.entry(id("ruined_portals"), BuiltinStructureSets.RUINED_PORTALS.location()), + Map.entry(id("nether_fossils"), BuiltinStructureSets.NETHER_FOSSILS.location()), + Map.entry(id("ancient_cities"), BuiltinStructureSets.ANCIENT_CITIES.location()), + Map.entry(id("trail_ruins"), BuiltinStructureSets.TRAIL_RUINS.location())); + private static final Set CONTROLLED_SETS = Set.of( + BuiltinStructureSets.MINESHAFTS.location(), BuiltinStructureSets.NETHER_COMPLEXES.location(), + BuiltinStructureSets.STRONGHOLDS.location(), BuiltinStructureSets.VILLAGES.location(), + BuiltinStructureSets.TRIAL_CHAMBERS.location(), FORTRESSES, BASTIONS, + BuiltinStructureSets.BURIED_TREASURES.location(), BuiltinStructureSets.SHIPWRECKS.location(), + BuiltinStructureSets.OCEAN_RUINS.location(), BuiltinStructureSets.PILLAGER_OUTPOSTS.location(), + BuiltinStructureSets.RUINED_PORTALS.location(), BuiltinStructureSets.NETHER_FOSSILS.location(), + BuiltinStructureSets.ANCIENT_CITIES.location(), BuiltinStructureSets.TRAIL_RUINS.location()); + + private AgeStructureSelection() {} + + private static ResourceLocation id(String path) { + return ResourceLocation.fromNamespaceAndPath("rivencraft", path); + } + + public static boolean feature(ResourceLocation id) { + return FEATURES.contains(id); + } + + public static boolean dungeons(ResourceLocation id) { + return id.equals(id("dungeons")); + } + + private static ResourceLocation setId(ResourceLocation feature) { + return STRUCTURE_SETS.get(feature); + } + + public static String error(ResourceLocation feature, AgeRuntimeProfile profile, + AgeBiomeSelection biomes, RegistryAccess registries) { + if (!feature(feature)) return "Unsupported structure feature " + feature; + if (profile.terrain() == AgeRuntimeProfile.Terrain.VOID) + return "Feature " + feature + " requires terrain, not Void"; + if (dungeons(feature)) { + if (profile.terrain() != AgeRuntimeProfile.Terrain.NOISE) + return "Feature " + feature + " needs enclosed noise terrain, not Flat"; + return placedExists(registries, id("dungeons")) ? null + : "Feature " + feature + " requires missing placed feature rivencraft:dungeons"; + } + if (!feature.equals(id("villages")) && !feature.equals(id("strongholds")) + && profile.terrain() != AgeRuntimeProfile.Terrain.NOISE) + return "Feature " + feature + " needs noise terrain, not Flat"; + if (feature.equals(id("nether_fortresses")) || feature.equals(id("bastions")) + || feature.equals(id("nether_fossils"))) { + if (!AgeBiomeSelection.isCave(profile)) + return "Feature " + feature + " requires Cave terrain with a Nether biome"; + } else if (feature.equals(id("villages")) || feature.equals(id("strongholds")) + || feature.equals(id("buried_treasure")) || feature.equals(id("shipwrecks")) + || feature.equals(id("ocean_ruins")) || feature.equals(id("pillager_outposts")) + || feature.equals(id("trail_ruins"))) { + if (AgeBiomeSelection.isCave(profile)) + return "Feature " + feature + " requires surface terrain, not Cave"; + } + ResourceLocation setId = setId(feature); + var set = registries.lookupOrThrow(Registries.STRUCTURE_SET) + .get(ResourceKey.create(Registries.STRUCTURE_SET, setId)); + if (set.isEmpty()) return "Feature " + feature + " requires missing structure set " + setId; + if (biomes == null) return null; + Set selected = Set.copyOf(biomes.biomes()); + boolean compatible = set.orElseThrow().value().structures().stream() + .flatMap(entry -> entry.structure().value().biomes().stream()) + .map(holder -> holder.unwrapKey().map(ResourceKey::location).orElse(null)) + .anyMatch(selected::contains); + return compatible ? null : "Feature " + feature + + " needs at least one biome allowed by structure set " + setId; + } + + private static boolean placedExists(RegistryAccess registries, ResourceLocation id) { + return registries.lookupOrThrow(Registries.PLACED_FEATURE) + .get(ResourceKey.create(Registries.PLACED_FEATURE, id)).isPresent(); + } + + public static List randomChoices(long ageSeed, AgeRuntimeProfile profile, + AgeBiomeSelection biomes, RegistryAccess registries) { + List eligible = new ArrayList<>(FEATURES.stream() + .filter(id -> error(id, profile, biomes, registries) == null).toList()); + Random random = new Random(ageSeed ^ 0x5354525543545552L); + Collections.shuffle(eligible, random); + int count = random.nextInt(3); + return eligible.stream().limit(count) + .map(id -> new AgeFeatureSelection.Choice(id, Optional.empty())).toList(); + } + + private static Set enabledSets(List choices) { + return choices.stream().filter(choice -> feature(choice.id()) && !dungeons(choice.id())) + .map(choice -> setId(choice.id())).collect(java.util.stream.Collectors.toUnmodifiableSet()); + } + + public static Stream> allowedSets(Stream> sets, + List choices) { + Set enabled = enabledSets(choices); + return sets.filter(holder -> holder.unwrapKey() + .map(key -> allowed(key.location(), enabled)).orElse(false)); + } + + public static HolderLookup filteredLookup(HolderLookup lookup, + List choices) { + Set enabled = enabledSets(choices); + return new HolderLookup<>() { + @Override + public Optional> get(ResourceKey key) { + return allowed(key.location(), enabled) ? lookup.get(key) : Optional.empty(); + } + + @Override + public Optional> get(TagKey key) { + return lookup.get(key); + } + + @Override + public Stream> listElements() { + return lookup.listElements().filter(holder -> allowed(holder.key().location(), enabled)); + } + + @Override + public Stream> listTags() { + return lookup.listTags(); + } + }; + } + + private static boolean allowed(ResourceLocation set, Set enabled) { + return !CONTROLLED_SETS.contains(set) || enabled.contains(set); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/book/FeatureWritingGameTests.java b/src/main/java/net/dnorberg/rivencraft/book/FeatureWritingGameTests.java index 222f7f0..c205eb8 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/FeatureWritingGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/book/FeatureWritingGameTests.java @@ -33,7 +33,12 @@ public final class FeatureWritingGameTests { "feature/huge_trees", "feature/rocks", "feature/lava_lakes", "feature/surface_lakes", "feature/deep_lakes", "feature/ravines", "feature/floating_islands", "feature/tendrils", "feature/skylands", - "feature/spheres", "feature/spikes"); + "feature/spheres", "feature/spikes", "feature/dungeons", + "feature/mineshafts", "feature/nether_fortresses", + "feature/strongholds", "feature/villages", "feature/trial_chambers", + "feature/bastions", "feature/buried_treasure", "feature/shipwrecks", + "feature/ocean_ruins", "feature/pillager_outposts", "feature/ruined_portals", + "feature/nether_fossils", "feature/ancient_cities", "feature/trail_ruins"); helper.assertTrue(PageItem.bundledCreativeSymbols().containsAll(pages), "Supported feature pages must appear in the creative and JEI symbol feed"); for (String path : pages) { @@ -176,7 +181,8 @@ public final class FeatureWritingGameTests { List random = AgeFeatureSelection.randomChoices(seed, caveProfile, registries); helper.assertTrue(random.stream().allMatch(choice -> AgeFeatureSelection.tendrils(choice.id()) || AgeFeatureSelection.spheres(choice.id()) - || AgeFeatureSelection.skylands(choice.id())), + || AgeFeatureSelection.skylands(choice.id()) + || AgeStructureSelection.feature(choice.id())), "Cave random Ages must not choose incompatible surface features"); if (random.stream().anyMatch(choice -> AgeFeatureSelection.tendrils(choice.id()))) found = true; } diff --git a/src/main/java/net/dnorberg/rivencraft/mixin/AgeDungeonPlacementMixin.java b/src/main/java/net/dnorberg/rivencraft/mixin/AgeDungeonPlacementMixin.java new file mode 100644 index 0000000..ede826f --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/mixin/AgeDungeonPlacementMixin.java @@ -0,0 +1,30 @@ +package net.dnorberg.rivencraft.mixin; + +import net.dnorberg.rivencraft.world.AgeFeatureChunkGenerator; +import net.dnorberg.rivencraft.world.AgeFlatChunkGenerator; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** Suppresses biome-default monster rooms only in new Rivencraft Ages. */ +@Mixin(ChunkGenerator.class) +public abstract class AgeDungeonPlacementMixin { + @Redirect(method = "applyBiomeDecoration", at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/levelgen/placement/PlacedFeature;placeWithBiomeCheck(Lnet/minecraft/world/level/WorldGenLevel;Lnet/minecraft/world/level/chunk/ChunkGenerator;Lnet/minecraft/util/RandomSource;Lnet/minecraft/core/BlockPos;)Z")) + private boolean rivencraft$gateAmbientDungeons(PlacedFeature feature, WorldGenLevel level, + ChunkGenerator generator, RandomSource random, BlockPos pos) { + if ((Object) this instanceof AgeFeatureChunkGenerator || (Object) this instanceof AgeFlatChunkGenerator) { + ResourceLocation id = level.registryAccess().registryOrThrow(Registries.PLACED_FEATURE).getKey(feature); + if (ResourceLocation.withDefaultNamespace("monster_room").equals(id) + || ResourceLocation.withDefaultNamespace("monster_room_deep").equals(id)) return false; + } + return feature.placeWithBiomeCheck(level, generator, random, pos); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/world/AgeFeatureChunkGenerator.java b/src/main/java/net/dnorberg/rivencraft/world/AgeFeatureChunkGenerator.java index 812865c..1d8abb4 100644 --- a/src/main/java/net/dnorberg/rivencraft/world/AgeFeatureChunkGenerator.java +++ b/src/main/java/net/dnorberg/rivencraft/world/AgeFeatureChunkGenerator.java @@ -8,6 +8,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import net.dnorberg.rivencraft.book.AgeFeatureSelection; +import net.dnorberg.rivencraft.book.AgeStructureSelection; +import net.minecraft.core.HolderLookup; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; @@ -25,6 +27,7 @@ import net.minecraft.world.level.biome.BiomeManager; import net.minecraft.world.level.biome.BiomeSource; 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.chunk.ProtoChunk; import net.minecraft.world.level.levelgen.GenerationStep; import net.minecraft.world.level.levelgen.LegacyRandomSource; @@ -37,6 +40,7 @@ import net.minecraft.world.level.levelgen.carver.CarvingContext; import net.minecraft.world.level.levelgen.carver.ConfiguredWorldCarver; import net.minecraft.world.level.levelgen.placement.BiomeFilter; import net.minecraft.world.level.levelgen.placement.PlacedFeature; +import net.minecraft.world.level.levelgen.structure.StructureSet; import net.minecraft.world.level.levelgen.placement.RarityFilter; import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; import net.minecraft.world.level.levelgen.feature.Feature; @@ -71,6 +75,12 @@ public final class AgeFeatureChunkGenerator extends NoiseBasedChunkGenerator { @Override protected MapCodec codec() { return CODEC; } + @Override + public ChunkGeneratorStructureState createState(HolderLookup sets, RandomState random, long seed) { + return ChunkGeneratorStructureState.createForNormal(random, seed, getBiomeSource(), + AgeStructureSelection.filteredLookup(sets, features)); + } + @Override public CompletableFuture fillFromNoise(Blender blender, RandomState random, StructureManager structures, ChunkAccess chunk) { diff --git a/src/main/java/net/dnorberg/rivencraft/world/AgeFlatChunkGenerator.java b/src/main/java/net/dnorberg/rivencraft/world/AgeFlatChunkGenerator.java index 9afa48d..aad8ae2 100644 --- a/src/main/java/net/dnorberg/rivencraft/world/AgeFlatChunkGenerator.java +++ b/src/main/java/net/dnorberg/rivencraft/world/AgeFlatChunkGenerator.java @@ -9,6 +9,7 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; import net.dnorberg.rivencraft.book.AgeFeatureSelection; +import net.dnorberg.rivencraft.book.AgeStructureSelection; import net.minecraft.Util; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; @@ -71,7 +72,8 @@ public final class AgeFlatChunkGenerator extends ChunkGenerator { Stream> entries = settings.structureOverrides() .map(HolderSet::stream) .orElseGet(() -> structures.listElements().map(holder -> (Holder) holder)); - return ChunkGeneratorStructureState.createForFlat(random, seed, biomeSource, entries); + return ChunkGeneratorStructureState.createForFlat(random, seed, biomeSource, + AgeStructureSelection.allowedSets(entries, features)); } @Override diff --git a/src/main/resources/assets/rivencraft/lang/en_us.json b/src/main/resources/assets/rivencraft/lang/en_us.json index d8cfdfb..e64c563 100644 --- a/src/main/resources/assets/rivencraft/lang/en_us.json +++ b/src/main/resources/assets/rivencraft/lang/en_us.json @@ -253,6 +253,36 @@ "symbol.rivencraft.feature.spheres.description": "Add wandering chains of solid spheres through the Age, with a random or selected block material.", "symbol.rivencraft.feature.spikes": "Spikes", "symbol.rivencraft.feature.spikes.description": "Add rare rising spikes on exposed solid ground, with a random or selected block material.", + "symbol.rivencraft.feature.dungeons": "Dungeons", + "symbol.rivencraft.feature.dungeons.description": "Add monster-room dungeons below ground in this Age.", + "symbol.rivencraft.feature.mineshafts": "Mineshafts", + "symbol.rivencraft.feature.mineshafts.description": "Allow abandoned mineshafts in compatible terrain and biomes.", + "symbol.rivencraft.feature.nether_fortresses": "Nether Fortresses", + "symbol.rivencraft.feature.nether_fortresses.description": "Allow Nether fortresses in a Cave Age with a compatible Nether biome.", + "symbol.rivencraft.feature.strongholds": "Strongholds", + "symbol.rivencraft.feature.strongholds.description": "Allow strongholds with End portal rooms in a compatible Age.", + "symbol.rivencraft.feature.villages": "Villages", + "symbol.rivencraft.feature.villages.description": "Allow biome-appropriate villages on surface terrain.", + "symbol.rivencraft.feature.trial_chambers": "Trial Chambers", + "symbol.rivencraft.feature.trial_chambers.description": "Allow underground Trial Chambers in compatible Minecraft 1.21.1 biomes.", + "symbol.rivencraft.feature.bastions": "Bastion Remnants", + "symbol.rivencraft.feature.bastions.description": "Allow Bastion Remnants in a Cave Age with a compatible Nether biome.", + "symbol.rivencraft.feature.buried_treasure": "Buried Treasure", + "symbol.rivencraft.feature.buried_treasure.description": "Allow buried treasure beneath compatible beaches.", + "symbol.rivencraft.feature.shipwrecks": "Shipwrecks", + "symbol.rivencraft.feature.shipwrecks.description": "Allow shipwrecks in compatible ocean and beach biomes.", + "symbol.rivencraft.feature.ocean_ruins": "Ocean Ruins", + "symbol.rivencraft.feature.ocean_ruins.description": "Allow warm and cold ocean ruins in compatible biomes.", + "symbol.rivencraft.feature.pillager_outposts": "Pillager Outposts", + "symbol.rivencraft.feature.pillager_outposts.description": "Allow pillager outposts in compatible surface biomes.", + "symbol.rivencraft.feature.ruined_portals": "Ruined Portals", + "symbol.rivencraft.feature.ruined_portals.description": "Allow ruined portals in compatible surface or Nether biomes.", + "symbol.rivencraft.feature.nether_fossils": "Nether Fossils", + "symbol.rivencraft.feature.nether_fossils.description": "Allow Nether fossils in a Cave Age with a compatible Nether biome.", + "symbol.rivencraft.feature.ancient_cities": "Ancient Cities", + "symbol.rivencraft.feature.ancient_cities.description": "Allow ancient cities in compatible Deep Dark biomes.", + "symbol.rivencraft.feature.trail_ruins": "Trail Ruins", + "symbol.rivencraft.feature.trail_ruins.description": "Allow buried trail ruins in compatible surface biomes.", "symbol.rivencraft.feature.lava_lakes": "Small Lakes", "symbol.rivencraft.feature.lava_lakes.description": "Add small surface lakes using a random fluid, or specify one with a fluid page.", "generator.rivencraft.standard": "Standard", diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/feature/ancient_cities.json b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/ancient_cities.json new file mode 100644 index 0000000..be9fa14 --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/ancient_cities.json @@ -0,0 +1,8 @@ +{ + "schema_version": 2, + "name": "symbol.rivencraft.feature.ancient_cities", + "description": "symbol.rivencraft.feature.ancient_cities.description", + "category": "rivencraft:feature", + "semantics": { "role": "feature", "value": "rivencraft:ancient_cities" }, + "poem": "Civilization Darkness Silence Ancient" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/feature/bastions.json b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/bastions.json new file mode 100644 index 0000000..ed1773f --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/bastions.json @@ -0,0 +1,8 @@ +{ + "schema_version": 2, + "name": "symbol.rivencraft.feature.bastions", + "description": "symbol.rivencraft.feature.bastions.description", + "category": "rivencraft:feature", + "semantics": { "role": "feature", "value": "rivencraft:bastions" }, + "poem": "Civilization Ruin Greed Nether" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/feature/buried_treasure.json b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/buried_treasure.json new file mode 100644 index 0000000..a6bdf18 --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/buried_treasure.json @@ -0,0 +1,8 @@ +{ + "schema_version": 2, + "name": "symbol.rivencraft.feature.buried_treasure", + "description": "symbol.rivencraft.feature.buried_treasure.description", + "category": "rivencraft:feature", + "semantics": { "role": "feature", "value": "rivencraft:buried_treasure" }, + "poem": "Treasure Sand Ocean Memory" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/feature/dungeons.json b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/dungeons.json new file mode 100644 index 0000000..3de9ce6 --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/dungeons.json @@ -0,0 +1,8 @@ +{ + "schema_version": 2, + "name": "symbol.rivencraft.feature.dungeons", + "description": "symbol.rivencraft.feature.dungeons.description", + "category": "rivencraft:feature", + "semantics": { "role": "feature", "value": "rivencraft:dungeons" }, + "poem": "Civilization Constraint Chain Resurrect" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/feature/mineshafts.json b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/mineshafts.json new file mode 100644 index 0000000..a92463a --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/mineshafts.json @@ -0,0 +1,8 @@ +{ + "schema_version": 2, + "name": "symbol.rivencraft.feature.mineshafts", + "description": "symbol.rivencraft.feature.mineshafts.description", + "category": "rivencraft:feature", + "semantics": { "role": "feature", "value": "rivencraft:mineshafts" }, + "poem": "Civilization Machine Motion Tradition" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/feature/nether_fortresses.json b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/nether_fortresses.json new file mode 100644 index 0000000..c3784f9 --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/nether_fortresses.json @@ -0,0 +1,8 @@ +{ + "schema_version": 2, + "name": "symbol.rivencraft.feature.nether_fortresses", + "description": "symbol.rivencraft.feature.nether_fortresses.description", + "category": "rivencraft:feature", + "semantics": { "role": "feature", "value": "rivencraft:nether_fortresses" }, + "poem": "Civilization Machine Power Entropy" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/feature/nether_fossils.json b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/nether_fossils.json new file mode 100644 index 0000000..bdc1b03 --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/nether_fossils.json @@ -0,0 +1,8 @@ +{ + "schema_version": 2, + "name": "symbol.rivencraft.feature.nether_fossils", + "description": "symbol.rivencraft.feature.nether_fossils.description", + "category": "rivencraft:feature", + "semantics": { "role": "feature", "value": "rivencraft:nether_fossils" }, + "poem": "Bone Death Nether Time" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/feature/ocean_ruins.json b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/ocean_ruins.json new file mode 100644 index 0000000..e6f0615 --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/ocean_ruins.json @@ -0,0 +1,8 @@ +{ + "schema_version": 2, + "name": "symbol.rivencraft.feature.ocean_ruins", + "description": "symbol.rivencraft.feature.ocean_ruins.description", + "category": "rivencraft:feature", + "semantics": { "role": "feature", "value": "rivencraft:ocean_ruins" }, + "poem": "Ocean Stone Ruin Time" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/feature/pillager_outposts.json b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/pillager_outposts.json new file mode 100644 index 0000000..9b70be6 --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/pillager_outposts.json @@ -0,0 +1,8 @@ +{ + "schema_version": 2, + "name": "symbol.rivencraft.feature.pillager_outposts", + "description": "symbol.rivencraft.feature.pillager_outposts.description", + "category": "rivencraft:feature", + "semantics": { "role": "feature", "value": "rivencraft:pillager_outposts" }, + "poem": "Civilization Conflict Watchtower Danger" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/feature/ruined_portals.json b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/ruined_portals.json new file mode 100644 index 0000000..8b0b420 --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/ruined_portals.json @@ -0,0 +1,8 @@ +{ + "schema_version": 2, + "name": "symbol.rivencraft.feature.ruined_portals", + "description": "symbol.rivencraft.feature.ruined_portals.description", + "category": "rivencraft:feature", + "semantics": { "role": "feature", "value": "rivencraft:ruined_portals" }, + "poem": "Portal Ruin Flame Memory" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/feature/shipwrecks.json b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/shipwrecks.json new file mode 100644 index 0000000..7ed1adf --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/shipwrecks.json @@ -0,0 +1,8 @@ +{ + "schema_version": 2, + "name": "symbol.rivencraft.feature.shipwrecks", + "description": "symbol.rivencraft.feature.shipwrecks.description", + "category": "rivencraft:feature", + "semantics": { "role": "feature", "value": "rivencraft:shipwrecks" }, + "poem": "Ocean Journey Ruin Timber" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/feature/strongholds.json b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/strongholds.json new file mode 100644 index 0000000..253bd8a --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/strongholds.json @@ -0,0 +1,8 @@ +{ + "schema_version": 2, + "name": "symbol.rivencraft.feature.strongholds", + "description": "symbol.rivencraft.feature.strongholds.description", + "category": "rivencraft:feature", + "semantics": { "role": "feature", "value": "rivencraft:strongholds" }, + "poem": "Civilization Wisdom Future Honor" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/feature/trail_ruins.json b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/trail_ruins.json new file mode 100644 index 0000000..b249a3d --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/trail_ruins.json @@ -0,0 +1,8 @@ +{ + "schema_version": 2, + "name": "symbol.rivencraft.feature.trail_ruins", + "description": "symbol.rivencraft.feature.trail_ruins.description", + "category": "rivencraft:feature", + "semantics": { "role": "feature", "value": "rivencraft:trail_ruins" }, + "poem": "Civilization Buried Path Memory" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/feature/trial_chambers.json b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/trial_chambers.json new file mode 100644 index 0000000..a49b30b --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/trial_chambers.json @@ -0,0 +1,8 @@ +{ + "schema_version": 2, + "name": "symbol.rivencraft.feature.trial_chambers", + "description": "symbol.rivencraft.feature.trial_chambers.description", + "category": "rivencraft:feature", + "semantics": { "role": "feature", "value": "rivencraft:trial_chambers" }, + "poem": "Civilization Trial Structure Challenge" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/feature/villages.json b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/villages.json new file mode 100644 index 0000000..309fd77 --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/villages.json @@ -0,0 +1,8 @@ +{ + "schema_version": 2, + "name": "symbol.rivencraft.feature.villages", + "description": "symbol.rivencraft.feature.villages.description", + "category": "rivencraft:feature", + "semantics": { "role": "feature", "value": "rivencraft:villages" }, + "poem": "Civilization Society Harmony Nurture" +} diff --git a/src/main/resources/data/rivencraft/worldgen/placed_feature/dungeons.json b/src/main/resources/data/rivencraft/worldgen/placed_feature/dungeons.json new file mode 100644 index 0000000..c1c8ac1 --- /dev/null +++ b/src/main/resources/data/rivencraft/worldgen/placed_feature/dungeons.json @@ -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 } + } + } + ] +} diff --git a/src/main/resources/data/rivencraft/worldgen/structure_set/bastions.json b/src/main/resources/data/rivencraft/worldgen/structure_set/bastions.json new file mode 100644 index 0000000..61c9812 --- /dev/null +++ b/src/main/resources/data/rivencraft/worldgen/structure_set/bastions.json @@ -0,0 +1,11 @@ +{ + "structures": [ + { "structure": "minecraft:bastion_remnant", "weight": 1 } + ], + "placement": { + "type": "minecraft:random_spread", + "spacing": 27, + "separation": 4, + "salt": 30084233 + } +} diff --git a/src/main/resources/data/rivencraft/worldgen/structure_set/fortresses.json b/src/main/resources/data/rivencraft/worldgen/structure_set/fortresses.json new file mode 100644 index 0000000..b019442 --- /dev/null +++ b/src/main/resources/data/rivencraft/worldgen/structure_set/fortresses.json @@ -0,0 +1,11 @@ +{ + "structures": [ + { "structure": "minecraft:fortress", "weight": 1 } + ], + "placement": { + "type": "minecraft:random_spread", + "spacing": 27, + "separation": 4, + "salt": 30084232 + } +} diff --git a/src/main/resources/rivencraft.mixins.json b/src/main/resources/rivencraft.mixins.json index f785152..c2bb406 100644 --- a/src/main/resources/rivencraft.mixins.json +++ b/src/main/resources/rivencraft.mixins.json @@ -3,6 +3,9 @@ "minVersion": "0.8", "package": "net.dnorberg.rivencraft.mixin", "compatibilityLevel": "JAVA_21", + "mixins": [ + "AgeDungeonPlacementMixin" + ], "client": [ "client.ClientLevelSoundMixin", "client.ImmersivePortalsGuiRenderingMixin",