Public Access
This commit is contained in:
1 parent
98a959f621
commit
a9a6f7205d
10 files changed
+248
-3
No files matched your search
@@ -13,6 +13,7 @@ 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.dnorberg.rivencraft.world.AgeDenseOres;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
@@ -52,6 +53,8 @@ public final class AgeFeatureSelection {
|
||||
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 && choice.id().equals(AgeDenseOres.ID))
|
||||
error = AgeDenseOres.error(profile, biomes, registries);
|
||||
if (error == null) error = materialError(choice);
|
||||
if (error != null) throw new IllegalArgumentException(error);
|
||||
if (surfaceVegetation(choice.id()) && biomes.biomes().stream()
|
||||
@@ -195,6 +198,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 (choice.equals(AgeDenseOres.ID))
|
||||
return AgeDenseOres.error(profile, null, registries);
|
||||
if (skylands(choice)) return null;
|
||||
if (tendrils(choice)) return null;
|
||||
if (spheres(choice)) return null;
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.util.UUID;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.world.AgeFeatureChunkGenerator;
|
||||
import net.dnorberg.rivencraft.world.AgeDenseOres;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.BlockSymbolPages;
|
||||
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
||||
@@ -38,7 +39,8 @@ public final class FeatureWritingGameTests {
|
||||
"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");
|
||||
"feature/nether_fossils", "feature/ancient_cities", "feature/trail_ruins",
|
||||
"feature/dense_ores");
|
||||
helper.assertTrue(PageItem.bundledCreativeSymbols().containsAll(pages),
|
||||
"Supported feature pages must appear in the creative and JEI symbol feed");
|
||||
for (String path : pages) {
|
||||
@@ -69,7 +71,8 @@ public final class FeatureWritingGameTests {
|
||||
|| AgeFeatureSelection.tendrils(choice.id())
|
||||
|| AgeFeatureSelection.skylands(choice.id())
|
||||
|| AgeFeatureSelection.spheres(choice.id())
|
||||
|| AgeFeatureSelection.spikes(choice.id())) continue;
|
||||
|| AgeFeatureSelection.spikes(choice.id())
|
||||
|| choice.id().equals(AgeDenseOres.ID)) continue;
|
||||
var placement = AgeFeatureSelection.placedFeature(choice.id());
|
||||
helper.assertTrue(registries.lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
.get(ResourceKey.create(Registries.PLACED_FEATURE, placement)).isPresent(),
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.book.AgeBiomeSelection;
|
||||
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationPlan;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationProfiles;
|
||||
import net.dnorberg.rivencraft.book.AgeRuntimeProfiles;
|
||||
import net.dnorberg.rivencraft.book.AgeTraits;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
import net.minecraft.world.level.levelgen.feature.configurations.OreConfiguration;
|
||||
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockMatchTest;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
/** Dense Ores remains authored-only and discovers ore configurations without a namespace allow-list. */
|
||||
@GameTestHolder(Rivencraft.MOD_ID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class AgeDenseOreGameTests {
|
||||
private AgeDenseOreGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void denseOresPageCompilesAndPersists(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
helper.assertTrue(PageItem.bundledCreativeSymbols().contains("feature/dense_ores"),
|
||||
"Dense Ores must appear in the Creative and JEI page feed");
|
||||
OrderedWriting writing = new OrderedWriting(List.of(WritingEntry.linkPanel(),
|
||||
page("terrain/standard"), page("biome/plains"), page("biome/single"),
|
||||
page("feature/dense_ores")));
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 55L, AgeGenerationProfiles.STANDARD,
|
||||
AgeTraits.select(55L), registries);
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(plan).stream()
|
||||
.map(AgeFeatureSelection.Choice::id).toList(), List.of(AgeDenseOres.ID),
|
||||
"Dense Ores must be saved as an authored feature");
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(AgeGenerationPlan.load(plan.save())),
|
||||
AgeFeatureSelection.resolved(plan), "Dense Ores must survive Age-plan reload");
|
||||
for (long seed = 0; seed < 32; seed++) {
|
||||
helper.assertTrue(AgeFeatureSelection.randomChoices(seed,
|
||||
AgeRuntimeProfiles.resolve(id("standard")), registries).stream()
|
||||
.noneMatch(choice -> choice.id().equals(AgeDenseOres.ID)),
|
||||
"Random Ages must not roll an authored-only Dense Ores page");
|
||||
}
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void denseOresRejectsUnsuitableTerrainAndBiome(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
helper.assertTrue(AgeDenseOres.error(AgeRuntimeProfiles.resolve(id("flat")), null, registries)
|
||||
.contains("noise terrain"), "Flat terrain must reject Dense Ores");
|
||||
helper.assertTrue(AgeDenseOres.error(AgeRuntimeProfiles.resolve(id("void")), null, registries)
|
||||
.contains("noise terrain"), "Void terrain must reject Dense Ores");
|
||||
helper.assertTrue(AgeDenseOres.error(AgeRuntimeProfiles.resolve(id("standard")),
|
||||
new AgeBiomeSelection(AgeTraits.BiomeDistribution.SINGLE,
|
||||
List.of(ResourceLocation.withDefaultNamespace("the_end"))), registries)
|
||||
.contains("biome with registered ore"),
|
||||
"A biome without ore placements must not accept a no-op Dense Ores page");
|
||||
helper.assertTrue(AgeDenseOres.error(AgeRuntimeProfiles.resolve(id("cave")),
|
||||
new AgeBiomeSelection(AgeTraits.BiomeDistribution.SINGLE,
|
||||
List.of(ResourceLocation.withDefaultNamespace("nether_wastes"))), registries) == null,
|
||||
"Nether ore placements must qualify for a Cave Age");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void recognizesOreTargetsButNotStoneBlobs(GameTestHelper helper) {
|
||||
helper.assertTrue(AgeDenseOres.isOre(ore(Blocks.IRON_ORE.defaultBlockState())),
|
||||
"Any namespace's OreConfiguration yielding an ore block must qualify");
|
||||
helper.assertTrue(!AgeDenseOres.isOre(ore(Blocks.STONE.defaultBlockState())),
|
||||
"Ordinary stone replacement must not be doubled as an ore");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private static Holder<PlacedFeature> ore(net.minecraft.world.level.block.state.BlockState output) {
|
||||
var config = new OreConfiguration(new BlockMatchTest(Blocks.STONE), output, 8);
|
||||
var feature = new ConfiguredFeature<>(Feature.ORE, config);
|
||||
return Holder.direct(new PlacedFeature(Holder.direct(feature), List.of()));
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.book.AgeBiomeSelection;
|
||||
import net.dnorberg.rivencraft.book.AgeRuntimeProfile;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.util.RandomSource;
|
||||
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.configurations.OreConfiguration;
|
||||
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.neoforged.neoforge.common.Tags;
|
||||
|
||||
/** Replays eligible biome ore placements once per authored Dense Ores page. */
|
||||
public final class AgeDenseOres {
|
||||
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "dense_ores");
|
||||
public static final TagKey<PlacedFeature> EXTRA_ORES = TagKey.create(Registries.PLACED_FEATURE, ID);
|
||||
|
||||
private AgeDenseOres() {}
|
||||
|
||||
public static String error(AgeRuntimeProfile profile, AgeBiomeSelection biomes, RegistryAccess registries) {
|
||||
if (profile.terrain() != AgeRuntimeProfile.Terrain.NOISE)
|
||||
return "Dense Ores needs noise terrain with replaceable underground stone or netherrack";
|
||||
if (biomes == null) return null;
|
||||
var registry = registries.lookupOrThrow(Registries.BIOME);
|
||||
boolean eligible = biomes.biomes().stream()
|
||||
.map(id -> registry.get(ResourceKey.create(Registries.BIOME, id)))
|
||||
.flatMap(java.util.Optional::stream)
|
||||
.anyMatch(holder -> oreFeatures(holder).stream().anyMatch(AgeDenseOres::isOre));
|
||||
return eligible ? null : "Dense Ores needs at least one biome with registered ore placements";
|
||||
}
|
||||
|
||||
/** Vanilla-style and modded OreConfiguration targets, or an explicit opt-in tag. */
|
||||
public static boolean isOre(Holder<PlacedFeature> holder) {
|
||||
if (holder.is(EXTRA_ORES)) return true;
|
||||
if (!(holder.value().feature().value().config() instanceof OreConfiguration ore)) return false;
|
||||
return ore.targetStates.stream().anyMatch(target -> isOreBlock(target.state));
|
||||
}
|
||||
|
||||
private static boolean isOreBlock(BlockState state) {
|
||||
if (state.is(Tags.Blocks.ORES)) return true;
|
||||
// Some third-party ores omit the common tag; require an ore configuration
|
||||
// as well so similarly named decorative features cannot be selected.
|
||||
String path = BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath();
|
||||
return path.endsWith("_ore") || path.contains("_ore_");
|
||||
}
|
||||
|
||||
private static java.util.List<Holder<PlacedFeature>> oreFeatures(Holder<Biome> biome) {
|
||||
// A mod may register its ore in a non-vanilla decoration step. Its
|
||||
// configuration and output, not its stage or namespace, decide eligibility.
|
||||
return biome.value().getGenerationSettings().features().stream()
|
||||
.flatMap(net.minecraft.core.HolderSet::stream).toList();
|
||||
}
|
||||
|
||||
public static void place(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator generator,
|
||||
long ageSeed, int occurrence) {
|
||||
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());
|
||||
Map<ResourceLocation, PlacedFeature> ores = new TreeMap<>();
|
||||
for (Holder<Biome> biome : biomes) {
|
||||
for (Holder<PlacedFeature> feature : oreFeatures(biome)) {
|
||||
if (!isOre(feature)) continue;
|
||||
feature.unwrapKey().ifPresent(key -> ores.putIfAbsent(key.location(), feature.value()));
|
||||
}
|
||||
}
|
||||
BlockPos origin = new BlockPos(chunk.getPos().getMinBlockX(), level.getMinBuildHeight(),
|
||||
chunk.getPos().getMinBlockZ());
|
||||
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);
|
||||
entry.getValue().placeWithBiomeCheck(level, generator, RandomSource.create(seed), origin);
|
||||
}
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,10 @@ public final class AgeFeatureChunkGenerator extends NoiseBasedChunkGenerator {
|
||||
spikeOccurrences.merge(choice.id(), 1, Integer::sum) - 1);
|
||||
continue;
|
||||
}
|
||||
if (choice.id().equals(AgeDenseOres.ID)) {
|
||||
AgeDenseOres.place(level, chunk, this, ageSeed, occurrence);
|
||||
continue;
|
||||
}
|
||||
if (AgeFeatureSelection.surfaceVegetation(choice.id())) {
|
||||
int surface = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 8);
|
||||
if (!AgeFeatureSelection.supportsSurfaceVegetation(
|
||||
|
||||
@@ -283,6 +283,8 @@
|
||||
"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.dense_ores": "Dense Ores",
|
||||
"symbol.rivencraft.feature.dense_ores.description": "Repeat compatible vanilla and modded ore placements once per page in this Age.",
|
||||
"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",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.dense_ores",
|
||||
"description": "symbol.rivencraft.feature.dense_ores.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:dense_ores" },
|
||||
"poem": "Nature Material Treasure Abundance"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": []
|
||||
}
|
||||
Reference in new issue
Block a user