mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Refine organic cave generation
- Add seeded cave surface variation and rounded basin undersides. - Keep dry cave intersections open while preserving fluid seals. - Apply room biome decorators and preserve their floor substrates. - Allow ceiling growth above reserved walking clearance. - Restrict feature placement by region across live and saved generation. - Cover cave geometry, placement, decoration, and regional filtering with regression tests.
This commit is contained in:
1 parent
c479e94c49
commit
7226fc4400
25 files changed
+785
-86
No files matched your search
+2
-1
@@ -82,7 +82,8 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
return CommitResult.REJECTED_BOUNDS;
|
||||
}
|
||||
SubterrainCell feature = engine.getSubterrainCell(mutation.x(), mutation.y(), mutation.z());
|
||||
if (SubterrainRasterizer.protectsPlacement(feature)) {
|
||||
if (SubterrainRasterizer.protectsPlacement(feature)
|
||||
|| feature != null && feature.solid() && delegate.isSolid(mutation.x(), mutation.y(), mutation.z())) {
|
||||
discard();
|
||||
return CommitResult.REJECTED_SUBTERRAIN;
|
||||
}
|
||||
|
||||
@@ -178,6 +178,9 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
if (y <= 0 || y >= writer.getMantle().getWorldHeight()) {
|
||||
return;
|
||||
}
|
||||
if (cell.solid() && !cell.room().reservedSolid()) {
|
||||
return;
|
||||
}
|
||||
if (cell.solid()) {
|
||||
writer.clearData(x, y, z, MatterCavern.class);
|
||||
} else {
|
||||
|
||||
@@ -64,6 +64,7 @@ import art.arcane.iris.generation.terrain.transform.TerrainTransformer;
|
||||
import art.arcane.iris.generation.noise.IrisInterpolator;
|
||||
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.generation.terrain.RegionSelection;
|
||||
import art.arcane.iris.generation.hydrology.IrisDeepFluidConfig;
|
||||
import art.arcane.iris.generation.hydrology.IrisSurfacePoolConfig;
|
||||
import art.arcane.iris.generation.hydrology.IrisHydrology;
|
||||
@@ -274,7 +275,8 @@ public class IrisComplex implements DataProvider {
|
||||
subterrainPlanner = new SubterrainPlanner(new SubterrainPlanner.Options(
|
||||
engine.getDimension().allowsSubterrainFeatures() ? engine.getDimension().getSubterrainFeatures() : List.of(),
|
||||
engine.getSeedManager().getBodies(),
|
||||
engine.getDimension().getMinHeight(), engine.getDimension().getMaxHeight()),
|
||||
engine.getDimension().getMinHeight(), engine.getDimension().getMaxHeight(),
|
||||
(x, z) -> regionStream.get(x, z).getLoadKey()),
|
||||
bounds -> transitionGenerationPlan == null || transitionGenerationPlan.allowsNewFootprint(
|
||||
bounds.minX(), bounds.minZ(), bounds.maxX(), bounds.maxZ()));
|
||||
biomeBoundsSamplingStep = engine.getDimension().getBiomeBoundsSamplingStep();
|
||||
@@ -376,15 +378,10 @@ public class IrisComplex implements DataProvider {
|
||||
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
|
||||
.zoom(engine.getDimension().getRegionZoom());
|
||||
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
|
||||
ProceduralStream<IrisRegion> proceduralRegionStream = focusedRegions != null ? focusedRegions
|
||||
: GenerationStreams.cache2D(regionStyleStream
|
||||
.selectRarity(compatRegionPool(engine)), "regionStream", engine, cacheSize);
|
||||
regionStream = focusedRegions != null || !imageMapRuntime.has(IrisImageMapApplication.REGION)
|
||||
? proceduralRegionStream : GenerationStreams.cache2D(proceduralRegionStream
|
||||
.convertAware2D((region, x, z) -> {
|
||||
IrisRegion mapped = imageMapRuntime.sampleRegion(x, z);
|
||||
return mapped == null ? region : mapped;
|
||||
}), "imageMappedRegionStream", engine, cacheSize);
|
||||
regionStream = focusedRegions != null ? focusedRegions
|
||||
: GenerationStreams.cache2D(RegionSelection.select(new RegionSelection.Options(
|
||||
engine.getDimension(), data, regionStyleStream, imageMapRuntime, null)),
|
||||
"regionStream", engine, cacheSize);
|
||||
regionIDStream = regionIdentityStream.convertCached((i) -> new UUID(Double.doubleToLongBits(i),
|
||||
String.valueOf(i * 38445).hashCode() * 3245556666L));
|
||||
caveBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
@@ -1849,17 +1846,6 @@ public class IrisComplex implements DataProvider {
|
||||
return inferred;
|
||||
}
|
||||
|
||||
/** The dimension's region pool with regions the version-content gate excluded removed. */
|
||||
private KList<IrisRegion> compatRegionPool(Engine engine) {
|
||||
KList<IrisRegion> pool = new KList<>();
|
||||
for (IrisRegion region : data.getRegionLoader().loadAll(engine.getDimension().getRegions())) {
|
||||
if (!region.isCompatExcluded()) {
|
||||
pool.add(region);
|
||||
}
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
private static <T extends IrisRegistrant> T compatUsable(T registrant) {
|
||||
return registrant == null || registrant.isCompatExcluded() ? null : registrant;
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
output.setRaw(localX, surfaceY, localZ, AIR);
|
||||
}
|
||||
}
|
||||
restoreSubterrainSolids(output, x, z);
|
||||
restoreSubterrainSolids(output, x, z, false);
|
||||
} finally {
|
||||
getEngine().getMetrics().getCarveApply().put(applyStopwatch.getMilliseconds());
|
||||
}
|
||||
@@ -272,6 +272,12 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
int x = worldX & 15;
|
||||
int z = worldZ & 15;
|
||||
NativeBlockState existing = output.getRaw(x, y, z);
|
||||
if (cell.solid() && !cell.room().reservedSolid() && (existing == null || !existing.isSolid())) {
|
||||
return;
|
||||
}
|
||||
if (materials.retainsFloorSupport(worldX, worldY, worldZ, cell, existing)) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
@@ -281,7 +287,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
});
|
||||
}
|
||||
|
||||
private void restoreSubterrainSolids(Hunk<NativeBlockState> output, int chunkX, int chunkZ) {
|
||||
private void restoreSubterrainSolids(Hunk<NativeBlockState> output, int chunkX, int chunkZ, boolean preserveFloorSupports) {
|
||||
SubterrainPlanner planner = getComplex().getSubterrainPlanner();
|
||||
if (planner == null || planner.isEmpty()) {
|
||||
return;
|
||||
@@ -290,7 +296,14 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
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()) {
|
||||
if (!cell.solid() || y <= 0 || y >= output.getHeight()) {
|
||||
return;
|
||||
}
|
||||
NativeBlockState existing = output.getRaw(worldX & 15, y, worldZ & 15);
|
||||
if (preserveFloorSupports && materials.retainsFloorSupport(worldX, worldY, worldZ, cell, existing)) {
|
||||
return;
|
||||
}
|
||||
if (cell.room().reservedSolid() || existing != null && existing.isSolid()) {
|
||||
output.setRaw(worldX & 15, y, worldZ & 15, materials.state(worldX, worldY, worldZ, cell));
|
||||
}
|
||||
});
|
||||
@@ -421,7 +434,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
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()) {
|
||||
if (feature.owned() && (!feature.solid() || feature.room().reservedSolid())) {
|
||||
context.output().setRaw(localX, y, localZ, SubterrainRasterizer.state(feature));
|
||||
if (feature.carve()) {
|
||||
context.columnMasks()[columnIndex].add(y);
|
||||
@@ -1007,28 +1020,27 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
}
|
||||
}
|
||||
}
|
||||
restoreSubterrainSolids(output, blockX >> 4, blockZ >> 4);
|
||||
restoreSubterrainSolids(output, blockX >> 4, blockZ >> 4, true);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
boolean reservedPassage = feature.owned() && feature.room().reservedPassage();
|
||||
int maxY = output.getHeight();
|
||||
IrisDecorator[] surfaceDecorators = floorBiome == null
|
||||
? new IrisDecorator[0]
|
||||
: floorBiome.getDecoratorBucket(IrisDecorationPart.NONE);
|
||||
if (surfaceDecorators.length > 0 && hasStableCaveFloorSupport(output, rx, zone.getFloor(), rz)) {
|
||||
if (!reservedPassage && surfaceDecorators.length > 0 && hasStableCaveFloorSupport(output, rx, zone.getFloor(), rz)) {
|
||||
decorant.getSurfaceDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, floorBiome, InferredType.CAVE, zone.getFloor() - 1, zone.airThickness());
|
||||
}
|
||||
|
||||
IrisDecorator[] ceilingDecorators = ceilingBiome == null
|
||||
? new IrisDecorator[0]
|
||||
: ceilingBiome.getDecoratorBucket(IrisDecorationPart.CEILING);
|
||||
if (ceilingDecorators.length > 0 && zone.getCeiling() + 1 < maxY && B.isSolid(output.getRaw(rx, zone.getCeiling() + 1, rz))) {
|
||||
decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, ceilingBiome, InferredType.CAVE, zone.getCeiling(), zone.airThickness());
|
||||
int ceilingSpace = zone.airThickness() - (reservedPassage ? 3 : 0);
|
||||
if (ceilingSpace > 0 && ceilingDecorators.length > 0 && zone.getCeiling() + 1 < maxY && B.isSolid(output.getRaw(rx, zone.getCeiling() + 1, rz))) {
|
||||
decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, ceilingBiome, InferredType.CAVE, zone.getCeiling(), ceilingSpace);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1075,6 +1087,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
}
|
||||
|
||||
IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, CaveInputs resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
|
||||
IrisBiome featureBiome = getEngine().getSubterrainBiome(worldX, y, worldZ);
|
||||
if (featureBiome != null) {
|
||||
return featureBiome;
|
||||
}
|
||||
if (isNaturalTerrainOpening(resolverState.complex, worldX, y, worldZ)) {
|
||||
return resolverState.complex.getTrueBiomeStream().get(worldX, worldZ);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package art.arcane.iris.generation.subterrain;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.pack.schema.annotation.MaxNumber;
|
||||
import art.arcane.iris.pack.schema.annotation.MinNumber;
|
||||
import art.arcane.iris.pack.schema.annotation.RegistryListResource;
|
||||
@@ -26,6 +28,9 @@ public class IrisSubterrainFeature {
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@Description("Biome that owns occupied cells in this feature. Solid rims and surrounding terrain retain their biome.")
|
||||
private String biome = "";
|
||||
@RegistryListResource(IrisRegion.class)
|
||||
@Description("Allowed region keys at the feature placement center. Empty allows all regions.")
|
||||
private KList<String> allowedRegions = new KList<>();
|
||||
@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)
|
||||
@@ -48,6 +53,10 @@ public class IrisSubterrainFeature {
|
||||
@MaxNumber(192)
|
||||
@Description("Main room height in blocks.")
|
||||
private int height = 32;
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Description("Seeded erosion and shape distortion strength for lobed basins, winding passages, uneven banks and irregular vaults.")
|
||||
private double shapeWarp = 0;
|
||||
@Description("Retained fluid. When omitted, lava tubes use LAVA and cenotes and terraces use WATER. Faults remain dry.")
|
||||
private IrisSubterrainFluid fluid;
|
||||
@MinNumber(0)
|
||||
|
||||
@@ -11,6 +11,7 @@ public final class SubterrainPlan {
|
||||
private final String id;
|
||||
private final SubterrainBounds bounds;
|
||||
private final double shapePhase;
|
||||
private final long shapeSeed;
|
||||
private final ThreadLocal<ColumnCache> columnCache = ThreadLocal.withInitial(ColumnCache::new);
|
||||
|
||||
SubterrainPlan(Options options) {
|
||||
@@ -21,11 +22,12 @@ public final class SubterrainPlan {
|
||||
rotated = options.rotated();
|
||||
id = options.id();
|
||||
shapePhase = (SubterrainPlanner.mix(id.hashCode() ^ definition.seedSalt()) >>> 11) * 0x1.0p-53 * Math.PI * 2;
|
||||
shapeSeed = SubterrainPlanner.mix(id.hashCode() ^ definition.seedSalt());
|
||||
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,
|
||||
bounds = new SubterrainBounds(centerX - extentX, baseY() - 2 - definition.baseDepth(), centerZ - extentZ,
|
||||
centerX + extentX, baseY() + definition.height() + 2
|
||||
+ (family() == IrisSubterrainFamily.LAVA_TUBE ? definition.chimneyHeight() : 0), centerZ + extentZ);
|
||||
}
|
||||
@@ -125,18 +127,20 @@ public final class SubterrainPlan {
|
||||
if (column == null) {
|
||||
return SubterrainCell.OUTSIDE;
|
||||
}
|
||||
int sealFloor = family() == IrisSubterrainFamily.CENOTE ? baseY() : column.floor();
|
||||
if (y < sealFloor - 2 || y > column.ceiling() + 2) {
|
||||
double horizontalOutside = Math.max(0, -column.boundary());
|
||||
double verticalOutside = Math.max(0, Math.max(column.baseFloor() - y, y - column.ceiling()));
|
||||
if (horizontalOutside * horizontalOutside + verticalOutside * verticalOutside > 4) {
|
||||
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;
|
||||
boolean passage = kind == SubterrainCell.Kind.AIR && column.pathDistance() <= 2.0
|
||||
&& y <= Math.max(column.floor(), column.head()) + 3;
|
||||
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);
|
||||
kind == SubterrainCell.Kind.SOLID && retainedSolid(column, u, v, y), kind);
|
||||
String material = switch (kind) {
|
||||
case SOLID -> definition.solid();
|
||||
case AIR -> "minecraft:cave_air";
|
||||
@@ -147,6 +151,12 @@ public final class SubterrainPlan {
|
||||
return new SubterrainCell(kind, material, room);
|
||||
}
|
||||
|
||||
private boolean retainedSolid(Column column, int u, int v, int y) {
|
||||
return family() != IrisSubterrainFamily.TECTONIC_FAULT && definition.fluidDepth() > 0 && y <= column.head() + 1
|
||||
|| column.interior() && y > column.floor() && y < column.ceiling()
|
||||
&& formation(column, u, v, y);
|
||||
}
|
||||
|
||||
private Column cachedColumn(int u, int v) {
|
||||
ColumnCache cache = columnCache.get();
|
||||
long key = ((long) u << 32) ^ (v & 0xffffffffL);
|
||||
@@ -159,7 +169,7 @@ public final class SubterrainPlan {
|
||||
case CENOTE -> cenote(u, v);
|
||||
case TECTONIC_FAULT -> fault(u, v);
|
||||
case TRAVERTINE_TERRACES -> terraces(u, v);
|
||||
case LAVA_TUBE -> throw new IllegalStateException("Tube columns depend on height");
|
||||
case LAVA_TUBE -> erodedTube(u, v);
|
||||
};
|
||||
int slot = cache.next;
|
||||
cache.keys[slot] = key;
|
||||
@@ -173,6 +183,9 @@ public final class SubterrainPlan {
|
||||
if (!column.interior() || y <= column.floor() || y >= column.ceiling() || column.rim()) {
|
||||
return SubterrainCell.Kind.SOLID;
|
||||
}
|
||||
if (erodedWall(column, u, v, y)) {
|
||||
return SubterrainCell.Kind.SOLID;
|
||||
}
|
||||
if (formation(column, u, v, y)) {
|
||||
return SubterrainCell.Kind.SOLID;
|
||||
}
|
||||
@@ -185,6 +198,18 @@ public final class SubterrainPlan {
|
||||
return SubterrainCell.Kind.AIR;
|
||||
}
|
||||
|
||||
private boolean erodedWall(Column column, int u, int v, int y) {
|
||||
double strength = definition.shapeWarp();
|
||||
double reach = Math.min(7, definition.radius() * 0.28) * strength;
|
||||
if (reach == 0 || column.boundary() >= reach || column.pathDistance() <= 2) {
|
||||
return false;
|
||||
}
|
||||
double height = y - baseY();
|
||||
double inset = reach * (0.48 + 0.34 * noise(u / 13.0 + height / 17.0, v / 13.0 - height / 19.0, 149)
|
||||
+ 0.18 * noise(u / 4.0 - height / 5.0, v / 4.0 + height / 7.0, 151));
|
||||
return column.boundary() < inset;
|
||||
}
|
||||
|
||||
private boolean formation(Column column, int u, int v, int y) {
|
||||
if (column.pathDistance() <= 3 || column.boundary() < 4) {
|
||||
return false;
|
||||
@@ -225,6 +250,9 @@ public final class SubterrainPlan {
|
||||
}
|
||||
|
||||
private Column cenote(int u, int v) {
|
||||
if (definition.shapeWarp() > 0) {
|
||||
return erodedCenote(u, v);
|
||||
}
|
||||
double distance = Math.hypot(u, v);
|
||||
int radius = definition.radius();
|
||||
if (distance > radius + 2) {
|
||||
@@ -237,7 +265,7 @@ public final class SubterrainPlan {
|
||||
double slope = bank * bank * (3 - 2 * bank);
|
||||
int floor = baseY() + (int) Math.round((definition.fluidDepth() + 2) * slope);
|
||||
boolean rim = definition.fluidDepth() > 0 && ceiling <= head + 2;
|
||||
return new Column(floor, ceiling, head, distance < radius, rim,
|
||||
return new Column(floor, floor, ceiling, head, distance < radius, rim,
|
||||
radius - distance, Math.abs(v), 0);
|
||||
}
|
||||
|
||||
@@ -249,12 +277,36 @@ public final class SubterrainPlan {
|
||||
int step = Math.max(2, definition.radius() / 4);
|
||||
int shelf = Math.max(0, (int) (Math.abs(footprint.offset()) - footprint.width() * 0.5) / step)
|
||||
* Math.max(2, definition.height() / 8);
|
||||
int ceiling = vaultedCeiling(u, footprint, baseY() + shelf, baseY() - 1);
|
||||
return new Column(baseY() + shelf, ceiling, baseY() - 1,
|
||||
int floor = baseY() + shelf;
|
||||
if (definition.shapeWarp() > 0) {
|
||||
floor = baseY() + (int) Math.round((1 - definition.shapeWarp()) * shelf
|
||||
+ definition.shapeWarp() * definition.height() * (0.10 + 0.08 * erosion(u, v, 43)));
|
||||
}
|
||||
int ceiling = vaultedCeiling(u, footprint, floor, baseY() - 1);
|
||||
return new Column(floor, floor, ceiling, baseY() - 1,
|
||||
footprint.boundary() > 0, false, footprint.boundary(), Math.abs(footprint.offset()), footprint.path());
|
||||
}
|
||||
|
||||
private Column tube(int u, int v, int y) {
|
||||
if (definition.shapeWarp() > 0) {
|
||||
Column column = cachedColumn(u, v);
|
||||
if (column == null || definition.chimneyHeight() == 0) {
|
||||
return column;
|
||||
}
|
||||
int halfLength = definition.length() / 2;
|
||||
double chimneyRadius = Math.hypot(Math.min(Math.abs(u - halfLength / 2), Math.abs(u + halfLength / 2)),
|
||||
v - column.pathOffset());
|
||||
int top = baseY() + definition.height() + definition.chimneyHeight();
|
||||
if (chimneyRadius < 3) {
|
||||
return new Column(column.floor(), column.baseFloor(), top, column.head(), column.interior(), column.rim(),
|
||||
column.boundary(), column.pathDistance(), column.pathOffset());
|
||||
}
|
||||
if (chimneyRadius <= 5 && y >= column.ceiling() - 1) {
|
||||
return new Column(column.ceiling() - 1, column.ceiling() - 1, top, column.head(), false, true,
|
||||
5 - chimneyRadius, column.pathDistance(), column.pathOffset());
|
||||
}
|
||||
return column;
|
||||
}
|
||||
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));
|
||||
@@ -282,12 +334,12 @@ public final class SubterrainPlan {
|
||||
if (chimneyRadius < 3) {
|
||||
ceiling = baseY() + definition.height() + definition.chimneyHeight();
|
||||
} else if (y >= tubeCeiling - 1) {
|
||||
return new Column(tubeCeiling - 1, baseY() + definition.height() + definition.chimneyHeight(),
|
||||
return new Column(tubeCeiling - 1, 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) < taperedWidth,
|
||||
return new Column(floor, floor, ceiling, head, Math.abs(u) < halfLength && Math.abs(offset) < taperedWidth,
|
||||
rim, Math.min(halfLength - Math.abs(u), taperedWidth - Math.abs(offset)), Math.abs(offset), path);
|
||||
}
|
||||
|
||||
@@ -298,12 +350,16 @@ public final class SubterrainPlan {
|
||||
}
|
||||
double warped = u + definition.radius() * 0.2 * Math.sin(v / (double) Math.max(6, definition.radius()) * 2.4 + shapePhase)
|
||||
+ definition.radius() * 0.08 * Math.sin(u / 29.0 + v / 19.0 + shapePhase);
|
||||
warped += definition.shapeWarp() * definition.radius() * (0.7 * noise(u / 43.0, v / 27.0, 71)
|
||||
+ 0.23 * noise(u / 13.0, v / 11.0, 73));
|
||||
double segment = Math.max(4.0, definition.length() / (double) definition.terraceCount());
|
||||
double terrace = (warped + definition.length() / 2.0) / segment;
|
||||
int index = Math.max(0, Math.min(definition.terraceCount() - 1, (int) Math.floor(terrace)));
|
||||
int rise = Math.max(1, (definition.height() / 3) / definition.terraceCount());
|
||||
int floor = baseY() + index * rise;
|
||||
int head = floor + definition.fluidDepth();
|
||||
floor += (int) Math.round(definition.shapeWarp() * Math.max(0, definition.fluidDepth() - 1)
|
||||
* (0.5 + 0.5 * erosion(u, v, 79)));
|
||||
double local = (terrace - Math.floor(terrace)) * segment;
|
||||
boolean rim = definition.fluidDepth() > 0 && (local <= 1.5 || local >= segment - 1.5 || footprint.boundary() <= 2);
|
||||
double pathOffset = Math.copySign(Math.max(0, footprint.width() - 4), footprint.offset());
|
||||
@@ -312,7 +368,7 @@ public final class SubterrainPlan {
|
||||
floor = Math.max(floor, head + 1);
|
||||
}
|
||||
int ceiling = vaultedCeiling(u, footprint, floor, head);
|
||||
return new Column(floor, ceiling, head, footprint.boundary() > 0,
|
||||
return new Column(floor, baseFloor(floor, footprint.boundary(), u, v), ceiling, head, footprint.boundary() > 0,
|
||||
false, footprint.boundary(), pathDistance, footprint.path() + (int) Math.round(pathOffset));
|
||||
}
|
||||
|
||||
@@ -330,6 +386,18 @@ public final class SubterrainPlan {
|
||||
double bend = family() == IrisSubterrainFamily.TECTONIC_FAULT ? 0.08 : 0.12;
|
||||
int path = (int) Math.round(definition.radius() * bend * Math.sin(u / 47.0 + shapePhase) * taper);
|
||||
double vaultWave = 0.87 + 0.07 * Math.sin(u / 37.0 + shapePhase) + 0.04 * Math.sin(u / 17.0 - shapePhase);
|
||||
if (definition.shapeWarp() > 0) {
|
||||
double strength = definition.shapeWarp();
|
||||
double scale = Math.max(12, definition.radius());
|
||||
double erodedWidth = definition.radius() * taper * (0.45
|
||||
+ 0.22 * noise(u / (scale * 1.8), 0.37, 11)
|
||||
+ 0.15 * noise(u / (scale * 0.48), 0.71, 13));
|
||||
double erodedPath = definition.radius() * taper * (0.72 * noise(u / (scale * 2.2), 0.19, 17)
|
||||
+ 0.25 * noise(u / (scale * 0.73), 0.53, 19));
|
||||
width += strength * (erodedWidth - width);
|
||||
path = (int) Math.round(path + strength * (erodedPath - path));
|
||||
vaultWave += strength * (0.76 + 0.18 * noise(u / (scale * 1.1), 0.31, 23) - vaultWave);
|
||||
}
|
||||
Profile profile = new Profile(Math.min(width, definition.radius() - Math.abs(path)), path, vaultWave);
|
||||
int slot = cache.profileNext;
|
||||
cache.profileKeys[slot] = u;
|
||||
@@ -348,6 +416,12 @@ public final class SubterrainPlan {
|
||||
double width = profile.width();
|
||||
int path = profile.path();
|
||||
double offset = v - path;
|
||||
if (definition.shapeWarp() > 0) {
|
||||
double strength = definition.shapeWarp();
|
||||
width += strength * (definition.radius() * 0.20 * noise(u / 17.0, v / 19.0, 29)
|
||||
+ Math.min(2.5, definition.radius() * 0.08) * noise(u / 5.0, v / 5.0, 31));
|
||||
width = Math.max(0, Math.min(width, definition.radius() - Math.abs(path)));
|
||||
}
|
||||
double boundary = Math.min(halfLength - Math.abs(u), width - Math.abs(offset));
|
||||
if (boundary < -2) {
|
||||
return null;
|
||||
@@ -362,9 +436,112 @@ public final class SubterrainPlan {
|
||||
double roofVariation = 0.05 * Math.sin(u / 37.0 + footprint.offset() / 17.0 + shapePhase) * arch
|
||||
+ 0.06 * footprint.offset() / Math.max(1, footprint.width()) * Math.sin(u / 41.0 - shapePhase);
|
||||
int ceiling = baseY() + (int) Math.floor(definition.height() * wave * (0.4 + 0.6 * arch + roofVariation));
|
||||
if (definition.shapeWarp() > 0) {
|
||||
double shoulder = 1 - smooth(Math.max(0, Math.min(1, footprint.boundary() / Math.min(8, footprint.width() * 0.6))));
|
||||
double erodedRoof = definition.height() * (wave - 0.08 - shoulder * 0.24
|
||||
+ 0.32 * noise(u / 23.0, footprint.offset() / 17.0, 35)
|
||||
+ 0.20 * erosion(u, footprint.offset(), 37));
|
||||
ceiling = baseY() + (int) Math.floor((1 - definition.shapeWarp()) * (ceiling - baseY())
|
||||
+ definition.shapeWarp() * erodedRoof);
|
||||
}
|
||||
return Math.min(baseY() + definition.height(), Math.max(Math.max(floor, head) + 3, ceiling));
|
||||
}
|
||||
|
||||
private Column erodedCenote(int u, int v) {
|
||||
double radius = definition.radius();
|
||||
if (Math.hypot(u, v) > radius + 2) {
|
||||
return null;
|
||||
}
|
||||
double strength = definition.shapeWarp();
|
||||
double scale = Math.max(8, radius * 0.65);
|
||||
double shiftedU = u + strength * radius * 0.18 * noise(u / scale, v / scale, 101);
|
||||
double shiftedV = v + strength * radius * 0.18 * noise(u / scale, v / scale, 103);
|
||||
double lobes = 0.69 + 0.24 * noise(u / scale, v / scale, 105)
|
||||
+ 0.13 * noise(u / (scale * 0.46), v / (scale * 0.46), 106);
|
||||
double outline = radius * (1 - strength + strength * lobes)
|
||||
+ strength * (radius * 0.07 * noise(u / 13.0, v / 13.0, 107)
|
||||
+ Math.min(2, radius * 0.05) * noise(u / 4.0, v / 4.0, 109));
|
||||
double distance = Math.hypot(shiftedU, shiftedV * (1 + 0.12 * strength));
|
||||
double boundary = Math.min(radius - Math.hypot(u, v), outline - distance);
|
||||
if (boundary < -2) {
|
||||
return null;
|
||||
}
|
||||
double normalized = Math.min(1, distance / Math.max(1, outline));
|
||||
double bankStart = 0.65 - strength * 0.11;
|
||||
double bankWidth = 0.25 + strength * 0.15;
|
||||
double bank = smooth(Math.max(0, Math.min(1, (normalized - bankStart) / bankWidth)));
|
||||
double bed = Math.max(0, 0.42 + 0.58 * erosion(u, v, 113));
|
||||
double bedRelief = definition.fluidDepth() == 0 ? definition.height() * 0.30
|
||||
: Math.max(1, definition.fluidDepth() - 1);
|
||||
int floor = baseY() + (int) Math.round(bank * (definition.fluidDepth() + 2 + strength)
|
||||
+ strength * bed * bedRelief);
|
||||
double arch = Math.sqrt(Math.max(0, 1 - normalized * normalized));
|
||||
double shoulder = 1 - smooth(Math.max(0, Math.min(1, boundary / Math.max(4, radius * 0.2))));
|
||||
double roof = definition.height() * ((1 - strength) * arch
|
||||
+ strength * (0.64 - 0.24 * shoulder
|
||||
+ 0.40 * noise(u / (scale * 0.6), v / (scale * 0.6), 123)
|
||||
+ 0.23 * erosion(u, v, 127)));
|
||||
int head = baseY() + definition.fluidDepth();
|
||||
int ceiling = Math.min(baseY() + definition.height(), Math.max(floor + 3, baseY() + (int) Math.floor(roof)));
|
||||
boolean rim = definition.fluidDepth() > 0 && ceiling <= head + 2;
|
||||
return new Column(floor, baseFloor(floor, boundary, u, v), ceiling, head, boundary > 0, rim, boundary, Math.abs(v), 0);
|
||||
}
|
||||
|
||||
private Column erodedTube(int u, int v) {
|
||||
Footprint footprint = footprint(u, v);
|
||||
if (footprint == null) {
|
||||
return null;
|
||||
}
|
||||
double cross = Math.min(1, Math.abs(footprint.offset()) / Math.max(1, footprint.width()));
|
||||
double bank = smooth(Math.max(0, Math.min(1, (cross - 0.38) / 0.55)));
|
||||
int head = baseY() + definition.fluidDepth();
|
||||
int floor = baseY() + (int) Math.round(bank * (definition.fluidDepth() + 3)
|
||||
+ definition.shapeWarp() * definition.height() * 0.11
|
||||
* Math.max(0, 0.35 + 0.65 * erosion(u, v, 137)));
|
||||
int ceiling = vaultedCeiling(u, footprint, floor, head);
|
||||
return new Column(floor, baseFloor(floor, footprint.boundary(), u, v), ceiling, head, footprint.boundary() > 0, false,
|
||||
footprint.boundary(), Math.abs(footprint.offset()), footprint.path());
|
||||
}
|
||||
|
||||
private int baseFloor(int floor, double boundary, int u, int v) {
|
||||
int depth = definition.baseDepth();
|
||||
if (depth == 0 || boundary <= 0) {
|
||||
return floor;
|
||||
}
|
||||
double taper = Math.sqrt(Math.min(1, boundary / Math.max(4, definition.radius() * 0.55)));
|
||||
double scale = Math.max(8, definition.radius() * 0.7);
|
||||
double relief = Math.max(0, Math.min(1, 0.73 + 0.25 * noise(u / scale, v / scale, 157)
|
||||
+ 0.14 * noise(u / 7.0, v / 7.0, 163)));
|
||||
return floor - (int) Math.round(depth * taper * relief);
|
||||
}
|
||||
|
||||
private double erosion(double u, double v, long salt) {
|
||||
return 0.58 * noise(u / 31.0, v / 29.0, salt)
|
||||
+ 0.29 * noise(u / 11.0, v / 13.0, salt + 1)
|
||||
+ 0.13 * noise(u / 4.0, v / 4.0, salt + 2);
|
||||
}
|
||||
|
||||
private double noise(double u, double v, long salt) {
|
||||
int x = (int) Math.floor(u);
|
||||
int z = (int) Math.floor(v);
|
||||
double dx = smooth(u - x);
|
||||
double dz = smooth(v - z);
|
||||
double near = lattice(x, z, salt);
|
||||
near += dx * (lattice(x + 1, z, salt) - near);
|
||||
double far = lattice(x, z + 1, salt);
|
||||
far += dx * (lattice(x + 1, z + 1, salt) - far);
|
||||
return near + dz * (far - near);
|
||||
}
|
||||
|
||||
private double lattice(int x, int z, long salt) {
|
||||
return (SubterrainPlanner.mix(shapeSeed ^ x * 0x9e3779b97f4a7c15L
|
||||
^ z * 0xc2b2ae3d27d4eb4fL ^ salt * 0x165667b19e3779f9L) >>> 11) * 0x1.0p-52 - 1;
|
||||
}
|
||||
|
||||
private double smooth(double value) {
|
||||
return value * value * (3 - 2 * value);
|
||||
}
|
||||
|
||||
private SubterrainPosition occupiedAnchor(int x, int preferredY, int z) {
|
||||
if (anchorCell(sample(x, preferredY, z))) {
|
||||
return new SubterrainPosition(x, preferredY, z);
|
||||
@@ -406,7 +583,7 @@ public final class SubterrainPlan {
|
||||
private record Footprint(double width, double offset, double boundary, int path) {
|
||||
}
|
||||
|
||||
private record Column(int floor, int ceiling, int head, boolean interior, boolean rim,
|
||||
private record Column(int floor, int baseFloor, int ceiling, int head, boolean interior, boolean rim,
|
||||
double boundary, double pathDistance, int pathOffset) {
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ public final class SubterrainPlanner {
|
||||
private final int maxYExclusive;
|
||||
private final ThreadLocal<CandidateTile> sampleTile = new ThreadLocal<>();
|
||||
private final Predicate<SubterrainBounds> allowedBounds;
|
||||
private final RegionResolver regionResolver;
|
||||
|
||||
public SubterrainPlanner(Options options) {
|
||||
this(options, bounds -> true);
|
||||
@@ -23,6 +24,7 @@ public final class SubterrainPlanner {
|
||||
public SubterrainPlanner(Options options, Predicate<SubterrainBounds> allowedBounds) {
|
||||
Objects.requireNonNull(options);
|
||||
this.allowedBounds = Objects.requireNonNull(allowedBounds);
|
||||
regionResolver = Objects.requireNonNull(options.regionResolver());
|
||||
seed = options.seed();
|
||||
minY = options.minY();
|
||||
maxYExclusive = options.maxYExclusive();
|
||||
@@ -49,6 +51,7 @@ public final class SubterrainPlanner {
|
||||
|
||||
private SubterrainPlanner(SubterrainPlanner source, Predicate<SubterrainBounds> allowedBounds) {
|
||||
definitions = source.definitions;
|
||||
regionResolver = source.regionResolver;
|
||||
seed = source.seed;
|
||||
minY = source.minY;
|
||||
maxYExclusive = source.maxYExclusive;
|
||||
@@ -128,10 +131,10 @@ public final class SubterrainPlanner {
|
||||
SubterrainCell selected = SubterrainCell.OUTSIDE;
|
||||
for (SubterrainPlan plan : candidates) {
|
||||
SubterrainCell cell = plan.sample(x, y, z);
|
||||
if (cell.solid()) {
|
||||
if (cell.solid() && cell.room().reservedSolid()) {
|
||||
return cell;
|
||||
}
|
||||
if (!selected.owned() && cell.owned()) {
|
||||
if (!selected.owned() && cell.owned() || selected.solid() && cell.occupied()) {
|
||||
selected = cell;
|
||||
}
|
||||
}
|
||||
@@ -164,7 +167,7 @@ public final class SubterrainPlanner {
|
||||
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 bottomMargin = definition.height() / 2 + 3 + definition.baseDepth();
|
||||
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;
|
||||
@@ -173,6 +176,10 @@ public final class SubterrainPlanner {
|
||||
|| z < Integer.MIN_VALUE + spacing || z > Integer.MAX_VALUE - spacing) {
|
||||
return null;
|
||||
}
|
||||
if (!definition.allowedRegions().isEmpty()
|
||||
&& !definition.allowedRegions().contains(regionResolver.regionAt((int) x, (int) z))) {
|
||||
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)));
|
||||
@@ -189,9 +196,16 @@ public final class SubterrainPlanner {
|
||||
return (value >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
|
||||
public record Options(List<IrisSubterrainFeature> features, long seed, int minY, int maxYExclusive) {
|
||||
@FunctionalInterface
|
||||
public interface RegionResolver {
|
||||
String regionAt(int x, int z);
|
||||
}
|
||||
|
||||
public record Options(List<IrisSubterrainFeature> features, long seed, int minY, int maxYExclusive,
|
||||
RegionResolver regionResolver) {
|
||||
public Options {
|
||||
features = List.copyOf(Objects.requireNonNull(features));
|
||||
Objects.requireNonNull(regionResolver);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,9 +213,14 @@ public final class SubterrainPlanner {
|
||||
}
|
||||
|
||||
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 length, int radius, int height, double shapeWarp, int fluidDepth, int chimneyHeight, int terraceCount,
|
||||
int pillarSpacing, double formationFraction, String solid, int priority,
|
||||
int minY, int maxY, long seedSalt) {
|
||||
int minY, int maxY, long seedSalt, Set<String> allowedRegions) {
|
||||
int baseDepth() {
|
||||
return family == IrisSubterrainFamily.TECTONIC_FAULT || fluidDepth == 0 ? 0
|
||||
: (int) Math.ceil(Math.min(24, radius * 0.45) * shapeWarp);
|
||||
}
|
||||
|
||||
static Definition from(IrisSubterrainFeature feature) {
|
||||
String id = Objects.requireNonNull(feature.getId(), "Subterrain id").trim();
|
||||
IrisSubterrainFamily family = Objects.requireNonNull(feature.getFamily(), "Subterrain family");
|
||||
@@ -214,6 +233,7 @@ public final class SubterrainPlanner {
|
||||
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
|
||||
|| !Double.isFinite(feature.getShapeWarp()) || feature.getShapeWarp() < 0 || feature.getShapeWarp() > 1
|
||||
|| 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)
|
||||
@@ -239,15 +259,21 @@ public final class SubterrainPlanner {
|
||||
if (!biome.isEmpty() && !biome.matches("[a-zA-Z0-9_/-]+")) {
|
||||
throw new IllegalArgumentException("Invalid subterrain biome key: " + id);
|
||||
}
|
||||
Set<String> allowedRegions = Set.copyOf(Objects.requireNonNull(feature.getAllowedRegions()));
|
||||
for (String region : allowedRegions) {
|
||||
if (!region.matches("[a-zA-Z0-9_/-]+")) {
|
||||
throw new IllegalArgumentException("Invalid subterrain region 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(),
|
||||
feature.getSpacing(), feature.getProbability(), length, radius, height, feature.getShapeWarp(), 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);
|
||||
(int) Math.floor(feature.getWorldYRange().getMax()), salt, allowedRegions);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ public final class SubterrainRasterizer {
|
||||
}
|
||||
|
||||
public static boolean protectsPlacement(SubterrainCell cell) {
|
||||
return cell != null && cell.owned() && (cell.solid() || cell.room().reservedPassage());
|
||||
return cell != null && cell.owned() && (cell.room().reservedSolid() || cell.room().reservedPassage());
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
|
||||
+38
-5
@@ -1,6 +1,8 @@
|
||||
package art.arcane.iris.generation.subterrain;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.decoration.IrisDecorationPart;
|
||||
import art.arcane.iris.generation.decoration.IrisDecorator;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
@@ -9,8 +11,10 @@ import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class SubterrainSurfaceMaterials {
|
||||
private final SubterrainPlanner planner;
|
||||
@@ -21,6 +25,7 @@ public final class SubterrainSurfaceMaterials {
|
||||
private final RNG random;
|
||||
private final int minimumY;
|
||||
private final Map<String, IrisBiome> biomes = new HashMap<>();
|
||||
private final Map<String, Set<String>> floorSupports = new HashMap<>();
|
||||
|
||||
public SubterrainSurfaceMaterials(SubterrainPlanner planner, List<SubterrainPlan> plans,
|
||||
IrisDimension dimension, IrisComplex complex, IrisData data,
|
||||
@@ -43,11 +48,7 @@ public final class SubterrainSurfaceMaterials {
|
||||
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);
|
||||
IrisBiome biome = biome(cell.room().biome());
|
||||
if (biome == null) {
|
||||
return fallback;
|
||||
}
|
||||
@@ -63,6 +64,31 @@ public final class SubterrainSurfaceMaterials {
|
||||
return SubterrainRasterizer.isRetainedSolid(candidate) ? candidate : fallback;
|
||||
}
|
||||
|
||||
public boolean retainsFloorSupport(int x, int worldY, int z, SubterrainCell cell, NativeBlockState existing) {
|
||||
if (!cell.solid() || cell.room().biome().isEmpty() || !SubterrainRasterizer.isRetainedSolid(existing)) {
|
||||
return false;
|
||||
}
|
||||
String key = cell.room().biome();
|
||||
Set<String> supports = floorSupports.get(key);
|
||||
if (supports == null) {
|
||||
supports = new HashSet<>();
|
||||
IrisBiome biome = biome(key);
|
||||
if (biome != null) {
|
||||
for (IrisDecorator decorator : biome.getDecoratorBucket(IrisDecorationPart.NONE)) {
|
||||
if (decorator.getForceBlock() == null) {
|
||||
continue;
|
||||
}
|
||||
NativeBlockState support = decorator.getForceBlock().getBlockData(data);
|
||||
if (SubterrainRasterizer.isRetainedSolid(support)) {
|
||||
supports.add(support.key());
|
||||
}
|
||||
}
|
||||
}
|
||||
floorSupports.put(key, supports);
|
||||
}
|
||||
return supports.contains(existing.key()) && surface(x, worldY, z, cell) == Surface.FLOOR;
|
||||
}
|
||||
|
||||
public Surface surface(int x, int worldY, int z, SubterrainCell cell) {
|
||||
if (!cell.solid()) {
|
||||
return Surface.INTERIOR;
|
||||
@@ -86,6 +112,13 @@ public final class SubterrainSurfaceMaterials {
|
||||
return neighbor.occupied() && neighbor.room().featureId().equals(cell.room().featureId());
|
||||
}
|
||||
|
||||
private IrisBiome biome(String key) {
|
||||
if (!biomes.containsKey(key)) {
|
||||
biomes.put(key, data.getBiomeLoader().load(key));
|
||||
}
|
||||
return biomes.get(key);
|
||||
}
|
||||
|
||||
public enum Surface {
|
||||
FLOOR,
|
||||
CEILING,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package art.arcane.iris.generation.terrain;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.generation.image.IrisImageMapApplication;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.studio.generation.BiomeBuffetLayout;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
||||
|
||||
public final class RegionSelection {
|
||||
private RegionSelection() {
|
||||
}
|
||||
|
||||
public static ProceduralStream<IrisRegion> select(Options options) {
|
||||
if (options.focused() != null) {
|
||||
return options.focused();
|
||||
}
|
||||
KList<IrisRegion> regions = new KList<>();
|
||||
for (IrisRegion region : options.data().getRegionLoader().loadAll(options.dimension().getRegions())) {
|
||||
if (!region.isCompatExcluded()) {
|
||||
regions.add(region);
|
||||
}
|
||||
}
|
||||
ProceduralStream<IrisRegion> selected = options.style().selectRarity(regions);
|
||||
if (!options.imageMaps().has(IrisImageMapApplication.REGION)) {
|
||||
return selected;
|
||||
}
|
||||
return selected.convertAware2D((region, x, z) -> {
|
||||
IrisRegion mapped = options.imageMaps().sampleRegion(x, z);
|
||||
return mapped == null ? region : mapped;
|
||||
});
|
||||
}
|
||||
|
||||
public static ProceduralStream<IrisRegion> restore(Snapshot snapshot) {
|
||||
IrisData data = snapshot.data();
|
||||
IrisDimension dimension = snapshot.dimension();
|
||||
ProceduralStream<IrisRegion> focused = null;
|
||||
if (snapshot.buffet() && dimension.getStudioMode().biomeSizeChunks() > 0) {
|
||||
BiomeBuffetLayout layout = new BiomeBuffetLayout(dimension, () -> data);
|
||||
if (!layout.cells().isEmpty()) {
|
||||
focused = ProceduralStream.of((x, z) -> layout.terrain(x, z).region(),
|
||||
Interpolated.of(value -> 0D, value -> layout.cells().getFirst().region()));
|
||||
}
|
||||
}
|
||||
if (focused == null) {
|
||||
IrisRegion focus = focusRegion(data, dimension);
|
||||
if (focus != null) {
|
||||
focused = ProceduralStream.of((x, z) -> focus, Interpolated.of(value -> 0D, value -> focus));
|
||||
}
|
||||
}
|
||||
return select(new Options(dimension, data,
|
||||
dimension.getRegionStyle().create(new RNG(snapshot.complexSeed()).nextParallelRNG(883), data)
|
||||
.stream().zoom(dimension.getRegionZoom()),
|
||||
IrisImageMapRuntime.compile(data, dimension, dimension.getMinHeight()), focused));
|
||||
}
|
||||
|
||||
private static IrisRegion focusRegion(IrisData data, IrisDimension dimension) {
|
||||
if (dimension.getFocus() != null && !dimension.getFocus().isBlank()) {
|
||||
IrisBiome focus = data.getBiomeLoader().load(dimension.getFocus());
|
||||
if (focus != null && !focus.isCompatExcluded()) {
|
||||
return dimension.resolveFocusRegion(focus.withInferredType(InferredType.LAND), () -> data);
|
||||
}
|
||||
}
|
||||
if (dimension.getFocusRegion() == null || dimension.getFocusRegion().isBlank()) {
|
||||
return null;
|
||||
}
|
||||
IrisRegion focus = data.getRegionLoader().load(dimension.getFocusRegion());
|
||||
return focus == null || focus.isCompatExcluded() ? null : focus;
|
||||
}
|
||||
|
||||
public record Options(IrisDimension dimension, IrisData data, ProceduralStream<Double> style,
|
||||
IrisImageMapRuntime imageMaps, ProceduralStream<IrisRegion> focused) {
|
||||
}
|
||||
|
||||
public record Snapshot(IrisDimension dimension, IrisData data, long complexSeed, boolean buffet) {
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,10 @@ import art.arcane.iris.generation.runtime.BiomeEnvironment;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.generation.terrain.RegionSelection;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.volmlib.util.cache.AtomicCache;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -521,11 +525,16 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
if (dimension.allowsSubterrainFeatures()) {
|
||||
SubterrainRasterizer.validateMaterials(dimension.getSubterrainFeatures());
|
||||
}
|
||||
AtomicCache<ProceduralStream<IrisRegion>> regionSelection = new AtomicCache<>();
|
||||
Definitions loaded = new Definitions(data, dimension, NativeBiomeSpawnSelection.retainedDerivatives(data),
|
||||
resolveFocusRegions(data, dimension), new SubterrainPlanner(new SubterrainPlanner.Options(
|
||||
dimension.allowsSubterrainFeatures() ? dimension.getSubterrainFeatures() : List.of(),
|
||||
new SeedManager(epoch.worldSeed()).getBodies(),
|
||||
dimension.getMinHeight(), dimension.getMaxHeight())));
|
||||
dimension.getMinHeight(), dimension.getMaxHeight(),
|
||||
(x, z) -> regionSelection.aquire(() -> RegionSelection.restore(new RegionSelection.Snapshot(
|
||||
dimension, data, new SeedManager(epoch.worldSeed()).getComplex(),
|
||||
engine.isStudio() && "bukkit".equals(IrisPlatforms.get().platformName()))))
|
||||
.get(x, z).getLoadKey())));
|
||||
data.prepareBlockDropRules();
|
||||
definitions.put(epochId, loaded);
|
||||
return loaded;
|
||||
|
||||
Reference in new issue
Block a user