Public Access
This commit is contained in:
1 parent
a9a6f7205d
commit
6dbe208c42
12 files changed
+263
-10
No files matched your search
@@ -27,7 +27,8 @@ import net.minecraft.world.level.block.Blocks;
|
||||
/** Validates and freezes supported feature pages and their preceding material modifiers. */
|
||||
public final class AgeFeatureSelection {
|
||||
private static final List<ResourceLocation> BLOCK_FEATURES = List.of(
|
||||
id("rocks"), id("floating_islands"), id("tendrils"), id("spheres"), id("spikes"));
|
||||
id("rocks"), id("floating_islands"), id("tendrils"), id("spheres"), id("spikes"),
|
||||
id("obelisks"));
|
||||
private static final List<ResourceLocation> FLUID_FEATURES = List.of(
|
||||
id("surface_lakes"), id("deep_lakes"), id("lava_lakes"));
|
||||
private static final long RANDOM_SKYLANDS_SALT = 0x534B594C414E4453L;
|
||||
@@ -121,6 +122,8 @@ public final class AgeFeatureSelection {
|
||||
|
||||
private static boolean compatible(ResourceLocation feature, AgeRuntimeProfile profile) {
|
||||
if (tendrils(feature) || spheres(feature)) return true;
|
||||
if (obelisks(feature)) return !AgeBiomeSelection.isCave(profile)
|
||||
&& profile.terrain() != AgeRuntimeProfile.Terrain.VOID;
|
||||
if (AgeBiomeSelection.isCave(profile)) return false;
|
||||
if (spikes(feature)) return profile.terrain() != AgeRuntimeProfile.Terrain.VOID;
|
||||
return floatingIslands(feature) || profile.terrain() == AgeRuntimeProfile.Terrain.NOISE;
|
||||
@@ -136,7 +139,8 @@ public final class AgeFeatureSelection {
|
||||
if (choice.material().isPresent()
|
||||
|| !isRock(choice.id()) && !isLake(choice.id())
|
||||
&& !floatingIslands(choice.id()) && !tendrils(choice.id())
|
||||
&& !spheres(choice.id()) && !spikes(choice.id())) {
|
||||
&& !spheres(choice.id()) && !spikes(choice.id())
|
||||
&& !obelisks(choice.id())) {
|
||||
resolved.add(choice);
|
||||
continue;
|
||||
}
|
||||
@@ -208,6 +212,11 @@ public final class AgeFeatureSelection {
|
||||
return "Feature " + choice + " needs an exposed solid surface, not Cave or Void terrain";
|
||||
return null;
|
||||
}
|
||||
if (obelisks(choice)) {
|
||||
if (AgeBiomeSelection.isCave(profile) || profile.terrain() == AgeRuntimeProfile.Terrain.VOID)
|
||||
return "Feature " + choice + " needs exposed solid surface terrain, not Cave or Void";
|
||||
return null;
|
||||
}
|
||||
if (floatingIslands(choice)) {
|
||||
if (AgeBiomeSelection.isCave(profile))
|
||||
return "Feature " + choice + " cannot form above Cave terrain";
|
||||
@@ -298,9 +307,13 @@ public final class AgeFeatureSelection {
|
||||
return choice.equals(id("spikes"));
|
||||
}
|
||||
|
||||
public static boolean obelisks(ResourceLocation choice) {
|
||||
return choice.equals(id("obelisks"));
|
||||
}
|
||||
|
||||
private static boolean solidFeature(ResourceLocation choice) {
|
||||
return isRock(choice) || floatingIslands(choice) || tendrils(choice)
|
||||
|| spheres(choice) || spikes(choice);
|
||||
|| spheres(choice) || spikes(choice) || obelisks(choice);
|
||||
}
|
||||
|
||||
public static boolean skylands(ResourceLocation choice) {
|
||||
|
||||
@@ -40,7 +40,7 @@ public final class FeatureWritingGameTests {
|
||||
"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/dense_ores");
|
||||
"feature/dense_ores", "feature/obelisks");
|
||||
helper.assertTrue(PageItem.bundledCreativeSymbols().containsAll(pages),
|
||||
"Supported feature pages must appear in the creative and JEI symbol feed");
|
||||
for (String path : pages) {
|
||||
@@ -72,6 +72,7 @@ public final class FeatureWritingGameTests {
|
||||
|| AgeFeatureSelection.skylands(choice.id())
|
||||
|| AgeFeatureSelection.spheres(choice.id())
|
||||
|| AgeFeatureSelection.spikes(choice.id())
|
||||
|| AgeFeatureSelection.obelisks(choice.id())
|
||||
|| choice.id().equals(AgeDenseOres.ID)) continue;
|
||||
var placement = AgeFeatureSelection.placedFeature(choice.id());
|
||||
helper.assertTrue(registries.lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
@@ -419,7 +420,8 @@ public final class FeatureWritingGameTests {
|
||||
|| AgeFeatureSelection.floatingIslands(choices.getFirst().id())
|
||||
|| AgeFeatureSelection.tendrils(choices.getFirst().id())
|
||||
|| AgeFeatureSelection.spheres(choices.getFirst().id())
|
||||
|| AgeFeatureSelection.spikes(choices.getFirst().id()),
|
||||
|| AgeFeatureSelection.spikes(choices.getFirst().id())
|
||||
|| AgeFeatureSelection.obelisks(choices.getFirst().id()),
|
||||
"The inferred feature must accept the material type");
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(AgeGenerationPlan.load(plan.save())), choices,
|
||||
"The inferred feature must be deterministic across a plan reload");
|
||||
|
||||
@@ -162,6 +162,7 @@ public final class AgeFeatureChunkGenerator extends NoiseBasedChunkGenerator {
|
||||
BlockPos origin = new BlockPos(x, level.getMinBuildHeight(), z);
|
||||
Map<AgeFeatureSelection.Choice, Integer> occurrences = new HashMap<>();
|
||||
Map<ResourceLocation, Integer> spikeOccurrences = new HashMap<>();
|
||||
int obeliskOccurrence = 0;
|
||||
for (int index = 0; index < features.size(); index++) {
|
||||
AgeFeatureSelection.Choice choice = features.get(index);
|
||||
int occurrence = occurrences.merge(choice, 1, Integer::sum) - 1;
|
||||
@@ -170,6 +171,10 @@ public final class AgeFeatureChunkGenerator extends NoiseBasedChunkGenerator {
|
||||
spikeOccurrences.merge(choice.id(), 1, Integer::sum) - 1);
|
||||
continue;
|
||||
}
|
||||
if (AgeFeatureSelection.obelisks(choice.id())) {
|
||||
ObelisksFeature.generate(level, chunk, ageSeed, choice, obeliskOccurrence++);
|
||||
continue;
|
||||
}
|
||||
if (choice.id().equals(AgeDenseOres.ID)) {
|
||||
AgeDenseOres.place(level, chunk, this, ageSeed, occurrence);
|
||||
continue;
|
||||
|
||||
@@ -124,10 +124,13 @@ public final class AgeFlatChunkGenerator extends ChunkGenerator {
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structures) {
|
||||
super.applyBiomeDecoration(level, chunk, structures);
|
||||
int occurrence = 0;
|
||||
int spikeOccurrence = 0;
|
||||
int obeliskOccurrence = 0;
|
||||
for (AgeFeatureSelection.Choice choice : features) {
|
||||
if (AgeFeatureSelection.spikes(choice.id()))
|
||||
SpikesFeature.generate(level, chunk, ageSeed, choice, occurrence++);
|
||||
SpikesFeature.generate(level, chunk, ageSeed, choice, spikeOccurrence++);
|
||||
if (AgeFeatureSelection.obelisks(choice.id()))
|
||||
ObelisksFeature.generate(level, chunk, ageSeed, choice, obeliskOccurrence++);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
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;
|
||||
|
||||
/** Rare one-chunk obelisk adapted from Legacy's 4x4 plinth and 2x12x2 shaft. */
|
||||
final class ObelisksFeature {
|
||||
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 ObelisksFeature() {}
|
||||
|
||||
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,
|
||||
AgeFeatureSelection.Choice choice, int occurrence) {
|
||||
Site site = site(ageSeed, chunk.getPos(), occurrence);
|
||||
if (site == null || choice.material().isEmpty()) return;
|
||||
BlockState material = BuiltInRegistries.BLOCK.get(choice.material().orElseThrow()).defaultBlockState();
|
||||
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.getMinBuildHeight() + 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.getMaxBuildHeight()) 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
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.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
/** Obelisks must retain ordered materials and generate a stable Legacy-sized shape. */
|
||||
@GameTestHolder(Rivencraft.MOD_ID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class ObelisksGameTests {
|
||||
private ObelisksGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void authoredObelisksKeepTheirMaterialAndTerrainRules(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
helper.assertTrue(PageItem.bundledCreativeSymbols().contains("feature/obelisks"),
|
||||
"Obelisks must appear in the Creative and JEI symbol feed");
|
||||
OrderedWriting writing = new OrderedWriting(List.of(WritingEntry.linkPanel(), page("terrain/standard"),
|
||||
block("obsidian"), page("feature/obelisks"), page("feature/obelisks")));
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 56L, AgeGenerationProfiles.STANDARD,
|
||||
AgeTraits.select(56L), registries);
|
||||
var choices = AgeFeatureSelection.resolved(plan);
|
||||
helper.assertValueEqual(choices.size(), 2, "Repeated Obelisks pages must remain separate");
|
||||
helper.assertValueEqual(choices.getFirst().material(),
|
||||
Optional.of(ResourceLocation.withDefaultNamespace("obsidian")),
|
||||
"A preceding Block page must set the first obelisk material");
|
||||
helper.assertTrue(choices.getLast().material().isPresent()
|
||||
&& AgeFeatureSelection.materialError(choices.getLast()) == null,
|
||||
"An unmodified Obelisks page must choose a valid material");
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(AgeGenerationPlan.load(plan.save())), choices,
|
||||
"Obelisk materials and order must survive Age-plan reload");
|
||||
for (String terrain : List.of("standard", "amplified", "island", "flat"))
|
||||
helper.assertTrue(AgeFeatureSelection.error(id("obelisks"),
|
||||
AgeRuntimeProfiles.resolve(id(terrain)), registries) == null,
|
||||
"Obelisks should be valid on exposed " + terrain + " terrain");
|
||||
for (String terrain : List.of("void", "cave"))
|
||||
helper.assertTrue(AgeFeatureSelection.error(id("obelisks"),
|
||||
AgeRuntimeProfiles.resolve(id(terrain)), registries) != null,
|
||||
"Obelisks must reject " + terrain + " terrain without an exposed surface");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void randomAgesOnlyChooseObelisksOnSupportedTerrain(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
boolean selected = false;
|
||||
for (long seed = 0; seed < 128; seed++) {
|
||||
selected |= AgeFeatureSelection.randomChoices(seed,
|
||||
AgeRuntimeProfiles.resolve(id("standard")), registries).stream()
|
||||
.anyMatch(choice -> AgeFeatureSelection.obelisks(choice.id()));
|
||||
for (String terrain : List.of("void", "cave"))
|
||||
helper.assertTrue(AgeFeatureSelection.randomChoices(seed,
|
||||
AgeRuntimeProfiles.resolve(id(terrain)), registries).stream()
|
||||
.noneMatch(choice -> AgeFeatureSelection.obelisks(choice.id())),
|
||||
"Random " + terrain + " Ages must not gain Obelisks");
|
||||
}
|
||||
helper.assertTrue(selected, "Random surface Ages should be able to select Obelisks");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void obeliskSitesAreSeededAndStayInsideTheirChunk(GameTestHelper helper) {
|
||||
ChunkPos chunk = new ChunkPos(123, -45);
|
||||
int selected = 0;
|
||||
for (long seed = 0; seed < 2048; seed++) {
|
||||
ObelisksFeature.Site site = ObelisksFeature.site(seed, chunk, 0);
|
||||
helper.assertTrue(java.util.Objects.equals(ObelisksFeature.site(seed, chunk, 0), site),
|
||||
"An Age seed must reproduce the same obelisk site");
|
||||
if (site == null) continue;
|
||||
selected++;
|
||||
helper.assertTrue(site.x() >= chunk.getMinBlockX() + 4
|
||||
&& site.x() + 3 < chunk.getMaxBlockX(),
|
||||
"The full 4x4 plinth must remain inside its source chunk");
|
||||
helper.assertTrue(site.z() >= chunk.getMinBlockZ() + 4
|
||||
&& site.z() + 3 < chunk.getMaxBlockZ(),
|
||||
"The full 4x4 plinth must remain inside its source chunk");
|
||||
}
|
||||
helper.assertTrue(selected > 0 && selected < 64,
|
||||
"The 1-in-128 roll should remain rare but reachable");
|
||||
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)));
|
||||
}
|
||||
|
||||
private static WritingEntry block(String path) {
|
||||
return WritingEntry.symbol(new SymbolReference(net.dnorberg.rivencraft.writing.BlockSymbolPages.symbolId(
|
||||
ResourceLocation.withDefaultNamespace(path))));
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ public final class WritingGrammar {
|
||||
} else if (role == Role.FEATURE && pendingBlock != null) {
|
||||
ResourceLocation feature = ((SymbolSemantics.ResourceValue) parsed.semantics().value()).id();
|
||||
if (!Set.of("rocks", "surface_lakes", "deep_lakes", "lava_lakes",
|
||||
"floating_islands", "tendrils", "spheres", "spikes")
|
||||
"floating_islands", "tendrils", "spheres", "spikes", "obelisks")
|
||||
.contains(feature.getPath()) || !feature.getNamespace().equals("rivencraft")) {
|
||||
diagnostics.add(new Diagnostic(Code.MISPLACED_MODIFIER, pendingBlock.index(),
|
||||
"This feature cannot consume a material page"));
|
||||
|
||||
@@ -285,6 +285,8 @@
|
||||
"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.obelisks": "Obelisks",
|
||||
"symbol.rivencraft.feature.obelisks.description": "Add rare tall obelisks on exposed solid ground, with a random or selected block material.",
|
||||
"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.obelisks",
|
||||
"description": "symbol.rivencraft.feature.obelisks.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:obelisks" },
|
||||
"poem": "Civilization Structure Power Obelisk"
|
||||
}
|
||||
Reference in new issue
Block a user