mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-07-22 06:40:45 +00:00
f
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,9 +153,20 @@ public class SchemaBuilderParityTest {
|
||||
.getJSONObject(stilt.getString("$ref").substring("#/definitions/".length()));
|
||||
JSONObject stiltProperties = stiltDefinition.getJSONObject("properties");
|
||||
JSONObject maxDepth = stiltProperties.getJSONObject("maxDepth");
|
||||
JSONObject overboreShape = properties.getJSONObject("overboreShape");
|
||||
JSONObject overboreErosionStrength = properties.getJSONObject("overboreErosionStrength");
|
||||
JSONObject overboreErosionFrequency = properties.getJSONObject("overboreErosionFrequency");
|
||||
String overboreShapeDefinition = overboreShape.getString("$ref")
|
||||
.substring("#/definitions/".length());
|
||||
|
||||
assertTrue(properties.has("structures"));
|
||||
assertTrue(properties.has("distribution"));
|
||||
assertEquals(List.of("BOX", "ROUNDED", "ERODED"), oneOfValues(
|
||||
schema.getJSONObject("definitions"), overboreShapeDefinition));
|
||||
assertEquals(0D, overboreErosionStrength.getDouble("minimum"), 0D);
|
||||
assertEquals(1D, overboreErosionStrength.getDouble("maximum"), 0D);
|
||||
assertEquals(0.001D, overboreErosionFrequency.getDouble("minimum"), 0D);
|
||||
assertEquals(1D, overboreErosionFrequency.getDouble("maximum"), 0D);
|
||||
assertEquals("object", stilt.getString("type"));
|
||||
assertTrue(stiltProperties.has("palette"));
|
||||
assertTrue(stiltProperties.has("supportNonOccluding"));
|
||||
@@ -190,6 +201,15 @@ public class SchemaBuilderParityTest {
|
||||
return values;
|
||||
}
|
||||
|
||||
private static List<String> oneOfValues(JSONObject definitions, String key) {
|
||||
JSONArray array = definitions.getJSONObject(key).getJSONArray("oneOf");
|
||||
List<String> values = new ArrayList<>(array.length());
|
||||
for (int index = 0; index < array.length(); index++) {
|
||||
values.add(array.getJSONObject(index).getString("const"));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
@Desc("Registry dependent model.")
|
||||
public static class RegistryModel {
|
||||
@Desc("Potion field.")
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package art.arcane.iris.core.tools;
|
||||
|
||||
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class IrisCreatorTeleportTest {
|
||||
@Test
|
||||
public void createTeleportTarget_onlySelectsProductionPlayerSender() {
|
||||
Player player = mock(Player.class);
|
||||
VolmitSender playerSender = mock(VolmitSender.class);
|
||||
VolmitSender consoleSender = mock(VolmitSender.class);
|
||||
doReturn(true).when(playerSender).isPlayer();
|
||||
doReturn(player).when(playerSender).player();
|
||||
doReturn(false).when(consoleSender).isPlayer();
|
||||
|
||||
assertSame(player, IrisCreator.createTeleportTarget(playerSender, false, false));
|
||||
assertNull(IrisCreator.createTeleportTarget(consoleSender, false, false));
|
||||
assertNull(IrisCreator.createTeleportTarget(playerSender, true, false));
|
||||
assertNull(IrisCreator.createTeleportTarget(playerSender, false, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void teleportSenderToCreatedWorld_loadsAndResolvesSafeEntryBeforeTeleport() {
|
||||
Player player = mock(Player.class);
|
||||
World world = mock(World.class);
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
WorldRuntimeControlService runtimeControl = mock(WorldRuntimeControlService.class);
|
||||
Location anchor = new Location(world, 32.5D, 80D, -15.5D);
|
||||
Location safeEntry = new Location(world, 32.5D, 94D, -15.5D);
|
||||
doReturn(anchor).when(runtimeControl).resolveEntryAnchor(world);
|
||||
doReturn(CompletableFuture.completedFuture(chunk))
|
||||
.when(runtimeControl).requestChunkAsync(world, 2, -1, true);
|
||||
doReturn(CompletableFuture.completedFuture(safeEntry))
|
||||
.when(runtimeControl).resolveSafeEntry(world, anchor);
|
||||
doReturn(CompletableFuture.completedFuture(true))
|
||||
.when(runtimeControl).teleport(player, safeEntry);
|
||||
|
||||
CompletableFuture<Boolean> result = IrisCreator.teleportSenderToCreatedWorld(player, world, runtimeControl);
|
||||
|
||||
assertTrue(result.join());
|
||||
InOrder order = inOrder(runtimeControl);
|
||||
order.verify(runtimeControl).resolveEntryAnchor(world);
|
||||
order.verify(runtimeControl).requestChunkAsync(world, 2, -1, true);
|
||||
order.verify(runtimeControl).resolveSafeEntry(world, anchor);
|
||||
order.verify(runtimeControl).teleport(player, safeEntry);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void teleportSenderToCreatedWorld_preservesFalseTeleportResult() {
|
||||
Player player = mock(Player.class);
|
||||
World world = mock(World.class);
|
||||
WorldRuntimeControlService runtimeControl = mock(WorldRuntimeControlService.class);
|
||||
Location anchor = new Location(world, 0.5D, 80D, 0.5D);
|
||||
doReturn(anchor).when(runtimeControl).resolveEntryAnchor(world);
|
||||
doReturn(CompletableFuture.completedFuture(mock(Chunk.class)))
|
||||
.when(runtimeControl).requestChunkAsync(world, 0, 0, true);
|
||||
doReturn(CompletableFuture.completedFuture(anchor))
|
||||
.when(runtimeControl).resolveSafeEntry(world, anchor);
|
||||
doReturn(CompletableFuture.completedFuture(false))
|
||||
.when(runtimeControl).teleport(player, anchor);
|
||||
|
||||
CompletableFuture<Boolean> result = IrisCreator.teleportSenderToCreatedWorld(player, world, runtimeControl);
|
||||
|
||||
assertFalse(result.join());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void teleportSenderToCreatedWorld_failsWhenSafeEntryCannotResolve() {
|
||||
Player player = mock(Player.class);
|
||||
World world = mock(World.class);
|
||||
WorldRuntimeControlService runtimeControl = mock(WorldRuntimeControlService.class);
|
||||
Location anchor = new Location(world, 0.5D, 80D, 0.5D);
|
||||
doReturn("irisworld").when(world).getName();
|
||||
doReturn(anchor).when(runtimeControl).resolveEntryAnchor(world);
|
||||
doReturn(CompletableFuture.completedFuture(mock(Chunk.class)))
|
||||
.when(runtimeControl).requestChunkAsync(world, 0, 0, true);
|
||||
doReturn(CompletableFuture.completedFuture(null))
|
||||
.when(runtimeControl).resolveSafeEntry(world, anchor);
|
||||
|
||||
CompletableFuture<Boolean> result = IrisCreator.teleportSenderToCreatedWorld(player, world, runtimeControl);
|
||||
|
||||
assertThrows(CompletionException.class, result::join);
|
||||
}
|
||||
}
|
||||
+35
-1
@@ -30,6 +30,7 @@ import art.arcane.iris.engine.object.IrisObject;
|
||||
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;
|
||||
@@ -469,8 +470,41 @@ public class IrisStructureLocatorContractTest {
|
||||
KList<PlacedStructurePiece> pieces = new KList<>();
|
||||
pieces.add(piece(0, 60, 0, 1, 80, 1));
|
||||
|
||||
assertEquals(Integer.valueOf(-17), IrisStructureLocator.resolveUndergroundBurialShift(
|
||||
assertEquals(Integer.valueOf(-14), IrisStructureLocator.resolveUndergroundBurialShift(
|
||||
engine, pieces, placement, 60, -63, 319));
|
||||
|
||||
placement.setOverboreErosionStrength(0D);
|
||||
assertEquals(Integer.valueOf(-1), IrisStructureLocator.resolveUndergroundBurialShift(
|
||||
engine, pieces, placement, 60, -63, 319));
|
||||
|
||||
placement.setOverboreShape(IrisStructureCarveShape.ROUNDED);
|
||||
assertEquals(Integer.valueOf(-1), IrisStructureLocator.resolveUndergroundBurialShift(
|
||||
engine, pieces, placement, 60, -63, 319));
|
||||
|
||||
placement.setOverboreHeight(0);
|
||||
assertEquals(Integer.valueOf(0), IrisStructureLocator.resolveUndergroundBurialShift(
|
||||
engine, pieces, placement, 60, -63, 319));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroOverboreRadiusDoesNotExpandTheBurialEnvelope() {
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getMinHeight()).thenReturn(-64);
|
||||
when(engine.getHeight(anyInt(), anyInt(), eq(true))).thenReturn(164);
|
||||
IrisStructurePlacement placement = new IrisStructurePlacement()
|
||||
.setUnderground(true)
|
||||
.setMinHeight(-64)
|
||||
.setMaxHeight(100)
|
||||
.setOverbore(true)
|
||||
.setOverboreShape(IrisStructureCarveShape.ROUNDED)
|
||||
.setOverboreRadius(0)
|
||||
.setOverboreHeight(0);
|
||||
KList<PlacedStructurePiece> pieces = new KList<>();
|
||||
pieces.add(piece(4, 60, 7, 4, 80, 7));
|
||||
|
||||
assertEquals(Integer.valueOf(0), IrisStructureLocator.resolveUndergroundBurialShift(
|
||||
engine, pieces, placement, 60, -63, 319));
|
||||
verify(engine, times(1)).getHeight(4, 7, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+37
-27
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisStructureCarveShape;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -8,47 +9,56 @@ import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisStructureComponentOverboreTest {
|
||||
@Test
|
||||
public void exactPieceInteriorAlwaysCarves() {
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(0.0, 0.0));
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(0.0, 1.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lowNoiseRemovesTheFormerFlatShell() {
|
||||
double oneBlockAtRadiusSix = 1.0 / 6.0;
|
||||
public void exactStructureFootprintAlwaysCarves() {
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(
|
||||
oneBlockAtRadiusSix * oneBlockAtRadiusSix, 0.0));
|
||||
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(0.25 * 0.25, 0.0));
|
||||
IrisStructureCarveShape.ERODED, 0D, 0D, 1D));
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(
|
||||
IrisStructureCarveShape.ROUNDED, 0D, 0D, 1D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void maximumNoiseStopsAtConfiguredReach() {
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(1.0, 1.0));
|
||||
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(1.000001, 1.0));
|
||||
public void boxModeKeepsStraightCandidateVolume() {
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(
|
||||
IrisStructureCarveShape.BOX, 100D, 0D, 1D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diagonalCornersUseEuclideanFalloff() {
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(1.0, 1.0));
|
||||
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(0.8 * 0.8 + 0.8 * 0.8, 1.0));
|
||||
public void roundedModeStopsAtConfiguredReach() {
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(
|
||||
IrisStructureCarveShape.ROUNDED, 1D, 0D, 1D));
|
||||
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(
|
||||
IrisStructureCarveShape.ROUNDED, 1.000001D, 1D, 0D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void boundaryNoiseIsClampedAndDeterministic() {
|
||||
assertEquals(0.2, IrisStructureComponent.overboreBoundaryLimit(-10.0), 0.0);
|
||||
assertEquals(1.0, IrisStructureComponent.overboreBoundaryLimit(10.0), 0.0);
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(0.36, 0.5));
|
||||
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(0.360001, 0.5));
|
||||
public void zeroErosionStrengthMatchesRoundedMode() {
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(
|
||||
IrisStructureCarveShape.ERODED, 1D, 0D, 0D));
|
||||
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(
|
||||
IrisStructureCarveShape.ERODED, 1.000001D, 1D, 0D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void candidateExtensionsDoNotScanImpossibleOuterShell() {
|
||||
assertEquals(6, IrisStructureComponent.overboreSideExtension(6));
|
||||
assertEquals(18, IrisStructureComponent.overboreUpExtension(10.0));
|
||||
public void zeroConfiguredCeilingHasNoErodedExtension() {
|
||||
assertEquals(0D, IrisStructureComponent.erodedUpReach(null, 0.16D, 1D, 0, 0, 0), 0D);
|
||||
}
|
||||
|
||||
long oldVolume = (41L + 18L) * (31L + 27L) * (35L + 18L);
|
||||
long newVolume = (41L + 12L) * (31L + 18L) * (35L + 12L);
|
||||
assertTrue(newVolume < oldVolume);
|
||||
@Test
|
||||
public void zeroErosionStrengthKeepsTheRoundedCeiling() {
|
||||
assertEquals(10D, IrisStructureComponent.erodedUpReach(null, 0.16D, 0D, 0, 0, 10), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void erosionStrengthAndNoiseAreClampedDeterministically() {
|
||||
assertEquals(0.2D, IrisStructureComponent.overboreBoundaryLimit(-10D, 0.8D), 1.0E-12D);
|
||||
assertEquals(1D, IrisStructureComponent.overboreBoundaryLimit(10D, 0.8D), 0D);
|
||||
assertEquals(0.5D, IrisStructureComponent.overboreBoundaryLimit(0.5D, 1D), 0D);
|
||||
assertEquals(1D, IrisStructureComponent.overboreBoundaryLimit(0D, -1D), 0D);
|
||||
assertEquals(0D, IrisStructureComponent.overboreBoundaryLimit(0D, 10D), 0D);
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(
|
||||
null, 0.25D, 0.5D, 1D));
|
||||
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(
|
||||
null, 0.250001D, 0.5D, 1D));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
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.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class StructureCarvingFootprintTest {
|
||||
@Test
|
||||
public void preservesIrregularFootprintInsteadOfFillingItsBoundingBox() {
|
||||
PlacedStructurePiece piece = piece(10, 40, 20, IrisObjectRotation.of(0, 0, 0),
|
||||
block(0, 0, 0), block(1, 0, 0), block(2, 0, 0),
|
||||
block(0, 0, 1), block(0, 0, 2));
|
||||
|
||||
StructureCarvingFootprint footprint = StructureCarvingFootprint.from(pieces(piece), 0);
|
||||
|
||||
assertNotNull(footprint);
|
||||
assertEquals(10, footprint.minX());
|
||||
assertEquals(12, footprint.maxX());
|
||||
assertEquals(20, footprint.minZ());
|
||||
assertEquals(22, footprint.maxZ());
|
||||
assertEquals(3, footprint.width());
|
||||
assertEquals(3, footprint.depth());
|
||||
assertEquals(0L, footprint.distanceSquaredAt(12, 20));
|
||||
assertEquals(4L, footprint.distanceSquaredAt(12, 22));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportsExactDistanceAndNearestColumnHeights() {
|
||||
PlacedStructurePiece piece = piece(0, 0, 0, IrisObjectRotation.of(0, 0, 0),
|
||||
block(0, 10, 0), block(0, 12, 0),
|
||||
block(4, 30, 0), block(4, 35, 0));
|
||||
|
||||
StructureCarvingFootprint footprint = StructureCarvingFootprint.from(pieces(piece), 2);
|
||||
StructureCarvingFootprint.Column nearestRight = footprint.sample(3, 2);
|
||||
StructureCarvingFootprint.Column tied = footprint.sample(2, 0);
|
||||
|
||||
assertNotNull(nearestRight);
|
||||
assertEquals(5L, nearestRight.distanceSquared());
|
||||
assertEquals(30, nearestRight.sourceMinY());
|
||||
assertEquals(35, nearestRight.sourceMaxY());
|
||||
assertNotNull(tied);
|
||||
assertEquals(4L, tied.distanceSquared());
|
||||
assertEquals(10, tied.sourceMinY());
|
||||
assertEquals(12, tied.sourceMaxY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rotatesAndMergesOverlappingColumns() {
|
||||
PlacedStructurePiece rotated = piece(10, 20, 30, IrisObjectRotation.of(0, 90, 0),
|
||||
block(2, 1, 0));
|
||||
PlacedStructurePiece overlapping = piece(10, 5, 28, IrisObjectRotation.of(0, 0, 0),
|
||||
block(0, 3, 0), block(0, 9, 0));
|
||||
|
||||
StructureCarvingFootprint footprint = StructureCarvingFootprint.from(
|
||||
pieces(rotated, overlapping), 0);
|
||||
|
||||
assertNotNull(footprint);
|
||||
assertEquals(10, footprint.minX());
|
||||
assertEquals(10, footprint.maxX());
|
||||
assertEquals(28, footprint.minZ());
|
||||
assertEquals(28, footprint.maxZ());
|
||||
assertEquals(0L, footprint.distanceSquaredAt(10, 28));
|
||||
assertEquals(8, footprint.sourceMinYAt(10, 28));
|
||||
assertEquals(21, footprint.sourceMaxYAt(10, 28));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesBruteForceSquaredDistancesAcrossSparseColumns() {
|
||||
int[][] sourceBlocks = new int[][]{
|
||||
block(0, 2, 0), block(3, 4, 1), block(1, 6, 4), block(5, 8, 5)
|
||||
};
|
||||
PlacedStructurePiece piece = piece(
|
||||
-8, 30, 11, IrisObjectRotation.of(0, 0, 0), sourceBlocks);
|
||||
|
||||
StructureCarvingFootprint footprint = StructureCarvingFootprint.from(pieces(piece), 3);
|
||||
|
||||
assertNotNull(footprint);
|
||||
for (int z = footprint.minZ(); z <= footprint.maxZ(); z++) {
|
||||
for (int x = footprint.minX(); x <= footprint.maxX(); x++) {
|
||||
long expected = Long.MAX_VALUE;
|
||||
for (int[] source : sourceBlocks) {
|
||||
long deltaX = (long) x - (piece.getX() + source[0]);
|
||||
long deltaZ = (long) z - (piece.getZ() + source[2]);
|
||||
expected = Math.min(expected, deltaX * deltaX + deltaZ * deltaZ);
|
||||
}
|
||||
assertEquals(expected, footprint.distanceSquaredAt(x, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsNullForEmptyInvalidOrOversizedInputs() {
|
||||
assertNull(StructureCarvingFootprint.from(null, 0));
|
||||
assertNull(StructureCarvingFootprint.from(new KList<>(), 0));
|
||||
|
||||
KList<PlacedStructurePiece> invalidPieces = new KList<>();
|
||||
invalidPieces.add((PlacedStructurePiece) null);
|
||||
invalidPieces.add(new PlacedStructurePiece(
|
||||
null, null, 0, 0, 0, null, 0, 0, 0, 0, 0, 0));
|
||||
assertNull(StructureCarvingFootprint.from(invalidPieces, 0));
|
||||
|
||||
PlacedStructurePiece piece = piece(0, 0, 0, IrisObjectRotation.of(0, 0, 0), block(0, 0, 0));
|
||||
assertNull(StructureCarvingFootprint.from(pieces(piece), 2, 24));
|
||||
PlacedStructurePiece overflowing = piece(
|
||||
Integer.MAX_VALUE, 0, 0, IrisObjectRotation.of(0, 0, 0), block(1, 0, 0));
|
||||
assertNull(StructureCarvingFootprint.from(pieces(overflowing), 0));
|
||||
|
||||
try {
|
||||
StructureCarvingFootprint.from(pieces(piece), -1);
|
||||
fail("Expected negative padding to fail");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertEquals("padding must be non-negative", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void includesTheEntirePaddingBoundary() {
|
||||
PlacedStructurePiece piece = piece(7, 14, -4, IrisObjectRotation.of(0, 0, 0), block(0, 0, 0));
|
||||
|
||||
StructureCarvingFootprint footprint = StructureCarvingFootprint.from(pieces(piece), 2, 25);
|
||||
|
||||
assertNotNull(footprint);
|
||||
assertEquals(5, footprint.minX());
|
||||
assertEquals(9, footprint.maxX());
|
||||
assertEquals(-6, footprint.minZ());
|
||||
assertEquals(-2, footprint.maxZ());
|
||||
assertEquals(5, footprint.width());
|
||||
assertEquals(5, footprint.depth());
|
||||
assertTrue(footprint.contains(5, -6));
|
||||
assertTrue(footprint.contains(9, -2));
|
||||
assertFalse(footprint.contains(4, -6));
|
||||
assertEquals(8L, footprint.distanceSquaredAt(5, -6));
|
||||
assertNull(footprint.sample(4, -6));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitAirDoesNotExpandTheExteriorFootprint() {
|
||||
IrisObject object = new IrisObject(1, 1, 1);
|
||||
PlatformBlockState solid = mock(PlatformBlockState.class);
|
||||
PlatformBlockState air = mock(PlatformBlockState.class);
|
||||
when(air.isAir()).thenReturn(true);
|
||||
object.getBlocks().put(new IrisBlockVector(0, 0, 0), solid);
|
||||
object.getBlocks().put(new IrisBlockVector(8, 4, 8), air);
|
||||
PlacedStructurePiece piece = new PlacedStructurePiece(
|
||||
null, object, 10, 20, 30, IrisObjectRotation.of(0, 0, 0),
|
||||
10, 20, 30, 18, 24, 38);
|
||||
|
||||
StructureCarvingFootprint footprint = StructureCarvingFootprint.from(pieces(piece), 0);
|
||||
|
||||
assertNotNull(footprint);
|
||||
assertEquals(10, footprint.minX());
|
||||
assertEquals(10, footprint.maxX());
|
||||
assertEquals(30, footprint.minZ());
|
||||
assertEquals(30, footprint.maxZ());
|
||||
}
|
||||
|
||||
private static PlacedStructurePiece piece(int x, int y, int z, IrisObjectRotation rotation,
|
||||
int[]... blocks) {
|
||||
IrisObject object = new IrisObject(1, 1, 1);
|
||||
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||
for (int[] block : blocks) {
|
||||
object.getBlocks().put(new IrisBlockVector(block[0], block[1], block[2]), state);
|
||||
}
|
||||
return new PlacedStructurePiece(
|
||||
null, object, x, y, z, rotation, x, y, z, x, y, z);
|
||||
}
|
||||
|
||||
private static KList<PlacedStructurePiece> pieces(PlacedStructurePiece... pieces) {
|
||||
KList<PlacedStructurePiece> result = new KList<>();
|
||||
for (PlacedStructurePiece piece : pieces) {
|
||||
result.add(piece);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int[] block(int x, int y, int z) {
|
||||
return new int[]{x, y, z};
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
public class IrisStructurePlacementCarveSettingsTest {
|
||||
@Test
|
||||
public void defaultsSelectErodedOverbore() {
|
||||
IrisStructurePlacement placement = new IrisStructurePlacement();
|
||||
|
||||
assertEquals(IrisStructureCarveShape.ERODED, placement.getOverboreShape());
|
||||
assertEquals(IrisStructureCarveShape.ERODED, placement.resolvedOverboreShape());
|
||||
assertEquals(0.8D, placement.getOverboreErosionStrength(), 0D);
|
||||
assertEquals(0.8D, placement.resolvedOverboreErosionStrength(), 0D);
|
||||
assertEquals(0.07D, placement.getOverboreErosionFrequency(), 0D);
|
||||
assertEquals(0.07D, placement.resolvedOverboreErosionFrequency(), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void carveShapesExposeTheirMaximumCeilingScale() {
|
||||
assertEquals(1D, IrisStructureCarveShape.BOX.maximumCeilingScale(), 0D);
|
||||
assertEquals(1D, IrisStructureCarveShape.ROUNDED.maximumCeilingScale(), 0D);
|
||||
assertEquals(1.8D, IrisStructureCarveShape.ERODED.maximumCeilingScale(), 0D);
|
||||
assertEquals(10, IrisStructureCarveShape.BOX.maximumCeilingExtension(10));
|
||||
assertEquals(10, IrisStructureCarveShape.ROUNDED.maximumCeilingExtension(10));
|
||||
assertEquals(18, IrisStructureCarveShape.ERODED.maximumCeilingExtension(10));
|
||||
assertEquals(10, IrisStructureCarveShape.ERODED.maximumCeilingExtension(10, 0D));
|
||||
assertEquals(14, IrisStructureCarveShape.ERODED.maximumCeilingExtension(10, 0.5D));
|
||||
assertEquals(0, IrisStructureCarveShape.ERODED.maximumCeilingExtension(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullAndNonFiniteValuesResolveToDefaults() {
|
||||
IrisStructurePlacement placement = new IrisStructurePlacement()
|
||||
.setOverboreShape(null)
|
||||
.setOverboreErosionStrength(Double.NaN)
|
||||
.setOverboreErosionFrequency(Double.POSITIVE_INFINITY);
|
||||
|
||||
assertEquals(IrisStructureCarveShape.ERODED, placement.resolvedOverboreShape());
|
||||
assertEquals(0.8D, placement.resolvedOverboreErosionStrength(), 0D);
|
||||
assertEquals(0.07D, placement.resolvedOverboreErosionFrequency(), 0D);
|
||||
|
||||
placement.setOverboreErosionStrength(Double.NEGATIVE_INFINITY);
|
||||
placement.setOverboreErosionFrequency(Double.NaN);
|
||||
|
||||
assertEquals(0.8D, placement.resolvedOverboreErosionStrength(), 0D);
|
||||
assertEquals(0.07D, placement.resolvedOverboreErosionFrequency(), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void finiteValuesClampToAuthoredBounds() {
|
||||
IrisStructurePlacement placement = new IrisStructurePlacement()
|
||||
.setOverboreErosionStrength(-1D)
|
||||
.setOverboreErosionFrequency(0D);
|
||||
|
||||
assertEquals(0D, placement.resolvedOverboreErosionStrength(), 0D);
|
||||
assertEquals(0.001D, placement.resolvedOverboreErosionFrequency(), 0D);
|
||||
|
||||
placement.setOverboreErosionStrength(2D);
|
||||
placement.setOverboreErosionFrequency(2D);
|
||||
|
||||
assertEquals(1D, placement.resolvedOverboreErosionStrength(), 0D);
|
||||
assertEquals(1D, placement.resolvedOverboreErosionFrequency(), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void numericSchemaBoundsMatchRuntimeBounds() throws NoSuchFieldException {
|
||||
assertBounds("overboreErosionStrength", 0D, 1D);
|
||||
assertBounds("overboreErosionFrequency", 0.001D, 1D);
|
||||
}
|
||||
|
||||
private void assertBounds(String fieldName, double expectedMinimum,
|
||||
double expectedMaximum) throws NoSuchFieldException {
|
||||
Field field = IrisStructurePlacement.class.getDeclaredField(fieldName);
|
||||
MinNumber minimum = field.getAnnotation(MinNumber.class);
|
||||
MaxNumber maximum = field.getAnnotation(MaxNumber.class);
|
||||
|
||||
assertNotNull(minimum);
|
||||
assertNotNull(maximum);
|
||||
assertEquals(expectedMinimum, minimum.value(), 0D);
|
||||
assertEquals(expectedMaximum, maximum.value(), 0D);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user