mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
Merge Iris generation changes
- Combine authored subterrain features with native CPU generation. - Verify dimension JSON and schemas preserve subterrain configuration.
This commit is contained in:
commit
4b4b418c61
76 files changed
+4248
-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;
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+4
@@ -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));
|
||||
|
||||
+21
-8
@@ -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;
|
||||
|
||||
+2
-1
@@ -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) {
|
||||
}
|
||||
}
|
||||
+18
-1
@@ -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);
|
||||
|
||||
+10
-2
@@ -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;
|
||||
@@ -591,6 +592,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]
|
||||
|
||||
+14
-6
@@ -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);
|
||||
}
|
||||
}
|
||||
+95
@@ -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");
|
||||
}
|
||||
|
||||
+12
-9
@@ -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
|
||||
}
|
||||
+42
-29
@@ -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());
|
||||
|
||||
Reference in new issue
Block a user