mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
f
This commit is contained in:
1 parent
ad41b6795c
commit
1d194e880b
12 files changed
+1208
-128
No files matched your search
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.core.tools;
|
||||
|
||||
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
|
||||
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||
import com.google.common.util.concurrent.AtomicDouble;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
@@ -46,13 +47,16 @@ import art.arcane.volmlib.util.scheduling.FoliaScheduler;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.WorldCreator;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -230,6 +234,7 @@ public class IrisCreator {
|
||||
addToBukkitYml();
|
||||
J.s(() -> IrisServices.get(MultiverseCoreLink.class).updateWorld(world, dimension));
|
||||
}
|
||||
scheduleSenderTeleport(world);
|
||||
|
||||
if (pregen != null) {
|
||||
CompletableFuture<Boolean> ff = new CompletableFuture<>();
|
||||
@@ -254,6 +259,73 @@ public class IrisCreator {
|
||||
return world;
|
||||
}
|
||||
|
||||
static Player createTeleportTarget(VolmitSender sender, boolean studio, boolean benchmark) {
|
||||
if (studio || benchmark || sender == null || !sender.isPlayer()) {
|
||||
return null;
|
||||
}
|
||||
return sender.player();
|
||||
}
|
||||
|
||||
static CompletableFuture<Boolean> teleportSenderToCreatedWorld(
|
||||
Player player,
|
||||
World world,
|
||||
WorldRuntimeControlService runtimeControl
|
||||
) {
|
||||
Player requiredPlayer = Objects.requireNonNull(player, "player");
|
||||
World requiredWorld = Objects.requireNonNull(world, "world");
|
||||
WorldRuntimeControlService requiredRuntime = Objects.requireNonNull(runtimeControl, "runtimeControl");
|
||||
Location entryAnchor = requiredRuntime.resolveEntryAnchor(requiredWorld);
|
||||
if (entryAnchor == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Unable to resolve the entry anchor for world \"" + requiredWorld.getName() + "\"."));
|
||||
}
|
||||
|
||||
int chunkX = entryAnchor.getBlockX() >> 4;
|
||||
int chunkZ = entryAnchor.getBlockZ() >> 4;
|
||||
return requiredRuntime.requestChunkAsync(requiredWorld, chunkX, chunkZ, true)
|
||||
.thenCompose(chunk -> requiredRuntime.resolveSafeEntry(requiredWorld, entryAnchor))
|
||||
.thenCompose(safeEntry -> {
|
||||
if (safeEntry == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Unable to resolve a safe entry for world \"" + requiredWorld.getName() + "\"."));
|
||||
}
|
||||
return requiredRuntime.teleport(requiredPlayer, safeEntry);
|
||||
});
|
||||
}
|
||||
|
||||
private void scheduleSenderTeleport(World world) {
|
||||
Player player = createTeleportTarget(sender, studio, benchmark);
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
CompletableFuture<Boolean> teleportFuture;
|
||||
try {
|
||||
teleportFuture = teleportSenderToCreatedWorld(player, world, WorldRuntimeControlService.get());
|
||||
} catch (Throwable e) {
|
||||
reportSenderTeleportFailure(player, world, e);
|
||||
return;
|
||||
}
|
||||
|
||||
teleportFuture.whenComplete((success, throwable) -> {
|
||||
if (throwable != null) {
|
||||
reportSenderTeleportFailure(player, world, throwable);
|
||||
return;
|
||||
}
|
||||
if (!Boolean.TRUE.equals(success)) {
|
||||
reportSenderTeleportFailure(player, world, new IllegalStateException(
|
||||
"The runtime teleport operation returned false for player \"" + player.getName() + "\"."));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reportSenderTeleportFailure(Player player, World world, Throwable throwable) {
|
||||
IrisLogging.reportError("World \"" + world.getName()
|
||||
+ "\" was created, but automatic teleport failed for player \"" + player.getName() + "\".", throwable);
|
||||
J.runEntity(player, () -> new VolmitSender(player).sendMessage(C.YELLOW
|
||||
+ "The world was created, but automatic teleport failed. Try /iris teleport world=" + world.getName()));
|
||||
}
|
||||
|
||||
private void reportStudioProgress(double progress, String stage) {
|
||||
BiConsumer<Double, String> consumer = studioProgressConsumer;
|
||||
if (consumer == null) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.IrisStructureCarveShape;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.object.NativeStructureSuppression;
|
||||
import art.arcane.iris.engine.object.ObjectPlaceMode;
|
||||
@@ -403,10 +404,12 @@ public final class IrisStructureLocator {
|
||||
}
|
||||
|
||||
int sideExtension = placement.isOverbore()
|
||||
? Math.max(1, placement.getOverboreRadius())
|
||||
? Math.max(0, placement.getOverboreRadius())
|
||||
: placement.isBore() ? Math.max(0, placement.getBorePadding()) : 0;
|
||||
IrisStructureCarveShape overboreShape = placement.resolvedOverboreShape();
|
||||
int topExtension = placement.isOverbore()
|
||||
? (int) Math.ceil(Math.max(1D, placement.getOverboreHeight()) * 1.8D)
|
||||
? overboreShape.maximumCeilingExtension(
|
||||
placement.getOverboreHeight(), placement.resolvedOverboreErosionStrength())
|
||||
: placement.isBore() ? Math.max(0, placement.getBorePadding()) : 0;
|
||||
int bottomExtension = placement.isOverbore() ? Math.max(0, placement.getOverboreFloor()) : 0;
|
||||
int bandMin = Math.max(worldMin, Math.min(placement.getMinHeight(), placement.getMaxHeight()));
|
||||
@@ -419,7 +422,8 @@ public final class IrisStructureLocator {
|
||||
|
||||
Long2IntOpenHashMap surfaceHeights = new Long2IntOpenHashMap();
|
||||
surfaceHeights.defaultReturnValue(Integer.MIN_VALUE);
|
||||
if (placement.isBore() && !placement.isOverbore()) {
|
||||
if ((placement.isBore() && !placement.isOverbore())
|
||||
|| (placement.isOverbore() && overboreShape == IrisStructureCarveShape.BOX)) {
|
||||
maximumShift = resolveBurialEnvelopeShift(
|
||||
engine,
|
||||
bounds[0] - sideExtension,
|
||||
|
||||
+160
-96
@@ -37,6 +37,7 @@ import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.ObjectPlaceMode;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.IrisStructureCarveShape;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.object.IrisStructureStiltSettings;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -65,10 +66,8 @@ import java.util.Objects;
|
||||
public class IrisStructureComponent extends IrisMantleComponent {
|
||||
private static final long MAX_BORE_VOLUME = 6_000_000L;
|
||||
private static final long MAX_OVERBORE_VOLUME = 48_000_000L;
|
||||
private static final double OVERBORE_MIN_BOUNDARY = 0.2;
|
||||
private static final double OVERBORE_MIN_BOUNDARY_SQUARED = OVERBORE_MIN_BOUNDARY * OVERBORE_MIN_BOUNDARY;
|
||||
private static final double OVERBORE_BOUNDARY_SPAN = 0.8;
|
||||
private static final double OVERBORE_MAX_UP_REACH = 1.8;
|
||||
private static final int MAX_OVERBORE_FOOTPRINT_COLUMNS = 1_048_576;
|
||||
private static final double OVERBORE_ROLL_FREQUENCY_RATIO = 3D / 7D;
|
||||
private static final MatterCavern CARVE_CAVERN = new MatterCavern(true, "", (byte) 3);
|
||||
|
||||
public IrisStructureComponent(EngineMantle engineMantle) {
|
||||
@@ -128,7 +127,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
||||
}
|
||||
|
||||
if (placement.isOverbore()) {
|
||||
overboreStructure(writer, pieces, placement.getOverboreRadius(), placement.getOverboreHeight(), placement.getOverboreFloor());
|
||||
overboreStructure(writer, pieces, placement);
|
||||
} else if (placement.isBore()) {
|
||||
boreStructure(writer, pieces, placement.getBorePadding());
|
||||
}
|
||||
@@ -286,102 +285,95 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private void overboreStructure(MantleWriter writer, KList<PlacedStructurePiece> pieces, int radius, int ceiling, int floorDepth) {
|
||||
private void overboreStructure(MantleWriter writer, KList<PlacedStructurePiece> pieces,
|
||||
IrisStructurePlacement placement) {
|
||||
int[] bounds = computePieceBounds(pieces);
|
||||
if (bounds == null) {
|
||||
return;
|
||||
}
|
||||
int margin = Math.max(1, radius);
|
||||
int head = Math.max(0, ceiling);
|
||||
int floorCut = Math.max(0, floorDepth);
|
||||
IrisStructureCarveShape shape = placement.resolvedOverboreShape();
|
||||
int margin = Math.max(0, placement.getOverboreRadius());
|
||||
int head = Math.max(0, placement.getOverboreHeight());
|
||||
int floorCut = Math.max(0, placement.getOverboreFloor());
|
||||
double strength = placement.resolvedOverboreErosionStrength();
|
||||
int mantleOffset = getEngineMantle().getEngine().getMinHeight();
|
||||
int worldMin = getEngineMantle().getEngine().getMinHeight() + 1;
|
||||
int worldMax = getEngineMantle().getEngine().getMinHeight() + getEngineMantle().getEngine().getHeight() - 1;
|
||||
int upExtension = shape.maximumCeilingExtension(head, strength);
|
||||
|
||||
double freq = 0.07;
|
||||
double rollFreq = 0.03;
|
||||
double reachSide = margin;
|
||||
double reachUp = head < 1 ? 1.0 : head;
|
||||
double reachDown = floorCut < 1 ? 1.0 : floorCut;
|
||||
double upReachMin = 0.4;
|
||||
double upReachSpan = 1.4;
|
||||
int sideExt = overboreSideExtension(margin);
|
||||
int upExt = overboreUpExtension(reachUp);
|
||||
|
||||
long work = 0L;
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
long wx = (long) (p.getMaxX() - p.getMinX() + 1) + 2L * sideExt;
|
||||
long wz = (long) (p.getMaxZ() - p.getMinZ() + 1) + 2L * sideExt;
|
||||
long wy = (long) (p.getMaxY() - p.getMinY() + 1) + upExt + floorCut;
|
||||
work += wx * wy * wz;
|
||||
}
|
||||
if (work > MAX_OVERBORE_VOLUME) {
|
||||
IrisLogging.warn("Skipping structure overbore of " + work + " blocks (cap " + MAX_OVERBORE_VOLUME + "); reduce overboreRadius/overboreHeight or use larger spacing.");
|
||||
if (shape == IrisStructureCarveShape.BOX) {
|
||||
carveOverboreBox(writer, bounds, margin, head, floorCut, mantleOffset, worldMin, worldMax);
|
||||
return;
|
||||
}
|
||||
|
||||
RNG noiseRng = new RNG(seed() + Cache.key(bounds[0], bounds[2]));
|
||||
CNG blob = CNG.signature(noiseRng);
|
||||
CNG roll = CNG.signature(noiseRng.nextParallelRNG(0x2A17));
|
||||
|
||||
if (IrisSettings.get().getGeneral().isDebug()) {
|
||||
IrisLogging.info("Overbore carving organic cavern: pieces=" + pieces.size() + " margin=" + margin + " head=" + head + " floorCut=" + floorCut + " work=" + work);
|
||||
long work = overboreCandidateVolume(bounds, margin, upExtension, floorCut);
|
||||
if (work > MAX_OVERBORE_VOLUME) {
|
||||
warnOverboreVolume(work);
|
||||
return;
|
||||
}
|
||||
StructureCarvingFootprint footprint = StructureCarvingFootprint.from(
|
||||
pieces, margin, MAX_OVERBORE_FOOTPRINT_COLUMNS);
|
||||
if (footprint == null) {
|
||||
IrisLogging.warn("Structure overbore footprint was empty or exceeded "
|
||||
+ MAX_OVERBORE_FOOTPRINT_COLUMNS + " columns; skipping its expanded carve.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
int pMinX = p.getMinX();
|
||||
int pMinY = p.getMinY();
|
||||
int pMinZ = p.getMinZ();
|
||||
int pMaxX = p.getMaxX();
|
||||
int pMaxY = p.getMaxY();
|
||||
int pMaxZ = p.getMaxZ();
|
||||
int exMinX = pMinX - sideExt;
|
||||
int exMaxX = pMaxX + sideExt;
|
||||
int exMinZ = pMinZ - sideExt;
|
||||
int exMaxZ = pMaxZ + sideExt;
|
||||
int exMinY = Math.max(worldMin, pMinY - floorCut);
|
||||
int exMaxY = Math.min(worldMax, pMaxY + upExt);
|
||||
for (int bx = exMinX; bx <= exMaxX; bx++) {
|
||||
double dx = bx < pMinX ? pMinX - bx : bx > pMaxX ? bx - pMaxX : 0;
|
||||
double nx = dx / reachSide;
|
||||
for (int bz = exMinZ; bz <= exMaxZ; bz++) {
|
||||
double dz = bz < pMinZ ? pMinZ - bz : bz > pMaxZ ? bz - pMaxZ : 0;
|
||||
double nz = dz / reachSide;
|
||||
double nxz = nx * nx + nz * nz;
|
||||
if (nxz > 1.0) {
|
||||
double frequency = placement.resolvedOverboreErosionFrequency();
|
||||
boolean eroded = shape == IrisStructureCarveShape.ERODED && strength > 0D;
|
||||
CNG blob = null;
|
||||
CNG roll = null;
|
||||
if (eroded) {
|
||||
RNG noiseRng = new RNG(seed() + Cache.key(bounds[0], bounds[2]));
|
||||
blob = CNG.signature(noiseRng);
|
||||
roll = CNG.signature(noiseRng.nextParallelRNG(0x2A17));
|
||||
}
|
||||
|
||||
if (IrisSettings.get().getGeneral().isDebug()) {
|
||||
IrisLogging.info("Overbore carving structure cavern: shape=" + shape + " pieces=" + pieces.size()
|
||||
+ " footprint=" + footprint.width() + "x" + footprint.depth()
|
||||
+ " margin=" + margin + " head=" + head + " floorCut=" + floorCut + " work=" + work);
|
||||
}
|
||||
|
||||
double sideReachSquared = (double) margin * margin;
|
||||
double downReach = Math.max(1D, floorCut);
|
||||
for (int bz = footprint.minZ(); bz <= footprint.maxZ(); bz++) {
|
||||
int footprintIndex = footprint.indexAt(footprint.minX(), bz);
|
||||
for (int bx = footprint.minX(); bx <= footprint.maxX(); bx++, footprintIndex++) {
|
||||
long horizontalDistanceSquared = footprint.distanceSquaredAt(footprintIndex);
|
||||
if (horizontalDistanceSquared < 0L || horizontalDistanceSquared > sideReachSquared) {
|
||||
continue;
|
||||
}
|
||||
double normalizedHorizontalDistanceSquared = sideReachSquared == 0D
|
||||
? 0D : horizontalDistanceSquared / sideReachSquared;
|
||||
int sourceMinY = footprint.sourceMinYAt(footprintIndex);
|
||||
int sourceMaxY = footprint.sourceMaxYAt(footprintIndex);
|
||||
double upReach = eroded
|
||||
? erodedUpReach(roll, frequency, strength, bx, bz, head)
|
||||
: Math.max(1D, head);
|
||||
int columnUpExtension = eroded
|
||||
? (int) Math.ceil(upReach) : head;
|
||||
int minY = Math.max(worldMin, sourceMinY - floorCut);
|
||||
int maxY = Math.min(worldMax, sourceMaxY + columnUpExtension);
|
||||
for (int by = minY; by <= maxY; by++) {
|
||||
double normalizedY = normalizedVerticalDistance(
|
||||
by, sourceMinY, sourceMaxY, upReach, downReach);
|
||||
double distanceSquared = normalizedHorizontalDistanceSquared
|
||||
+ normalizedY * normalizedY;
|
||||
if (distanceSquared <= 0D) {
|
||||
writer.carveDataIfAbsent(bx, by - mantleOffset, bz, CARVE_CAVERN);
|
||||
continue;
|
||||
}
|
||||
double w = roll.fitDouble(0.0, 1.0, bx * rollFreq, bz * rollFreq) * 0.7
|
||||
+ roll.fitDouble(0.0, 1.0, bx * rollFreq * 3.0, bz * rollFreq * 3.0) * 0.3;
|
||||
double contrast = (w - 0.5) * 2.6 + 0.5;
|
||||
if (contrast < 0.0) {
|
||||
contrast = 0.0;
|
||||
} else if (contrast > 1.0) {
|
||||
contrast = 1.0;
|
||||
if (distanceSquared > 1D) {
|
||||
continue;
|
||||
}
|
||||
double upReach = reachUp * (upReachMin + upReachSpan * contrast);
|
||||
if (upReach < 1.0) {
|
||||
upReach = 1.0;
|
||||
if (!eroded) {
|
||||
writer.carveDataIfAbsent(bx, by - mantleOffset, bz, CARVE_CAVERN);
|
||||
continue;
|
||||
}
|
||||
for (int by = exMinY; by <= exMaxY; by++) {
|
||||
double ny;
|
||||
if (by > pMaxY) {
|
||||
ny = (by - pMaxY) / upReach;
|
||||
} else if (by < pMinY) {
|
||||
ny = (pMinY - by) / reachDown;
|
||||
} else {
|
||||
ny = 0.0;
|
||||
}
|
||||
double distanceSquared = nxz + ny * ny;
|
||||
if (distanceSquared > 1.0) {
|
||||
continue;
|
||||
}
|
||||
if (distanceSquared > OVERBORE_MIN_BOUNDARY_SQUARED) {
|
||||
double n = blob.fitDouble(0.0, 1.0, bx * freq, by * freq, bz * freq);
|
||||
if (!shouldCarveOverboreCell(distanceSquared, n)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
double noise = blob.fitDouble(
|
||||
0D, 1D, bx * frequency, by * frequency, bz * frequency);
|
||||
if (shouldCarveOverboreCell(shape, distanceSquared, noise, strength)) {
|
||||
writer.carveDataIfAbsent(bx, by - mantleOffset, bz, CARVE_CAVERN);
|
||||
}
|
||||
}
|
||||
@@ -389,28 +381,100 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
||||
}
|
||||
}
|
||||
|
||||
static int overboreSideExtension(int radius) {
|
||||
return Math.max(1, radius);
|
||||
private void carveOverboreBox(MantleWriter writer, int[] bounds, int margin, int head, int floorCut,
|
||||
int mantleOffset, int worldMin, int worldMax) {
|
||||
int minX = bounds[0] - margin;
|
||||
int minY = Math.max(worldMin, bounds[1] - floorCut);
|
||||
int minZ = bounds[2] - margin;
|
||||
int maxX = bounds[3] + margin;
|
||||
int maxY = Math.min(worldMax, bounds[4] + head);
|
||||
int maxZ = bounds[5] + margin;
|
||||
long volume = inclusiveVolume(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
if (volume > MAX_OVERBORE_VOLUME) {
|
||||
warnOverboreVolume(volume);
|
||||
return;
|
||||
}
|
||||
for (int bx = minX; bx <= maxX; bx++) {
|
||||
for (int by = minY; by <= maxY; by++) {
|
||||
for (int bz = minZ; bz <= maxZ; bz++) {
|
||||
writer.carveDataIfAbsent(bx, by - mantleOffset, bz, CARVE_CAVERN);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int overboreUpExtension(double reachUp) {
|
||||
return (int) Math.ceil(Math.max(1.0, reachUp) * OVERBORE_MAX_UP_REACH);
|
||||
static double erodedUpReach(CNG roll, double frequency, double strength, int x, int z, int head) {
|
||||
if (head <= 0) {
|
||||
return 0D;
|
||||
}
|
||||
double clampedStrength = Math.max(0D, Math.min(1D, strength));
|
||||
if (clampedStrength <= 0D) {
|
||||
return Math.max(1D, head);
|
||||
}
|
||||
double rollFrequency = frequency * OVERBORE_ROLL_FREQUENCY_RATIO;
|
||||
double sample = roll.fitDouble(0D, 1D, x * rollFrequency, z * rollFrequency) * 0.7D
|
||||
+ roll.fitDouble(0D, 1D, x * rollFrequency * 3D, z * rollFrequency * 3D) * 0.3D;
|
||||
double contrast = Math.max(0D, Math.min(1D, (sample - 0.5D) * 2.6D + 0.5D));
|
||||
double roundedReach = Math.max(1D, head);
|
||||
double erodedReach = Math.max(1D, head * (0.4D + 1.4D * contrast));
|
||||
return roundedReach + clampedStrength * (erodedReach - roundedReach);
|
||||
}
|
||||
|
||||
static double overboreBoundaryLimit(double noise) {
|
||||
private static double normalizedVerticalDistance(int y, int sourceMinY, int sourceMaxY,
|
||||
double upReach, double downReach) {
|
||||
if (y > sourceMaxY) {
|
||||
return (y - sourceMaxY) / upReach;
|
||||
}
|
||||
if (y < sourceMinY) {
|
||||
return (sourceMinY - y) / downReach;
|
||||
}
|
||||
return 0D;
|
||||
}
|
||||
|
||||
private static long overboreCandidateVolume(int[] bounds, int margin, int upExtension,
|
||||
int floorCut) {
|
||||
return inclusiveVolume(
|
||||
bounds[0] - margin, bounds[1] - floorCut, bounds[2] - margin,
|
||||
bounds[3] + margin, bounds[4] + upExtension, bounds[5] + margin);
|
||||
}
|
||||
|
||||
private static long inclusiveVolume(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
long width = (long) maxX - minX + 1L;
|
||||
long height = (long) maxY - minY + 1L;
|
||||
long depth = (long) maxZ - minZ + 1L;
|
||||
if (width <= 0L || height <= 0L || depth <= 0L
|
||||
|| width > Long.MAX_VALUE / height
|
||||
|| width * height > Long.MAX_VALUE / depth) {
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
return width * height * depth;
|
||||
}
|
||||
|
||||
private static void warnOverboreVolume(long volume) {
|
||||
IrisLogging.warn("Skipping structure overbore of " + volume + " blocks (cap "
|
||||
+ MAX_OVERBORE_VOLUME + "); reduce overboreRadius/overboreHeight or use larger spacing.");
|
||||
}
|
||||
|
||||
static double overboreBoundaryLimit(double noise, double strength) {
|
||||
double clampedNoise = Math.max(0.0, Math.min(1.0, noise));
|
||||
return OVERBORE_MIN_BOUNDARY + OVERBORE_BOUNDARY_SPAN * clampedNoise;
|
||||
double clampedStrength = Math.max(0.0, Math.min(1.0, strength));
|
||||
return 1D - clampedStrength * (1D - clampedNoise);
|
||||
}
|
||||
|
||||
static boolean shouldCarveOverboreCell(double distanceSquared, double noise) {
|
||||
static boolean shouldCarveOverboreCell(IrisStructureCarveShape shape, double distanceSquared,
|
||||
double noise, double strength) {
|
||||
if (distanceSquared <= 0.0) {
|
||||
return true;
|
||||
}
|
||||
if (distanceSquared > 1.0) {
|
||||
return false;
|
||||
}
|
||||
double limit = overboreBoundaryLimit(noise);
|
||||
return distanceSquared <= limit * limit;
|
||||
IrisStructureCarveShape resolvedShape = shape == null ? IrisStructureCarveShape.ERODED : shape;
|
||||
return switch (resolvedShape) {
|
||||
case BOX -> true;
|
||||
case ROUNDED -> distanceSquared <= 1D;
|
||||
case ERODED -> {
|
||||
double limit = overboreBoundaryLimit(noise, strength);
|
||||
yield distanceSquared <= 1D && distanceSquared <= limit * limit;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void clearIntersectingObjectTrees(MantleWriter writer, IrisStructureLocator.ResolvedPlacement resolved) {
|
||||
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.engine.framework.PlacedStructurePiece;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import it.unimi.dsi.fastutil.longs.Long2LongMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
final class StructureCarvingFootprint {
|
||||
static final int DEFAULT_MAX_CELLS = 1_048_576;
|
||||
|
||||
private final int minX;
|
||||
private final int maxX;
|
||||
private final int minZ;
|
||||
private final int maxZ;
|
||||
private final int width;
|
||||
private final int depth;
|
||||
private final long[] distanceSquared;
|
||||
private final int[] nearestSourceIds;
|
||||
private final int[] sourceMinY;
|
||||
private final int[] sourceMaxY;
|
||||
|
||||
private StructureCarvingFootprint(FootprintData data) {
|
||||
minX = data.minX();
|
||||
maxX = data.maxX();
|
||||
minZ = data.minZ();
|
||||
maxZ = data.maxZ();
|
||||
width = data.width();
|
||||
depth = data.depth();
|
||||
distanceSquared = data.distanceSquared();
|
||||
nearestSourceIds = data.nearestSourceIds();
|
||||
sourceMinY = data.sourceMinY();
|
||||
sourceMaxY = data.sourceMaxY();
|
||||
}
|
||||
|
||||
static StructureCarvingFootprint from(KList<PlacedStructurePiece> pieces, int padding) {
|
||||
return from(pieces, padding, DEFAULT_MAX_CELLS);
|
||||
}
|
||||
|
||||
static StructureCarvingFootprint from(KList<PlacedStructurePiece> pieces, int padding, int maxCells) {
|
||||
if (padding < 0) {
|
||||
throw new IllegalArgumentException("padding must be non-negative");
|
||||
}
|
||||
if (maxCells < 1) {
|
||||
throw new IllegalArgumentException("maxCells must be positive");
|
||||
}
|
||||
if (pieces == null || pieces.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SourceCollection sources = collectSources(pieces);
|
||||
if (sources == null || sources.columns().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Bounds bounds = paddedBounds(sources.bounds(), padding, maxCells);
|
||||
if (bounds == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int[] nearestSourceIds = new int[bounds.cellCount()];
|
||||
Arrays.fill(nearestSourceIds, -1);
|
||||
int sourceCount = sources.columns().size();
|
||||
int[] sourceX = new int[sourceCount];
|
||||
int[] sourceZ = new int[sourceCount];
|
||||
int[] sourceMinY = new int[sourceCount];
|
||||
int[] sourceMaxY = new int[sourceCount];
|
||||
int sourceId = 0;
|
||||
for (Long2LongMap.Entry entry : sources.columns().long2LongEntrySet()) {
|
||||
int localX = unpackX(entry.getLongKey()) - bounds.minX();
|
||||
int localZ = unpackZ(entry.getLongKey()) - bounds.minZ();
|
||||
long heights = entry.getLongValue();
|
||||
sourceX[sourceId] = localX;
|
||||
sourceZ[sourceId] = localZ;
|
||||
sourceMinY[sourceId] = unpackMinY(heights);
|
||||
sourceMaxY[sourceId] = unpackMaxY(heights);
|
||||
nearestSourceIds[localZ * bounds.width() + localX] = sourceId;
|
||||
sourceId++;
|
||||
}
|
||||
|
||||
transformRows(nearestSourceIds, sourceX, sourceZ, bounds.width(), bounds.depth());
|
||||
long[] distanceSquared = transformColumns(
|
||||
nearestSourceIds, sourceX, sourceZ, bounds.width(), bounds.depth());
|
||||
FootprintData data = new FootprintData(
|
||||
bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(),
|
||||
bounds.width(), bounds.depth(), distanceSquared, nearestSourceIds,
|
||||
sourceMinY, sourceMaxY);
|
||||
return new StructureCarvingFootprint(data);
|
||||
}
|
||||
|
||||
int minX() {
|
||||
return minX;
|
||||
}
|
||||
|
||||
int maxX() {
|
||||
return maxX;
|
||||
}
|
||||
|
||||
int minZ() {
|
||||
return minZ;
|
||||
}
|
||||
|
||||
int maxZ() {
|
||||
return maxZ;
|
||||
}
|
||||
|
||||
int width() {
|
||||
return width;
|
||||
}
|
||||
|
||||
int depth() {
|
||||
return depth;
|
||||
}
|
||||
|
||||
boolean contains(int worldX, int worldZ) {
|
||||
return worldX >= minX && worldX <= maxX && worldZ >= minZ && worldZ <= maxZ;
|
||||
}
|
||||
|
||||
int indexAt(int worldX, int worldZ) {
|
||||
if (!contains(worldX, worldZ)) {
|
||||
return -1;
|
||||
}
|
||||
return (worldZ - minZ) * width + worldX - minX;
|
||||
}
|
||||
|
||||
long distanceSquaredAt(int worldX, int worldZ) {
|
||||
return distanceSquaredAt(requireIndex(worldX, worldZ));
|
||||
}
|
||||
|
||||
long distanceSquaredAt(int index) {
|
||||
return distanceSquared[index];
|
||||
}
|
||||
|
||||
int sourceMinYAt(int worldX, int worldZ) {
|
||||
return sourceMinYAt(requireIndex(worldX, worldZ));
|
||||
}
|
||||
|
||||
int sourceMinYAt(int index) {
|
||||
return sourceMinY[nearestSourceIds[index]];
|
||||
}
|
||||
|
||||
int sourceMaxYAt(int worldX, int worldZ) {
|
||||
return sourceMaxYAt(requireIndex(worldX, worldZ));
|
||||
}
|
||||
|
||||
int sourceMaxYAt(int index) {
|
||||
return sourceMaxY[nearestSourceIds[index]];
|
||||
}
|
||||
|
||||
Column sample(int worldX, int worldZ) {
|
||||
int index = indexAt(worldX, worldZ);
|
||||
if (index < 0) {
|
||||
return null;
|
||||
}
|
||||
return new Column(distanceSquaredAt(index), sourceMinYAt(index), sourceMaxYAt(index));
|
||||
}
|
||||
|
||||
private static SourceCollection collectSources(KList<PlacedStructurePiece> pieces) {
|
||||
Long2LongOpenHashMap columns = new Long2LongOpenHashMap();
|
||||
MutableBounds bounds = new MutableBounds();
|
||||
for (PlacedStructurePiece piece : pieces) {
|
||||
if (piece == null) {
|
||||
continue;
|
||||
}
|
||||
IrisObject object = piece.getObject();
|
||||
IrisObjectRotation rotation = piece.getRotation();
|
||||
if (object == null || rotation == null || object.getBlocks() == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBlockVector local : object.getBlocks().keys()) {
|
||||
if (local == null || B.isAir(object.getBlocks().get(local))) {
|
||||
continue;
|
||||
}
|
||||
IrisBlockVector rotated = rotation.rotate(local.clone());
|
||||
long worldX = (long) piece.getX() + rotated.getBlockX();
|
||||
long worldY = (long) piece.getY() + rotated.getBlockY();
|
||||
long worldZ = (long) piece.getZ() + rotated.getBlockZ();
|
||||
if (!fitsInteger(worldX) || !fitsInteger(worldY) || !fitsInteger(worldZ)) {
|
||||
return null;
|
||||
}
|
||||
mergeColumn(columns, (int) worldX, (int) worldY, (int) worldZ);
|
||||
bounds.include((int) worldX, (int) worldZ);
|
||||
}
|
||||
}
|
||||
return columns.isEmpty() ? null : new SourceCollection(columns, bounds.freeze());
|
||||
}
|
||||
|
||||
private static Bounds paddedBounds(MutableBoundsSnapshot sourceBounds, int padding, int maxCells) {
|
||||
long paddedMinX = (long) sourceBounds.minX() - padding;
|
||||
long paddedMaxX = (long) sourceBounds.maxX() + padding;
|
||||
long paddedMinZ = (long) sourceBounds.minZ() - padding;
|
||||
long paddedMaxZ = (long) sourceBounds.maxZ() + padding;
|
||||
if (!fitsInteger(paddedMinX) || !fitsInteger(paddedMaxX)
|
||||
|| !fitsInteger(paddedMinZ) || !fitsInteger(paddedMaxZ)) {
|
||||
return null;
|
||||
}
|
||||
long width = paddedMaxX - paddedMinX + 1L;
|
||||
long depth = paddedMaxZ - paddedMinZ + 1L;
|
||||
if (width < 1L || depth < 1L || width > Integer.MAX_VALUE || depth > Integer.MAX_VALUE
|
||||
|| width > maxCells / depth) {
|
||||
return null;
|
||||
}
|
||||
int cellCount = (int) (width * depth);
|
||||
return new Bounds(
|
||||
(int) paddedMinX, (int) paddedMaxX, (int) paddedMinZ, (int) paddedMaxZ,
|
||||
(int) width, (int) depth, cellCount);
|
||||
}
|
||||
|
||||
private static void mergeColumn(Long2LongOpenHashMap columns, int x, int y, int z) {
|
||||
long key = pack(x, z);
|
||||
if (!columns.containsKey(key)) {
|
||||
columns.put(key, packHeights(y, y));
|
||||
return;
|
||||
}
|
||||
long existing = columns.get(key);
|
||||
columns.put(key, packHeights(
|
||||
Math.min(y, unpackMinY(existing)), Math.max(y, unpackMaxY(existing))));
|
||||
}
|
||||
|
||||
private static void transformRows(int[] nearestSourceIds, int[] sourceX, int[] sourceZ,
|
||||
int width, int depth) {
|
||||
for (int z = 0; z < depth; z++) {
|
||||
int rowStart = z * width;
|
||||
int previousSource = -1;
|
||||
for (int x = 0; x < width; x++) {
|
||||
int index = rowStart + x;
|
||||
int sourceId = nearestSourceIds[index];
|
||||
if (isExplicitSource(sourceId, x, z, sourceX, sourceZ)) {
|
||||
previousSource = sourceId;
|
||||
} else {
|
||||
nearestSourceIds[index] = previousSource;
|
||||
}
|
||||
}
|
||||
int nextSource = -1;
|
||||
for (int x = width - 1; x >= 0; x--) {
|
||||
int index = rowStart + x;
|
||||
int currentSource = nearestSourceIds[index];
|
||||
if (isExplicitSource(currentSource, x, z, sourceX, sourceZ)) {
|
||||
nextSource = currentSource;
|
||||
}
|
||||
if (isBetterHorizontalSource(nextSource, currentSource, x, sourceX)) {
|
||||
nearestSourceIds[index] = nextSource;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long[] transformColumns(int[] nearestSourceIds, int[] sourceX, int[] sourceZ,
|
||||
int width, int depth) {
|
||||
long[] distanceSquared = new long[nearestSourceIds.length];
|
||||
int[] siteRows = new int[depth];
|
||||
int[] siteSourceIds = new int[depth];
|
||||
int[] starts = new int[depth];
|
||||
long[] siteCosts = new long[depth];
|
||||
for (int x = 0; x < width; x++) {
|
||||
int siteCount = buildEnvelope(
|
||||
nearestSourceIds, sourceX, width, depth, x,
|
||||
siteRows, siteSourceIds, starts, siteCosts);
|
||||
int siteIndex = 0;
|
||||
for (int z = 0; z < depth; z++) {
|
||||
while (siteIndex + 1 < siteCount && starts[siteIndex + 1] <= z) {
|
||||
siteIndex++;
|
||||
}
|
||||
int sourceId = siteSourceIds[siteIndex];
|
||||
int index = z * width + x;
|
||||
long deltaX = (long) x - sourceX[sourceId];
|
||||
long deltaZ = (long) z - sourceZ[sourceId];
|
||||
nearestSourceIds[index] = sourceId;
|
||||
distanceSquared[index] = deltaX * deltaX + deltaZ * deltaZ;
|
||||
}
|
||||
}
|
||||
return distanceSquared;
|
||||
}
|
||||
|
||||
private static int buildEnvelope(int[] nearestSourceIds, int[] sourceX, int width, int depth,
|
||||
int x, int[] siteRows, int[] siteSourceIds,
|
||||
int[] starts, long[] siteCosts) {
|
||||
int siteCount = 0;
|
||||
for (int z = 0; z < depth; z++) {
|
||||
int sourceId = nearestSourceIds[z * width + x];
|
||||
if (sourceId < 0) {
|
||||
continue;
|
||||
}
|
||||
long deltaX = (long) x - sourceX[sourceId];
|
||||
long cost = deltaX * deltaX;
|
||||
int start = Integer.MIN_VALUE;
|
||||
while (siteCount > 0) {
|
||||
start = firstStrictlyBetterAt(
|
||||
siteRows[siteCount - 1], siteCosts[siteCount - 1], z, cost);
|
||||
if (start > starts[siteCount - 1]) {
|
||||
break;
|
||||
}
|
||||
siteCount--;
|
||||
}
|
||||
if (siteCount == 0) {
|
||||
start = Integer.MIN_VALUE;
|
||||
}
|
||||
siteRows[siteCount] = z;
|
||||
siteSourceIds[siteCount] = sourceId;
|
||||
siteCosts[siteCount] = cost;
|
||||
starts[siteCount] = start;
|
||||
siteCount++;
|
||||
}
|
||||
return siteCount;
|
||||
}
|
||||
|
||||
private static int firstStrictlyBetterAt(int existingRow, long existingCost,
|
||||
int candidateRow, long candidateCost) {
|
||||
long numerator = candidateCost + (long) candidateRow * candidateRow
|
||||
- existingCost - (long) existingRow * existingRow;
|
||||
long denominator = 2L * (candidateRow - existingRow);
|
||||
long start = Math.floorDiv(numerator, denominator) + 1L;
|
||||
if (start < Integer.MIN_VALUE) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
if (start > Integer.MAX_VALUE) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
return (int) start;
|
||||
}
|
||||
|
||||
private static boolean isExplicitSource(int sourceId, int x, int z,
|
||||
int[] sourceX, int[] sourceZ) {
|
||||
return sourceId >= 0 && sourceX[sourceId] == x && sourceZ[sourceId] == z;
|
||||
}
|
||||
|
||||
private static boolean isBetterHorizontalSource(int candidateId, int currentId,
|
||||
int x, int[] sourceX) {
|
||||
if (candidateId < 0) {
|
||||
return false;
|
||||
}
|
||||
if (currentId < 0) {
|
||||
return true;
|
||||
}
|
||||
long candidateDelta = (long) x - sourceX[candidateId];
|
||||
long currentDelta = (long) x - sourceX[currentId];
|
||||
long candidateDistance = candidateDelta * candidateDelta;
|
||||
long currentDistance = currentDelta * currentDelta;
|
||||
return candidateDistance < currentDistance
|
||||
|| candidateDistance == currentDistance && sourceX[candidateId] < sourceX[currentId];
|
||||
}
|
||||
|
||||
private int requireIndex(int worldX, int worldZ) {
|
||||
int index = indexAt(worldX, worldZ);
|
||||
if (index < 0) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"coordinate outside carving footprint: " + worldX + "," + worldZ);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
private static boolean fitsInteger(long value) {
|
||||
return value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
private static long pack(int x, int z) {
|
||||
return (long) x << 32 | z & 0xffffffffL;
|
||||
}
|
||||
|
||||
private static int unpackX(long packed) {
|
||||
return (int) (packed >> 32);
|
||||
}
|
||||
|
||||
private static int unpackZ(long packed) {
|
||||
return (int) packed;
|
||||
}
|
||||
|
||||
private static long packHeights(int minY, int maxY) {
|
||||
return (long) minY << 32 | maxY & 0xffffffffL;
|
||||
}
|
||||
|
||||
private static int unpackMinY(long packed) {
|
||||
return (int) (packed >> 32);
|
||||
}
|
||||
|
||||
private static int unpackMaxY(long packed) {
|
||||
return (int) packed;
|
||||
}
|
||||
|
||||
record Column(long distanceSquared, int sourceMinY, int sourceMaxY) {
|
||||
}
|
||||
|
||||
private record Bounds(int minX, int maxX, int minZ, int maxZ,
|
||||
int width, int depth, int cellCount) {
|
||||
}
|
||||
|
||||
private record FootprintData(int minX, int maxX, int minZ, int maxZ,
|
||||
int width, int depth, long[] distanceSquared,
|
||||
int[] nearestSourceIds, int[] sourceMinY, int[] sourceMaxY) {
|
||||
}
|
||||
|
||||
private record SourceCollection(Long2LongOpenHashMap columns, MutableBoundsSnapshot bounds) {
|
||||
}
|
||||
|
||||
private record MutableBoundsSnapshot(int minX, int maxX, int minZ, int maxZ) {
|
||||
}
|
||||
|
||||
private static final class MutableBounds {
|
||||
private int minX = Integer.MAX_VALUE;
|
||||
private int maxX = Integer.MIN_VALUE;
|
||||
private int minZ = Integer.MAX_VALUE;
|
||||
private int maxZ = Integer.MIN_VALUE;
|
||||
|
||||
private void include(int x, int z) {
|
||||
minX = Math.min(minX, x);
|
||||
maxX = Math.max(maxX, x);
|
||||
minZ = Math.min(minZ, z);
|
||||
maxZ = Math.max(maxZ, z);
|
||||
}
|
||||
|
||||
private MutableBoundsSnapshot freeze() {
|
||||
return new MutableBoundsSnapshot(minX, maxX, minZ, maxZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Controls the shape of the expanded terrain carve around an overbored structure.")
|
||||
public enum IrisStructureCarveShape {
|
||||
@Desc("Carves the full expanded rectangular volume with straight walls, ceiling, and floor.")
|
||||
BOX,
|
||||
|
||||
@Desc("Carves a smooth rounded volume around the structure without erosion noise.")
|
||||
ROUNDED,
|
||||
|
||||
@Desc("Carves a rounded volume whose outer boundary is eroded by deterministic noise.")
|
||||
ERODED;
|
||||
|
||||
public double maximumCeilingScale() {
|
||||
return maximumCeilingScale(1D);
|
||||
}
|
||||
|
||||
public double maximumCeilingScale(double erosionStrength) {
|
||||
double strength = Double.isFinite(erosionStrength)
|
||||
? Math.max(0D, Math.min(1D, erosionStrength)) : 1D;
|
||||
return switch (this) {
|
||||
case BOX, ROUNDED -> 1D;
|
||||
case ERODED -> 1D + 0.8D * strength;
|
||||
};
|
||||
}
|
||||
|
||||
public int maximumCeilingExtension(int configuredHeight) {
|
||||
return maximumCeilingExtension(configuredHeight, 1D);
|
||||
}
|
||||
|
||||
public int maximumCeilingExtension(int configuredHeight, double erosionStrength) {
|
||||
return (int) Math.ceil(Math.max(0, configuredHeight) * maximumCeilingScale(erosionStrength));
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,11 @@ import lombok.experimental.Accessors;
|
||||
@Desc("Attaches structures to a biome, region or dimension and controls where and how often they generate. This is independent of a structure's own native generation, so you can add custom placements in tandem with native generation or instead of it.")
|
||||
@Data
|
||||
public class IrisStructurePlacement {
|
||||
private static final double DEFAULT_OVERBORE_EROSION_STRENGTH = 0.8D;
|
||||
private static final double DEFAULT_OVERBORE_EROSION_FREQUENCY = 0.07D;
|
||||
private static final double MIN_OVERBORE_EROSION_FREQUENCY = 0.001D;
|
||||
private static final double MAX_OVERBORE_EROSION_FREQUENCY = 1D;
|
||||
|
||||
@ArrayType(type = String.class, min = 1)
|
||||
@RegistryListStructure
|
||||
@Desc("Editable Iris structure resources to place here. Every key must resolve to a structures/*.json resource in this pack. Live vanilla, mod, and datapack registry keys are controlled through importedStructures unless explicitly cloned into Iris resources.")
|
||||
@@ -98,7 +103,7 @@ public class IrisStructurePlacement {
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(128)
|
||||
@Desc("when overbore=true, how many blocks of terrain to carve horizontally outward from the structure's bounding box. Larger values produce a wider open cavern around the structure. Larger values also widen the mantle write window for every chunk near the structure, so increase it deliberately.")
|
||||
@Desc("when overbore=true, how many blocks of terrain to carve horizontally outward. BOX expands the assembled structure bounds; ROUNDED and ERODED expand the transformed non-air block footprint. Larger values produce a wider open cavern and widen the mantle write window for nearby chunks, so increase this deliberately.")
|
||||
private int overboreRadius = 24;
|
||||
|
||||
@MinNumber(0)
|
||||
@@ -111,9 +116,41 @@ public class IrisStructurePlacement {
|
||||
@Desc("when overbore=true, how many blocks of terrain to carve below the structure's floor. 0 keeps the floor solid for support; small values recess the cavern floor around the structure.")
|
||||
private int overboreFloor = 0;
|
||||
|
||||
@Desc("when overbore=true, controls whether the expanded carve has straight, rounded, or noise-eroded boundaries.")
|
||||
private IrisStructureCarveShape overboreShape = IrisStructureCarveShape.ERODED;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("when overboreShape=ERODED, controls how strongly noise cuts into the rounded outer shell. 0 matches ROUNDED; 1 may erode back to the mandatory structure footprint but never remove its required clearance.")
|
||||
private double overboreErosionStrength = DEFAULT_OVERBORE_EROSION_STRENGTH;
|
||||
|
||||
@MinNumber(MIN_OVERBORE_EROSION_FREQUENCY)
|
||||
@MaxNumber(MAX_OVERBORE_EROSION_FREQUENCY)
|
||||
@Desc("when overboreShape=ERODED, controls the spatial frequency of boundary noise. Higher values produce smaller, busier erosion features.")
|
||||
private double overboreErosionFrequency = DEFAULT_OVERBORE_EROSION_FREQUENCY;
|
||||
|
||||
@Desc("Optional foundation columns placed beneath the assembled structure's occupied bottom cells. Columns pass through air and fluids until they reach solid ground, up to maxDepth.")
|
||||
private IrisStructureStiltSettings stilt = null;
|
||||
|
||||
@Desc("If false, this placement is skipped underwater.")
|
||||
private boolean underwater = false;
|
||||
|
||||
public IrisStructureCarveShape resolvedOverboreShape() {
|
||||
return overboreShape == null ? IrisStructureCarveShape.ERODED : overboreShape;
|
||||
}
|
||||
|
||||
public double resolvedOverboreErosionStrength() {
|
||||
if (!Double.isFinite(overboreErosionStrength)) {
|
||||
return DEFAULT_OVERBORE_EROSION_STRENGTH;
|
||||
}
|
||||
return Math.max(0D, Math.min(1D, overboreErosionStrength));
|
||||
}
|
||||
|
||||
public double resolvedOverboreErosionFrequency() {
|
||||
if (!Double.isFinite(overboreErosionFrequency)) {
|
||||
return DEFAULT_OVERBORE_EROSION_FREQUENCY;
|
||||
}
|
||||
return Math.max(MIN_OVERBORE_EROSION_FREQUENCY,
|
||||
Math.min(MAX_OVERBORE_EROSION_FREQUENCY, overboreErosionFrequency));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user