Public Access
This commit is contained in:
1 parent
a9040e8712
commit
66b4c56eb6
12 files changed
+217
-10
No files matched your search
@@ -28,6 +28,7 @@ public final class AgeFeatureSelection {
|
||||
id("rocks"), id("floating_islands"), id("tendrils"));
|
||||
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;
|
||||
|
||||
private AgeFeatureSelection() {}
|
||||
|
||||
@@ -139,7 +140,7 @@ public final class AgeFeatureSelection {
|
||||
return List.copyOf(resolved);
|
||||
}
|
||||
|
||||
/** Page-free Ages roll up to two compatible material/feature pairs once at creation. */
|
||||
/** Page-free Ages roll up to two material features, plus rare Skylands, once at creation. */
|
||||
public static List<Choice> randomChoices(long ageSeed, AgeRuntimeProfile profile, RegistryAccess registries) {
|
||||
Random random = new Random(ageSeed ^ 0x4D4154455249414CL);
|
||||
int count = random.nextInt(3);
|
||||
@@ -157,6 +158,9 @@ public final class AgeFeatureSelection {
|
||||
if (!materials.isEmpty()) choices.add(new Choice(feature,
|
||||
Optional.of(materials.get(random.nextInt(materials.size())))));
|
||||
}
|
||||
if (profile.terrain() == AgeRuntimeProfile.Terrain.NOISE
|
||||
&& Math.floorMod(mix(ageSeed ^ RANDOM_SKYLANDS_SALT), 8) == 0)
|
||||
choices.add(new Choice(id("skylands"), Optional.empty()));
|
||||
return List.copyOf(choices);
|
||||
}
|
||||
|
||||
@@ -181,6 +185,7 @@ public final class AgeFeatureSelection {
|
||||
}
|
||||
|
||||
public static String error(ResourceLocation choice, AgeRuntimeProfile profile, RegistryAccess registries) {
|
||||
if (skylands(choice)) return null;
|
||||
if (tendrils(choice)) return null;
|
||||
if (floatingIslands(choice)) {
|
||||
if (AgeBiomeSelection.isCave(profile))
|
||||
@@ -264,6 +269,10 @@ public final class AgeFeatureSelection {
|
||||
return choice.equals(id("tendrils"));
|
||||
}
|
||||
|
||||
public static boolean skylands(ResourceLocation choice) {
|
||||
return choice.equals(id("skylands"));
|
||||
}
|
||||
|
||||
public static boolean supportsSurfaceVegetation(ResourceLocation id, RegistryAccess registries) {
|
||||
var biome = registries.lookupOrThrow(Registries.BIOME)
|
||||
.get(ResourceKey.create(Registries.BIOME, id));
|
||||
|
||||
@@ -68,12 +68,18 @@ public final class AgePersistenceGameTests {
|
||||
helper.assertTrue(pageFree.plan() == null && panelOnly.plan() == null,
|
||||
"Page-free and Link-Panel-only books must retain the random Age path");
|
||||
for (AgeDefinition age : List.of(pageFree, panelOnly)) {
|
||||
helper.assertTrue(age.randomFeatures().size() <= 2,
|
||||
helper.assertTrue(age.randomFeatures().stream().filter(choice -> choice.material().isPresent())
|
||||
.count() <= 2,
|
||||
"A random Age may roll at most two material features");
|
||||
helper.assertTrue(age.randomFeatures().stream().filter(choice ->
|
||||
AgeFeatureSelection.skylands(choice.id())).count() <= 1,
|
||||
"A random Age may roll at most one Skylands filter");
|
||||
for (AgeFeatureSelection.Choice choice : age.randomFeatures()) {
|
||||
helper.assertTrue(choice.material().isPresent()
|
||||
helper.assertTrue(AgeFeatureSelection.skylands(choice.id())
|
||||
? choice.material().isEmpty()
|
||||
: choice.material().isPresent()
|
||||
&& AgeFeatureSelection.materialError(choice) == null,
|
||||
"Random features must each carry a valid block or fluid material");
|
||||
"Random material features need valid blocks or fluids; Skylands needs none");
|
||||
}
|
||||
}
|
||||
AgeRegistrySavedData registry = AgeRegistrySavedData.get(helper.getLevel());
|
||||
|
||||
@@ -32,7 +32,7 @@ public final class FeatureWritingGameTests {
|
||||
Set<String> pages = Set.of("feature/caves", "feature/flowers", "feature/trees",
|
||||
"feature/huge_trees", "feature/rocks", "feature/lava_lakes",
|
||||
"feature/surface_lakes", "feature/deep_lakes", "feature/ravines",
|
||||
"feature/floating_islands", "feature/tendrils");
|
||||
"feature/floating_islands", "feature/tendrils", "feature/skylands");
|
||||
helper.assertTrue(PageItem.bundledCreativeSymbols().containsAll(pages),
|
||||
"Supported feature pages must appear in the creative and JEI symbol feed");
|
||||
for (String path : pages) {
|
||||
@@ -60,7 +60,8 @@ public final class FeatureWritingGameTests {
|
||||
for (AgeFeatureSelection.Choice choice : AgeFeatureSelection.resolved(plan)) {
|
||||
if (AgeFeatureSelection.caves(choice.id()) || AgeFeatureSelection.ravines(choice.id())
|
||||
|| AgeFeatureSelection.floatingIslands(choice.id())
|
||||
|| AgeFeatureSelection.tendrils(choice.id())) continue;
|
||||
|| AgeFeatureSelection.tendrils(choice.id())
|
||||
|| AgeFeatureSelection.skylands(choice.id())) continue;
|
||||
var placement = AgeFeatureSelection.placedFeature(choice.id());
|
||||
helper.assertTrue(registries.lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
.get(ResourceKey.create(Registries.PLACED_FEATURE, placement)).isPresent(),
|
||||
@@ -178,6 +179,41 @@ public final class FeatureWritingGameTests {
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void skylandsFiltersBaseTerrainIndependentlyOfIslandFeatures(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
for (String terrain : List.of("standard", "amplified", "island", "flat", "void", "cave")) {
|
||||
OrderedWriting writing = new OrderedWriting(List.of(panel(), page("terrain/" + terrain),
|
||||
page("feature/skylands"), page("feature/floating_islands")));
|
||||
if (terrain.equals("cave")) {
|
||||
writing = new OrderedWriting(List.of(panel(), page("terrain/cave"),
|
||||
page("feature/skylands")));
|
||||
}
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 707L,
|
||||
AgeGenerationProfiles.STANDARD, AgeTraits.select(707L), registries);
|
||||
List<AgeFeatureSelection.Choice> resolved = AgeFeatureSelection.resolved(plan);
|
||||
helper.assertValueEqual(resolved.getFirst(), new AgeFeatureSelection.Choice(
|
||||
id("skylands"), Optional.empty()),
|
||||
"Skylands must be a separate material-free feature on " + terrain);
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(AgeGenerationPlan.load(plan.save())), resolved,
|
||||
"Skylands and any Floating Islands page must survive a plan reload");
|
||||
}
|
||||
rejects(helper, new OrderedWriting(List.of(panel(), page("terrain/standard"),
|
||||
block("stone"), page("feature/skylands"))), "cannot consume a material page");
|
||||
List<AgeFeatureSelection.Choice> randomNoise = AgeFeatureSelection.randomChoices(7L,
|
||||
AgeRuntimeProfiles.resolve(id("standard")), registries);
|
||||
helper.assertTrue(randomNoise.stream().anyMatch(choice -> AgeFeatureSelection.skylands(choice.id())
|
||||
&& choice.material().isEmpty()),
|
||||
"A page-free random noise Age must be able to roll material-free Skylands");
|
||||
for (String terrain : List.of("flat", "void")) {
|
||||
helper.assertTrue(AgeFeatureSelection.randomChoices(7L,
|
||||
AgeRuntimeProfiles.resolve(id(terrain)), registries).stream()
|
||||
.noneMatch(choice -> AgeFeatureSelection.skylands(choice.id())),
|
||||
"Random " + terrain + " Ages must not roll a filter without useful upper terrain");
|
||||
}
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void voidBiomesAreIndependentOfFloatingIslands(GameTestHelper helper) {
|
||||
OrderedWriting voidDefault = new OrderedWriting(List.of(panel(), page("terrain/void")));
|
||||
|
||||
@@ -76,8 +76,13 @@ public final class AgeFeatureChunkGenerator extends NoiseBasedChunkGenerator {
|
||||
StructureManager structures, ChunkAccess chunk) {
|
||||
CompletableFuture<ChunkAccess> filled = super.fillFromNoise(blender, random, structures, chunk);
|
||||
if (features.stream().noneMatch(choice -> AgeFeatureSelection.floatingIslands(choice.id())
|
||||
|| AgeFeatureSelection.tendrils(choice.id()))) return filled;
|
||||
|| AgeFeatureSelection.tendrils(choice.id())
|
||||
|| AgeFeatureSelection.skylands(choice.id()))) return filled;
|
||||
return filled.thenApply(result -> {
|
||||
// Legacy's Skylands primer filter removes base terrain before large
|
||||
// features add their own blocks to the chunk.
|
||||
if (features.stream().anyMatch(choice -> AgeFeatureSelection.skylands(choice.id())))
|
||||
SkylandsFeature.filter(result, ageSeed);
|
||||
Map<ResourceLocation, Integer> occurrences = new HashMap<>();
|
||||
for (AgeFeatureSelection.Choice choice : features) {
|
||||
if (!AgeFeatureSelection.floatingIslands(choice.id())
|
||||
|
||||
@@ -100,6 +100,8 @@ public final class AgeFlatChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (features.stream().anyMatch(choice -> AgeFeatureSelection.skylands(choice.id())))
|
||||
SkylandsFeature.filter(chunk, ageSeed);
|
||||
Map<ResourceLocation, Integer> occurrences = new HashMap<>();
|
||||
for (AgeFeatureSelection.Choice choice : features) {
|
||||
if (!AgeFeatureSelection.floatingIslands(choice.id())
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Subtractive Skylands terrain filter adapted from Mystcraft Legacy's
|
||||
* SymbolSkylands (LGPL-3.0-or-later). Legacy keeps solid terrain above a
|
||||
* seven-octave cutoff near Y=76 and discards liquid above that cutoff.
|
||||
*/
|
||||
final class SkylandsFeature {
|
||||
private static final long SALT = 0x534B594C414E4453L;
|
||||
private static final int BASE_CUTOFF = 76;
|
||||
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
|
||||
|
||||
private SkylandsFeature() {}
|
||||
|
||||
static void filter(ChunkAccess chunk, long ageSeed) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
int minY = chunk.getMinBuildHeight();
|
||||
int maxY = chunk.getMaxBuildHeight();
|
||||
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, false);
|
||||
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,31 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
/** Checks the Legacy-style filter independently of a slow dimension-generation test. */
|
||||
@GameTestHolder(Rivencraft.MOD_ID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class SkylandsFeatureGameTests {
|
||||
private SkylandsFeatureGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void skylandsOnlyKeepsUpperSolidTerrain(GameTestHelper helper) {
|
||||
int cutoff = SkylandsFeature.cutoff(707L, 32, -48);
|
||||
helper.assertValueEqual(SkylandsFeature.cutoff(707L, 32, -48), cutoff,
|
||||
"Skylands cutoff must be deterministic for an Age seed and world column");
|
||||
helper.assertTrue(SkylandsFeature.removes(Blocks.STONE.defaultBlockState(), cutoff, cutoff),
|
||||
"Solid terrain at the cutoff must be removed");
|
||||
helper.assertFalse(SkylandsFeature.removes(Blocks.STONE.defaultBlockState(), cutoff + 1, cutoff),
|
||||
"Solid terrain above the cutoff must remain");
|
||||
helper.assertTrue(SkylandsFeature.removes(Blocks.WATER.defaultBlockState(), cutoff + 1, cutoff),
|
||||
"Liquids above the cutoff must be removed");
|
||||
helper.assertFalse(SkylandsFeature.removes(Blocks.AIR.defaultBlockState(), cutoff - 1, cutoff),
|
||||
"Air must remain air");
|
||||
helper.succeed();
|
||||
}
|
||||
}
|
||||
@@ -247,6 +247,8 @@
|
||||
"symbol.rivencraft.feature.floating_islands.description": "Add high floating islands above the chosen surface terrain.",
|
||||
"symbol.rivencraft.feature.tendrils": "Tendrils",
|
||||
"symbol.rivencraft.feature.tendrils.description": "Add winding solid tendrils through the Age, using a random block or a preceding Block page.",
|
||||
"symbol.rivencraft.feature.skylands": "Skylands",
|
||||
"symbol.rivencraft.feature.skylands.description": "Remove terrain below a shifting sky cutoff; unlike Floating Islands, this does not add land.",
|
||||
"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.skylands",
|
||||
"description": "symbol.rivencraft.feature.skylands.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:skylands" },
|
||||
"poem": "Terrain Sky Void Suspension"
|
||||
}
|
||||
Reference in new issue
Block a user