mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +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:
+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;
|
||||
|
||||
@@ -43,7 +43,7 @@ public class SubterrainLocatorTest {
|
||||
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("basin").setBiome("wet-cave")
|
||||
.setProbability(1).setWorldYRange(new IrisRange(-100, 100)).setRadius(28).setHeight(40)
|
||||
.setFluidDepth(8).setFluid(fluid).setPillarSpacing(0).setFormationFraction(0);
|
||||
return new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 74119L, -128, 256));
|
||||
return new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 74119L, -128, 256, (x, z) -> "test-region"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -101,6 +101,6 @@ public class SubterrainLocatorTest {
|
||||
private static SubterrainPlanner planner() {
|
||||
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("basin").setBiome("wet-cave")
|
||||
.setProbability(1).setWorldYRange(new IrisRange(-48, 48));
|
||||
return new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 74119L, -64, 320));
|
||||
return new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 74119L, -64, 320, (x, z) -> "test-region"));
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -131,6 +131,28 @@ public class CaveObjectPlacementTransactionTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void drySkinProtectsExistingRockButAllowsAnAlreadyOpenCaveJoin() {
|
||||
for (boolean solid : new boolean[]{true, false}) {
|
||||
IObjectPlacer delegate = createPlacer(128, 80, 20, 60);
|
||||
SubterrainRoom room = new SubterrainRoom("cenote", "dry-cave", IrisSubterrainFamily.CENOTE,
|
||||
4, 35, 7, 4, 21, 7, 19, 61, 0, 18, false, false, SubterrainCell.Kind.SOLID);
|
||||
when(delegate.getEngine().getSubterrainCell(4, 30, 7))
|
||||
.thenReturn(new SubterrainCell(SubterrainCell.Kind.SOLID, "minecraft:stone", room));
|
||||
when(delegate.isSolid(4, 30, 7)).thenReturn(solid);
|
||||
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 20, 10);
|
||||
NativeBlockState decoration = mock(NativeBlockState.class);
|
||||
transaction.set(4, 30, 7, decoration);
|
||||
assertEquals(solid ? CaveObjectPlacementTransaction.CommitResult.REJECTED_SUBTERRAIN
|
||||
: CaveObjectPlacementTransaction.CommitResult.COMMITTED, transaction.commit());
|
||||
if (solid) {
|
||||
verify(delegate, never()).set(anyInt(), anyInt(), anyInt(), any());
|
||||
} else {
|
||||
verify(delegate).set(4, 30, 7, decoration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ownedWaterAndLavaUseTheirExclusiveAbsoluteFluidHeadAtTheOriginalAnchor() {
|
||||
for (int minimumY : new int[]{-64, 32}) {
|
||||
|
||||
+1
-1
@@ -195,7 +195,7 @@ public class NaturalCaveProceduralPlacementTest {
|
||||
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("natural-room").setProbability(1)
|
||||
.setRadius(32).setHeight(40).setFluid(options.fluid()).setFluidDepth(options.fluidDepth())
|
||||
.setPillarSpacing(0).setFormationFraction(0).setWorldYRange(new IrisRange(20, 120));
|
||||
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 83L, 0, 256));
|
||||
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 83L, 0, 256, (x, z) -> "test-region"));
|
||||
SubterrainPlan plan = planner.plansForBounds(0, 0, 512, 512).getFirst();
|
||||
int x = plan.centerX() + 8;
|
||||
int z = plan.centerZ() + 8;
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ public class SubterrainBiomeOwnershipTest {
|
||||
@Test
|
||||
public void occupiedFeatureOwnsExactVolumeAndYWithoutSurfaceDepthGate() {
|
||||
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("basin").setBiome("basin-biome").setProbability(1);
|
||||
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -64, 320));
|
||||
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -64, 320, (x, z) -> "test-region"));
|
||||
SubterrainPlan plan = planner.plansForBounds(0, 0, 512, 512).getFirst();
|
||||
Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ public class WorldEntitySpawnerUndergroundOwnershipTest {
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
when(chunk.getWorld()).thenReturn(world);
|
||||
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("basin").setBiome("basin-biome").setProbability(1);
|
||||
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -64, 320));
|
||||
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -64, 320, (x, z) -> "test-region"));
|
||||
SubterrainPlan plan = planner.plansForBounds(0, 0, 512, 512).getFirst();
|
||||
SubterrainPosition anchor = plan.anchor();
|
||||
Location candidate = new Location(world, anchor.x(), anchor.y(), anchor.z());
|
||||
|
||||
+25
@@ -83,6 +83,31 @@ public class IrisCarveModifierBoundarySupportTest {
|
||||
assertSame(resolvedCeiling, ceiling);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void authoredRoomKeepsItsBiomeInsideNaturalTerrainOpening() {
|
||||
IrisBiome room = new IrisBiome();
|
||||
IrisBiome surface = new IrisBiome();
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
ProceduralStream<IrisBiome> biomes = mock(ProceduralStream.class);
|
||||
doReturn(true).when(complex).isTerrain3DOpening(40, 42, 44);
|
||||
doReturn(biomes).when(complex).getTrueBiomeStream();
|
||||
doReturn(surface).when(biomes).get(40, 44);
|
||||
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
|
||||
Engine engine = mock(Engine.class);
|
||||
doReturn(engine).when(modifier).getEngine();
|
||||
doReturn(complex).when(engine).getComplex();
|
||||
doReturn(room).when(engine).getSubterrainBiome(40, 42, 44);
|
||||
IrisCarveModifier.CaveInputs inputs = new IrisCarveModifier.CaveInputs(engine);
|
||||
|
||||
assertSame(room, modifier.resolveCaveBoundaryBiome(
|
||||
new MatterCavern(true, "carving/other", (byte) 0), 40, 42, 44,
|
||||
inputs, new Long2ObjectOpenHashMap<>(), new HashMap<>()));
|
||||
assertSame(room, modifier.resolveCaveBoundaryBiome(
|
||||
null, 40, 42, 44,
|
||||
inputs, new Long2ObjectOpenHashMap<>(), new HashMap<>()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void terrainOpeningKeepsItsSurfaceBiomeOverCaveMatter() {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package art.arcane.iris.generation.stage;
|
||||
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainCell;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainRoom;
|
||||
import art.arcane.iris.generation.decoration.IrisCeilingDecorator;
|
||||
import art.arcane.iris.generation.decoration.IrisSurfaceDecorator;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
@@ -78,6 +80,28 @@ public class IrisNaturalCaveScanTest {
|
||||
verifyScan(16, 16);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reservedWalkingPassageKeepsFloorClearAndDecoratesHighCeiling() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
SubterrainRoom room = mock(SubterrainRoom.class);
|
||||
doReturn(true).when(room).reservedPassage();
|
||||
SubterrainCell passage = new SubterrainCell(SubterrainCell.Kind.AIR, "minecraft:cave_air", room);
|
||||
Engine engine = fixture.modifier.getEngine();
|
||||
doReturn(passage).when(engine).getSubterrainCell(-32, 2, 48);
|
||||
Hunk<NativeBlockState> output = Hunk.newArrayHunk(1, 32, 1);
|
||||
for (int y = 0; y < 32; y++) {
|
||||
output.setRaw(0, y, 0, y >= 2 && y <= 12 ? fixture.air : fixture.stone);
|
||||
}
|
||||
|
||||
fixture.modifier.decorateNaturalCaves(-32, 48, output, null);
|
||||
|
||||
assertEquals(List.of("ceiling:0:0:12:6"), fixture.calls);
|
||||
assertSame(fixture.air, output.getRaw(0, 2, 0));
|
||||
assertSame(fixture.air, output.getRaw(0, 3, 0));
|
||||
assertSame(fixture.air, output.getRaw(0, 4, 0));
|
||||
assertSame(fixture.ceiling, output.getRaw(0, 12, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decorationReusesOnlyTheCarveSnapshotOfItsOwnChunk() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
|
||||
+163
-9
@@ -20,6 +20,87 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class SubterrainPlannerTest {
|
||||
@Test
|
||||
public void warpedFamiliesHaveAsymmetricSeededReliefAndRetainedFluids() {
|
||||
for (IrisSubterrainFamily family : IrisSubterrainFamily.values()) {
|
||||
IrisSubterrainFeature feature = feature(family).setShapeWarp(1).setRadius(16)
|
||||
.setHeight(28).setLength(family == IrisSubterrainFamily.TECTONIC_FAULT ? 208 : 80)
|
||||
.setChimneyHeight(0);
|
||||
SubterrainPlanner planner = planner(feature);
|
||||
for (int seed = 0; seed < 3; seed++) {
|
||||
SubterrainPlan plan = new SubterrainPlan(new SubterrainPlan.Options(
|
||||
SubterrainPlanner.Definition.from(feature), 0, 0, 0, false, feature.getId() + ":" + seed));
|
||||
SubterrainPlan rotated = new SubterrainPlan(new SubterrainPlan.Options(
|
||||
SubterrainPlanner.Definition.from(feature), 0, 0, 0, true, plan.id()));
|
||||
SubterrainPosition anchor = plan.anchor();
|
||||
assertTrue(plan.sample(anchor.x(), anchor.y(), anchor.z()).occupied());
|
||||
Set<Integer> floors = new HashSet<>();
|
||||
Set<Integer> ceilings = new HashSet<>();
|
||||
int asymmetric = 0;
|
||||
SubterrainBounds bounds = plan.bounds();
|
||||
for (int x = bounds.minX(); x <= bounds.maxX(); x += 2) {
|
||||
for (int z = bounds.minZ(); z <= bounds.maxZ(); z += 2) {
|
||||
for (int y = bounds.minY(); y <= bounds.maxY(); y += 2) {
|
||||
SubterrainCell cell = plan.sample(x, y, z);
|
||||
assertEquals(cell.kind(), rotated.sample(z, y, x).kind());
|
||||
if (cell.occupied()) {
|
||||
floors.add(cell.room().floorY());
|
||||
ceilings.add(cell.room().ceilingY());
|
||||
assertTrue(x > bounds.minX() && x < bounds.maxX());
|
||||
assertTrue(z > bounds.minZ() && z < bounds.maxZ());
|
||||
assertTrue(y > bounds.minY() && y < bounds.maxY());
|
||||
}
|
||||
if (cell.kind() != plan.sample(-x, y, -z).kind()) {
|
||||
asymmetric++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(family + " floors", floors.size() >= 3);
|
||||
assertTrue(family + " ceilings", ceilings.size() >= 6);
|
||||
assertTrue(family + " asymmetry", asymmetric > 100);
|
||||
if (family != IrisSubterrainFamily.TECTONIC_FAULT) {
|
||||
assertTrue(family + " retained fluid", assertRetained(planner, List.of(plan),
|
||||
family == IrisSubterrainFamily.LAVA_TUBE ? SubterrainCell.Kind.LAVA : SubterrainCell.Kind.WATER) > 30);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shapeWarpRejectsInvalidStrengthsAndSnapshotsConfiguration() {
|
||||
for (double invalid : new double[]{-0.1, 1.1, Double.NaN, Double.POSITIVE_INFINITY}) {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> planner(feature(IrisSubterrainFamily.CENOTE).setShapeWarp(invalid)));
|
||||
}
|
||||
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE).setShapeWarp(0.9);
|
||||
SubterrainPlanner.Definition definition = SubterrainPlanner.Definition.from(feature);
|
||||
feature.setShapeWarp(0.2);
|
||||
assertEquals(0.9, definition.shapeWarp(), 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializedWarpedDefinitionRestoresTheSamePlan() {
|
||||
Gson gson = new Gson();
|
||||
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE).setShapeWarp(0.93);
|
||||
String saved = gson.toJson(feature);
|
||||
IrisSubterrainFeature restored = gson.fromJson(saved, IrisSubterrainFeature.class);
|
||||
assertEquals(0.93, restored.getShapeWarp(), 0);
|
||||
assertEquals(SubterrainPlanner.Definition.from(feature), SubterrainPlanner.Definition.from(restored));
|
||||
SubterrainPlan before = direct(feature);
|
||||
feature.setShapeWarp(0);
|
||||
SubterrainPlan after = direct(restored);
|
||||
assertEquals(before.anchor(), after.anchor());
|
||||
SubterrainBounds bounds = before.bounds();
|
||||
for (int x = bounds.minX(); x <= bounds.maxX(); x += 3) {
|
||||
for (int z = bounds.minZ(); z <= bounds.maxZ(); z += 3) {
|
||||
for (int y = bounds.minY(); y <= bounds.maxY(); y += 3) {
|
||||
assertEquals(before.sample(x, y, z), after.sample(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void terraceAnchorsUseOccupiedPathsWhenCenterIsPillarOrUpperStep() {
|
||||
for (IrisSubterrainFluid fluid : IrisSubterrainFluid.values()) {
|
||||
@@ -314,7 +395,8 @@ public class SubterrainPlannerTest {
|
||||
}
|
||||
previous = layer;
|
||||
}
|
||||
assertTrue(plan.sample(0, 0, 0).room().reservedPassage());
|
||||
assertTrue(plan.sample(0, -10, 0).room().reservedPassage());
|
||||
assertFalse(plan.sample(0, 0, 0).room().reservedPassage());
|
||||
assertFalse(plan.sample(0, 0, 0).solid());
|
||||
int formationCells = 0;
|
||||
for (int x = -20; x <= 20; x++) {
|
||||
@@ -338,9 +420,9 @@ public class SubterrainPlannerTest {
|
||||
IrisSubterrainFeature cenote = feature(IrisSubterrainFamily.CENOTE);
|
||||
IrisSubterrainFeature fault = feature(IrisSubterrainFamily.TECTONIC_FAULT);
|
||||
List<IrisSubterrainFeature> features = new ArrayList<>(List.of(cenote, fault));
|
||||
SubterrainPlanner first = new SubterrainPlanner(new SubterrainPlanner.Options(features, 7123, -64, 97));
|
||||
SubterrainPlanner first = new SubterrainPlanner(new SubterrainPlanner.Options(features, 7123, -64, 97, (x, z) -> "test-region"));
|
||||
Collections.reverse(features);
|
||||
SubterrainPlanner reordered = new SubterrainPlanner(new SubterrainPlanner.Options(features, 7123, -64, 97));
|
||||
SubterrainPlanner reordered = new SubterrainPlanner(new SubterrainPlanner.Options(features, 7123, -64, 97, (x, z) -> "test-region"));
|
||||
List<SubterrainPlan> plans = first.plansForBounds(-1024, -1024, 1024, 1024);
|
||||
assertEquals(plans.stream().map(SubterrainPlan::id).toList(),
|
||||
reordered.plansForBounds(-1024, -1024, 1024, 1024).stream().map(SubterrainPlan::id).toList());
|
||||
@@ -367,7 +449,7 @@ public class SubterrainPlannerTest {
|
||||
feature.setRadius(8).setBiome("changed").setId("changed");
|
||||
assertEquals(before, planner.plansForBounds(-1024, -1024, 1024, 1024).stream().map(SubterrainPlan::id).toList());
|
||||
assertEquals("subterranean/example", first(planner).biome());
|
||||
SubterrainPlanner anotherSeed = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 999, -64, 97));
|
||||
SubterrainPlanner anotherSeed = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 999, -64, 97, (x, z) -> "test-region"));
|
||||
assertNotEquals(before, anotherSeed.plansForBounds(-1024, -1024, 1024, 1024).stream().map(SubterrainPlan::id).toList());
|
||||
}
|
||||
|
||||
@@ -384,9 +466,81 @@ public class SubterrainPlannerTest {
|
||||
SubterrainPlanner.Definition definition = SubterrainPlanner.Definition.from(feature);
|
||||
SubterrainPlan upper = new SubterrainPlan(new SubterrainPlan.Options(definition, 32, 0, 0, false, "fault:other"));
|
||||
SubterrainPlanner planner = planner(feature);
|
||||
assertTrue(upper.sample(32, -14, 0).occupied());
|
||||
assertTrue(lower.sample(32, -14, 0).solid());
|
||||
assertTrue(planner.sample(List.of(upper, lower), 32, -14, 0).solid());
|
||||
assertTrue(upper.sample(25, -13, 0).occupied());
|
||||
assertTrue(lower.sample(25, -13, 0).solid());
|
||||
assertTrue(lower.sample(25, -13, 0).room().reservedSolid());
|
||||
assertTrue(planner.sample(List.of(upper, lower), 25, -13, 0).solid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dryFeatureIntersectionsJoinAcrossEitherShellPriority() {
|
||||
IrisSubterrainFeature basin = feature(IrisSubterrainFamily.CENOTE).setFluidDepth(0).setShapeWarp(1);
|
||||
IrisSubterrainFeature fault = feature(IrisSubterrainFamily.TECTONIC_FAULT).setShapeWarp(1);
|
||||
SubterrainPlan first = direct(basin);
|
||||
SubterrainPlan second = direct(fault);
|
||||
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(basin, fault), 7, -64, 97, (x, z) -> "test-region"));
|
||||
int joined = 0;
|
||||
for (int x = -32; x <= 32; x++) {
|
||||
for (int z = -32; z <= 32; z++) {
|
||||
for (int y = -16; y <= 16; y++) {
|
||||
SubterrainCell shell = first.sample(x, y, z);
|
||||
SubterrainCell passage = second.sample(x, y, z);
|
||||
if (shell.solid() && !shell.room().reservedSolid() && passage.kind() == SubterrainCell.Kind.AIR) {
|
||||
assertEquals(passage, planner.sample(List.of(first, second), x, y, z));
|
||||
assertEquals(passage, planner.sample(List.of(second, first), x, y, z));
|
||||
joined++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(joined > 100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exteriorCornersAndRaisedBanksDoNotExtrudeToFlatBoundingPlanes() {
|
||||
SubterrainPlan plan = direct(feature(IrisSubterrainFamily.CENOTE).setFluidDepth(0));
|
||||
assertTrue(plan.sample(32, -14, 0).solid());
|
||||
assertFalse(plan.sample(33, -16, 0).owned());
|
||||
assertFalse(plan.sample(30, -17, 0).owned());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundedWetBasesHaveReliefAndRemainInsideThePlacementBand() {
|
||||
for (IrisSubterrainFamily family : List.of(IrisSubterrainFamily.CENOTE,
|
||||
IrisSubterrainFamily.LAVA_TUBE, IrisSubterrainFamily.TRAVERTINE_TERRACES)) {
|
||||
IrisSubterrainFeature feature = feature(family).setShapeWarp(1).setChimneyHeight(0);
|
||||
SubterrainPlan plan = direct(feature);
|
||||
SubterrainBounds bounds = plan.bounds();
|
||||
int maximumBaseDepth = SubterrainPlanner.Definition.from(feature).baseDepth();
|
||||
Set<Integer> bottomHeights = new HashSet<>();
|
||||
int deepest = 0;
|
||||
for (int x = bounds.minX(); x <= bounds.maxX(); x += 2) {
|
||||
for (int z = bounds.minZ(); z <= bounds.maxZ(); z += 2) {
|
||||
for (int y = bounds.minY(); y <= bounds.maxY(); y++) {
|
||||
SubterrainCell cell = plan.sample(x, y, z);
|
||||
if (!cell.solid() || !cell.room().reservedSolid()) {
|
||||
continue;
|
||||
}
|
||||
bottomHeights.add(y);
|
||||
int depth = cell.room().floorY() - y;
|
||||
assertTrue(depth <= maximumBaseDepth + 2);
|
||||
deepest = Math.max(deepest, depth);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(family + " base relief", bottomHeights.size() > 5);
|
||||
assertTrue(family + " rock depth", deepest > 6);
|
||||
feature.setWorldYRange(new IrisRange(0, feature.getHeight() + 6));
|
||||
assertTrue(planner(feature).plansForBounds(-512, -512, 512, 512).isEmpty());
|
||||
feature.setWorldYRange(new IrisRange(0, 80));
|
||||
List<SubterrainPlan> placedPlans = planner(feature).plansForBounds(-512, -512, 512, 512);
|
||||
assertFalse(placedPlans.isEmpty());
|
||||
for (SubterrainPlan placed : placedPlans) {
|
||||
assertTrue(placed.bounds().minY() >= 0);
|
||||
assertTrue(placed.bounds().maxY() <= 80);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
@@ -413,7 +567,7 @@ public class SubterrainPlannerTest {
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void duplicateIdentifiersAreRejected() {
|
||||
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE);
|
||||
new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature, feature), 1, -64, 97));
|
||||
new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature, feature), 1, -64, 97, (x, z) -> "test-region"));
|
||||
}
|
||||
|
||||
private int assertRetained(SubterrainPlanner planner, List<SubterrainPlan> candidates, SubterrainCell.Kind kind) {
|
||||
@@ -450,7 +604,7 @@ public class SubterrainPlannerTest {
|
||||
}
|
||||
|
||||
private SubterrainPlanner planner(IrisSubterrainFeature feature) {
|
||||
return new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 7123, -64, 97));
|
||||
return new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 7123, -64, 97, (x, z) -> "test-region"));
|
||||
}
|
||||
|
||||
private SubterrainPlan first(SubterrainPlanner planner) {
|
||||
|
||||
+6
-3
@@ -31,7 +31,7 @@ public class SubterrainRasterizerTest {
|
||||
.setFluid(IrisSubterrainFluid.LAVA).setProbability(1).setLength(64).setRadius(12)
|
||||
.setHeight(24).setPillarSpacing(0).setFormationFraction(0)
|
||||
.setBiome("underground/lava").setWorldYRange(new IrisRange(-48, 60));
|
||||
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 83L, -64, 128));
|
||||
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 83L, -64, 128, (x, z) -> "test-region"));
|
||||
SubterrainPlan plan = planner.plansForBounds(-512, -512, 512, 512).getFirst();
|
||||
int[] fluids = {0};
|
||||
SubterrainRasterizer.rasterize(planner, plan.centerX() >> 4, plan.centerZ() >> 4, (x, y, z, cell) -> {
|
||||
@@ -85,7 +85,7 @@ public class SubterrainRasterizerTest {
|
||||
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("retained-cenote")
|
||||
.setProbability(1).setRadius(20).setHeight(24).setPillarSpacing(12)
|
||||
.setWorldYRange(new IrisRange(-48, 60));
|
||||
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 83L, -64, 128));
|
||||
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 83L, -64, 128, (x, z) -> "test-region"));
|
||||
SubterrainPlan plan = planner.plansForBounds(-512, -512, 512, 512).getFirst();
|
||||
int firstX = plan.centerX() >> 4;
|
||||
int firstZ = plan.centerZ() >> 4;
|
||||
@@ -121,7 +121,10 @@ public class SubterrainRasterizerTest {
|
||||
0, 0, 0, 0, 0, 0, -8, 12, 10, -4, false, false, SubterrainCell.Kind.AIR);
|
||||
assertFalse(SubterrainRasterizer.protectsPlacement(SubterrainCell.OUTSIDE));
|
||||
assertFalse(SubterrainRasterizer.protectsPlacement(new SubterrainCell(SubterrainCell.Kind.AIR, "minecraft:cave_air", room)));
|
||||
assertTrue(SubterrainRasterizer.protectsPlacement(new SubterrainCell(SubterrainCell.Kind.SOLID, "minecraft:stone", room)));
|
||||
assertFalse(SubterrainRasterizer.protectsPlacement(new SubterrainCell(SubterrainCell.Kind.SOLID, "minecraft:stone", room)));
|
||||
SubterrainRoom seal = new SubterrainRoom("room", "biome", IrisSubterrainFamily.CENOTE,
|
||||
0, 0, 0, 0, 0, 0, -8, 12, 10, -4, false, true, SubterrainCell.Kind.SOLID);
|
||||
assertTrue(SubterrainRasterizer.protectsPlacement(new SubterrainCell(SubterrainCell.Kind.SOLID, "minecraft:stone", seal)));
|
||||
SubterrainRoom passage = new SubterrainRoom("room", "biome", IrisSubterrainFamily.CENOTE,
|
||||
0, 0, 0, 0, 0, 0, -8, 12, 10, -4, true, false, SubterrainCell.Kind.AIR);
|
||||
assertTrue(SubterrainRasterizer.protectsPlacement(new SubterrainCell(SubterrainCell.Kind.AIR, "minecraft:cave_air", passage)));
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package art.arcane.iris.generation.subterrain;
|
||||
|
||||
import art.arcane.iris.pack.value.IrisRange;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import com.google.gson.Gson;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class SubterrainRegionSelectionTest {
|
||||
@Test
|
||||
public void restrictedRoomsOnlyPlaceInTheirAllowedRegion() {
|
||||
IrisSubterrainFeature feature = feature().setAllowedRegions(new KList<>("hot"));
|
||||
SubterrainPlanner planner = planner(feature, (x, z) -> x < 0 ? "hot" : "frozen");
|
||||
List<SubterrainPlan> plans = planner.plansForBounds(-1024, -1024, 1024, 1024);
|
||||
assertFalse(plans.isEmpty());
|
||||
for (SubterrainPlan plan : plans) {
|
||||
assertTrue(plan.centerX() < 0);
|
||||
SubterrainPosition anchor = plan.anchor();
|
||||
assertTrue(planner.sample(anchor.x(), anchor.y(), anchor.z()).occupied());
|
||||
}
|
||||
assertTrue(planner(feature, (x, z) -> "frozen").plansForBounds(-1024, -1024, 1024, 1024).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unrestrictedRoomsDoNotSampleRegions() {
|
||||
SubterrainPlanner planner = planner(feature(), (x, z) -> {
|
||||
throw new AssertionError("Unrestricted room queried region");
|
||||
});
|
||||
assertFalse(planner.plansForBounds(-1024, -1024, 1024, 1024).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void restrictionsSurviveSerializationSnapshotAndFootprintFiltering() {
|
||||
IrisSubterrainFeature feature = feature().setAllowedRegions(new KList<>("hot"));
|
||||
Gson gson = new Gson();
|
||||
IrisSubterrainFeature restored = gson.fromJson(gson.toJson(feature), IrisSubterrainFeature.class);
|
||||
SubterrainPlanner planner = planner(restored, (x, z) -> "hot");
|
||||
List<SubterrainPlan> expected = planner.plansForBounds(-1024, -1024, 1024, 1024);
|
||||
restored.getAllowedRegions().clear();
|
||||
restored.getAllowedRegions().add("frozen");
|
||||
SubterrainPlanner restricted = planner.restrictTo(bounds -> bounds.maxZ() < 0);
|
||||
List<SubterrainPlan> actual = restricted.plansForBounds(-1024, -1024, 1024, 1024);
|
||||
assertFalse(actual.isEmpty());
|
||||
assertEquals(expected.stream().filter(plan -> plan.bounds().maxZ() < 0).map(SubterrainPlan::id).toList(),
|
||||
actual.stream().map(SubterrainPlan::id).toList());
|
||||
assertTrue(planner(restored, (x, z) -> "hot").plansForBounds(-1024, -1024, 1024, 1024).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidRegionKeysAreRejected() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> planner(feature().setAllowedRegions(new KList<>("bad region")), (x, z) -> "hot"));
|
||||
}
|
||||
|
||||
private IrisSubterrainFeature feature() {
|
||||
return new IrisSubterrainFeature().setId("regional-room").setProbability(1).setSpacing(256)
|
||||
.setRadius(16).setHeight(24).setFluidDepth(0).setPillarSpacing(0).setFormationFraction(0)
|
||||
.setWorldYRange(new IrisRange(-64, 64));
|
||||
}
|
||||
|
||||
private SubterrainPlanner planner(IrisSubterrainFeature feature, SubterrainPlanner.RegionResolver regions) {
|
||||
return new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1337L, -64, 128, regions));
|
||||
}
|
||||
}
|
||||
+59
-3
@@ -2,6 +2,8 @@ package art.arcane.iris.generation.subterrain;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.biome.IrisBiomePaletteLayer;
|
||||
import art.arcane.iris.generation.decoration.IrisDecorationPart;
|
||||
import art.arcane.iris.generation.decoration.IrisDecorator;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.generation.block.IrisBlockData;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
@@ -20,6 +22,7 @@ import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -60,7 +63,7 @@ public class SubterrainSurfaceMaterialsTest {
|
||||
default -> fixture.fallback;
|
||||
};
|
||||
assertSame(expected, fixture.materials.state(position.x(), position.y(), position.z(), before));
|
||||
assertTrue(SubterrainRasterizer.protectsPlacement(before));
|
||||
assertEquals(before.room().reservedSolid(), SubterrainRasterizer.protectsPlacement(before));
|
||||
assertEquals(before, fixture.cell(position));
|
||||
int chunkX = position.x() >> 4;
|
||||
int chunkZ = position.z() >> 4;
|
||||
@@ -100,10 +103,61 @@ public class SubterrainSurfaceMaterialsTest {
|
||||
output.setRaw(x, y, z, null);
|
||||
modifier.preserveSubterrain((position.x() >> 4) << 4, (position.z() >> 4) << 4, output);
|
||||
NativeBlockState expected = fixture.materials.state(position.x(), position.y(), position.z(), fixture.cell(position));
|
||||
assertSame(expected, output.getRaw(x, y, z));
|
||||
assertSame(fixture.cell(position).room().reservedSolid() ? expected : null, output.getRaw(x, y, z));
|
||||
output.setRaw(x, y, z, fixture.fallback);
|
||||
modifier.preserveSubterrain((position.x() >> 4) << 4, (position.z() >> 4) << 4, output);
|
||||
assertSame(expected, output.getRaw(x, y, z));
|
||||
if (!fixture.cell(position).room().reservedSolid()) {
|
||||
for (String existingKey : List.of("minecraft:cave_air", "minecraft:water", "minecraft:lava")) {
|
||||
NativeBlockState opening = B.getState(existingKey);
|
||||
output.setRaw(x, y, z, opening);
|
||||
modifier.preserveSubterrain((position.x() >> 4) << 4, (position.z() >> 4) << 4, output);
|
||||
assertSame(opening, output.getRaw(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void floorDecoratorSubstratesSurviveBothPreservationPasses() throws Exception {
|
||||
try (Fixture fixture = new Fixture(IrisSubterrainFamily.CENOTE)) {
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getComplex()).thenReturn(fixture.complex);
|
||||
when(engine.getData()).thenReturn(fixture.data);
|
||||
when(engine.getDimension()).thenReturn(fixture.dimension);
|
||||
when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(-64).maxHeight(128).build());
|
||||
when(fixture.complex.getSubterrainPlanner()).thenReturn(fixture.planner);
|
||||
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
|
||||
doReturn(engine).when(modifier).getEngine();
|
||||
Field random = IrisCarveModifier.class.getDeclaredField("rng");
|
||||
random.setAccessible(true);
|
||||
random.set(modifier, fixture.random);
|
||||
Method restore = IrisCarveModifier.class.getDeclaredMethod("restoreSubterrainSolids", Hunk.class, int.class, int.class, boolean.class);
|
||||
restore.setAccessible(true);
|
||||
SubterrainPosition position = fixture.surfaces().get(SubterrainSurfaceMaterials.Surface.FLOOR);
|
||||
int chunkX = position.x() >> 4;
|
||||
int chunkZ = position.z() >> 4;
|
||||
int x = position.x() & 15;
|
||||
int y = position.y() + 64;
|
||||
int z = position.z() & 15;
|
||||
Hunk<NativeBlockState> output = Hunk.newArrayHunk(16, 192, 16);
|
||||
for (String key : List.of("minecraft:moss_block", "minecraft:crimson_nylium")) {
|
||||
NativeBlockState support = solid(key);
|
||||
IrisBlockData forceBlock = mock(IrisBlockData.class);
|
||||
when(forceBlock.getBlockData(fixture.data)).thenReturn(support);
|
||||
when(fixture.biome.getDecoratorBucket(IrisDecorationPart.NONE))
|
||||
.thenReturn(new IrisDecorator[]{new IrisDecorator().setForceBlock(forceBlock)});
|
||||
output.setRaw(x, y, z, support);
|
||||
|
||||
restore.invoke(modifier, output, chunkX, chunkZ, true);
|
||||
assertSame(support, output.getRaw(x, y, z));
|
||||
modifier.preserveSubterrain(chunkX << 4, chunkZ << 4, output);
|
||||
assertSame(support, output.getRaw(x, y, z));
|
||||
|
||||
when(support.isOccluding()).thenReturn(false);
|
||||
modifier.preserveSubterrain(chunkX << 4, chunkZ << 4, output);
|
||||
assertSame(fixture.floor, output.getRaw(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,13 +237,14 @@ public class SubterrainSurfaceMaterialsTest {
|
||||
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("room").setBiome("room-biome")
|
||||
.setFamily(family).setProbability(1).setRadius(12).setHeight(16).setLength(200)
|
||||
.setChimneyHeight(8).setWorldYRange(new IrisRange(-48, 64));
|
||||
planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 83L, -64, 128));
|
||||
planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 83L, -64, 128, (x, z) -> "test-region"));
|
||||
plan = planner.plansForBounds(-512, -512, 512, 512).getFirst();
|
||||
plans = planner.plansForBounds(plan.bounds().minX() - 1, plan.bounds().minZ() - 1,
|
||||
plan.bounds().maxX() + 1, plan.bounds().maxZ() + 1);
|
||||
ResourceLoader<IrisBiome> loader = mock(ResourceLoader.class);
|
||||
when(data.getBiomeLoader()).thenReturn(loader);
|
||||
when(loader.load("room-biome")).thenReturn(biome);
|
||||
when(biome.getDecoratorBucket(IrisDecorationPart.NONE)).thenReturn(new IrisDecorator[0]);
|
||||
IrisBiomePaletteLayer wallPalette = mock(IrisBiomePaletteLayer.class);
|
||||
when(biome.getWall()).thenReturn(wallPalette);
|
||||
when(wallPalette.get(eq(random), anyDouble(), anyDouble(), anyDouble(), eq(data))).thenReturn(wall);
|
||||
@@ -240,6 +295,7 @@ public class SubterrainSurfaceMaterialsTest {
|
||||
Map<SubterrainPosition, String> result = new HashMap<>();
|
||||
for (SubterrainPosition chunk : chunks) {
|
||||
Hunk<NativeBlockState> output = Hunk.newArrayHunk(16, 192, 16);
|
||||
output.fill(fallback);
|
||||
for (int draw = 0; draw < 19; draw++) {
|
||||
random.nextLong();
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ public class SchemaBuilderIdentityTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final String EXPECTED_SCHEMA_DIGEST = "6f6dae0cd1077c45fe3f5ba512c1da4b0ab43bc8516f2d759bc8f7a798eb1c58";
|
||||
private static final String EXPECTED_SCHEMA_DIGEST = "cc10defa73f5288d10714cdbdef6bda866acfc656efe54e062fac9995bc5607f";
|
||||
|
||||
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
|
||||
IrisDimension.class,
|
||||
@@ -138,7 +138,7 @@ public class SchemaBuilderIdentityTest {
|
||||
IrisDimension dimension = gson.fromJson("""
|
||||
{"subterrainFeatures":[{"id":"flooded-vault","family":"CENOTE",
|
||||
"biome":"pack/cavern","fluid":"LAVA","worldYRange":{"min":-40,"max":24},
|
||||
"spacing":384,"probability":0.75,"radius":28,"fluidDepth":6}]}
|
||||
"spacing":384,"probability":0.75,"radius":28,"fluidDepth":6,"shapeWarp":0.94,"allowedRegions":["hot"]}]}
|
||||
""", IrisDimension.class);
|
||||
assertEquals(1, dimension.getSubterrainFeatures().size());
|
||||
IrisSubterrainFeature feature = dimension.getSubterrainFeatures().get(0);
|
||||
@@ -152,6 +152,8 @@ public class SchemaBuilderIdentityTest {
|
||||
assertEquals(0.75D, feature.getProbability(), 0D);
|
||||
assertEquals(28, feature.getRadius());
|
||||
assertEquals(6, feature.getFluidDepth());
|
||||
assertEquals(0.94D, feature.getShapeWarp(), 0D);
|
||||
assertEquals(List.of("hot"), feature.getAllowedRegions());
|
||||
IrisDimension restored = gson.fromJson(gson.toJson(dimension), IrisDimension.class);
|
||||
assertEquals(feature, restored.getSubterrainFeatures().get(0));
|
||||
assertTrue(new IrisDimension().getSubterrainFeatures().isEmpty());
|
||||
@@ -178,11 +180,14 @@ public class SchemaBuilderIdentityTest {
|
||||
assertTrue(properties.has("biome"));
|
||||
assertTrue(properties.has("fluid"));
|
||||
assertTrue(properties.has("worldYRange"));
|
||||
assertEquals("array", properties.getJSONObject("allowedRegions").getString("type"));
|
||||
assertEquals(32, properties.getJSONObject("spacing").getInt("minimum"));
|
||||
assertEquals(8192, properties.getJSONObject("spacing").getInt("maximum"));
|
||||
assertEquals(0D, properties.getJSONObject("probability").getDouble("minimum"), 0D);
|
||||
assertEquals(1D, properties.getJSONObject("probability").getDouble("maximum"), 0D);
|
||||
assertEquals(32, properties.getJSONObject("fluidDepth").getInt("maximum"));
|
||||
assertEquals(0D, properties.getJSONObject("shapeWarp").getDouble("minimum"), 0D);
|
||||
assertEquals(1D, properties.getJSONObject("shapeWarp").getDouble("maximum"), 0D);
|
||||
}
|
||||
|
||||
private static String digest(String value) {
|
||||
|
||||
@@ -213,7 +213,7 @@ public class SavedBiomeCaptureTest {
|
||||
public void capturesFeatureBoundariesAtExactBlockCoordinates() throws Exception {
|
||||
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("exact-basin").setBiome("authored-biome")
|
||||
.setProbability(1).setPillarSpacing(0).setFormationFraction(0);
|
||||
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -64, 320));
|
||||
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -64, 320, (x, z) -> "test-region"));
|
||||
SubterrainPlan plan = planner.plansForBounds(0, 0, 512, 512).getFirst();
|
||||
SubterrainPosition anchor = plan.anchor();
|
||||
int edgeX = plan.bounds().minX();
|
||||
|
||||
@@ -33,7 +33,7 @@ public class SavedSubterrainQueriesTest {
|
||||
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("old-basin").setProbability(1D)
|
||||
.setBiome("retired-cave").setRadius(28).setHeight(40).setFluidDepth(8)
|
||||
.setWorldYRange(new IrisRange(-100, 100)).setFormationFraction(0).setPillarSpacing(0);
|
||||
SubterrainPlanner historical = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -128, 256));
|
||||
SubterrainPlanner historical = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -128, 256, (x, z) -> "test-region"));
|
||||
SubterrainPlan plan = historical.plansForBounds(0, 0, 511, 511).getFirst();
|
||||
SubterrainPosition anchor = plan.anchor();
|
||||
SubterrainLocator.Result target = new SubterrainLocator.Result(plan.id(), plan.family(), plan.biome(),
|
||||
@@ -126,6 +126,6 @@ public class SavedSubterrainQueriesTest {
|
||||
|
||||
private static SubterrainPlanner planner(String id) {
|
||||
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId(id).setProbability(1D);
|
||||
return new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -64, 320));
|
||||
return new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -64, 320, (x, z) -> "test-region"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user