Add subterrain features

- Generate authored underground rooms, materials, fluids, and formations.
- Preserve underground biome ownership across generation and saved terrain.
- Add subterrain searches and regression coverage.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-04 09:59:57 -04:00
1 parent be0d3b0156
commit 5858eb366d
76 files changed
+4187 -201

No files matched your search

@@ -163,7 +163,7 @@ final class DecoratorCore {
bd.withProperty("half", bottomHalfValue(half)),
data, x, z, realX, lowerY, realZ, mantle);
data.set(x, lowerY, z, lower);
data.set(x, upperY, z, upper);
data.set(x, upperY, z, IrisProceduralBlocks.normalizeWaterlogging(upper, data.get(x, upperY, z), true));
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
@@ -210,7 +210,7 @@ final class DecoratorCore {
if (applyFixFaces) {
bd = fixFacesForHunk(bd, data, x, z, realX, height, realZ, mantle);
}
data.set(x, height, z, bd);
data.set(x, height, z, IrisProceduralBlocks.normalizeWaterlogging(bd, data.get(x, height, z), true));
}
static void placeStackUp(IrisDecorator decorator, int x, int z, int realX, int realZ,
@@ -258,7 +258,7 @@ final class DecoratorCore {
} else if (i == 0 && !opts.underwater && !canGoOn(block, support)) {
break;
}
data.set(x, y, z, stackedVineBlock(block, stack, i));
data.set(x, y, z, IrisProceduralBlocks.normalizeWaterlogging(stackedVineBlock(block, stack, i), existing, true));
placed++;
}
if (placed > 0 && placed < stack) {
@@ -356,8 +356,8 @@ final class DecoratorCore {
try {
NativeBlockState upper = bd.withProperty("half", topHalfValue(half));
NativeBlockState lower = bd.withProperty("half", bottomHalfValue(half));
data.set(xf, lowerY, zf, lower);
data.set(xf, upperY, zf, upper);
data.set(xf, lowerY, zf, IrisProceduralBlocks.normalizeWaterlogging(lower, data.get(xf, lowerY, zf), true));
data.set(xf, upperY, zf, IrisProceduralBlocks.normalizeWaterlogging(upper, data.get(xf, upperY, zf), true));
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
@@ -366,7 +366,7 @@ final class DecoratorCore {
}
if (max > 1 && height + 1 < data.getHeight()) {
data.set(xf, height + 1, zf, bd);
data.set(xf, height + 1, zf, IrisProceduralBlocks.normalizeWaterlogging(bd, data.get(xf, height + 1, zf), true));
}
}
@@ -411,7 +411,7 @@ final class DecoratorCore {
hasSpikes = true;
}
bd = stackedVineBlock(bd, stack, i);
data.set(xf, h, zf, bd);
data.set(xf, h, zf, IrisProceduralBlocks.normalizeWaterlogging(bd, data.get(xf, h, zf), true));
placed++;
}
if (placed > 0 && placed < stack) {
@@ -433,6 +433,7 @@ final class DecoratorCore {
static NativeBlockState fixFacesForHunk(NativeBlockState b, Hunk<NativeBlockState> hunk, int rX, int rZ,
int x, int y, int z, EngineMantle mantle) {
b = IrisProceduralBlocks.normalizeWaterlogging(b, hunk.get(rX, y, rZ), true);
if (!B.isVineBlock(b)) {
return b;
}
@@ -539,7 +540,8 @@ final class DecoratorCore {
if (!IrisSpeleothems.canPlace(spike, data, x, z, y, upward, allowWater)) {
return;
}
data.set(x, y, z, IrisSpeleothems.orient(spike, data.get(x, y, z), upward));
NativeBlockState existing = data.get(x, y, z);
data.set(x, y, z, IrisProceduralBlocks.normalizeWaterlogging(IrisSpeleothems.orient(spike, existing, upward), existing, true));
IrisSpeleothems.finishColumn(data, x, z, y, 1, upward);
}
@@ -123,6 +123,9 @@ public final class IrisProceduralBlocks {
* rather than building the search needles, and prefers a "[" match over a "," match.
*/
private static int propertyStart(String key, String property) {
if (key == null) {
return -1;
}
int bracket = key.indexOf('[');
if (bracket < 0) {
return -1;
@@ -142,6 +145,16 @@ public final class IrisProceduralBlocks {
return -1;
}
public static NativeBlockState normalizeWaterlogging(NativeBlockState state, NativeBlockState existing, boolean enabled) {
if (state == null || !hasProperty(state, "waterlogged")) {
return state;
}
boolean authored = "true".equals(propertyValue(state, "waterlogged"));
boolean submerged = existing != null && (existing.isWater() || "true".equals(propertyValue(existing, "waterlogged")));
String value = submerged && (enabled || authored) ? "true" : "false";
return value.equals(propertyValue(state, "waterlogged")) ? state : state.withProperty("waterlogged", value);
}
public static IrisObject assemble(Map<Vector3i, NativeBlockState> blocks) {
if (blocks == null || blocks.isEmpty()) {
return null;
@@ -19,6 +19,7 @@
package art.arcane.iris.generation.decoration;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.pack.loading.IrisData;
@@ -43,7 +44,7 @@ public interface IrisProceduralPlacement {
IrisObjectPlacement asPlacement();
IrisObject getVariantObject(IrisData data, RNG rng);
IrisObject getVariantObject(IrisData data, RNG rng, SubterrainRoom room);
KList<IrisObject> getVariantObjects(IrisData data);
}
@@ -81,10 +81,7 @@ public final class IrisSpeleothems {
}
static NativeBlockState orient(NativeBlockState state, NativeBlockState existing, boolean upward) {
if (existing != null && existing.isWater() && !state.isWaterLogged()) {
state = state.withProperty("waterlogged", "true");
}
return spikeBlock(state, upward, 0);
return spikeBlock(IrisProceduralBlocks.normalizeWaterlogging(state, existing, true), upward, 0);
}
static boolean isSturdy(NativeBlockState surface, boolean upward) {
@@ -20,6 +20,7 @@ package art.arcane.iris.generation.decoration.coral;
import art.arcane.iris.generation.cave.CarvingMode;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
@@ -220,7 +221,7 @@ public class IrisCoral implements IrisProceduralPlacement {
});
}
public IrisObject getVariantObject(IrisData data, RNG rng) {
public IrisObject getVariantObject(IrisData data, RNG rng, SubterrainRoom room) {
KList<IrisObject> baked = getVariantObjects(data);
if (baked == null || baked.isEmpty()) {
return null;
@@ -20,6 +20,7 @@ package art.arcane.iris.generation.decoration.crystal;
import art.arcane.iris.generation.cave.CarvingMode;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
@@ -201,7 +202,7 @@ public class IrisCrystal implements IrisProceduralPlacement {
});
}
public IrisObject getVariantObject(IrisData data, RNG rng) {
public IrisObject getVariantObject(IrisData data, RNG rng, SubterrainRoom room) {
KList<IrisObject> baked = getVariantObjects(data);
if (baked == null || baked.isEmpty()) {
return null;
@@ -37,6 +37,10 @@ public final class FormationGenerator {
int hi = Math.max(f.getHeightMin(), f.getHeightMax());
int height = Math.max(3, rng.i(lo, hi + 1));
return generate(f, variantIndex, rng, data, height);
}
public static IrisObject generate(IrisFormation f, int variantIndex, RNG rng, IrisData data, int height) {
int wLo = Math.min(f.getBaseWidthMin(), f.getBaseWidthMax());
int wHi = Math.max(f.getBaseWidthMin(), f.getBaseWidthMax());
double baseRadius = Math.max(1, rng.i(wLo, wHi + 1));
@@ -19,6 +19,7 @@
package art.arcane.iris.generation.decoration.formation;
import art.arcane.iris.generation.cave.CarvingMode;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
@@ -255,6 +256,26 @@ public class IrisFormation implements IrisProceduralPlacement {
@Description("How many blocks the tip of an OVERHANG curls downward.")
private int overhangDrop = 3;
@MinNumber(0)
@MaxNumber(1)
@Description("For cave placements in an authored subterrain room, size the formation to this fraction of the room vault. Zero uses heightMin and heightMax.")
private double roomHeightFraction = 0;
@Override
public IrisObject getVariantObject(IrisData data, RNG rng, SubterrainRoom room) {
if (room == null || roomHeightFraction <= 0) {
KList<IrisObject> baked = getVariantObjects(data);
return baked == null || baked.isEmpty() ? null : baked.get(rng.i(baked.size()));
}
IrisObject object = FormationGenerator.generate(this, rng.i(Math.max(1, variants)), rng, data,
Math.max(1, (int) Math.floor(room.vaultHeight() * Math.min(1D, roomHeightFraction))));
if (object != null) {
object.setLoadKey("procedural/" + name + "/" + room.featureId());
object.setLoader(data);
}
return object;
}
public KList<IrisObject> getVariantObjects(IrisData data) {
return variantCache.aquire(() -> {
KList<IrisObject> baked = new KList<>();
@@ -274,14 +295,6 @@ public class IrisFormation implements IrisProceduralPlacement {
});
}
public IrisObject getVariantObject(IrisData data, RNG rng) {
KList<IrisObject> baked = getVariantObjects(data);
if (baked == null || baked.isEmpty()) {
return null;
}
return baked.get(rng.i(baked.size()));
}
public IrisObjectPlacement asPlacement() {
IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setMode(mode);
@@ -20,6 +20,7 @@ package art.arcane.iris.generation.decoration.fungus;
import art.arcane.iris.generation.cave.CarvingMode;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
@@ -233,7 +234,7 @@ public class IrisFungus implements IrisProceduralPlacement {
});
}
public IrisObject getVariantObject(IrisData data, RNG rng) {
public IrisObject getVariantObject(IrisData data, RNG rng, SubterrainRoom room) {
KList<IrisObject> baked = getVariantObjects(data);
if (baked == null || baked.isEmpty()) {
return null;
@@ -20,6 +20,7 @@ package art.arcane.iris.generation.decoration.ruin;
import art.arcane.iris.generation.cave.CarvingMode;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
@@ -189,7 +190,7 @@ public class IrisRuin implements IrisProceduralPlacement {
});
}
public IrisObject getVariantObject(IrisData data, RNG rng) {
public IrisObject getVariantObject(IrisData data, RNG rng, SubterrainRoom room) {
KList<IrisObject> baked = getVariantObjects(data);
if (baked == null || baked.isEmpty()) {
return null;
@@ -20,6 +20,7 @@ package art.arcane.iris.generation.decoration.tree;
import art.arcane.iris.generation.cave.CarvingMode;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
@@ -311,7 +312,7 @@ public class IrisProceduralTree implements IrisProceduralPlacement {
});
}
public IrisObject getVariantObject(IrisData data, RNG rng) {
public IrisObject getVariantObject(IrisData data, RNG rng, SubterrainRoom room) {
KList<IrisObject> baked = getVariantObjects(data);
if (baked == null || baked.isEmpty()) {
return null;
@@ -0,0 +1,125 @@
package art.arcane.iris.generation.locator;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
public final class SubterrainLocator {
private static final int SEARCH_TILE_SIZE = 512;
private SubterrainLocator() {
}
public static Optional<Result> nearest(Engine engine, Query query, int x, int worldY, int z, int maximumDistance) {
return nearest(engine, query, x, worldY, z, maximumDistance, ignored -> true);
}
public static Optional<Result> nearest(Engine engine, Query query, int x, int worldY, int z,
int maximumDistance, Predicate<Result> allowed) {
Objects.requireNonNull(engine, "engine");
return nearest(engine.getComplex().getSubterrainPlanner(), query, x, worldY, z, maximumDistance, allowed);
}
public static Optional<Result> nearest(SubterrainPlanner planner, Query query, int x, int worldY, int z,
int maximumDistance, Predicate<Result> allowed) {
Objects.requireNonNull(planner, "planner");
Objects.requireNonNull(query, "query");
Objects.requireNonNull(allowed, "allowed");
if (maximumDistance < 0 || maximumDistance > 1_048_576) {
throw new IllegalArgumentException("Subterrain search distance must be between 0 and 1048576 blocks");
}
if (planner.isEmpty()) {
return Optional.empty();
}
long centerTileX = Math.floorDiv(x, SEARCH_TILE_SIZE);
long centerTileZ = Math.floorDiv(z, SEARCH_TILE_SIZE);
int tileRadius = maximumDistance / SEARCH_TILE_SIZE + 1;
Set<String> visited = new HashSet<>();
Result nearest = null;
double nearestDistance = (double) maximumDistance * maximumDistance;
int inspectedTiles = 0;
for (int ring = 0; ring <= tileRadius; ring++) {
for (int offsetX = -ring; offsetX <= ring; offsetX++) {
int offsetZStep = Math.abs(offsetX) == ring ? 1 : Math.max(1, ring * 2);
for (int offsetZ = -ring; offsetZ <= ring; offsetZ += offsetZStep) {
long tileX = (centerTileX + offsetX) * SEARCH_TILE_SIZE;
long tileZ = (centerTileZ + offsetZ) * SEARCH_TILE_SIZE;
long tileMaxX = tileX + SEARCH_TILE_SIZE - 1L;
long tileMaxZ = tileZ + SEARCH_TILE_SIZE - 1L;
if (tileMaxX < Integer.MIN_VALUE || tileX > Integer.MAX_VALUE
|| tileMaxZ < Integer.MIN_VALUE || tileZ > Integer.MAX_VALUE) {
continue;
}
double deltaTileX = x < tileX ? tileX - x : x > tileMaxX ? (long) x - tileMaxX : 0;
double deltaTileZ = z < tileZ ? tileZ - z : z > tileMaxZ ? (long) z - tileMaxZ : 0;
if (deltaTileX * deltaTileX + deltaTileZ * deltaTileZ > nearestDistance) {
continue;
}
if (++inspectedTiles > 65_536) {
throw new IllegalStateException("Subterrain search exceeded 65536 analytic tiles; use a smaller search radius");
}
for (SubterrainPlan plan : planner.plansForBounds(
(int) Math.max(Integer.MIN_VALUE, tileX), (int) Math.max(Integer.MIN_VALUE, tileZ),
(int) Math.min(Integer.MAX_VALUE, tileMaxX), (int) Math.min(Integer.MAX_VALUE, tileMaxZ))) {
if (!visited.add(plan.id()) || !query.matches(plan)) {
continue;
}
SubterrainPosition anchor = plan.anchor();
double deltaX = (double) anchor.x() - x;
double deltaY = (double) anchor.y() - worldY;
double deltaZ = (double) anchor.z() - z;
double distance = deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ;
if (distance > nearestDistance) {
continue;
}
SubterrainCell owner = planner.sample(anchor.x(), anchor.y(), anchor.z());
if (!owner.occupied() || !owner.room().featureId().equals(plan.id())) {
continue;
}
Result candidate = new Result(plan.id(), plan.family(), plan.biome(), anchor.x(), anchor.y(), anchor.z());
if (!allowed.test(candidate) || nearest != null && distance == nearestDistance
&& candidate.featureId().compareTo(nearest.featureId()) >= 0) {
continue;
}
nearest = candidate;
nearestDistance = distance;
}
}
}
long leftDistance = (long) x - (centerTileX - ring) * SEARCH_TILE_SIZE + 1;
long rightDistance = (centerTileX + ring + 1) * SEARCH_TILE_SIZE - x;
long frontDistance = (long) z - (centerTileZ - ring) * SEARCH_TILE_SIZE + 1;
long backDistance = (centerTileZ + ring + 1) * SEARCH_TILE_SIZE - z;
long remainingDistance = Math.min(Math.min(leftDistance, rightDistance), Math.min(frontDistance, backDistance));
if ((double) remainingDistance * remainingDistance > nearestDistance) {
break;
}
}
return Optional.ofNullable(nearest);
}
public record Query(String featureId, IrisSubterrainFamily family, String biome) {
public Query {
featureId = Objects.requireNonNullElse(featureId, "").trim();
biome = Objects.requireNonNullElse(biome, "").trim();
}
public boolean matches(SubterrainPlan plan) {
return (featureId.isEmpty() || plan.id().equals(featureId) || plan.id().startsWith(featureId + ":"))
&& (family == null || plan.family() == family)
&& (biome.isEmpty() || plan.biome().equals(biome));
}
}
public record Result(String featureId, IrisSubterrainFamily family, String biome, int x, int y, int z) {
}
}
@@ -19,6 +19,8 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.generation.cache.Cache;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
@@ -79,6 +81,11 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
discard();
return CommitResult.REJECTED_BOUNDS;
}
SubterrainCell feature = engine.getSubterrainCell(mutation.x(), mutation.y(), mutation.z());
if (SubterrainRasterizer.protectsPlacement(feature)) {
discard();
return CommitResult.REJECTED_SUBTERRAIN;
}
HydrologyCaveCell hydrology = delegate.getData(
mutation.x(), mutation.y(), mutation.z(), HydrologyCaveCell.class);
if (hydrology != null && hydrology.protectsPlacement()) {
@@ -173,6 +180,15 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
return delegate.getFluidHeight();
}
@Override
public int getFluidHeight(int x, int z) {
SubterrainCell feature = engine == null ? null : engine.getSubterrainCell(x, anchorY, z);
if (feature != null && feature.fluid()) {
return feature.room().fluidHeadY() + 1 - engine.getMinHeight();
}
return delegate.getFluidHeight(x, z);
}
@Override
public boolean isDebugSmartBore() {
return delegate.isDebugSmartBore();
@@ -261,7 +277,8 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
EMPTY,
REJECTED_TRANSITION,
REJECTED_BOUNDS,
REJECTED_HYDROLOGY
REJECTED_HYDROLOGY,
REJECTED_SUBTERRAIN
}
private interface BufferedMutation {
@@ -19,6 +19,9 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.UpperDimensionContext;
import art.arcane.iris.generation.biome.IrisBiome;
@@ -38,6 +41,7 @@ import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.TreeSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.IdentityHashMap;
@@ -160,6 +164,31 @@ public class MantleCarvingComponent extends IrisMantleComponent {
surfaceFluidBoundaries
);
}
rasterizeSubterrain(writer, x, z, complex);
}
private void rasterizeSubterrain(MantleWriter writer, int chunkX, int chunkZ, IrisComplex complex) {
SubterrainPlanner planner = complex.getSubterrainPlanner();
if (planner == null || planner.isEmpty()) {
return;
}
int minimumY = getEngineMantle().getEngine().getWorld().minHeight();
SubterrainRasterizer.rasterize(planner, chunkX, chunkZ, (x, worldY, z, cell) -> {
int y = worldY - minimumY;
if (y <= 0 || y >= writer.getMantle().getWorldHeight()) {
return;
}
if (cell.solid()) {
writer.clearData(x, y, z, MatterCavern.class);
} else {
writer.setData(x, y, z, SubterrainRasterizer.cavern(cell));
}
if (cell.solid()) {
writer.setData(x, y, z, SubterrainRasterizer.state(cell));
} else {
writer.clearData(x, y, z, NativeBlockState.class);
}
});
}
private static int maxSurfaceBreakDepth(List<WeightedProfile> weightedProfiles) {
@@ -343,33 +372,45 @@ public class MantleCarvingComponent extends IrisMantleComponent {
continue;
}
Map<IrisCaveProfile, double[]> rootProfileColumnWeights = new IdentityHashMap<>();
List<IrisCaveProfile> rootProfileOrder = new ArrayList<>();
IrisRange worldYRange = entry.getWorldYRange();
for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) {
IrisDimensionCarvingEntry resolvedEntry = IrisDimensionCarvingResolver.resolveFromRoot(
engine, entry, baseX + (columnIndex >> 4), baseZ + (columnIndex & 15), resolverState);
IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(engine, resolvedEntry, resolverState);
if (resolvedBiome == null) {
continue;
TreeSet<Integer> boundaries = new TreeSet<>();
int minimumY = engine.getWorld().minHeight();
int maximumY = engine.getWorld().maxHeight();
boundaries.add(minimumY);
boundaries.add(maximumY);
for (IrisDimensionCarvingEntry candidate : entries) {
if (candidate != null && candidate.getWorldYRange() != null) {
boundaries.add(Math.max(minimumY, Math.min(maximumY, (int) Math.ceil(candidate.getWorldYRange().getMin()))));
boundaries.add(Math.max(minimumY, Math.min(maximumY, (int) Math.floor(candidate.getWorldYRange().getMax()) + 1)));
}
IrisCaveProfile profile = resolvedBiome.getCaveProfile();
if (!isProfileEnabled(profile)) {
continue;
}
double[] columnWeights = rootProfileColumnWeights.get(profile);
if (columnWeights == null) {
columnWeights = blendScratch.acquireWeights();
rootProfileColumnWeights.put(profile, columnWeights);
rootProfileOrder.add(profile);
}
columnWeights[columnIndex] = 1D;
}
for (IrisCaveProfile profile : rootProfileOrder) {
weightedProfiles.add(new WeightedProfile(profile, rootProfileColumnWeights.get(profile), -1D, worldYRange, weightedProfiles.size()));
Integer previous = null;
for (int boundary : boundaries) {
if (previous != null && boundary > previous
&& (entry.getWorldYRange() == null || entry.getWorldYRange().contains(previous))) {
Map<IrisCaveProfile, double[]> rootProfileColumnWeights = new IdentityHashMap<>();
List<IrisCaveProfile> rootProfileOrder = new ArrayList<>();
for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) {
IrisDimensionCarvingEntry resolvedEntry = IrisDimensionCarvingResolver.resolveFromRoot(
engine, entry, baseX + (columnIndex >> 4), previous, baseZ + (columnIndex & 15), resolverState);
IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(engine, resolvedEntry, resolverState);
IrisCaveProfile profile = resolvedBiome == null ? null : resolvedBiome.getCaveProfile();
if (!isProfileEnabled(profile)) {
continue;
}
double[] columnWeights = rootProfileColumnWeights.get(profile);
if (columnWeights == null) {
columnWeights = blendScratch.acquireWeights();
rootProfileColumnWeights.put(profile, columnWeights);
rootProfileOrder.add(profile);
}
columnWeights[columnIndex] = 1D;
}
IrisRange worldYRange = new IrisRange(previous, boundary - 1);
for (IrisCaveProfile profile : rootProfileOrder) {
weightedProfiles.add(new WeightedProfile(profile, rootProfileColumnWeights.get(profile), -1D, worldYRange, weightedProfiles.size()));
}
}
previous = boundary;
}
}
@@ -444,7 +485,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(getEngineMantle().getEngine(), worldY, resolverState);
IrisBiome resolvedCarvingBiome = null;
if (rootCarvingEntry != null) {
IrisDimensionCarvingEntry resolvedCarvingEntry = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), rootCarvingEntry, worldX, worldZ, resolverState);
IrisDimensionCarvingEntry resolvedCarvingEntry = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), rootCarvingEntry, worldX, worldY, worldZ, resolverState);
resolvedCarvingBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedCarvingEntry, resolverState);
if (resolvedCarvingBiome != null) {
caveBiome = resolvedCarvingBiome;
@@ -12,6 +12,7 @@ import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.hydrology.IrisDeepFluidConfig;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.hydrology.IrisHydrology;
@@ -765,17 +766,33 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
}
for (Map.Entry<CavePosition, NativeBlockState> entry : resolvedSurfaceWrites.entrySet()) {
CavePosition position = entry.getKey();
if (featureOwns(writer, context, position)) {
continue;
}
writer.setData(position.x(), position.y(), position.z(), entry.getValue());
}
for (CavePosition position : sortedPositions(publication.fluidUpdates())) {
if (featureOwns(writer, context, position)) {
continue;
}
writer.setData(position.x(), position.y(), position.z(), UpdateMatter.ON);
}
for (Map.Entry<CavePosition, HydrologyCaveCell> entry : sortedEntries(publication.caveCells())) {
CavePosition position = entry.getKey();
if (featureOwns(writer, context, position)) {
continue;
}
writer.setData(position.x(), position.y(), position.z(), entry.getValue());
}
}
private static boolean featureOwns(MantleWriter writer, ChunkContext context, CavePosition position) {
IrisComplex complex = context.getComplex();
SubterrainPlanner planner = complex.getSubterrainPlanner();
return planner != null && !planner.isEmpty()
&& planner.sample(position.x(), position.y() + writer.getEngine().getWorld().minHeight(), position.z()).owned();
}
private static void validateLayer(HydrologyColumnLayer layer, int worldHeight) {
if (layer.bedY() < 0
|| layer.fluidHeadY() < layer.bedY()
@@ -37,6 +37,8 @@ import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.structure.object.IrisObjectPlacementAlignment;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.structure.object.IrisObjectTranslate;
import art.arcane.iris.structure.object.IrisObjectVacuum;
@@ -46,6 +48,10 @@ import art.arcane.iris.generation.decoration.tree.IrisProceduralTree;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.structure.object.ObjectPlaceMode;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -69,9 +75,11 @@ import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
@ComponentFlag(ReservedFlag.OBJECT)
public class MantleObjectComponent extends IrisMantleComponent {
@@ -250,7 +258,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
+ " regionCavePlacers=" + region.getCarvingObjects().size());
}
ObjectPlacementSummary summary = placeObjects(writer, rng, x, z, surfaceBiome, caveBiome, region, complex, traceRegen);
placeProceduralObjects(writer, rng, x, z, surfaceBiome, caveBiome, region);
placeProceduralObjects(writer, rng, x, z, surfaceBiome, caveBiome, region, complex);
UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext();
IrisDimension dimension = getDimension();
if (upperCtx != null && dimension.isUpperDimensionObjects()) {
@@ -603,7 +611,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
@ChunkCoordinates
private void placeProceduralObjects(ObjectPassPlacer writer, RNG rng, int x, int z, IrisBiome surfaceBiome, IrisBiome caveBiome, IrisRegion region) {
private void placeProceduralObjects(ObjectPassPlacer writer, RNG rng, int x, int z, IrisBiome surfaceBiome, IrisBiome caveBiome, IrisRegion region, IrisComplex complex) {
IrisCaveProfile surfaceCaveProfile = resolveCaveProfile(surfaceBiome.getCaveProfile(), region.getCaveProfile());
IrisCaveProfile regionCaveProfile = resolveCaveProfile(region.getCaveProfile(), caveBiome == null ? null : caveBiome.getCaveProfile());
placeProceduralFrom(writer, rng, x, z, surfaceBiome.getProceduralObjects(), surfaceBiome.getName(), surfaceCaveProfile, surfaceBiome.getLoadKey());
@@ -612,6 +620,23 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisCaveProfile caveProfile = resolveCaveProfile(caveBiome.getCaveProfile(), region.getCaveProfile());
placeProceduralFrom(writer, rng, x, z, caveBiome.getProceduralObjects(), caveBiome.getName(), caveProfile, caveBiome.getLoadKey());
}
if (complex.getSubterrainPlanner() != null && !complex.getSubterrainPlanner().isEmpty()) {
Set<String> placed = new HashSet<>();
placed.add(surfaceBiome.getLoadKey());
if (caveBiome != null) {
placed.add(caveBiome.getLoadKey());
}
for (SubterrainPlan plan : complex.getSubterrainPlanner().plansForBounds(x << 4, z << 4, (x << 4) + 15, (z << 4) + 15)) {
if (plan.biome().isEmpty() || !placed.add(plan.biome())) {
continue;
}
IrisBiome featureBiome = getData().getBiomeLoader().load(plan.biome());
if (featureBiome != null) {
placeProceduralFrom(writer, rng, x, z, featureBiome.getProceduralObjects(), featureBiome.getName(),
resolveCaveProfile(featureBiome.getCaveProfile(), region.getCaveProfile()), featureBiome.getLoadKey());
}
}
}
}
@ChunkCoordinates
@@ -660,16 +685,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
IObjectPlacer placer = golden ? new GoldenDebugObjectPlacer(basePlacer, scope + "/" + p.getName()) : basePlacer;
int density = Math.max(1, p.getDensity());
for (int i = 0; i < density; i++) {
IrisObject variant = placement.scaleObject(rng, p.getVariantObject(getData(), rng), getDimension());
if (variant == null) {
if (golden) {
IrisLogging.debug("Goldendebug procedural pick: chunk=" + x + "," + z
+ " placement=" + p.getName()
+ " densityIndex=" + i
+ " variant=null");
}
continue;
}
CavePlacementAnchor caveAnchor = null;
if (carving) {
caveAnchor = findCavePlacementAnchor(
@@ -694,6 +709,16 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
continue;
}
IrisObject variant = placement.scaleObject(rng, p.getVariantObject(getData(), rng, proceduralRoom(caveAnchor)), getDimension());
if (variant == null) {
if (golden) {
IrisLogging.debug("Goldendebug procedural pick: chunk=" + x + "," + z
+ " placement=" + p.getName()
+ " densityIndex=" + i
+ " variant=null");
}
continue;
}
int xx = caveAnchor == null ? rng.i(blockX, blockX + 16) : caveAnchor.x();
int zz = caveAnchor == null ? rng.i(blockZ, blockZ + 16) : caveAnchor.z();
int id = rng.i(0, Integer.MAX_VALUE);
@@ -730,6 +755,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
caveFloorY,
zz,
effectivePlacement,
anchorMode == IrisCaveAnchorMode.FLOOR && effectivePlacement.getMode() != ObjectPlaceMode.CEILING_HANG
? IrisObjectPlacementAlignment.FLOOR : IrisObjectPlacementAlignment.CENTER,
minDepthBelowSurface,
id,
"procedural",
@@ -773,6 +800,14 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
}
private SubterrainRoom proceduralRoom(CavePlacementAnchor anchor) {
if (anchor == null) {
return null;
}
SubterrainCell cell = getEngineMantle().getEngine().getSubterrainCell(anchor.x(), anchor.y(), anchor.z());
return cell == null ? null : cell.room();
}
static boolean passesProceduralChance(RNG rng, double chance) {
if (chance <= 0.0) {
return false;
@@ -827,6 +862,11 @@ public class MantleObjectComponent extends IrisMantleComponent {
private boolean acceptsCaveAnchorAt(int x, int y, int z, boolean underwater,
MatterCavern cavern, HydrologyCaveCell hydrology) {
SubterrainCell feature = getEngineMantle().getEngine().getSubterrainCell(x, y, z);
if (feature != null && feature.owned()) {
return !SubterrainRasterizer.protectsPlacement(feature)
&& (underwater ? feature.fluid() : feature.kind() == SubterrainCell.Kind.AIR);
}
return acceptsCaveAnchorFluid(underwater, hydrology == null ? cavern : hydrology.asCavern(),
hydrology, y, getDimension().getCaveLavaHeight());
}
@@ -862,6 +902,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int anchorY,
int z,
IrisObjectPlacement placement,
IrisObjectPlacementAlignment alignment,
int minDepthBelowSurface,
int id,
String markerContext,
@@ -875,12 +916,13 @@ public class MantleObjectComponent extends IrisMantleComponent {
CaveObjectPlacementTransaction.CommitResult.REJECTED_TRANSITION);
}
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(placer, anchorY, minDepthBelowSurface);
int placeY = anchorY;
int placeY = alignment == IrisObjectPlacementAlignment.FLOOR
? proceduralFloorY(placer, x, anchorY, z) : anchorY;
if (placement.getMode() == ObjectPlaceMode.CEILING_HANG) {
placeY = Math.max(1, transaction.getCaveCeiling(x, z) - 1 - Math.floorDiv(object.getH(), 2));
}
String marker = placementMarker(object, id, markerContext);
int result = object.place(x, placeY, z, transaction, placement, rng, (block, data) -> {
BiConsumer<BlockPosition, NativeBlockState> listener = (block, data) -> {
if (marker != null) {
transaction.setData(block.getX(), block.getY(), block.getZ(), marker);
}
@@ -890,7 +932,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (placement.isDolphinTarget() && placement.isUnderwater() && B.isStorageChest(data)) {
transaction.setData(block.getX(), block.getY(), block.getZ(), MatterStructurePOI.BURIED_TREASURE);
}
}, null, getData());
};
int result = alignment == IrisObjectPlacementAlignment.FLOOR
? object.placeOnFloor(x, placeY, z, transaction, placement, rng, listener, getData())
: object.place(x, placeY, z, transaction, placement, rng, listener, null, getData());
if (result < 0) {
transaction.discard();
return new ContainedPlacementResult(result, CaveObjectPlacementTransaction.CommitResult.EMPTY);
@@ -1147,6 +1192,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
y,
z,
effectivePlacement,
IrisObjectPlacementAlignment.CENTER,
objectMinDepthBelowSurface,
id,
"cave",
@@ -1502,6 +1548,17 @@ public class MantleObjectComponent extends IrisMantleComponent {
return resolvedPlacement;
}
static int proceduralFloorY(IObjectPlacer placer, int x, int scannedY, int z) {
int floorY = scannedY;
for (int depth = 0; depth < 3 && floorY > 1; depth++) {
if (!placer.isCarved(x, floorY - 1, z)) {
break;
}
floorY--;
}
return floorY;
}
static int caveAnchorScanUpperBound(int worldHeight, int surfaceY, int minDepthBelowSurface) {
int maxAnchorY = CaveObjectPlacementTransaction.maxBuriedY(worldHeight, surfaceY, minDepthBelowSurface);
if (maxAnchorY <= 1) {
@@ -38,6 +38,8 @@ import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.function.Function3;
@@ -364,10 +366,13 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
MantleChunk<Matter> chunk = acquireChunk(cx, cz);
if (chunk == null) return;
if (protectsSubterrainPlacement(x, y, z)) {
return;
}
synchronized (chunk) {
Matter matter = chunk.getOrCreate(y >> 4);
if ((t instanceof NativeBlockState || t instanceof MatterCavern)
&& hasProtectedHydrology(matter, x, y, z)) {
&& (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z))) {
return;
}
if (t instanceof NativeBlockState) {
@@ -399,7 +404,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
synchronized (chunk) {
Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) {
if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
return false;
}
MatterSlice<MatterCavern> cavernSlice = matter.getSlice(MatterCavern.class);
@@ -432,7 +437,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
synchronized (chunk) {
Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) {
if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
return false;
}
MatterSlice<NativeBlockState> blockSlice = matter.getSlice(NativeBlockState.class);
@@ -467,7 +472,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
synchronized (chunk) {
Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) {
if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
return;
}
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
@@ -497,7 +502,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
if (matter == null) {
return;
}
if (hasProtectedHydrology(matter, x, y, z)) {
if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
return;
}
MatterSlice<NativeBlockState> blockSlice = matter.getSlice(NativeBlockState.class);
@@ -578,10 +583,14 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
public NativeBlockState getPrerequisiteBlock(int x, int y, int z) {
NativeBlockState block = getPrerequisiteDataIfPresent(x, y, z, NativeBlockState.class);
return block == null ? AIR.get() : block;
return block == null ? subterrainFallback(x, y, z) : block;
}
public boolean isPrerequisiteCarved(int x, int y, int z) {
SubterrainCell feature = getEngine().getSubterrainCell(x, y, z);
if (feature != null && feature.owned()) {
return feature.carve();
}
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
if (transformed != null) {
return y >= 0 && y < transformed.topY() && !transformed.isSolid(y);
@@ -729,7 +738,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
public void clearData(int x, int y, int z, Class<?> type) {
if (!allowsWrite(x, z) || y < 0 || y >= mantle.getWorldHeight()) {
if (!allowsWrite(x, z) || y < 0 || y >= mantle.getWorldHeight() || protectsSubterrainPlacement(x, y, z)) {
return;
}
@@ -745,7 +754,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
return;
}
if ((type == NativeBlockState.class || type == MatterCavern.class)
&& hasProtectedHydrology(matter, x, y, z)) {
&& (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z))) {
return;
}
capturePreObjectOriginal(matter, x, y, z, type);
@@ -754,6 +763,12 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
}
private boolean protectsSubterrainPlacement(int x, int y, int z) {
Integer priority = activeComponentPriority.get();
return priority != null && priority >= OBJECT_COMPONENT_PRIORITY
&& SubterrainRasterizer.protectsPlacement(getEngine().getSubterrainCell(x, y, z));
}
private static boolean hasProtectedHydrology(Matter matter, int x, int y, int z) {
MatterSlice<HydrologyCaveCell> slice = matter.getSlice(HydrologyCaveCell.class);
if (slice == null) {
@@ -785,7 +800,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
synchronized (chunk) {
Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) {
if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
return;
}
MatterSlice<NativeBlockState> blockSlice = matter.slice(NativeBlockState.class);
@@ -957,9 +972,12 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
// Read-only probe: getDataIfPresent returns the identical answer without materializing
// a 16^3 section + slice on a miss the way getData's getOrCreate path does.
NativeBlockState block = getDataIfPresent(x, y, z, NativeBlockState.class);
if (block == null)
return AIR.get();
return block;
return block == null ? subterrainFallback(x, y, z) : block;
}
private NativeBlockState subterrainFallback(int x, int y, int z) {
SubterrainCell cell = getEngine().getSubterrainCell(x, y, z);
return cell != null && cell.owned() ? SubterrainRasterizer.state(cell) : AIR.get();
}
@Override
@@ -969,6 +987,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
@Override
public boolean isCarved(int x, int y, int z) {
SubterrainCell feature = getEngine().getSubterrainCell(x, y, z);
if (feature != null && feature.owned()) {
return feature.carve();
}
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
if (transformed != null) {
return y >= 0 && y < transformed.topY() && !transformed.isSolid(y);
@@ -6,6 +6,7 @@ import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
@@ -228,7 +229,8 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
}
private void setData(int x, int y, int z, Object data, SetMutation replayed) {
if (data == null || y < 0 || y >= worldHeight) {
if (data == null || y < 0 || y >= worldHeight
|| protectsSubterrain(x, y, z)) {
return;
}
if (data instanceof NativeBlockState && !canSetBlock(x, y, z)) {
@@ -351,7 +353,13 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
if (y == 0 && writer.getEngine().getDimension().isBedrock()) {
return false;
}
return !hasProtectedHydrology(x, y, z);
return !hasProtectedHydrology(x, y, z)
&& !protectsSubterrain(x, y, z);
}
private boolean protectsSubterrain(int x, int y, int z) {
Engine engine = getEngine();
return engine != null && SubterrainRasterizer.protectsPlacement(engine.getSubterrainCell(x, y, z));
}
private boolean hasProtectedHydrology(int x, int y, int z) {
@@ -82,6 +82,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
if (getEngine().getDimensionStackContext() == null) {
registerStage(sCustom);
}
registerStage((x, z, k, p, m, c) -> cave.preserveSubterrain(x, z, k));
}
void generateTerrainMatterForChunk(int x, int z, boolean multicore, ChunkContext context) {
@@ -19,6 +19,8 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.world.history.ChunkGenerationSemantics;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipStore;
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
@@ -328,6 +330,9 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
default IrisRegion getRegion(int x, int y, int z) {
if (getSubterrainCell(x, y, z).occupied()) {
return getComplex().getRegionStream().get(x, z);
}
DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) {
DimensionStackLayout.Layer layer = dimensionStackContext.getLayout(x, z).layerAt(y);
@@ -340,8 +345,30 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
void generateMatter(int x, int z, boolean multicore, ChunkContext context);
@BlockCoordinates
default SubterrainCell getSubterrainCell(int x, int y, int z) {
if (getComplex() == null || getDimension() == null
|| !getDimension().allowsSubterrainFeatures()
|| getDimension().getSubterrainFeatures() == null || getDimension().getSubterrainFeatures().isEmpty()) {
return SubterrainCell.OUTSIDE;
}
SubterrainPlanner planner = getComplex().getSubterrainPlanner();
return planner == null ? SubterrainCell.OUTSIDE : planner.sample(x, y + getWorld().minHeight(), z);
}
@BlockCoordinates
default IrisBiome getSubterrainBiome(int x, int y, int z) {
SubterrainCell cell = getSubterrainCell(x, y, z);
return cell.occupied() && !cell.room().biome().isEmpty()
? getData().getBiomeLoader().load(cell.room().biome()) : null;
}
@BlockCoordinates
default IrisBiome getCaveOrMantleBiome(int x, int y, int z) {
IrisBiome featureBiome = getSubterrainBiome(x, y, z);
if (featureBiome != null) {
return featureBiome;
}
IrisBiome mantleBiome = mantleCaveBiome(x, y, z);
return mantleBiome != null ? mantleBiome : getCaveBiome(x, y, z);
}
@@ -401,6 +428,11 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
for (int index = 0; index < biomes.length; index++) {
int y = index * step;
regions[index] = region;
IrisBiome featureBiome = getSubterrainBiome(x, y, z);
if (featureBiome != null) {
biomes[index] = featureBiome;
continue;
}
if (y > caveTop || complex.isTerrain3DSurface(x, y, z)) {
if (surface == null) {
surface = getSurfaceBiome(x, z);
@@ -458,6 +490,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
default IrisBiome getCaveBiome(int x, int y, int z, IrisDimensionCarvingResolver.State state) {
IrisBiome featureBiome = getSubterrainBiome(x, y, z);
if (featureBiome != null) {
return featureBiome;
}
IrisBiome configuredBiome = resolveConfiguredCaveBiome(x, y, z, state);
if (configuredBiome != null) {
return configuredBiome;
@@ -490,6 +526,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
IrisBiome surfaceBiome,
int surfaceY
) {
IrisBiome featureBiome = getSubterrainBiome(x, y, z);
if (featureBiome != null) {
return featureBiome;
}
IrisBiome configuredBiome = resolveConfiguredCaveBiome(x, y, z, state);
if (configuredBiome != null) {
return configuredBiome;
@@ -719,6 +759,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
boolean isStudio();
default IrisBiome getBiome(int x, int y, int z) {
IrisBiome featureBiome = getSubterrainBiome(x, y, z);
if (featureBiome != null) {
return featureBiome;
}
DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) {
DimensionStackLayout layout = dimensionStackContext.getLayout(x, z);
@@ -743,6 +787,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
}
default IrisBiome getBiomeOrMantle(int x, int y, int z) {
IrisBiome featureBiome = getSubterrainBiome(x, y, z);
if (featureBiome != null) {
return featureBiome;
}
DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) {
DimensionStackLayout layout = dimensionStackContext.getLayout(x, z);
@@ -118,7 +118,7 @@ public final class GenerationCacheWarmer {
if (placement == null) {
continue;
}
placement.getVariantObject(data, root.nextParallelRNG(counter[0]++));
placement.getVariantObject(data, root.nextParallelRNG(counter[0]++), null);
}
}
}
@@ -22,6 +22,8 @@ import art.arcane.iris.generation.stream.GenerationStreams;
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
import art.arcane.iris.generation.stream.CachedStream2D;
import art.arcane.iris.generation.stream.ProvisionalSampling;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.configuration.IrisSettings;
@@ -243,6 +245,8 @@ public class IrisComplex implements DataProvider {
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
private Set<IrisBiome> generatorBiomes;
private IrisHydrologyRuntime hydrologyRuntime;
@Setter(AccessLevel.NONE)
private final transient SubterrainPlanner subterrainPlanner;
private transient IrisImageMapRuntime imageMapRuntime;
// Copy-on-write: reads happen per column on every burst thread; the synchronizedMap
// monitor was taken on every HIT. Writes are once per biome and bounded, so a fresh map
@@ -264,6 +268,15 @@ public class IrisComplex implements DataProvider {
IrisComplex(Engine engine, boolean simple, TransitionGenerationPlan transitionGenerationPlan, boolean detached) {
terrainEngine = engine;
if (engine.getDimension().allowsSubterrainFeatures()) {
SubterrainRasterizer.validateMaterials(engine.getDimension().getSubterrainFeatures());
}
subterrainPlanner = new SubterrainPlanner(new SubterrainPlanner.Options(
engine.getDimension().allowsSubterrainFeatures() ? engine.getDimension().getSubterrainFeatures() : List.of(),
engine.getSeedManager().getBodies(),
engine.getDimension().getMinHeight(), engine.getDimension().getMaxHeight()),
bounds -> transitionGenerationPlan == null || transitionGenerationPlan.allowsNewFootprint(
bounds.minX(), bounds.minZ(), bounds.maxX(), bounds.maxZ()));
biomeBoundsSamplingStep = engine.getDimension().getBiomeBoundsSamplingStep();
this.detached = detached;
this.transitionGenerationPlan = transitionGenerationPlan;
@@ -18,6 +18,7 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.runtime.EngineBackgroundTasks.BackgroundTaskDrain;
import art.arcane.iris.generation.runtime.EngineRuntimeBuilder.RuntimeAssembly;
import art.arcane.iris.pack.loading.IrisData;
@@ -589,6 +590,31 @@ public class IrisEngine implements Engine {
return context == null || context.getEngine() != this || context.getChunkContext() == null;
}
@BlockCoordinates
@Override
public SubterrainCell getSubterrainCell(int x, int y, int z) {
if (!hasGenerationRuntimeScope() && generationHistoryRuntimeRouter != null) {
Optional<SubterrainCell> saved = generationHistoryRuntimeRouter.biomes()
.subterrainCell(x, y + getWorld().minHeight(), z);
if (saved.isPresent()) {
return saved.get();
}
}
return Engine.super.getSubterrainCell(x, y, z);
}
@BlockCoordinates
@Override
public IrisBiome getSubterrainBiome(int x, int y, int z) {
SubterrainCell cell = getSubterrainCell(x, y, z);
if (!cell.occupied() || cell.room().biome().isEmpty()) {
return null;
}
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, y, z, false);
IrisData data = saved.isPresent() ? saved.get().data() : getData();
return data.getBiomeLoader().load(cell.room().biome());
}
@BlockCoordinates
@Override
public IrisBiome getBiome(int x, int y, int z) {
@@ -24,6 +24,9 @@ import art.arcane.iris.world.IrisToolbelt;
import art.arcane.volmlib.util.math.Rarity;
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.decoration.IrisSurface;
import art.arcane.iris.world.entity.IrisSpawnGroup;
import org.bukkit.Location;
import art.arcane.iris.world.entity.IrisEntitySpawn;
import art.arcane.iris.world.entity.EntitySpawnSeed;
import art.arcane.iris.world.entity.IrisMarker;
@@ -404,41 +407,94 @@ final class WorldEntitySpawner {
if (!ambientAllowed(chunk, initial)) {
return;
}
ChunkCounter counter = initial ? null : new ChunkCounter(chunk.getEntities());
ChunkCounter counter = new ChunkCounter(initial ? new Entity[0] : chunk.getEntities());
List<SpawnSelection> pool = new ArrayList<>();
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()),
environment.biome(), chunk, initial, counter);
environment.biome(), chunk, initial, counter, false);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()),
null, chunk, initial, counter);
null, chunk, initial, counter, false);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.biome().getEntitySpawners()),
null, chunk, initial, counter);
null, chunk, initial, counter, false);
RNG random = initial ? EntitySpawnSeed.chunk(manager.getEngine().getSeedManager().getEntity(),
chunk.getX(), chunk.getZ()) : new RNG(RNG.r.nextLong());
if (environment.dimension().hasUndergroundSpawners(manager.getEngine())) {
spawnUnderground(chunk, initial, counter, random.nextParallelRNG(0x5A17D3B48269C0EFL));
}
SpawnSelection selected = selectSpawn(pool, random);
if (selected == null) {
return;
}
int capacity = initial ? Math.max(0, selected.spawner().getMaxEntitiesPerChunk())
: counter.remainingCapacity(selected.entry(), selected.spawner(), manager.getEngine());
int capacity = counter.remainingCapacity(selected.entry(), selected.spawner(), manager.getEngine());
int spawned = selected.entry().spawn(manager.getEngine(), chunk, random, capacity,
new SpawnContext(selected.spawner(), null, initial));
actuallySpawned.addAndGet(spawned);
counter.recordSpawned(selected.entry(), manager.getEngine(), spawned);
if (spawned > 0 && !initial) {
selected.spawner().spawn(manager.getEngine(), chunk.getX(), chunk.getZ());
}
}
private static boolean matchesUndergroundSurface(IrisEntitySpawn entry, Engine engine, IrisSurface surface) {
IrisEntity entity = entry.getRealEntity(engine);
return entity != null && entity.getSurface().isFluid() == surface.isFluid()
&& (!surface.isFluid() || entity.getSurface() == surface);
}
private void spawnUnderground(Chunk chunk, boolean initial, ChunkCounter counter, RNG random) {
if (J.isFolia() && !J.isOwnedByCurrentRegion(chunk.getWorld(), chunk.getX(), chunk.getZ())) {
return;
}
for (IrisSurface surface : List.of(IrisSurface.LAND, IrisSurface.WATER, IrisSurface.LAVA)) {
Location candidate = IrisEntitySpawn.findLiveCaveSpawnLocation(
chunk, random.nextParallelRNG(surface.ordinal()), surface);
if (candidate == null) {
continue;
}
Engine engine = manager.getEngine();
BiomeEnvironment environment;
try {
environment = engine.getBiomeOrMantleEnvironment(candidate.getBlockX(),
candidate.getBlockY() - engine.getWorld().minHeight(), candidate.getBlockZ());
} catch (SavedBiomeUnavailableException unavailable) {
continue;
}
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(environment)) {
List<SpawnSelection> pool = new ArrayList<>();
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()),
environment.biome(), chunk, initial, counter, true);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()),
null, chunk, initial, counter, true);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.biome().getEntitySpawners()),
null, chunk, initial, counter, true);
pool.removeIf(selection -> !matchesUndergroundSurface(selection.entry(), engine, surface));
SpawnSelection selected = selectSpawn(pool, random);
if (selected == null) {
continue;
}
int capacity = counter.remainingCapacity(selected.entry(), selected.spawner(), engine);
int spawned = selected.entry().spawn(engine, candidate, random, capacity,
new SpawnContext(selected.spawner(), null, initial));
actuallySpawned.addAndGet(spawned);
counter.recordSpawned(selected.entry(), engine, spawned);
if (spawned > 0 && !initial) {
selected.spawner().spawn(engine, chunk.getX(), chunk.getZ());
}
return;
}
}
}
private void collectSpawns(List<SpawnSelection> pool, KList<IrisSpawner> spawners, IrisBiome biome,
Chunk chunk, boolean initial, ChunkCounter counter) {
Chunk chunk, boolean initial, ChunkCounter counter, boolean underground) {
for (IrisSpawner spawner : spawners) {
if (!spawnerAllowed(spawner, manager.getEngine(), chunk.getX(), chunk.getZ(), initial)
if ((spawner.getGroup() == IrisSpawnGroup.CAVE) != underground
|| !spawnerAllowed(spawner, manager.getEngine(), chunk.getX(), chunk.getZ(), initial)
|| biome != null && !spawner.isValid(biome)) {
continue;
}
for (IrisEntitySpawn entry : initial ? spawner.getInitialSpawns() : spawner.getSpawns()) {
IrisEntity entity = entry.getRealEntity(manager.getEngine());
int capacity = initial ? Math.max(0, spawner.getMaxEntitiesPerChunk())
: counter.remainingCapacity(entry, spawner, manager.getEngine());
int capacity = counter.remainingCapacity(entry, spawner, manager.getEngine());
if (entity != null && capacity > 0) {
pool.add(new SpawnSelection(entry, spawner));
}
@@ -503,6 +559,13 @@ final class WorldEntitySpawner {
}
}
void recordSpawned(IrisEntitySpawn entry, Engine engine, int spawned) {
IrisEntity entity = entry.getRealEntity(engine);
if (spawned > 0 && entity != null) {
counts.merge(entity.spawnCategory(), spawned, Integer::sum);
}
}
int remainingCapacity(IrisEntitySpawn entry, IrisSpawner spawner, Engine engine) {
IrisEntity entity = entry.getRealEntity(engine);
return spawner.remainingCapacity(entity, counts);
@@ -19,6 +19,12 @@
package art.arcane.iris.generation.stage;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import java.util.List;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.generation.subterrain.SubterrainSurfaceMaterials;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.runtime.UpperDimensionContext;
@@ -144,7 +150,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
worldHeightSpan,
caveLavaHeight,
chunkBlockX,
chunkBlockZ
chunkBlockZ,
getComplex().getSubterrainPlanner(),
getComplex().getSubterrainPlanner() == null ? List.of() : getComplex().getSubterrainPlanner().plansForBounds(chunkBlockX - 1, chunkBlockZ - 1, chunkBlockX + 16, chunkBlockZ + 16)
);
CarveResolver carveResolver = new CarveResolver(resolutionContext);
terrain.forEachCell(carveResolver::apply);
@@ -161,7 +169,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
IrisData wallData = getData();
walls.forEach((rx, yy, rz, cavern) -> {
HydrologyCaveCell hydrology = terrain.hydrology(rx, yy, rz);
if (hydrology != null && hydrology.protectsPlacement()) {
if (hydrology != null && hydrology.protectsPlacement()
|| subterrainCell(rx + chunkBlockX, yy, rz + chunkBlockZ).owned()) {
return;
}
int worldX = rx + chunkBlockX;
@@ -233,6 +242,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
output.setRaw(localX, surfaceY, localZ, AIR);
}
}
restoreSubterrainSolids(output, x, z);
} finally {
getEngine().getMetrics().getCarveApply().put(applyStopwatch.getMilliseconds());
}
@@ -242,6 +252,58 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
}
}
private SubterrainCell subterrainCell(int x, int y, int z) {
SubterrainCell cell = getEngine().getSubterrainCell(x, y, z);
return cell == null ? SubterrainCell.OUTSIDE : cell;
}
public void preserveSubterrain(int blockX, int blockZ, Hunk<NativeBlockState> output) {
SubterrainPlanner planner = getComplex().getSubterrainPlanner();
if (planner == null || planner.isEmpty()) {
return;
}
int minimumY = getEngine().getWorld().minHeight();
SubterrainSurfaceMaterials materials = subterrainMaterials(planner, blockX >> 4, blockZ >> 4);
SubterrainRasterizer.rasterize(planner, blockX >> 4, blockZ >> 4, (worldX, worldY, worldZ, cell) -> {
int y = worldY - minimumY;
if (y <= 0 || y >= output.getHeight()) {
return;
}
int x = worldX & 15;
int z = worldZ & 15;
NativeBlockState existing = output.getRaw(x, y, z);
NativeBlockState planned = materials.state(worldX, worldY, worldZ, cell);
if (cell.solid() || cell.room().reservedPassage() || cell.fluid() && (existing == null || B.isAir(existing) || B.isFluid(existing))) {
output.setRaw(x, y, z, planned);
} else {
output.setRaw(x, y, z, IrisProceduralBlocks.normalizeWaterlogging(existing, cell.kind() == SubterrainCell.Kind.WATER ? planned : null, true));
}
});
}
private void restoreSubterrainSolids(Hunk<NativeBlockState> output, int chunkX, int chunkZ) {
SubterrainPlanner planner = getComplex().getSubterrainPlanner();
if (planner == null || planner.isEmpty()) {
return;
}
int minimumY = getEngine().getWorld().minHeight();
SubterrainSurfaceMaterials materials = subterrainMaterials(planner, chunkX, chunkZ);
SubterrainRasterizer.rasterize(planner, chunkX, chunkZ, (worldX, worldY, worldZ, cell) -> {
int y = worldY - minimumY;
if (cell.solid() && y > 0 && y < output.getHeight()) {
output.setRaw(worldX & 15, y, worldZ & 15, materials.state(worldX, worldY, worldZ, cell));
}
});
}
private SubterrainSurfaceMaterials subterrainMaterials(SubterrainPlanner planner, int chunkX, int chunkZ) {
int blockX = chunkX << 4;
int blockZ = chunkZ << 4;
return new SubterrainSurfaceMaterials(planner,
planner.plansForBounds(blockX - 1, blockZ - 1, blockX + 16, blockZ + 16),
getEngine().getDimension(), getComplex(), getData(), rng, getEngine().getWorld().minHeight());
}
static boolean hasExplicitCarveIntent(MatterCavern cavern) {
return cavern != null && (isFluidIntent(cavern) || cavern.isLava() || cavern.getLiquid() == 3);
}
@@ -357,6 +419,19 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
int localX = x & 15;
int localZ = z & 15;
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
SubterrainCell feature = context.subterrainPlanner() == null ? SubterrainCell.OUTSIDE : context.subterrainPlanner().sample(context.subterrainPlans(),
context.chunkBlockX() + localX, y + getEngine().getWorld().minHeight(), context.chunkBlockZ() + localZ);
if (feature.owned()) {
context.output().setRaw(localX, y, localZ, SubterrainRasterizer.state(feature));
if (feature.carve()) {
context.columnMasks()[columnIndex].add(y);
if (!feature.room().biome().isEmpty()) {
context.scratch().customCaveBiomePresent = true;
}
}
return;
}
if (context.upperSurfaceHeights() != null && y >= context.upperSurfaceHeights()[columnIndex]) {
return;
}
@@ -432,7 +507,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
int worldHeightSpan,
int caveLavaHeight,
int chunkBlockX,
int chunkBlockZ
int chunkBlockZ,
SubterrainPlanner subterrainPlanner,
List<SubterrainPlan> subterrainPlans
) {
}
@@ -904,7 +981,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
zone.setFloor(floor);
zone.setCeiling(y - 1);
if (zone.isValid(resolver.height)
&& !isNaturalTerrainOpening(getComplex(), worldX, floor, worldZ)) {
&& (subterrainCell(worldX, floor, worldZ).occupied()
|| !isNaturalTerrainOpening(getComplex(), worldX, floor, worldZ))) {
if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) {
mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING);
}
@@ -929,10 +1007,15 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
}
}
}
restoreSubterrainSolids(output, blockX >> 4, blockZ >> 4);
}
private void decorateZone(Hunk<NativeBlockState> output, CaveZone zone,
int rx, int rz, int xx, int zz, IrisBiome floorBiome, IrisBiome ceilingBiome) {
SubterrainCell feature = subterrainCell(xx, zone.getFloor(), zz);
if (feature.owned() && feature.room().reservedPassage()) {
return;
}
int maxY = output.getHeight();
IrisDecorator[] surfaceDecorators = floorBiome == null
? new IrisDecorator[0]
@@ -26,6 +26,7 @@ import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.decoration.FloatingDecorator;
import art.arcane.iris.generation.decoration.IrisSeaSurfaceDecorator;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.runtime.EngineAssignedModifier;
import art.arcane.iris.generation.runtime.EngineDecorator;
import art.arcane.iris.generation.biome.FloatingBottomPaletteMode;
@@ -244,7 +245,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
continue;
}
int y = sample.islandBaseY + k;
if (y < 0 || y >= chunkHeight) {
if (y < 0 || y >= chunkHeight || featureOwns(wx, y, wz)) {
continue;
}
IrisFloatingChildBiomes entry = sample.entryAt(k);
@@ -275,7 +276,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
continue;
}
int y = sample.islandBaseY + k;
if (y < 0 || y >= chunkHeight) {
if (y < 0 || y >= chunkHeight || featureOwns(wx, y, wz)) {
continue;
}
boolean hasSolidBelow = false;
@@ -340,7 +341,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
int topY = sample.topY();
int max = Math.max(1, chunkHeight - topY);
if (topY + 1 < chunkHeight) {
if (topY + 1 < chunkHeight && !featureOwns(wx, topY, wz) && !featureOwns(wx, topY + 1, wz)) {
NativeBlockState above = output.get(xf, topY + 1, zf);
if (above == null || above.isAir()) {
try {
@@ -362,7 +363,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
continue;
}
int y = sample.islandBaseY + k;
if (y < 0 || y >= chunkHeight) {
if (y < 0 || y >= chunkHeight || featureOwns(wx, y, wz)) {
continue;
}
boolean hasSolidBelow = false;
@@ -376,7 +377,9 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
fluidTopY = y;
}
}
if (fluidTopY > 0 && fluidTopY + 1 < chunkHeight && B.isAir(output.get(xf, fluidTopY + 1, zf))) {
if (fluidTopY > 0 && fluidTopY + 1 < chunkHeight
&& !featureOwns(wx, fluidTopY, wz) && !featureOwns(wx, fluidTopY + 1, wz)
&& B.isAir(output.get(xf, fluidTopY + 1, zf))) {
try {
seaSurfaceDecorator.decorate(xf, zf, wx, wx + 1, wx - 1, wz, wz + 1, wz - 1, output, target, fluidTopY, chunkHeight);
} catch (Throwable e) {
@@ -389,6 +392,11 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
}
}
private boolean featureOwns(int x, int y, int z) {
SubterrainCell cell = getEngine().getSubterrainCell(x, y, z);
return cell != null && cell.owned();
}
private void writeIslandSkyBiomes(
IrisBiome parent,
int wx,
@@ -406,7 +414,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
continue;
}
int y = sample.islandBaseY + k;
if (y < 0 || y >= chunkHeight) {
if (y < 0 || y >= chunkHeight || featureOwns(wx, y, wz)) {
continue;
}
if (!shouldWriteHostBiomeMarker(stackLayout, y)) {
@@ -0,0 +1,8 @@
package art.arcane.iris.generation.subterrain;
public enum IrisSubterrainFamily {
TECTONIC_FAULT,
CENOTE,
LAVA_TUBE,
TRAVERTINE_TERRACES
}
@@ -0,0 +1,77 @@
package art.arcane.iris.generation.subterrain;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.pack.schema.annotation.MaxNumber;
import art.arcane.iris.pack.schema.annotation.MinNumber;
import art.arcane.iris.pack.schema.annotation.RegistryListResource;
import art.arcane.iris.pack.schema.annotation.Snippet;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.volmlib.util.documentation.Description;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@Accessors(chain = true)
@Snippet("subterrain-feature")
@Description("Bounded, deterministic underground rooms, passages, retained fluids and formations.")
public class IrisSubterrainFeature {
@Description("Unique stable feature definition id. Placement is derived from this id and the world seed.")
private String id = "";
@Description("Enable this feature definition.")
private boolean enabled = true;
@Description("Deliberate underground geometry family.")
private IrisSubterrainFamily family = IrisSubterrainFamily.CENOTE;
@RegistryListResource(IrisBiome.class)
@Description("Biome that owns occupied cells in this feature. Solid rims and surrounding terrain retain their biome.")
private String biome = "";
@Description("Inclusive absolute world Y bounds. The entire feature including seals must fit inside this band.")
private IrisRange worldYRange = new IrisRange(-48, 48);
@MinNumber(32)
@MaxNumber(8192)
@Description("Placement grid spacing in blocks. Each grid cell supplies at most one seed-derived candidate.")
private int spacing = 512;
@MinNumber(0)
@MaxNumber(1)
@Description("Probability that each placement grid cell contains this feature.")
private double probability = 0.35;
@MinNumber(16)
@MaxNumber(2048)
@Description("Passage or fault length in blocks. Tectonic faults require at least 200 blocks.")
private int length = 256;
@MinNumber(6)
@MaxNumber(256)
@Description("Cenote horizontal radius, or half-width of faults, tubes and terraces.")
private int radius = 32;
@MinNumber(8)
@MaxNumber(192)
@Description("Main room height in blocks.")
private int height = 32;
@Description("Retained fluid. When omitted, lava tubes use LAVA and cenotes and terraces use WATER. Faults remain dry.")
private IrisSubterrainFluid fluid;
@MinNumber(0)
@MaxNumber(32)
@Description("Depth of retained water or lava above the basin floor. Faults remain dry.")
private int fluidDepth = 4;
@MinNumber(0)
@MaxNumber(96)
@Description("Height of connected lava-tube hornito shafts above the tube vault.")
private int chimneyHeight = 16;
@MinNumber(2)
@MaxNumber(16)
@Description("Number of stepped travertine basins.")
private int terraceCount = 6;
@MinNumber(0)
@MaxNumber(128)
@Description("Grid distance between continuous floor-to-ceiling pillars. Zero disables pillars.")
private int pillarSpacing = 24;
@MinNumber(0)
@MaxNumber(0.4)
@Description("Fraction of available vault height occupied by each stalactite or stalagmite. Zero disables formations.")
private double formationFraction = 0.15;
@Description("Block state used for seals, pillars, rims, walkways and formations.")
private String solid = "minecraft:stone";
@Description("Priority between authored features. Higher values win; stable feature ids break ties.")
private int priority = 0;
}
@@ -0,0 +1,36 @@
package art.arcane.iris.generation.subterrain;
import com.google.gson.JsonParseException;
import com.google.gson.TypeAdapter;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
@JsonAdapter(IrisSubterrainFluid.Adapter.class)
public enum IrisSubterrainFluid {
WATER,
LAVA;
public static final class Adapter extends TypeAdapter<IrisSubterrainFluid> {
@Override
public void write(JsonWriter writer, IrisSubterrainFluid value) throws IOException {
writer.value(value.name());
}
@Override
public IrisSubterrainFluid read(JsonReader reader) throws IOException {
if (reader.peek() != JsonToken.STRING) {
throw new JsonParseException("Subterrain fluid must be WATER or LAVA");
}
String value = reader.nextString();
try {
return IrisSubterrainFluid.valueOf(value);
} catch (IllegalArgumentException error) {
throw new JsonParseException("Unknown subterrain fluid: " + value + "; expected WATER or LAVA", error);
}
}
}
}
@@ -0,0 +1,11 @@
package art.arcane.iris.generation.subterrain;
public record SubterrainBounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
public boolean contains(int x, int y, int z) {
return x >= minX && x <= maxX && y >= minY && y <= maxY && z >= minZ && z <= maxZ;
}
public boolean intersects(int lowX, int lowZ, int highX, int highZ) {
return minX <= highX && maxX >= lowX && minZ <= highZ && maxZ >= lowZ;
}
}
@@ -0,0 +1,29 @@
package art.arcane.iris.generation.subterrain;
public record SubterrainCell(Kind kind, String material, SubterrainRoom room) {
public static final SubterrainCell OUTSIDE = new SubterrainCell(Kind.OUTSIDE, "", null);
public boolean owned() {
return kind != Kind.OUTSIDE;
}
public boolean occupied() {
return kind == Kind.AIR || fluid();
}
public boolean solid() {
return kind == Kind.SOLID;
}
public boolean carve() {
return occupied();
}
public boolean fluid() {
return kind == Kind.WATER || kind == Kind.LAVA;
}
public enum Kind {
OUTSIDE, SOLID, AIR, WATER, LAVA
}
}
@@ -0,0 +1,265 @@
package art.arcane.iris.generation.subterrain;
import java.util.Objects;
public final class SubterrainPlan {
private final SubterrainPlanner.Definition definition;
private final int centerX;
private final int centerY;
private final int centerZ;
private final boolean rotated;
private final String id;
private final SubterrainBounds bounds;
SubterrainPlan(Options options) {
definition = Objects.requireNonNull(options.definition());
centerX = options.centerX();
centerY = options.centerY();
centerZ = options.centerZ();
rotated = options.rotated();
id = options.id();
int along = family() == IrisSubterrainFamily.CENOTE ? definition.radius() : definition.length() / 2;
int across = definition.radius();
int extentX = (rotated ? across : along) + 2;
int extentZ = (rotated ? along : across) + 2;
bounds = new SubterrainBounds(centerX - extentX, baseY() - 2, centerZ - extentZ,
centerX + extentX, baseY() + definition.height() + 2
+ (family() == IrisSubterrainFamily.LAVA_TUBE ? definition.chimneyHeight() : 0), centerZ + extentZ);
}
public String id() {
return id;
}
public IrisSubterrainFamily family() {
return definition.family();
}
public String biome() {
return definition.biome();
}
public int priority() {
return definition.priority();
}
public SubterrainBounds bounds() {
return bounds;
}
public int centerX() {
return centerX;
}
public int centerY() {
return centerY;
}
public int centerZ() {
return centerZ;
}
public SubterrainPosition anchor() {
int y = family() == IrisSubterrainFamily.LAVA_TUBE ? centerY : baseY() + definition.fluidDepth() + 2;
SubterrainPosition anchor = occupiedAnchor(centerX, y, centerZ);
if (anchor != null) {
return anchor;
}
if (family() == IrisSubterrainFamily.TRAVERTINE_TERRACES) {
int path = definition.radius() - 4;
anchor = occupiedAnchor(rotated ? centerX + path : centerX, y,
rotated ? centerZ : centerZ + path);
if (anchor != null) {
return anchor;
}
int firstTerrace = -definition.length() / 2 + 1;
anchor = occupiedAnchor(rotated ? centerX + path : centerX + firstTerrace, y,
rotated ? centerZ + firstTerrace : centerZ + path);
if (anchor != null) {
return anchor;
}
}
throw new IllegalStateException("Subterrain plan has no occupied anchor: " + id);
}
public SubterrainCell sample(int x, int y, int z) {
if (!bounds.contains(x, y, z)) {
return SubterrainCell.OUTSIDE;
}
int u = rotated ? z - centerZ : x - centerX;
int v = rotated ? x - centerX : z - centerZ;
Column column = switch (family()) {
case CENOTE -> cenote(u, v);
case TECTONIC_FAULT -> fault(u, v);
case LAVA_TUBE -> tube(u, v, y);
case TRAVERTINE_TERRACES -> terraces(u, v);
};
if (column == null || y < column.floor() - 2 || y > column.ceiling() + 2) {
return SubterrainCell.OUTSIDE;
}
SubterrainCell.Kind kind = classify(column, u, v, y);
int pathX = rotated ? centerX + column.pathOffset() : x;
int pathZ = rotated ? z : centerZ + column.pathOffset();
boolean passage = kind == SubterrainCell.Kind.AIR && column.pathDistance() <= 2.0;
SubterrainRoom room = new SubterrainRoom(id, biome(), family(), centerX, centerY, centerZ,
pathX, Math.max(column.floor() + 2, column.head() + 2), pathZ,
column.floor(), column.ceiling(), column.boundary(), column.head(), passage,
kind == SubterrainCell.Kind.SOLID, kind);
String material = switch (kind) {
case SOLID -> definition.solid();
case AIR -> "minecraft:cave_air";
case WATER -> "minecraft:water";
case LAVA -> "minecraft:lava";
case OUTSIDE -> "";
};
return new SubterrainCell(kind, material, room);
}
private SubterrainCell.Kind classify(Column column, int u, int v, int y) {
if (!column.interior() || y <= column.floor() || y >= column.ceiling() || column.rim()) {
return SubterrainCell.Kind.SOLID;
}
if (formation(column, u, v, y)) {
return SubterrainCell.Kind.SOLID;
}
if (y <= column.head()) {
return switch (definition.fluid()) {
case WATER -> SubterrainCell.Kind.WATER;
case LAVA -> SubterrainCell.Kind.LAVA;
};
}
return SubterrainCell.Kind.AIR;
}
private boolean formation(Column column, int u, int v, int y) {
if (column.pathDistance() <= 3 || column.boundary() < 4) {
return false;
}
int spacing = definition.pillarSpacing();
if (spacing > 0 && Math.abs(Math.floorMod(u + spacing / 2, spacing) - spacing / 2) <= 1
&& Math.abs(Math.floorMod(v + spacing / 2, spacing) - spacing / 2) <= 1) {
return true;
}
if (column.head() >= y) {
return false;
}
int cellU = Math.floorDiv(u, 12);
int cellV = Math.floorDiv(v, 12);
long hash = SubterrainPlanner.mix(definition.seedSalt() ^ cellU * 73428767L ^ cellV * 912931L);
int anchorU = cellU * 12 + 3 + (int) Math.floorMod(hash, 6);
int anchorV = cellV * 12 + 3 + (int) Math.floorMod(hash >>> 8, 6);
int distance = Math.max(Math.abs(u - anchorU), Math.abs(v - anchorV));
int height = (int) Math.floor((column.ceiling() - column.floor() - 1) * definition.formationFraction());
int taperHeight = Math.max(0, height - distance * Math.max(1, height / 3));
return distance <= 2 && taperHeight > 0
&& (y <= column.floor() + taperHeight || y >= column.ceiling() - taperHeight);
}
private Column cenote(int u, int v) {
double distance = Math.hypot(u, v);
int radius = definition.radius();
if (distance > radius + 2) {
return null;
}
double normalized = Math.min(1, distance / radius);
int ceiling = baseY() + Math.max(3, (int) Math.floor(definition.height() * Math.sqrt(1 - normalized * normalized)));
int head = baseY() + definition.fluidDepth();
boolean rim = definition.fluidDepth() > 0 && ceiling <= head + 2;
return new Column(baseY(), ceiling, head, distance < radius, rim,
radius - distance, Math.abs(v), 0);
}
private Column fault(int u, int v) {
int halfLength = definition.length() / 2;
int radius = definition.radius();
if (Math.abs(u) > halfLength + 2 || Math.abs(v) > radius + 2) {
return null;
}
int step = Math.max(2, radius / 4);
int shelf = Math.max(0, (Math.abs(v) - radius / 2) / step) * Math.max(2, definition.height() / 8);
return new Column(baseY() + shelf, baseY() + definition.height(), baseY() - 1,
Math.abs(u) < halfLength && Math.abs(v) < radius, false,
Math.min(halfLength - Math.abs(u), radius - Math.abs(v)), Math.abs(v), 0);
}
private Column tube(int u, int v, int y) {
int halfLength = definition.length() / 2;
int path = (int) Math.round(Math.sin(u * Math.PI / Math.max(1, halfLength)) * definition.radius() * 0.15);
int width = Math.max(6, (int) Math.floor(definition.radius() * 0.8));
int offset = v - path;
if (Math.abs(u) > halfLength + 2 || Math.abs(offset) > width + 2) {
return null;
}
double arch = Math.sqrt(Math.max(0, 1 - Math.pow(Math.min(1, Math.abs(offset) / (double) width), 2)));
int floor = centerY - Math.max(2, (int) Math.floor(definition.height() * 0.5 * arch));
int ceiling = centerY + Math.max(2, (int) Math.floor(definition.height() * 0.5 * arch));
int head = baseY() + definition.fluidDepth();
if (Math.abs(offset) >= width * 0.55) {
floor = Math.max(floor, head + 2);
}
int chimneyAlong = Math.min(Math.abs(u - halfLength / 2), Math.abs(u + halfLength / 2));
double chimneyRadius = Math.hypot(chimneyAlong, offset);
if (definition.chimneyHeight() > 0 && chimneyRadius <= 5) {
int tubeCeiling = ceiling;
if (chimneyRadius < 3) {
ceiling = baseY() + definition.height() + definition.chimneyHeight();
} else if (y >= tubeCeiling - 1) {
return new Column(tubeCeiling - 1, baseY() + definition.height() + definition.chimneyHeight(),
head, false, true, 5 - chimneyRadius, Math.abs(offset), path);
}
}
boolean rim = definition.fluidDepth() > 0 && floor >= head && floor < head + 2;
return new Column(floor, ceiling, head, Math.abs(u) < halfLength && Math.abs(offset) < width,
rim, Math.min(halfLength - Math.abs(u), width - Math.abs(offset)), Math.abs(offset), path);
}
private Column terraces(int u, int v) {
int halfLength = definition.length() / 2;
int radius = definition.radius();
if (Math.abs(u) > halfLength + 2 || Math.abs(v) > radius + 2) {
return null;
}
int segment = Math.max(4, definition.length() / definition.terraceCount());
int index = Math.max(0, Math.min(definition.terraceCount() - 1, Math.floorDiv(u + halfLength, segment)));
int rise = Math.max(1, (definition.height() / 3) / definition.terraceCount());
int floor = baseY() + index * rise;
int head = floor + definition.fluidDepth();
int local = Math.floorMod(u + halfLength, segment);
boolean rim = definition.fluidDepth() > 0 && (local <= 1 || local >= segment - 2 || Math.abs(v) >= radius - 2);
int ceiling = baseY() + definition.height();
if (rim) {
floor = head + 1;
}
double pathDistance = Math.abs(Math.abs(v) - (radius - 4));
if (pathDistance <= 2) {
floor = Math.max(floor, head + 1);
}
return new Column(floor, ceiling, head, Math.abs(u) < halfLength && Math.abs(v) < radius,
false, Math.min(halfLength - Math.abs(u), radius - Math.abs(v)), pathDistance,
v < 0 ? -radius + 4 : radius - 4);
}
private SubterrainPosition occupiedAnchor(int x, int preferredY, int z) {
if (sample(x, preferredY, z).occupied()) {
return new SubterrainPosition(x, preferredY, z);
}
for (int candidateY = bounds.minY(); candidateY <= bounds.maxY(); candidateY++) {
if (sample(x, candidateY, z).occupied()) {
return new SubterrainPosition(x, candidateY, z);
}
}
return null;
}
private int baseY() {
return centerY - definition.height() / 2;
}
record Options(SubterrainPlanner.Definition definition, int centerX, int centerY, int centerZ,
boolean rotated, String id) {
}
private record Column(int floor, int ceiling, int head, boolean interior, boolean rim,
double boundary, double pathDistance, int pathOffset) {
}
}
@@ -0,0 +1,253 @@
package art.arcane.iris.generation.subterrain;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
public final class SubterrainPlanner {
private final List<Definition> definitions;
private final long seed;
private final int minY;
private final int maxYExclusive;
private final ThreadLocal<CandidateTile> sampleTile = new ThreadLocal<>();
private final Predicate<SubterrainBounds> allowedBounds;
public SubterrainPlanner(Options options) {
this(options, bounds -> true);
}
public SubterrainPlanner(Options options, Predicate<SubterrainBounds> allowedBounds) {
Objects.requireNonNull(options);
this.allowedBounds = Objects.requireNonNull(allowedBounds);
seed = options.seed();
minY = options.minY();
maxYExclusive = options.maxYExclusive();
if (minY >= maxYExclusive) {
throw new IllegalArgumentException("Subterrain world height is empty");
}
if (options.features().size() > 64) {
throw new IllegalArgumentException("Subterrain supports at most 64 feature definitions");
}
List<Definition> loaded = new ArrayList<>();
Set<String> identifiers = new HashSet<>();
for (IrisSubterrainFeature feature : options.features()) {
if (feature == null || !feature.isEnabled()) {
continue;
}
Definition definition = Definition.from(feature);
if (!identifiers.add(definition.id())) {
throw new IllegalArgumentException("Duplicate subterrain id: " + definition.id());
}
loaded.add(definition);
}
definitions = List.copyOf(loaded);
}
private SubterrainPlanner(SubterrainPlanner source, Predicate<SubterrainBounds> allowedBounds) {
definitions = source.definitions;
seed = source.seed;
minY = source.minY;
maxYExclusive = source.maxYExclusive;
this.allowedBounds = source.allowedBounds.and(Objects.requireNonNull(allowedBounds));
}
public SubterrainPlanner restrictTo(Predicate<SubterrainBounds> allowedBounds) {
return new SubterrainPlanner(this, allowedBounds);
}
public boolean isEmpty() {
return definitions.isEmpty();
}
public List<SubterrainPlan> plansForBounds(int minX, int minZ, int maxX, int maxZ) {
if (minX > maxX || minZ > maxZ) {
throw new IllegalArgumentException("Subterrain query bounds are inverted");
}
List<SubterrainPlan> result = new ArrayList<>();
for (Definition definition : definitions) {
long firstX = Math.floorDiv(minX, definition.spacing()) - 1L;
long lastX = Math.floorDiv(maxX, definition.spacing()) + 1L;
long firstZ = Math.floorDiv(minZ, definition.spacing()) - 1L;
long lastZ = Math.floorDiv(maxZ, definition.spacing()) + 1L;
if ((lastX - firstX + 1L) * (lastZ - firstZ + 1L) > 65_536L) {
throw new IllegalArgumentException("Subterrain candidate query exceeds 65536 grid cells; use bounded search tiles");
}
for (long gridX = firstX; gridX <= lastX; gridX++) {
for (long gridZ = firstZ; gridZ <= lastZ; gridZ++) {
SubterrainPlan plan = create(definition, gridX, gridZ);
if (plan != null && plan.bounds().intersects(minX, minZ, maxX, maxZ)) {
result.add(plan);
}
}
}
}
result.sort(Comparator.comparingInt(SubterrainPlan::priority).reversed().thenComparing(SubterrainPlan::id));
return List.copyOf(result);
}
public SubterrainCell sample(int x, int y, int z) {
if (isEmpty() || y < minY || y >= maxYExclusive) {
return SubterrainCell.OUTSIDE;
}
int chunkX = Math.floorDiv(x, 16);
int chunkZ = Math.floorDiv(z, 16);
CandidateTile tile = sampleTile.get();
if (tile == null || tile.chunkX() != chunkX || tile.chunkZ() != chunkZ) {
long baseX = (long) chunkX * 16;
long baseZ = (long) chunkZ * 16;
tile = new CandidateTile(chunkX, chunkZ, plansForBounds(
(int) Math.max(Integer.MIN_VALUE, baseX - 1),
(int) Math.max(Integer.MIN_VALUE, baseZ - 1),
(int) Math.min(Integer.MAX_VALUE, baseX + 16),
(int) Math.min(Integer.MAX_VALUE, baseZ + 16)));
sampleTile.set(tile);
}
return sample(tile.plans(), x, y, z);
}
public SubterrainCell sample(List<SubterrainPlan> candidates, int x, int y, int z) {
SubterrainCell selected = raw(candidates, x, y, z);
if (!selected.fluid()) {
return selected;
}
if (exposed(raw(candidates, x - 1, y, z), selected)
|| exposed(raw(candidates, x + 1, y, z), selected)
|| exposed(raw(candidates, x, y, z - 1), selected)
|| exposed(raw(candidates, x, y, z + 1), selected)
|| exposed(raw(candidates, x, y - 1, z), selected)) {
return solid(selected);
}
return selected;
}
private SubterrainCell raw(List<SubterrainPlan> candidates, int x, int y, int z) {
SubterrainCell selected = SubterrainCell.OUTSIDE;
for (SubterrainPlan plan : candidates) {
SubterrainCell cell = plan.sample(x, y, z);
if (cell.solid()) {
return cell;
}
if (!selected.owned() && cell.owned()) {
selected = cell;
}
}
return selected;
}
private boolean exposed(SubterrainCell neighbor, SubterrainCell fluid) {
return !neighbor.solid() && neighbor.kind() != fluid.kind();
}
private SubterrainCell solid(SubterrainCell fluid) {
SubterrainRoom room = fluid.room();
SubterrainRoom sealed = new SubterrainRoom(room.featureId(), room.biome(), room.family(),
room.centerX(), room.centerY(), room.centerZ(), room.pathX(), room.pathY(), room.pathZ(),
room.floorY(), room.ceilingY(), room.boundaryDistance(), room.fluidHeadY(), false, true,
SubterrainCell.Kind.SOLID);
for (Definition definition : definitions) {
if (room.featureId().startsWith(definition.id() + ":")) {
return new SubterrainCell(SubterrainCell.Kind.SOLID, definition.solid(), sealed);
}
}
throw new IllegalStateException("Unknown subterrain fluid owner: " + room.featureId());
}
private SubterrainPlan create(Definition definition, long gridX, long gridZ) {
long value = mix(seed ^ definition.seedSalt() ^ mix(gridX) ^ Long.rotateLeft(mix(gridZ), 23));
if (unit(value) >= definition.probability()) {
return null;
}
int spacing = definition.spacing();
long x = gridX * spacing + spacing / 4L + Math.floorMod(mix(value + 1), Math.max(1, spacing / 2));
long z = gridZ * spacing + spacing / 4L + Math.floorMod(mix(value + 2), Math.max(1, spacing / 2));
int bottomMargin = definition.height() / 2 + 3;
int topMargin = definition.height() - definition.height() / 2 + 3
+ (definition.family() == IrisSubterrainFamily.LAVA_TUBE ? definition.chimneyHeight() : 0);
long low = (long) Math.max(minY, definition.minY()) + bottomMargin;
long high = (long) Math.min(maxYExclusive - 1, definition.maxY()) - topMargin;
if (low > high || x < Integer.MIN_VALUE + spacing || x > Integer.MAX_VALUE - spacing
|| z < Integer.MIN_VALUE + spacing || z > Integer.MAX_VALUE - spacing) {
return null;
}
int y = (int) (low + Math.floorMod(mix(value + 3), high - low + 1));
SubterrainPlan plan = new SubterrainPlan(new SubterrainPlan.Options(definition, (int) x, y, (int) z,
(value & 1L) != 0, definition.id() + ":" + Long.toUnsignedString(value, 16)));
return allowedBounds.test(plan.bounds()) ? plan : null;
}
static long mix(long value) {
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
return value ^ (value >>> 31);
}
private static double unit(long value) {
return (value >>> 11) * 0x1.0p-53;
}
public record Options(List<IrisSubterrainFeature> features, long seed, int minY, int maxYExclusive) {
public Options {
features = List.copyOf(Objects.requireNonNull(features));
}
}
private record CandidateTile(int chunkX, int chunkZ, List<SubterrainPlan> plans) {
}
record Definition(String id, IrisSubterrainFamily family, IrisSubterrainFluid fluid, String biome, int spacing, double probability,
int length, int radius, int height, int fluidDepth, int chimneyHeight, int terraceCount,
int pillarSpacing, double formationFraction, String solid, int priority,
int minY, int maxY, long seedSalt) {
static Definition from(IrisSubterrainFeature feature) {
String id = Objects.requireNonNull(feature.getId(), "Subterrain id").trim();
IrisSubterrainFamily family = Objects.requireNonNull(feature.getFamily(), "Subterrain family");
IrisSubterrainFluid fluid = Objects.requireNonNullElse(feature.getFluid(),
family == IrisSubterrainFamily.LAVA_TUBE ? IrisSubterrainFluid.LAVA : IrisSubterrainFluid.WATER);
int length = feature.getLength();
int radius = feature.getRadius();
int height = feature.getHeight();
int chimney = feature.getChimneyHeight();
int horizontalReach = family == IrisSubterrainFamily.CENOTE ? radius : Math.max(length / 2, radius);
if (!id.matches("[a-zA-Z0-9_/-]+") || length < 16 || length > 2048 || radius < 6 || radius > 256
|| height < 8 || height > 192 || chimney < 0 || chimney > 96
|| feature.getSpacing() < Math.max(32, 2 * horizontalReach + 8) || feature.getSpacing() > 8192
|| !Double.isFinite(feature.getProbability()) || feature.getProbability() < 0 || feature.getProbability() > 1
|| feature.getFluidDepth() < 0 || feature.getFluidDepth() > Math.min(32, height / 3)
|| feature.getTerraceCount() < 2 || feature.getTerraceCount() > 16
|| feature.getPillarSpacing() < 0 || feature.getPillarSpacing() > 128
|| !Double.isFinite(feature.getFormationFraction()) || feature.getFormationFraction() < 0
|| feature.getFormationFraction() > 0.4 || (family == IrisSubterrainFamily.TECTONIC_FAULT && length < 200)) {
throw new IllegalArgumentException("Invalid subterrain dimensions or placement settings: " + id);
}
if (feature.getWorldYRange() == null || !Double.isFinite(feature.getWorldYRange().getMin())
|| !Double.isFinite(feature.getWorldYRange().getMax())
|| feature.getWorldYRange().getMin() < Integer.MIN_VALUE
|| feature.getWorldYRange().getMax() > Integer.MAX_VALUE
|| feature.getWorldYRange().getMin() > feature.getWorldYRange().getMax()) {
throw new IllegalArgumentException("Invalid subterrain world Y range: " + id);
}
String solid = Objects.requireNonNull(feature.getSolid(), "Subterrain solid").trim();
if (!solid.matches("(?:[a-z0-9_.-]+:)?[a-z0-9_/.-]+(?:\\[[a-z0-9_=,.-]+\\])?")
|| solid.matches("(?:minecraft:)?(?:air|cave_air|void_air|water|lava)(?:\\[.*\\])?")) {
throw new IllegalArgumentException("Invalid subterrain solid block state: " + id);
}
String biome = Objects.requireNonNullElse(feature.getBiome(), "").trim();
if (!biome.isEmpty() && !biome.matches("[a-zA-Z0-9_/-]+")) {
throw new IllegalArgumentException("Invalid subterrain biome key: " + id);
}
long salt = 0xcbf29ce484222325L;
for (int i = 0; i < id.length(); i++) {
salt = (salt ^ id.charAt(i)) * 0x100000001b3L;
}
return new Definition(id, family, fluid, biome,
feature.getSpacing(), feature.getProbability(), length, radius, height, feature.getFluidDepth(),
chimney, feature.getTerraceCount(), feature.getPillarSpacing(), feature.getFormationFraction(),
solid, feature.getPriority(), (int) Math.ceil(feature.getWorldYRange().getMin()),
(int) Math.floor(feature.getWorldYRange().getMax()), salt);
}
}
}
@@ -0,0 +1,4 @@
package art.arcane.iris.generation.subterrain;
public record SubterrainPosition(int x, int y, int z) {
}
@@ -0,0 +1,84 @@
package art.arcane.iris.generation.subterrain;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.matter.MatterCavern;
import java.util.List;
import java.util.Objects;
public final class SubterrainRasterizer {
private SubterrainRasterizer() {
}
public static void validateMaterials(List<IrisSubterrainFeature> features) {
for (IrisSubterrainFeature feature : features) {
if (feature == null || !feature.isEnabled()) {
continue;
}
NativeBlockState state = B.getStateOrNull(feature.getSolid(), false);
if (!isRetainedSolid(state)) {
throw new IllegalArgumentException("Subterrain feature " + feature.getId()
+ " requires a full, opaque, dry vanilla solid block without gravity: " + feature.getSolid());
}
}
}
public static boolean isRetainedSolid(NativeBlockState state) {
return state != null && state.isSolid() && state.isOccluding() && !state.isFluid()
&& !state.isWaterLogged() && !state.isCustom() && !IrisProceduralBlocks.isGravityAffected(state);
}
public static void rasterize(SubterrainPlanner planner, int chunkX, int chunkZ, CellConsumer consumer) {
Objects.requireNonNull(planner);
Objects.requireNonNull(consumer);
int baseX = chunkX << 4;
int baseZ = chunkZ << 4;
List<SubterrainPlan> plans = planner.plansForBounds(baseX - 1, baseZ - 1, baseX + 16, baseZ + 16);
if (plans.isEmpty()) {
return;
}
int lowY = Integer.MAX_VALUE;
int highY = Integer.MIN_VALUE;
for (SubterrainPlan plan : plans) {
lowY = Math.min(lowY, plan.bounds().minY());
highY = Math.max(highY, plan.bounds().maxY());
}
for (int x = baseX; x < baseX + 16; x++) {
for (int z = baseZ; z < baseZ + 16; z++) {
for (int y = lowY; y <= highY; y++) {
SubterrainCell cell = planner.sample(plans, x, y, z);
if (cell.owned()) {
consumer.accept(x, y, z, cell);
}
}
}
}
}
public static MatterCavern cavern(SubterrainCell cell) {
if (!cell.carve()) {
return null;
}
byte liquid = switch (cell.kind()) {
case WATER -> 1;
case LAVA -> 2;
default -> 3;
};
return new MatterCavern(true, cell.room().biome(), liquid);
}
public static NativeBlockState state(SubterrainCell cell) {
return IrisProceduralBlocks.normalizeWaterlogging(B.getState(cell.material()), null, false);
}
public static boolean protectsPlacement(SubterrainCell cell) {
return cell != null && cell.owned() && (cell.solid() || cell.room().reservedPassage());
}
@FunctionalInterface
public interface CellConsumer {
void accept(int x, int worldY, int z, SubterrainCell cell);
}
}
@@ -0,0 +1,10 @@
package art.arcane.iris.generation.subterrain;
public record SubterrainRoom(String featureId, String biome, IrisSubterrainFamily family,
int centerX, int centerY, int centerZ, int pathX, int pathY, int pathZ,
int floorY, int ceilingY, double boundaryDistance, int fluidHeadY,
boolean reservedPassage, boolean reservedSolid, SubterrainCell.Kind occupancy) {
public int vaultHeight() {
return Math.max(0, ceilingY - floorY - 1);
}
}
@@ -0,0 +1,95 @@
package art.arcane.iris.generation.subterrain;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public final class SubterrainSurfaceMaterials {
private final SubterrainPlanner planner;
private final List<SubterrainPlan> plans;
private final IrisDimension dimension;
private final IrisComplex complex;
private final IrisData data;
private final RNG random;
private final int minimumY;
private final Map<String, IrisBiome> biomes = new HashMap<>();
public SubterrainSurfaceMaterials(SubterrainPlanner planner, List<SubterrainPlan> plans,
IrisDimension dimension, IrisComplex complex, IrisData data,
RNG random, int minimumY) {
this.planner = planner;
this.plans = plans;
this.dimension = dimension;
this.complex = complex;
this.data = data;
this.random = random;
this.minimumY = minimumY;
}
public NativeBlockState state(int x, int worldY, int z, SubterrainCell cell) {
NativeBlockState fallback = SubterrainRasterizer.state(cell);
if (!cell.solid() || cell.room().biome().isEmpty()) {
return fallback;
}
Surface surface = surface(x, worldY, z, cell);
if (surface == Surface.INTERIOR) {
return fallback;
}
String key = cell.room().biome();
if (!biomes.containsKey(key)) {
biomes.put(key, data.getBiomeLoader().load(key));
}
IrisBiome biome = biomes.get(key);
if (biome == null) {
return fallback;
}
NativeBlockState candidate;
if (surface == Surface.WALL) {
candidate = biome.getWall().get(random, x, worldY - minimumY, z, data);
} else {
KList<NativeBlockState> layers = surface == Surface.FLOOR
? biome.generateLayers(dimension, x, z, random, 1, worldY + 1 - minimumY, data, complex)
: biome.generateCeilingLayers(dimension, x, z, random, 1, worldY - 1 - minimumY, data, complex);
candidate = layers.isEmpty() ? null : layers.getFirst();
}
return SubterrainRasterizer.isRetainedSolid(candidate) ? candidate : fallback;
}
public Surface surface(int x, int worldY, int z, SubterrainCell cell) {
if (!cell.solid()) {
return Surface.INTERIOR;
}
if (sameRoom(planner.sample(plans, x, worldY + 1, z), cell)) {
return Surface.FLOOR;
}
if (sameRoom(planner.sample(plans, x, worldY - 1, z), cell)) {
return Surface.CEILING;
}
if (sameRoom(planner.sample(plans, x - 1, worldY, z), cell)
|| sameRoom(planner.sample(plans, x + 1, worldY, z), cell)
|| sameRoom(planner.sample(plans, x, worldY, z - 1), cell)
|| sameRoom(planner.sample(plans, x, worldY, z + 1), cell)) {
return Surface.WALL;
}
return Surface.INTERIOR;
}
private static boolean sameRoom(SubterrainCell neighbor, SubterrainCell cell) {
return neighbor.occupied() && neighbor.room().featureId().equals(cell.room().featureId());
}
public enum Surface {
FLOOR,
CEILING,
WALL,
INTERIOR
}
}
@@ -18,6 +18,7 @@
package art.arcane.iris.generation.terrain;
import art.arcane.iris.world.entity.IrisSpawnGroup;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.biome.IrisCustomBiomeAliasResolver;
@@ -30,6 +31,7 @@ import art.arcane.iris.generation.decoration.IrisOreGenerator;
import art.arcane.iris.generation.decoration.IrisOreBands;
import art.arcane.iris.generation.decoration.tree.IrisTreeSettings;
import art.arcane.iris.generation.hydrology.IrisDeepFluidConfig;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.hydrology.IrisHydrology;
import art.arcane.iris.generation.hydrology.IrisRiverHydrology;
import art.arcane.iris.generation.hydrology.IrisRiverPolicy;
@@ -86,6 +88,7 @@ import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CNG;
@@ -143,6 +146,7 @@ public class IrisDimension extends IrisRegistrant {
private final transient AtomicCache<Double> sinr = new AtomicCache<>();
private final transient AtomicCache<Double> cosr = new AtomicCache<>();
private final transient AtomicCache<Double> rad = new AtomicCache<>();
private final transient AtomicCache<Boolean> undergroundSpawnTables = new AtomicCache<>();
private final transient AtomicCache<Boolean> featuresUsed = new AtomicCache<>();
private final transient AtomicCache<Map<String, IrisDimensionCarvingEntry>> carvingEntryIndex = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>();
@@ -222,6 +226,9 @@ public class IrisDimension extends IrisRegistrant {
@ArrayType(type = IrisDimensionCarvingEntry.class, min = 1)
@Description("Dimension-level cave biome carving overrides with absolute world Y ranges")
private KList<IrisDimensionCarvingEntry> carving = new KList<>();
@ArrayType(type = IrisSubterrainFeature.class, min = 1)
@Description("Deterministic bounded underground features with owned geometry, biome, fluids and room formations.")
private KList<IrisSubterrainFeature> subterrainFeatures = new KList<>();
@Description("Profile-driven 3D cave configuration")
private IrisCaveProfile caveProfile = new IrisCaveProfile();
@Description("Dimension-owned surface, underground, grotto, and deep-fluid hydrology configuration.")
@@ -733,6 +740,14 @@ public class IrisDimension extends IrisRegistrant {
}
}
if (getSubterrainFeatures() != null) {
for (IrisSubterrainFeature feature : getSubterrainFeatures()) {
if (feature != null && feature.isEnabled()) {
addReachableBiomeKey(pending, feature.getBiome());
}
}
}
Set<String> visited = new HashSet<>();
Map<String, IrisDimensionCarvingEntry> carvingEntryIndex = getCarvingEntryIndex();
while (!pending.isEmpty()) {
@@ -1187,6 +1202,44 @@ public class IrisDimension extends IrisRegistrant {
return new IrisDimensionType(getBaseDimension(), options, getLogicalHeight(), getMaxHeight() - getMinHeight(), getMinHeight());
}
public boolean hasUndergroundSpawners(DataProvider provider) {
return undergroundSpawnTables.aquire(() -> {
IrisData data = provider.getData();
if (hasCaveSpawners(data, entitySpawners)) {
return true;
}
for (IrisRegion region : getAllRegions(provider)) {
if (region != null && hasCaveSpawners(data, region.getEntitySpawners())) {
return true;
}
}
for (IrisBiome biome : getReachableBiomes(provider)) {
if (biome != null && hasCaveSpawners(data, biome.getEntitySpawners())) {
return true;
}
}
return false;
});
}
private static boolean hasCaveSpawners(IrisData data, KList<String> keys) {
if (keys == null || keys.isEmpty() || data.getSpawnerLoader() == null) {
return false;
}
KList<IrisSpawner> spawners = data.getSpawnerLoader().loadAll(keys);
for (IrisSpawner spawner : spawners) {
if (spawner != null && !spawner.isCompatExcluded() && spawner.getGroup() == IrisSpawnGroup.CAVE) {
return true;
}
}
return false;
}
public boolean allowsSubterrainFeatures() {
return useMantle && carvingEnabled && mode != null && mode.getType() == IrisDimensionModeType.OVERWORLD
&& !disabledComponents.contains(ReservedFlag.CARVED);
}
public boolean hasUpperDimension() {
return upperDimension != null && !upperDimension.isEmpty() && !upperDimension.equalsIgnoreCase("none");
}
@@ -40,7 +40,7 @@ public final class IrisDimensionCarvingResolver {
if (root == null) {
return null;
}
IrisDimensionCarvingEntry resolved = resolveBoundFromRoot(engine, root, worldX, worldZ, data, resolvedState);
IrisDimensionCarvingEntry resolved = resolveBoundFromRoot(engine, root, worldX, worldY, worldZ, data, resolvedState);
return resolved == null ? null : resolveBoundEntryBiome(data, resolved, resolvedState);
}
@@ -54,14 +54,14 @@ public final class IrisDimensionCarvingResolver {
return resolveBoundRootEntry(engine, worldY, data, resolvedState);
}
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ) {
return resolveFromRoot(engine, rootEntry, worldX, worldZ, threadState());
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldY, int worldZ) {
return resolveFromRoot(engine, rootEntry, worldX, worldY, worldZ, threadState());
}
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ, State state) {
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldY, int worldZ, State state) {
State resolvedState = state == null ? threadState() : state;
IrisData data = resolvedState.bind(engine);
return resolveBoundFromRoot(engine, rootEntry, worldX, worldZ, data, resolvedState);
return resolveBoundFromRoot(engine, rootEntry, worldX, worldY, worldZ, data, resolvedState);
}
public static IrisBiome resolveEntryBiome(Engine engine, IrisDimensionCarvingEntry entry) {
@@ -109,7 +109,7 @@ public final class IrisDimensionCarvingResolver {
return resolved;
}
private static IrisDimensionCarvingEntry resolveBoundFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ, IrisData data, State resolvedState) {
private static IrisDimensionCarvingEntry resolveBoundFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldY, int worldZ, IrisData data, State resolvedState) {
if (rootEntry == null) {
return null;
}
@@ -127,7 +127,7 @@ public final class IrisDimensionCarvingResolver {
ColumnResolutions columns = resolvedState.columns;
int slot = ((worldX & 31) << 5) | (worldZ & 31);
long coordinate = ((long) worldX << 32) | (worldZ & 0xffffffffL);
if (columns != null && columns.roots[slot] == rootEntry && columns.coordinates[slot] == coordinate) {
if (columns != null && columns.roots[slot] == rootEntry && columns.coordinates[slot] == coordinate && columns.worldYs[slot] == worldY) {
return columns.entries[slot];
}
@@ -138,7 +138,8 @@ public final class IrisDimensionCarvingResolver {
while (depth > 0) {
cacheable &= fixedChildStyle(current);
IrisDimensionCarvingEntry selected = selectChild(engine, data, current, worldX, worldZ, entryIndex, resolvedState);
if (selected == null || selected == current) {
if (selected == null || selected == current || selected.getWorldYRange() != null
&& !selected.getWorldYRange().contains(worldY)) {
break;
}
@@ -156,6 +157,7 @@ public final class IrisDimensionCarvingResolver {
resolvedState.columns = columns;
}
columns.coordinates[slot] = coordinate;
columns.worldYs[slot] = worldY;
columns.entries[slot] = current;
columns.roots[slot] = rootEntry;
}
@@ -332,7 +334,7 @@ public final class IrisDimensionCarvingResolver {
if (root == null) {
return null;
}
IrisDimensionCarvingEntry resolved = resolveBoundFromRoot(engine, root, worldX, worldZ, data, state);
IrisDimensionCarvingEntry resolved = resolveBoundFromRoot(engine, root, worldX, worldY, worldZ, data, state);
return resolved == null ? null : resolveBoundEntryBiome(data, resolved, state);
}
}
@@ -399,6 +401,7 @@ public final class IrisDimensionCarvingResolver {
private static final class ColumnResolutions {
private final long[] coordinates = new long[1024];
private final int[] worldYs = new int[1024];
private final IrisDimensionCarvingEntry[] roots = new IrisDimensionCarvingEntry[1024];
private final IrisDimensionCarvingEntry[] entries = new IrisDimensionCarvingEntry[1024];
}
@@ -538,6 +538,10 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
if (engine == null || engine.isClosed() || engine.getComplex() == null) {
return false;
}
if (engine.getDimension().allowsSubterrainFeatures()
&& engine.getDimension().getSubterrainFeatures() != null && !engine.getDimension().getSubterrainFeatures().isEmpty()) {
return false;
}
int worldMinHeight = engine.getWorld().minHeight();
int internalY = (quartY << 2) - worldMinHeight;
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
@@ -888,6 +892,10 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
int worldMinHeight = engine.getWorld().minHeight();
int internalY = blockY - worldMinHeight;
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
IrisBiome featureBiome = engine.getSubterrainBiome(blockX, internalY, blockZ);
if (featureBiome != null) {
return createBiomeResolution(featureBiome, true, engine.getDimension(), blockX, blockY, blockZ);
}
DimensionStackLayout.Layer stackLayer = includeDimensionStack
? stackLayout == null
? resolveDimensionStackLayer(engine, blockX, internalY, blockZ)
@@ -337,7 +337,14 @@ public class IrisObject extends IrisRegistrant {
public int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer<BlockPosition, NativeBlockState> listener, CarveResult c, IrisData rdata) {
IrisObject placedObject = config.isSmartBore() ? IrisObjectShaping.smartBoredVariant(this) : this;
return new IrisObjectPlacementRunner(placedObject).place(x, yv, z, oplacer, config, rng, listener, c, rdata);
return new IrisObjectPlacementRunner(placedObject, IrisObjectPlacementAlignment.CENTER).place(x, yv, z, oplacer, config, rng, listener, c, rdata);
}
public int placeOnFloor(int x, int firstAirY, int z, IObjectPlacer placer, IrisObjectPlacement config,
RNG rng, BiConsumer<BlockPosition, NativeBlockState> listener, IrisData data) {
IrisObject placedObject = config.isSmartBore() ? IrisObjectShaping.smartBoredVariant(this) : this;
return new IrisObjectPlacementRunner(placedObject, IrisObjectPlacementAlignment.FLOOR)
.place(x, firstAirY, z, placer, config, rng, listener, null, data);
}
KList<IrisBlockVector> getSurfaceSupportOffsets() {
@@ -0,0 +1,6 @@
package art.arcane.iris.structure.object;
public enum IrisObjectPlacementAlignment {
CENTER,
FLOOR
}
@@ -68,9 +68,11 @@ final class IrisObjectPlacementRunner {
private static final ConcurrentHashMap<String, Long> IMPLAUSIBLE_BEDROCK_WARNS = new ConcurrentHashMap<>();
private final IrisObject self;
private final IrisObjectPlacementAlignment alignment;
IrisObjectPlacementRunner(IrisObject self) {
IrisObjectPlacementRunner(IrisObject self, IrisObjectPlacementAlignment alignment) {
this.self = self;
this.alignment = Objects.requireNonNull(alignment);
}
int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer<BlockPosition, NativeBlockState> listener, CarveResult c, IrisData rdata) {
@@ -169,7 +171,19 @@ final class IrisObjectPlacementRunner {
TransformedBounds placementBounds = transformedBounds(spin, translating, translateOffset, ceilingHang, warpMargin);
boolean bail = false;
if (config.isFromBottom()) {
if (alignment == IrisObjectPlacementAlignment.FLOOR) {
if (yv < 0 || ceilingHang || rawStructurePiece) {
throw new IllegalArgumentException("Floor alignment requires an explicit floor Y and a floor placement mode.");
}
int minimumY = minimumOccupiedY(spin);
if (minimumY == Integer.MAX_VALUE) {
return -1;
}
y = yv - minimumY;
if (!config.isForcePlace() && shouldBailForCarvingAnchor(placer, config, x, yv, z)) {
bail = true;
}
} else if (config.isFromBottom()) {
// todo Convert this to a dedicated mode.
y = (self.getH() + 1) + rty;
if (!config.isForcePlace()) {
@@ -290,7 +304,7 @@ final class IrisObjectPlacementRunner {
}
}
if (yv >= 0 && config.isBottom() && !rawStructurePiece) {
if (yv >= 0 && config.isBottom() && !rawStructurePiece && alignment != IrisObjectPlacementAlignment.FLOOR) {
y += Math.floorDiv(self.h, 2);
CarvingMode carvingMode = config.getCarvingSupport();
if (!config.isForcePlace() && !carvingMode.equals(CarvingMode.CARVING_ONLY)) {
@@ -608,9 +622,7 @@ final class IrisObjectPlacementRunner {
continue;
}
if (waterlogCandidate && IrisProceduralBlocks.hasProperty(data, "waterlogged") && shouldAutoWaterlogBlock(placer, config, yv, xx, yy, zz)) {
data = data.withProperty("waterlogged", "true");
}
data = IrisProceduralBlocks.normalizeWaterlogging(data, placer.get(xx, yy, zz), waterlogCandidate);
if (!rawStructurePiece && B.isVineBlock(data)) {
data = attachVineFaces(placer, data, xx, yy, zz);
@@ -810,7 +822,7 @@ final class IrisObjectPlacementRunner {
}
}
}
placer.set(xx, j, zz, d);
placer.set(xx, j, zz, IrisProceduralBlocks.normalizeWaterlogging(d, placer.get(xx, j, zz), waterlogCandidate));
}
} else {
int scan = 0;
@@ -832,7 +844,7 @@ final class IrisObjectPlacementRunner {
}
}
}
placer.set(xx, j, zz, d);
placer.set(xx, j, zz, IrisProceduralBlocks.normalizeWaterlogging(d, placer.get(xx, j, zz), waterlogCandidate));
}
}
continue;
@@ -840,9 +852,7 @@ final class IrisObjectPlacementRunner {
int highest = placer.getHighest(xx, zz, self.getLoader(), true);
if (IrisProceduralBlocks.hasProperty(d, "waterlogged") && shouldAutoWaterlogBlock(placer, config, yv, xx, highest, zz)) {
d = d.withProperty("waterlogged", "true");
}
int lowerBound = highest - 1;
if (settings != null) {
@@ -882,7 +892,7 @@ final class IrisObjectPlacementRunner {
if (B.isVineBlock(d)) {
d = attachVineFaces(placer, d, xx, j, zz);
}
placer.set(xx, j, zz, d);
placer.set(xx, j, zz, IrisProceduralBlocks.normalizeWaterlogging(d, placer.get(xx, j, zz), waterlogCandidate));
}
}
@@ -940,6 +950,26 @@ final class IrisObjectPlacementRunner {
return offsets;
}
private int minimumOccupiedY(SpinKernel spin) {
int minimumY = Integer.MAX_VALUE;
self.readLock.lock();
try {
VectorMap<NativeBlockState>.Cursor cursor = self.blocks.cursor();
while (cursor.next()) {
NativeBlockState state = cursor.value();
if (state == null || state.isAir()) {
continue;
}
IrisBlockVector position = cursor.key().clone();
spin.rotate(position);
minimumY = Math.min(minimumY, (int) Math.round(position.getY()));
}
} finally {
self.readLock.unlock();
}
return minimumY;
}
private TransformedBounds transformedBounds(SpinKernel spin, boolean translating, IrisBlockVector translateOffset,
boolean ceilingHang, int margin) {
int sourceMinX = -self.getCenter().getBlockX();
@@ -1218,23 +1248,6 @@ final class IrisObjectPlacementRunner {
|| placer.isCarved(x, y - 3, z);
}
private boolean shouldAutoWaterlogBlock(IObjectPlacer placer, IrisObjectPlacement placement, int yv, int x, int y, int z) {
if (!(placement.isWaterloggable() || placement.isUnderwater())) {
return false;
}
if (yv >= 0 && placement.getCarvingSupport().equals(CarvingMode.CARVING_ONLY)) {
return false;
}
NativeBlockState existing = placer.get(x, y, z);
if (existing == null) {
return false;
}
return B.isWater(existing) || B.isWaterLogged(existing);
}
private static NativeBlockState attachVineFaces(IObjectPlacer placer, NativeBlockState data, int x, int y, int z) {
NativeBlockState result = data;
for (String face : IrisProceduralBlocks.FACE_PROPERTIES) {
@@ -124,6 +124,22 @@ public class IrisEntitySpawn implements Rarity {
return spawned;
}
public int spawn(Engine engine, Location candidate, RNG random, int remainingCapacity, SpawnContext context) {
if (remainingCapacity <= 0) {
return 0;
}
int count = Math.min(remainingCapacity, LootResolver.inclusive(random, minSpawns, maxSpawns));
long batchSeed = random.getSeed();
int spawned = 0;
for (int ordinal = 0; ordinal < count; ordinal++) {
if (lightAllowed(candidate, context)
&& spawn100(engine, candidate, EntitySpawnSeed.entity(batchSeed, ordinal), false) != null) {
spawned++;
}
}
return spawned;
}
public static Integer selectSurfaceSpawnY(IrisSpawnGroup group, IrisSurface surface,
int floor, int top, RNG rng) {
if (group == IrisSpawnGroup.CAVE) {
@@ -206,7 +222,7 @@ public class IrisEntitySpawn implements Rarity {
return selectCaveSpawnLocation(markers, chunk.getWorld(), rng);
}
static Location findLiveCaveSpawnLocation(Chunk chunk, RNG rng, IrisSurface surface) {
public static Location findLiveCaveSpawnLocation(Chunk chunk, RNG rng, IrisSurface surface) {
boolean fluid = surface.isFluid();
World world = chunk.getWorld();
int minimumY = world.getMinHeight() + 1;
@@ -306,6 +306,10 @@ public final class GenerationSemanticCapture {
snapshot = IrisDimensionCarvingResolver.snapshot(engine);
minimumY = engine.getWorld().minHeight();
}
IrisBiome featureBiome = engine.getSubterrainBiome(x, y, z);
if (featureBiome != null) {
return featureBiome;
}
IrisBiome configured = snapshot.resolveBiome(x, y + minimumY, z);
if (configured != null) {
return configured;
@@ -2,6 +2,8 @@ package art.arcane.iris.world.history;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.locator.SubterrainLocator;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.structure.placement.IrisStructureLocator;
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
@@ -9,6 +11,8 @@ import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import java.math.BigInteger;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
@@ -19,6 +23,57 @@ public final class GenerationSemanticQueries {
private GenerationSemanticQueries() {
}
public static Optional<SubterrainLocator.Result> nearestSubterrain(
Engine engine, SubterrainLocator.Query query, int x, int worldY, int z, int maximumDistance) {
Engine requiredEngine = Objects.requireNonNull(engine, "engine");
Objects.requireNonNull(query, "query");
GenerationHistoryRuntimeRouter router = requiredEngine instanceof IrisEngine irisEngine
? irisEngine.getGenerationHistoryRuntimeRouter().orElse(null) : null;
if (router == null) {
return SubterrainLocator.nearest(requiredEngine, query, x, worldY, z, maximumDistance);
}
GenerationHistory history = router.history();
SubterrainLocator.Result nearest = null;
try {
for (GenerationActivation activation : history.manifest().activations()) {
if (history.manifest().pendingActivation().map(pending ->
pending.activationId() == activation.activationId()).orElse(false)) {
continue;
}
SubterrainPlanner planner = router.biomes().subterrainPlanner(activation.activationId());
SubterrainLocator.Result candidate = SubterrainLocator.nearest(planner, query, x, worldY, z,
maximumDistance, result -> allowsSubterrainPrediction(requiredEngine, history,
activation.activationId(), result)).orElse(null);
if (candidate != null && (nearest == null || distanceSquared(x, worldY, z, candidate)
.compareTo(distanceSquared(x, worldY, z, nearest)) < 0)) {
nearest = candidate;
}
}
} catch (IOException failure) {
throw new UncheckedIOException("Unable to locate saved subterrain features", failure);
}
return Optional.ofNullable(nearest);
}
private static boolean allowsSubterrainPrediction(Engine engine, GenerationHistory history,
long activationId, SubterrainLocator.Result result) {
int chunkX = Math.floorDiv(result.x(), 16);
int chunkZ = Math.floorDiv(result.z(), 16);
GenerationActivation owner = history.resolveActivation(chunkX, chunkZ);
if (owner.activationId() != activationId) {
return false;
}
if (history.isActiveUnowned(chunkX, chunkZ)) {
return engine.getComplex().allowsNewDiscreteContentAt(result.x(), result.z());
}
return true;
}
private static BigInteger distanceSquared(int x, int y, int z, SubterrainLocator.Result result) {
BigInteger deltaY = BigInteger.valueOf((long) y - result.y());
return horizontalDistanceSquared(x, z, result.x(), result.z()).add(deltaY.multiply(deltaY));
}
public static IrisStructureLocator.LocateResult nearestStructure(
Engine engine,
String key,
@@ -3,6 +3,7 @@ package art.arcane.iris.world.history;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import java.io.IOException;
import java.util.ArrayList;
@@ -30,9 +31,12 @@ public final class SavedBiomeCapture {
stage.chunkX(), stage.chunkZ(), activationId, minimumY, height));
TransitionGenerationPlan transition = engine.getComplex().getTransitionGenerationPlan();
Map<Long, Optional<SavedBiomeChunk>> historical = new HashMap<>();
Map<Integer, List<SavedBiomeChunk.Span>> vertical = new HashMap<>(16);
SubterrainPlanner planner = engine.getComplex().getSubterrainPlanner();
boolean exactVolume = planner != null && !planner.plansForBounds(startX, startZ,
Math.addExact(startX, 15), Math.addExact(startZ, 15)).isEmpty();
Map<Integer, List<SavedBiomeChunk.Span>> vertical = new HashMap<>(exactVolume ? 256 : 16);
Cells cells = new Cells(activationId);
Column column = new Column(engine, minimumY, height);
Column column = new Column(engine, minimumY, height, exactVolume ? 1 : 4);
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
int blockX = startX + localX;
@@ -51,13 +55,13 @@ public final class SavedBiomeCapture {
if (transition != null) {
surfaceCell = historicalCell(new Sample(blockX, surfaceY, blockZ, true), surfaceCell, transition, saved, historical);
}
int quartX = localX & ~3;
int quartZ = localZ & ~3;
int quartKey = quartX * 16 + quartZ;
List<SavedBiomeChunk.Span> spans = vertical.get(quartKey);
int sampleX = exactVolume ? localX : localX & ~3;
int sampleZ = exactVolume ? localZ : localZ & ~3;
int columnKey = sampleX * 16 + sampleZ;
List<SavedBiomeChunk.Span> spans = vertical.get(columnKey);
if (spans == null) {
spans = captureVertical(column, startX + quartX, startZ + quartZ, cells, transition, saved, historical, floating);
vertical.put(quartKey, spans);
spans = captureVertical(column, startX + sampleX, startZ + sampleZ, cells, transition, saved, historical, floating);
vertical.put(columnKey, spans);
}
SavedBiomeChunk.Cell caveBase = cells.cell(engine.getCaveBiome(blockX, blockZ), region);
result.column(localX, localZ, new SavedBiomeChunk.Column(surfaceCell, caveBase, spans));
@@ -69,16 +73,16 @@ public final class SavedBiomeCapture {
private static List<SavedBiomeChunk.Span> captureVertical(Column column, int blockX, int blockZ, Cells cells,
TransitionGenerationPlan transition, SavedBiomeRuntime saved,
Map<Long, Optional<SavedBiomeChunk>> historical, FloatingBiomeOverlay floating) throws IOException {
column.engine().getBiomeOrMantleColumn(blockX, blockZ, 4, column.biomes(), column.regions());
column.engine().getBiomeOrMantleColumn(blockX, blockZ, column.step(), column.biomes(), column.regions());
int minimumY = column.minimumY();
int maximumY = minimumY + column.height();
List<SavedBiomeChunk.Span> spans = new ArrayList<>();
SavedBiomeChunk.Cell previous = null;
int startY = minimumY;
for (int index = 0; index < column.biomes().length; index++) {
int worldY = minimumY + index * 4;
int worldY = minimumY + index * column.step();
SavedBiomeChunk.Cell current = cells.cell(column.biomes()[index], column.regions()[index]);
if (floating != null) {
if (floating != null && !column.engine().getSubterrainCell(blockX, index * column.step(), blockZ).occupied()) {
current = overlay(current, floating.volumeAt(blockX & 15, worldY - minimumY, blockZ & 15));
}
if (transition != null) {
@@ -135,9 +139,10 @@ public final class SavedBiomeCapture {
Objects.requireNonNull(region, "region").getLoadKey());
}
private record Column(Engine engine, int minimumY, int height, IrisBiome[] biomes, IrisRegion[] regions) {
private Column(Engine engine, int minimumY, int height) {
this(engine, minimumY, height, new IrisBiome[(height + 3) / 4], new IrisRegion[(height + 3) / 4]);
private record Column(Engine engine, int minimumY, int height, int step, IrisBiome[] biomes, IrisRegion[] regions) {
private Column(Engine engine, int minimumY, int height, int step) {
this(engine, minimumY, height, step, new IrisBiome[(height + step - 1) / step],
new IrisRegion[(height + step - 1) / step]);
}
}
@@ -3,6 +3,10 @@ package art.arcane.iris.world.history;
import art.arcane.iris.generation.runtime.GenerationClosedException;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.SeedManager;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.generation.runtime.BiomeEnvironment;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisDimension;
@@ -10,6 +14,7 @@ import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.spi.IrisLogging;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
@@ -17,6 +22,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -39,6 +45,7 @@ public final class SavedBiomeRuntime implements AutoCloseable {
private final GenerationHistory history;
private final SavedBiomeStore store;
private final Map<String, Definitions> definitions = new ConcurrentHashMap<>();
private final Map<Long, SubterrainPlanner> subterrainPlanners = new ConcurrentHashMap<>();
private final LinkedHashMap<Long, PreparedQuery> queries = new LinkedHashMap<>(32, 0.75F, true);
private final Map<Long, PendingQuery> pending = new HashMap<>();
private final ReentrantReadWriteLock consumption = new ReentrantReadWriteLock();
@@ -135,6 +142,41 @@ public final class SavedBiomeRuntime implements AutoCloseable {
return resolve(blockX, 0, blockZ, QueryKind.CAVE_BASE);
}
public Optional<SubterrainCell> subterrainCell(int blockX, int worldY, int blockZ) {
int chunkX = Math.floorDiv(blockX, 16);
int chunkZ = Math.floorDiv(blockZ, 16);
if (history.isActiveUnowned(chunkX, chunkZ) && history.semantics(chunkX, chunkZ).isEmpty()) {
return Optional.empty();
}
try {
long activationId = history.resolveActivation(chunkX, chunkZ).activationId();
return Optional.of(subterrainPlanner(activationId).sample(blockX, worldY, blockZ));
} catch (IOException failure) {
throw new UncheckedIOException("Unable to read saved subterrain definitions", failure);
}
}
public SubterrainPlanner subterrainPlanner(long activationId) throws IOException {
consumption.readLock().lock();
try {
requireOpen();
SubterrainPlanner cached = subterrainPlanners.get(activationId);
if (cached != null) {
return cached;
}
GenerationActivation activation = history.manifest().activation(activationId).orElseThrow(() ->
new IOException("Saved subterrain generation is missing: " + activationId));
TransitionGenerationPlan transition = activation.isInitial() ? null : history.transitionPlan(activationId);
SubterrainPlanner planner = definitions(activationId).subterrainPlanner().restrictTo(bounds ->
transition == null || transition.allowsNewFootprint(
bounds.minX(), bounds.minZ(), bounds.maxX(), bounds.maxZ()));
SubterrainPlanner existing = subterrainPlanners.putIfAbsent(activationId, planner);
return existing == null ? planner : existing;
} finally {
consumption.readLock().unlock();
}
}
private Optional<BiomeEnvironment> resolve(int blockX, int worldY, int blockZ, QueryKind kind) {
int chunkX = Math.floorDiv(blockX, 16);
int chunkZ = Math.floorDiv(blockZ, 16);
@@ -254,6 +296,7 @@ public final class SavedBiomeRuntime implements AutoCloseable {
}
}
definitions.clear();
subterrainPlanners.clear();
} finally {
consumption.writeLock().unlock();
}
@@ -466,8 +509,14 @@ public final class SavedBiomeRuntime implements AutoCloseable {
if (dimension == null) {
throw new IOException("Historical dimension is no longer supported: " + epoch.dimensionContract().dimensionKey());
}
if (dimension.allowsSubterrainFeatures()) {
SubterrainRasterizer.validateMaterials(dimension.getSubterrainFeatures());
}
Definitions loaded = new Definitions(data, dimension, NativeBiomeSpawnSelection.retainedDerivatives(data),
resolveFocusRegions(data, dimension));
resolveFocusRegions(data, dimension), new SubterrainPlanner(new SubterrainPlanner.Options(
dimension.allowsSubterrainFeatures() ? dimension.getSubterrainFeatures() : List.of(),
new SeedManager(epoch.worldSeed()).getBodies(),
dimension.getMinHeight(), dimension.getMaxHeight())));
data.prepareBlockDropRules();
definitions.put(epochId, loaded);
return loaded;
@@ -540,7 +589,7 @@ public final class SavedBiomeRuntime implements AutoCloseable {
}
private record Definitions(IrisData data, IrisDimension dimension, Map<String, String> nativeDerivatives,
Map<String, IrisRegion> focusRegions) {
Map<String, IrisRegion> focusRegions, SubterrainPlanner subterrainPlanner) {
private BiomeEnvironment environment(SavedBiomeChunk.Cell cell) {
IrisBiome biome = data.getBiomeLoader().load(cell.biomeKey());
IrisRegion region = data.getRegionLoader().load(cell.regionKey());