Compare commits

..
1 Commits
Author SHA1 Message Date
dnorberg a9040e8712 Port Tendrils feature generation (#46)
Build / build (push) Canceled after 0s
Add the writable symbol, adapt Legacy's branching solid cave-node geometry across all terrain types, and test authored and random Age selection.
2026-10-04 10:31:44 -04:00
11 changed files with 260 additions and 20 deletions

No files matched your search

+17
View File
@@ -73,6 +73,23 @@ common than in the first test build (one source chunk in 128 instead of 64).
Sky Islands should retain its terrain islands and gain occasional additional
feature islands. Try Cave terrain too; the Binder should reject that pairing.
The **Tendrils** feature adapts Legacy's winding, branching cave-node geometry
into solid tubes. It is additive on all current terrain families, including
Flat, Void, and Cave; it does not change biome selection. A preceding full-
collision Block page chooses the tendril material. Otherwise the Age seed
chooses a valid Block Pages material, consistent with other authored material
features. Page-free random Ages can select Tendrils as a solid-material feature,
including on Cave terrain where the surface material features are unavailable.
The shape and placement use only the Age seed and source-chunk coordinates, so
revisiting an Age recreates the same geometry.
For a clear visual check, bind a Void Age with a Glowstone Block page followed
by Tendrils. Fly around world Y=0–120 across several chunks: branching solid
forms should appear in the otherwise empty dimension, continue across chunk
borders, and remain in place after leaving and returning. Repeat with a lone
Tendrils page to confirm its seed-chosen material, and with Cave terrain to
confirm the feature does not require a surface biome.
## Written biome selection (generation version 16)
Biome pages follow the terrain page and precede the distribution-control page
+1 -1
View File
@@ -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 and vanilla data-pack generation | Compile only features for which registered 1.21.1 placed features/structures exist. Defer bespoke structures and polish to milestone 7. |
| 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 | Compile supported vanilla placements and port bespoke shapes explicitly. Remaining bespoke structures and polish stay in later issues. |
| `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).
+1 -1
View File
@@ -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.11.7
mod_version=0.4.12.0
immersive_portals_version=6.0.7
terrablender_version=4.1.0.8
serene_seasons_version_id=B6nTjh4b
@@ -24,7 +24,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"));
private static final List<ResourceLocation> BLOCK_FEATURES = List.of(
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"));
@@ -112,6 +113,7 @@ public final class AgeFeatureSelection {
}
private static boolean compatible(ResourceLocation feature, AgeRuntimeProfile profile) {
if (tendrils(feature)) return true;
if (AgeBiomeSelection.isCave(profile)) return false;
return floatingIslands(feature) || profile.terrain() == AgeRuntimeProfile.Terrain.NOISE;
}
@@ -124,7 +126,8 @@ public final class AgeFeatureSelection {
for (Choice choice : choices(plan.symbols().stream().map(symbol -> symbol.semantics()).toList(),
plan.runtimeProfile(), plan.ageSeed())) {
if (choice.material().isPresent()
|| !isRock(choice.id()) && !isLake(choice.id()) && !floatingIslands(choice.id())) {
|| !isRock(choice.id()) && !isLake(choice.id())
&& !floatingIslands(choice.id()) && !tendrils(choice.id())) {
resolved.add(choice);
continue;
}
@@ -147,7 +150,7 @@ public final class AgeFeatureSelection {
List<Choice> choices = new ArrayList<>();
for (ResourceLocation feature : features) {
if (choices.size() >= count) break;
var category = isRock(feature) || floatingIslands(feature)
var category = isRock(feature) || floatingIslands(feature) || tendrils(feature)
? BlockSymbolPages.CATEGORY : BlockSymbolPages.FLUID_CATEGORY;
List<ResourceLocation> materials = BlockSymbolPages.availableBlocks(category).stream()
.filter(id -> materialError(new Choice(feature, Optional.of(id))) == null).toList();
@@ -158,7 +161,7 @@ public final class AgeFeatureSelection {
}
private static List<ResourceLocation> shuffledMaterials(long ageSeed, ResourceLocation feature) {
var category = isRock(feature) || floatingIslands(feature)
var category = isRock(feature) || floatingIslands(feature) || tendrils(feature)
? BlockSymbolPages.CATEGORY : BlockSymbolPages.FLUID_CATEGORY;
List<ResourceLocation> pool = new ArrayList<>(BlockSymbolPages.availableBlocks(category).stream()
.filter(id -> materialError(new Choice(feature, Optional.of(id))) == null).toList());
@@ -178,6 +181,7 @@ public final class AgeFeatureSelection {
}
public static String error(ResourceLocation choice, AgeRuntimeProfile profile, RegistryAccess registries) {
if (tendrils(choice)) return null;
if (floatingIslands(choice)) {
if (AgeBiomeSelection.isCave(profile))
return "Feature " + choice + " cannot form above Cave terrain";
@@ -216,7 +220,7 @@ public final class AgeFeatureSelection {
if (block.isEmpty() || block.orElseThrow() == Blocks.AIR) {
return "Feature " + choice.id() + " references missing block material " + material;
}
if (isRock(choice.id()) || floatingIslands(choice.id())) {
if (isRock(choice.id()) || floatingIslands(choice.id()) || tendrils(choice.id())) {
if (!BlockSymbolPages.isRockMaterial(block.orElseThrow())) {
return "Feature " + choice.id() + " needs a full solid block without a block entity: " + material;
}
@@ -256,6 +260,10 @@ public final class AgeFeatureSelection {
return choice.equals(ResourceLocation.fromNamespaceAndPath("rivencraft", "floating_islands"));
}
public static boolean tendrils(ResourceLocation choice) {
return choice.equals(id("tendrils"));
}
public static boolean supportsSurfaceVegetation(ResourceLocation id, RegistryAccess registries) {
var biome = registries.lookupOrThrow(Registries.BIOME)
.get(ResourceKey.create(Registries.BIOME, id));
@@ -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/floating_islands", "feature/tendrils");
helper.assertTrue(PageItem.bundledCreativeSymbols().containsAll(pages),
"Supported feature pages must appear in the creative and JEI symbol feed");
for (String path : pages) {
@@ -59,7 +59,8 @@ public final class FeatureWritingGameTests {
AgeFeatureSelection.resolved(plan), "Features must survive the saved-plan round trip");
for (AgeFeatureSelection.Choice choice : AgeFeatureSelection.resolved(plan)) {
if (AgeFeatureSelection.caves(choice.id()) || AgeFeatureSelection.ravines(choice.id())
|| AgeFeatureSelection.floatingIslands(choice.id())) continue;
|| AgeFeatureSelection.floatingIslands(choice.id())
|| AgeFeatureSelection.tendrils(choice.id())) continue;
var placement = AgeFeatureSelection.placedFeature(choice.id());
helper.assertTrue(registries.lookupOrThrow(Registries.PLACED_FEATURE)
.get(ResourceKey.create(Registries.PLACED_FEATURE, placement)).isPresent(),
@@ -136,6 +137,47 @@ public final class FeatureWritingGameTests {
helper.succeed();
}
@GameTest(template = "empty")
public static void tendrilsAreAuthoredAndCanAppearInRandomAges(GameTestHelper helper) {
var registries = helper.getLevel().registryAccess();
AgeRuntimeProfiles.profiles().forEach((profileId, profile) ->
helper.assertTrue(AgeFeatureSelection.error(id("tendrils"), profile, registries) == null,
"Tendrils must support loaded terrain profile " + profileId));
for (String terrain : List.of("standard", "amplified", "island", "flat", "void", "cave")) {
OrderedWriting writing = new OrderedWriting(List.of(panel(), page("terrain/" + terrain),
page("feature/tendrils")));
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 450L, AgeGenerationProfiles.STANDARD,
AgeTraits.select(450L), registries);
List<AgeFeatureSelection.Choice> choices = AgeFeatureSelection.resolved(plan);
helper.assertValueEqual(choices.size(), 1, "Tendrils must compile on " + terrain + " terrain");
helper.assertValueEqual(choices.getFirst().id(), id("tendrils"),
"Tendrils must not replace the terrain page");
helper.assertTrue(choices.getFirst().material().isPresent()
&& AgeFeatureSelection.materialError(choices.getFirst()) == null,
"Unmodified Tendrils must choose a full-block material");
helper.assertValueEqual(AgeFeatureSelection.resolved(AgeGenerationPlan.load(plan.save())), choices,
"The Tendrils material must survive a plan reload");
}
OrderedWriting materialWriting = new OrderedWriting(List.of(panel(), page("terrain/void"),
block("glowstone"), page("feature/tendrils")));
AgeGenerationPlan materialPlan = AgeGenerationPlan.compile(materialWriting, 450L,
AgeGenerationProfiles.STANDARD, AgeTraits.select(450L), registries);
helper.assertValueEqual(AgeFeatureSelection.resolved(materialPlan), List.of(
new AgeFeatureSelection.Choice(id("tendrils"), Optional.of(
ResourceLocation.withDefaultNamespace("glowstone")))),
"A preceding Block page must set the Tendrils material");
var caveProfile = AgeRuntimeProfiles.resolve(id("cave"));
boolean found = false;
for (long seed = 0; seed < 12; seed++) {
List<AgeFeatureSelection.Choice> random = AgeFeatureSelection.randomChoices(seed, caveProfile, registries);
helper.assertTrue(random.stream().allMatch(choice -> AgeFeatureSelection.tendrils(choice.id())),
"Cave random Ages must not choose incompatible surface features");
if (!random.isEmpty()) found = true;
}
helper.assertTrue(found, "A page-free random Cave Age must be able to roll Tendrils");
helper.succeed();
}
@GameTest(template = "empty")
public static void voidBiomesAreIndependentOfFloatingIslands(GameTestHelper helper) {
OrderedWriting voidDefault = new OrderedWriting(List.of(panel(), page("terrain/void")));
@@ -275,7 +317,8 @@ public final class FeatureWritingGameTests {
helper.assertTrue(material.equals("water")
? AgeFeatureSelection.isLake(choices.getFirst().id())
: AgeFeatureSelection.isRock(choices.getFirst().id())
|| AgeFeatureSelection.floatingIslands(choices.getFirst().id()),
|| AgeFeatureSelection.floatingIslands(choices.getFirst().id())
|| AgeFeatureSelection.tendrils(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");
@@ -75,13 +75,18 @@ public final class AgeFeatureChunkGenerator extends NoiseBasedChunkGenerator {
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState random,
StructureManager structures, ChunkAccess chunk) {
CompletableFuture<ChunkAccess> filled = super.fillFromNoise(blender, random, structures, chunk);
if (features.stream().noneMatch(choice -> AgeFeatureSelection.floatingIslands(choice.id()))) return filled;
if (features.stream().noneMatch(choice -> AgeFeatureSelection.floatingIslands(choice.id())
|| AgeFeatureSelection.tendrils(choice.id()))) return filled;
return filled.thenApply(result -> {
int occurrence = 0;
Map<ResourceLocation, Integer> occurrences = new HashMap<>();
for (AgeFeatureSelection.Choice choice : features) {
if (!AgeFeatureSelection.floatingIslands(choice.id())) continue;
FloatingIslandFeature.generate(result, getBiomeSource(), random,
ageSeed ^ (long) occurrence++ * 0x9E3779B97F4A7C15L, choice);
if (!AgeFeatureSelection.floatingIslands(choice.id())
&& !AgeFeatureSelection.tendrils(choice.id())) continue;
int occurrence = occurrences.merge(choice.id(), 1, Integer::sum) - 1;
long featureSeed = ageSeed ^ (long) occurrence * 0x9E3779B97F4A7C15L;
if (AgeFeatureSelection.floatingIslands(choice.id()))
FloatingIslandFeature.generate(result, getBiomeSource(), random, featureSeed, choice);
else TendrilFeature.generate(result, featureSeed, choice);
}
return result;
});
@@ -3,7 +3,9 @@ package net.dnorberg.rivencraft.world;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
@@ -12,6 +14,7 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.HolderSet;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
@@ -97,11 +100,15 @@ public final class AgeFlatChunkGenerator extends ChunkGenerator {
}
}
}
int islandOccurrence = 0;
Map<ResourceLocation, Integer> occurrences = new HashMap<>();
for (AgeFeatureSelection.Choice choice : features) {
if (!AgeFeatureSelection.floatingIslands(choice.id())) continue;
FloatingIslandFeature.generate(chunk, biomeSource, random,
ageSeed ^ (long) islandOccurrence++ * 0x9E3779B97F4A7C15L, choice);
if (!AgeFeatureSelection.floatingIslands(choice.id())
&& !AgeFeatureSelection.tendrils(choice.id())) continue;
int occurrence = occurrences.merge(choice.id(), 1, Integer::sum) - 1;
long featureSeed = ageSeed ^ (long) occurrence * 0x9E3779B97F4A7C15L;
if (AgeFeatureSelection.floatingIslands(choice.id()))
FloatingIslandFeature.generate(chunk, biomeSource, random, featureSeed, choice);
else TendrilFeature.generate(chunk, featureSeed, choice);
}
return CompletableFuture.completedFuture(chunk);
}
@@ -0,0 +1,150 @@
package net.dnorberg.rivencraft.world;
import java.util.Random;
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.util.Mth;
import net.minecraft.world.level.ChunkPos;
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;
/**
* Winding solid volumes adapted from Mystcraft Legacy's SymbolTendrils and
* MapGenCavesMyst (LGPL-3.0-or-later). Generation stays inside the target chunk,
* while source-chunk seeds make the same tendril meet seamlessly at its borders.
*/
final class TendrilFeature {
private static final long SALT = 0x54454E4452494C53L;
private static final int SOURCE_RANGE = 8;
private static final int SOURCE_RATE = 15;
private static final int SOURCE_SIZE = 18;
private TendrilFeature() {}
static void generate(ChunkAccess chunk, long ageSeed, AgeFeatureSelection.Choice choice) {
BlockState material = choice.material()
.map(id -> BuiltInRegistries.BLOCK.get(id).defaultBlockState())
.orElse(Blocks.OAK_LOG.defaultBlockState());
ChunkPos target = chunk.getPos();
Random master = new Random(ageSeed ^ SALT);
long xSeed = master.nextLong();
long zSeed = master.nextLong();
for (int sourceX = target.x - SOURCE_RANGE; sourceX <= target.x + SOURCE_RANGE; sourceX++) {
for (int sourceZ = target.z - SOURCE_RANGE; sourceZ <= target.z + SOURCE_RANGE; sourceZ++) {
Random source = new Random((long) sourceX * xSeed ^ (long) sourceZ * zSeed ^ ageSeed ^ SALT);
int nodes = source.nextInt(source.nextInt(source.nextInt(SOURCE_SIZE) + 1) + 1);
if (source.nextInt(SOURCE_RATE) != 0) continue;
for (int node = 0; node < nodes; node++) {
double x = sourceX * 16 + source.nextInt(16);
double y = source.nextInt(source.nextInt(120) + 8);
double z = sourceZ * 16 + source.nextInt(16);
int branches = 1;
if (source.nextInt(4) == 0) {
trace(chunk, material, source.nextLong(), x, y, z,
1.0F + source.nextFloat() * 6.0F, 0.0F, 0.0F, -1, -1, 0.5D);
branches += source.nextInt(4);
}
for (int branch = 0; branch < branches; branch++) {
float yaw = source.nextFloat() * Mth.TWO_PI;
float pitch = (source.nextFloat() - 0.5F) / 4.0F;
float radius = source.nextFloat() * 2.0F + source.nextFloat();
if (source.nextInt(10) == 0)
radius *= source.nextFloat() * source.nextFloat() * 3.0F + 1.0F;
trace(chunk, material, source.nextLong(), x, y, z,
radius, yaw, pitch, 0, 0, 1.0D);
}
}
}
}
}
private static void trace(ChunkAccess chunk, BlockState material, long seed,
double x, double y, double z, float radius, float yaw, float pitch,
int step, int length, double verticalScale) {
Random random = new Random(seed);
if (length <= 0) {
int maximum = SOURCE_RANGE * 16 - 16;
length = maximum - random.nextInt(maximum / 4);
}
boolean singleNode = step == -1;
if (singleNode) step = length / 2;
int splitAt = random.nextInt(length / 2) + length / 4;
boolean slowPitch = random.nextInt(6) == 0;
float yawDrift = 0.0F;
float pitchDrift = 0.0F;
double middleX = chunk.getPos().getMinBlockX() + 8;
double middleZ = chunk.getPos().getMinBlockZ() + 8;
for (; step < length; step++) {
double horizontalRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
double verticalRadius = horizontalRadius * verticalScale;
float horizontalMotion = Mth.cos(pitch);
x += Mth.cos(yaw) * horizontalMotion;
y += Mth.sin(pitch);
z += Mth.sin(yaw) * horizontalMotion;
pitch *= slowPitch ? 0.92F : 0.7F;
pitch += pitchDrift * 0.1F;
yaw += yawDrift * 0.1F;
pitchDrift = pitchDrift * 0.9F
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
yawDrift = yawDrift * 0.75F
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
if (!singleNode && step == splitAt && radius > 1.0F) {
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
yaw - Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
yaw + Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
return;
}
if (!singleNode && random.nextInt(4) == 0) continue;
double distanceX = x - middleX;
double distanceZ = z - middleZ;
double remaining = length - step;
double reach = radius + 18.0D;
if (distanceX * distanceX + distanceZ * distanceZ - remaining * remaining > reach * reach)
return;
if (x >= middleX - 16.0D - horizontalRadius * 2.0D
&& x <= middleX + 16.0D + horizontalRadius * 2.0D
&& z >= middleZ - 16.0D - horizontalRadius * 2.0D
&& z <= middleZ + 16.0D + horizontalRadius * 2.0D) {
fillNode(chunk, material, x, y, z, horizontalRadius, verticalRadius);
}
if (singleNode) break;
}
}
private static void fillNode(ChunkAccess chunk, BlockState material, double centerX, double centerY,
double centerZ, double horizontalRadius, double verticalRadius) {
int originX = chunk.getPos().getMinBlockX();
int originZ = chunk.getPos().getMinBlockZ();
int minX = Math.max(0, Mth.floor(centerX - horizontalRadius) - originX - 1);
int maxX = Math.min(16, Mth.floor(centerX + horizontalRadius) - originX + 2);
int minZ = Math.max(0, Mth.floor(centerZ - horizontalRadius) - originZ - 1);
int maxZ = Math.min(16, Mth.floor(centerZ + horizontalRadius) - originZ + 2);
int minY = Math.max(chunk.getMinBuildHeight() + 1, Mth.floor(centerY - verticalRadius) - 1);
int maxY = Math.min(chunk.getMaxBuildHeight(), Mth.floor(centerY + verticalRadius) + 2);
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
for (int worldY = minY; worldY < maxY; worldY++) {
double dy = (worldY + 0.5D - centerY) / verticalRadius;
if (dy <= -0.7D || dy * dy >= 1.0D) continue;
for (int localZ = minZ; localZ < maxZ; localZ++) {
double dz = (originZ + localZ + 0.5D - centerZ) / horizontalRadius;
for (int localX = minX; localX < maxX; localX++) {
double dx = (originX + localX + 0.5D - centerX) / horizontalRadius;
if (dx * dx + dy * dy + dz * dz >= 1.0D) continue;
pos.set(originX + localX, worldY, originZ + localZ);
if (chunk.getBlockState(pos).is(Blocks.BEDROCK)) continue;
chunk.setBlockState(pos, material, false);
surface.update(localX, worldY, localZ, material);
floor.update(localX, worldY, localZ, material);
}
}
}
}
}
@@ -77,7 +77,7 @@ public final class WritingGrammar {
pendingBlock = parsed;
} 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")
if (!Set.of("rocks", "surface_lakes", "deep_lakes", "lava_lakes", "floating_islands", "tendrils")
.contains(feature.getPath()) || !feature.getNamespace().equals("rivencraft")) {
diagnostics.add(new Diagnostic(Code.MISPLACED_MODIFIER, pendingBlock.index(),
"This feature cannot consume a material page"));
@@ -245,6 +245,8 @@
"symbol.rivencraft.feature.ravines.description": "Add vanilla canyon carving to noise terrain.",
"symbol.rivencraft.feature.floating_islands": "Floating Islands",
"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.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.tendrils",
"description": "symbol.rivencraft.feature.tendrils.description",
"category": "rivencraft:feature",
"semantics": { "role": "feature", "value": "rivencraft:tendrils" },
"poem": "Terrain Thread Structure Winding"
}