Merged placement reach

- True object placement reach bounds shrink the carve window from 13 to 8 chunks
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-21 21:47:25 -04:00
commit 29e7e0384d
7 files changed
+1560 -271

No files matched your search

@@ -44,7 +44,6 @@ import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.RNG;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
@@ -598,7 +597,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
@Override
protected int computeRadius() {
int maxObjectExtent = 0;
Map<String, IrisBlockVector> sizeCache = new HashMap<>();
Map<String, PlacementReach.Extent> extents = new HashMap<>();
Set<String> warnedLargeObjects = new HashSet<>();
try {
IrisData data = getData();
@@ -608,15 +607,17 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
continue;
}
for (IrisFloatingChildBiomes entry : entries) {
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getFloatingObjects(), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getExtraObjects(), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getTopObjectOverrides(), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getBottomObjectOverrides(), data, sizeCache, warnedLargeObjects));
PlacementScan scan = new PlacementScan(data, extents, warnedLargeObjects,
entry.hasObjectShrink() ? entry.getShrinkScale().getMaxScale() : 1D);
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getFloatingObjects(), false));
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getExtraObjects(), true));
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getTopObjectOverrides(), true));
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getBottomObjectOverrides(), true));
try {
IrisBiome target = entry.getRealBiome(biome, data);
if (target != null) {
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveTopObjects(target), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveBottomObjects(target), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.resolveTopObjects(target), true));
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.resolveBottomObjects(target), true));
} else {
maxObjectExtent = Math.max(maxObjectExtent, MINIMUM_PLACEMENT_RADIUS);
}
@@ -635,44 +636,66 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
return maxObjectExtent;
}
private int computePlacementRadius(KList<IrisObjectPlacement> placements, IrisData data,
Map<String, IrisBlockVector> sizeCache,
Set<String> warnedLargeObjects) {
if (placements == null) {
return MINIMUM_PLACEMENT_RADIUS;
private final class PlacementScan {
private final IrisData data;
private final Map<String, PlacementReach.Extent> extents;
private final Set<String> warnedLargeObjects;
private final double shrink;
private PlacementScan(IrisData data, Map<String, PlacementReach.Extent> extents, Set<String> warnedLargeObjects,
double shrink) {
this.data = data;
this.extents = extents;
this.warnedLargeObjects = warnedLargeObjects;
this.shrink = shrink;
}
if (placements.isEmpty()) {
return 0;
}
int radius = MINIMUM_PLACEMENT_RADIUS;
for (IrisObjectPlacement placement : placements) {
if (placement == null || placement.getPlace() == null) {
continue;
private int radius(KList<IrisObjectPlacement> placements, boolean anchored) {
if (placements == null) {
return MINIMUM_PLACEMENT_RADIUS;
}
for (String objectKey : placement.getPlace()) {
try {
IrisBlockVector size = sizeCache.get(objectKey);
if (size == null) {
File file = data.getObjectLoader().findFile(objectKey);
if (file == null) {
if (placements.isEmpty()) {
return 0;
}
int radius = MINIMUM_PLACEMENT_RADIUS;
for (IrisObjectPlacement placement : placements) {
if (placement == null || placement.getPlace() == null) {
continue;
}
PlacementReach.Profile profile = PlacementReach.profile(placement);
double scale = placement.getMaximumScale(getDimension());
for (String objectKey : placement.getPlace()) {
try {
PlacementReach.Extent extent = extent(objectKey);
if (extent == null) {
continue;
}
size = IrisObject.sampleSize(file);
sizeCache.put(objectKey, size);
PlacementReach.Extent scaled = extent.scaled(scale).scaled(shrink);
int reach = anchored ? profile.anchored(scaled).horizontal() : profile.of(scaled).horizontal();
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Floating object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
}
radius = Math.max(radius, reach);
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
}
int reach = MantleObjectComponent.calculatePlacementReach(size, placement, placement.getMaximumScale(getDimension()));
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Floating object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
}
radius = Math.max(radius, reach);
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
}
}
return radius;
}
private PlacementReach.Extent extent(String objectKey) {
if (extents.containsKey(objectKey)) {
return extents.get(objectKey);
}
IrisObject object = data.getObjectLoader().load(objectKey);
PlacementReach.Extent extent = object == null ? null : PlacementReach.Extent.of(object);
extents.put(objectKey, extent);
return extent;
}
return radius;
}
private static final class PlacementSequence {
private int ordinal;
@@ -54,7 +54,6 @@ import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
@@ -69,12 +68,12 @@ import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.generation.block.DataProvider;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@@ -94,6 +93,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
private final ObjectSourcePlanCache sourcePlans;
private final AtomicLong lastSourcePlanStatsLog = new AtomicLong();
private volatile int collisionRuleState;
private volatile int inputReach;
public MantleObjectComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.OBJECT, 2);
@@ -134,7 +134,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
@Override
public int getInputRadius() {
return calculateInputRadius(getRadius(), hasCollisionRules());
int radius = getRadius();
return calculateInputRadius(radius, inputReach, hasCollisionRules());
}
@Override
@@ -297,11 +298,11 @@ public class MantleObjectComponent extends IrisMantleComponent {
return inputRadius >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) inputRadius;
}
static int calculateInputRadius(int radius, boolean sourceAnchoredCollisions) {
static int calculateInputRadius(int radius, int inputReach, boolean sourceAnchoredCollisions) {
int normalizedRadius = Math.max(0, radius);
long sourceChunkRadius = sourceChunkRadius(normalizedRadius);
long sourceLegs = sourceAnchoredCollisions ? 2L : 1L;
long inputRadius = (sourceChunkRadius * 16L * sourceLegs) + normalizedRadius + 1L;
long inputRadius = (sourceChunkRadius * 16L * sourceLegs) + Math.max(normalizedRadius, inputReach) + 1L;
return inputRadius >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) inputRadius;
}
@@ -1370,7 +1371,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int id = rng.i(0, Integer.MAX_VALUE);
IrisObjectPlacement placement = objectPlacement.toPlacement(v.getLoadKey());
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
placement.setRotation(buildUpsideDownRotation());
placement.setRotation(upsideDownRotation());
placement.setCarvingSupport(CarvingMode.ANYWHERE);
if (forcePlace) {
placement.setForcePlace(true);
@@ -1408,7 +1409,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
}
private IrisObjectRotation buildUpsideDownRotation() {
private static IrisObjectRotation upsideDownRotation() {
IrisObjectRotation rt = new IrisObjectRotation();
rt.setEnabled(true);
rt.setXAxis(new IrisAxisRotationClamp(true, true, 180D, 180D, 90D));
@@ -1785,135 +1786,13 @@ public class MantleObjectComponent extends IrisMantleComponent {
protected int computeRadius() {
KSet<String> warnedLargeObjects = new KSet<>();
int radius = computeDimensionRadius(getDimension(), this::getData, warnedLargeObjects);
PlacementReach reach = new ReachScan(getDimension(), this::getData, warnedLargeObjects).dimension();
UpperDimensionContext upperContext = activeUpperContext();
if (upperContext != null) {
radius = Math.max(radius, computeDimensionRadius(
upperContext.getDimension(),
upperContext,
warnedLargeObjects
));
reach = reach.union(new ReachScan(upperContext.getDimension(), upperContext, warnedLargeObjects).upperDimension());
}
return radius;
}
private int computeDimensionRadius(
IrisDimension dimension,
DataProvider dataProvider,
KSet<String> warnedLargeObjects
) {
KMap<String, IrisBlockVector> sizeCache = new KMap<>();
int radius = 0;
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
if (region == null) {
continue;
}
radius = Math.max(radius, computePlacementRadius(
region.getObjects(),
dimension,
dataProvider,
sizeCache,
warnedLargeObjects
));
radius = Math.max(radius, computeProceduralRadius(region.getProceduralObjects(), dimension, dataProvider));
}
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
if (biome == null) {
continue;
}
radius = Math.max(radius, computePlacementRadius(
biome.getObjects(),
dimension,
dataProvider,
sizeCache,
warnedLargeObjects
));
radius = Math.max(radius, computeProceduralRadius(biome.getProceduralObjects(), dimension, dataProvider));
}
return radius;
}
private int computePlacementRadius(
KList<IrisObjectPlacement> placements,
IrisDimension dimension,
DataProvider dataProvider,
KMap<String, IrisBlockVector> sizeCache,
KSet<String> warnedLargeObjects
) {
int radius = 0;
for (IrisObjectPlacement placement : placements) {
if (placement == null) {
continue;
}
for (String objectKey : placement.getPlace()) {
try {
IrisBlockVector size = loadObjectSize(dataProvider, sizeCache, objectKey);
if (size == null) {
continue;
}
int reach = calculatePlacementReach(size, placement, placement.getMaximumScale(dimension));
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
}
radius = Math.max(radius, reach);
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
}
}
}
return radius;
}
private int computeProceduralRadius(IrisProceduralObjects procedural, IrisDimension dimension, DataProvider dataProvider) {
if (procedural == null || procedural.isEmpty()) {
return 0;
}
int radius = 0;
for (IrisProceduralPlacement placement : procedural.getAllPlacements()) {
if (placement == null) {
continue;
}
KList<IrisObject> variants = placement.getVariantObjects(dataProvider.getData());
if (variants == null) {
continue;
}
IrisObjectPlacement objectPlacement = placement.asPlacement();
for (IrisObject variant : variants) {
if (variant == null) {
continue;
}
IrisBlockVector size = new IrisBlockVector(variant.getW(), variant.getH(), variant.getD());
radius = Math.max(radius, calculatePlacementReach(size, objectPlacement, objectPlacement.getMaximumScale(dimension)));
}
}
return radius;
}
static int calculatePlacementReach(IrisBlockVector size, IrisObjectPlacement placement, double scale) {
if (size == null) {
return 0;
}
if (placement == null) {
return Math.max(Math.abs(size.getBlockX()), Math.abs(size.getBlockZ()));
}
int width = scaledDimension(size.getBlockX(), scale);
int height = scaledDimension(size.getBlockY(), scale);
int depth = scaledDimension(size.getBlockZ(), scale);
IrisObjectRotation rotation = placement.getRotation();
boolean rotateX = rotation != null && rotation.isEnabled() && rotation.getXAxis() != null && rotation.getXAxis().isEnabled();
boolean rotateZ = rotation != null && rotation.isEnabled() && rotation.getZAxis() != null && rotation.getZAxis().isEnabled();
int footprint = rotateX || rotateZ ? Math.max(width, Math.max(height, depth)) : Math.max(width, depth);
int translation = calculateTranslationReach(placement.getTranslate(), rotation, rotateX || rotateZ);
int warp = placement.getWarp() != null && !placement.getWarp().isFlat()
? (int) Math.ceil(Math.abs(placement.getWarp().getMultiplier()) / 2D)
: 0;
int vacuum = IrisObjectVacuum.isVacuumMode(placement.getMode())
? 2 * IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings())
: 0;
long reach = (long) footprint + translation + warp + vacuum;
return reach > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) reach;
inputReach = reach.input();
return reach.horizontal();
}
static boolean allowsObjectPlacement(
@@ -1926,9 +1805,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (complex == null || object == null) {
return true;
}
int reach = calculatePlacementReach(
new IrisBlockVector(object.getW(), object.getH(), object.getD()),
placement, 1D);
int reach = transitionFootprint(object, placement);
return complex.allowsNewGenerationFootprint(
saturatedOffset(anchorX, -reach),
saturatedOffset(anchorZ, -reach),
@@ -1936,20 +1813,34 @@ public class MantleObjectComponent extends IrisMantleComponent {
saturatedOffset(anchorZ, reach));
}
private static int transitionFootprint(IrisObject object, IrisObjectPlacement placement) {
int width = Math.abs(object.getW());
int height = Math.abs(object.getH());
int depth = Math.abs(object.getD());
if (placement == null) {
return Math.max(width, depth);
}
IrisObjectRotation rotation = placement.getRotation();
boolean rotateX = rotation != null && rotation.isEnabled() && rotation.getXAxis() != null && rotation.getXAxis().isEnabled();
boolean rotateZ = rotation != null && rotation.isEnabled() && rotation.getZAxis() != null && rotation.getZAxis().isEnabled();
int footprint = rotateX || rotateZ ? Math.max(width, Math.max(height, depth)) : Math.max(width, depth);
int translation = transitionTranslation(placement.getTranslate(), rotation, rotateX || rotateZ);
int warp = placement.getWarp() != null && !placement.getWarp().isFlat()
? (int) Math.ceil(Math.abs(placement.getWarp().getMultiplier()) / 2D)
: 0;
int vacuum = IrisObjectVacuum.isVacuumMode(placement.getMode())
? 2 * IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings())
: 0;
long reach = (long) footprint + translation + warp + vacuum;
return reach > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) reach;
}
private static int saturatedOffset(int coordinate, int offset) {
long result = (long) coordinate + offset;
return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, result));
}
private static int scaledDimension(int dimension, double scale) {
int absoluteDimension = Math.abs(dimension);
if (scale == 1D) {
return absoluteDimension;
}
return (int) Math.ceil(absoluteDimension * scale);
}
private static int calculateTranslationReach(IrisObjectTranslate translate, IrisObjectRotation rotation, boolean rotateVertically) {
private static int transitionTranslation(IrisObjectTranslate translate, IrisObjectRotation rotation, boolean rotateVertically) {
if (translate == null || !translate.canTranslate()) {
return 0;
}
@@ -1963,22 +1854,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
return rotateY ? (int) Math.ceil(Math.hypot(x, z)) : (int) Math.max(Math.abs(x), Math.abs(z));
}
private IrisBlockVector loadObjectSize(
DataProvider dataProvider,
KMap<String, IrisBlockVector> sizeCache,
String objectKey
) {
return sizeCache.computeIfAbsent(objectKey, k -> {
try {
return IrisObject.sampleSize(dataProvider.getData().getObjectLoader().findFile(objectKey));
} catch (IOException e) {
IrisLogging.reportError(e);
}
return null;
});
}
private UpperDimensionContext activeUpperContext() {
if (!getDimension().isUpperDimensionObjects()) {
return null;
@@ -2047,6 +1922,187 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
}
private static final class ReachScan {
private final IrisDimension dimension;
private final DataProvider dataProvider;
private final KSet<String> warnedLargeObjects;
private final Map<String, PlacementReach.Extent> extents;
private final Set<ObjectPlaceMode> caveVacuumModes;
private ReachScan(IrisDimension dimension, DataProvider dataProvider, KSet<String> warnedLargeObjects) {
this.dimension = dimension;
this.dataProvider = dataProvider;
this.warnedLargeObjects = warnedLargeObjects;
this.extents = new HashMap<>();
this.caveVacuumModes = caveVacuumModes(dimension, dataProvider);
}
private PlacementReach dimension() {
PlacementReach reach = PlacementReach.NONE;
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
if (region != null) {
reach = reach.union(placements(region.getObjects())).union(procedural(region.getProceduralObjects()));
}
}
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
if (biome != null) {
reach = reach.union(placements(biome.getObjects())).union(procedural(biome.getProceduralObjects()));
}
}
return reach.withReadMargin(PlacementReach.supportReadMargin(dimension.getObjectSurfaceSupportBuffer()));
}
private PlacementReach upperDimension() {
PlacementReach reach = PlacementReach.NONE;
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
if (region != null) {
reach = reach.union(upperPlacements(region.getSurfaceObjects()));
}
}
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
if (biome != null) {
reach = reach.union(upperPlacements(biome.getSurfaceObjects()));
}
}
return reach;
}
private PlacementReach placements(KList<IrisObjectPlacement> placements) {
PlacementReach reach = PlacementReach.NONE;
if (placements == null) {
return reach;
}
for (IrisObjectPlacement placement : placements) {
if (placement != null) {
reach = reach.union(objects(placement, profiles(placement)));
}
}
return reach;
}
private PlacementReach upperPlacements(KList<IrisObjectPlacement> placements) {
PlacementReach reach = PlacementReach.NONE;
if (placements == null) {
return reach;
}
for (IrisObjectPlacement placement : placements) {
if (placement == null) {
continue;
}
IrisObjectPlacement upper = placement.toPlacement(placement.getPlace().toArray(new String[0]));
upper.setMode(ObjectPlaceMode.CENTER_HEIGHT);
upper.setRotation(upsideDownRotation());
upper.setCarvingSupport(CarvingMode.ANYWHERE);
reach = reach.union(objects(placement, new KList<>(PlacementReach.profile(upper))));
}
return reach;
}
private PlacementReach objects(IrisObjectPlacement placement, KList<PlacementReach.Profile> profiles) {
PlacementReach reach = PlacementReach.NONE;
double scale = placement.getMaximumScale(dimension);
for (String objectKey : placement.getPlace()) {
try {
PlacementReach.Extent extent = extent(objectKey);
if (extent == null) {
continue;
}
PlacementReach.Extent scaled = extent.scaled(scale);
for (PlacementReach.Profile profile : profiles) {
PlacementReach objectReach = profile.of(scaled);
if (objectReach.horizontal() > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Object " + objectKey + " has a large placement reach (" + objectReach.horizontal() + " blocks) and may increase memory usage!");
}
reach = reach.union(objectReach);
}
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
}
}
return reach;
}
private PlacementReach procedural(IrisProceduralObjects procedural) {
PlacementReach reach = PlacementReach.NONE;
if (procedural == null || procedural.isEmpty()) {
return reach;
}
for (IrisProceduralPlacement placement : procedural.getAllPlacements()) {
if (placement == null) {
continue;
}
KList<IrisObject> variants = placement.getVariantObjects(dataProvider.getData());
if (variants == null) {
continue;
}
IrisObjectPlacement objectPlacement = placement.asPlacement();
KList<PlacementReach.Profile> profiles = profiles(objectPlacement);
double scale = objectPlacement.getMaximumScale(dimension);
for (IrisObject variant : variants) {
if (variant == null) {
continue;
}
PlacementReach.Extent extent = PlacementReach.Extent.of(variant).scaled(scale);
for (PlacementReach.Profile profile : profiles) {
reach = reach.union(profile.of(extent));
}
}
}
return reach;
}
private KList<PlacementReach.Profile> profiles(IrisObjectPlacement placement) {
KList<PlacementReach.Profile> profiles = new KList<>(PlacementReach.profile(placement));
if (placement.getMode() != ObjectPlaceMode.CENTER_HEIGHT
|| placement.getCarvingSupport() == CarvingMode.SURFACE_ONLY) {
return profiles;
}
for (ObjectPlaceMode mode : caveVacuumModes) {
IrisObjectPlacement cave = placement.toPlacement(placement.getPlace().toArray(new String[0]));
cave.setMode(mode);
profiles.add(PlacementReach.profile(cave));
}
return profiles;
}
private PlacementReach.Extent extent(String objectKey) {
if (extents.containsKey(objectKey)) {
return extents.get(objectKey);
}
IrisObject object = dataProvider.getData().getObjectLoader().load(objectKey);
PlacementReach.Extent extent = object == null ? null : PlacementReach.Extent.of(object);
extents.put(objectKey, extent);
return extent;
}
private static Set<ObjectPlaceMode> caveVacuumModes(IrisDimension dimension, DataProvider dataProvider) {
Set<ObjectPlaceMode> modes = EnumSet.noneOf(ObjectPlaceMode.class);
addCaveVacuumMode(modes, dimension.getCaveProfile());
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
if (region != null) {
addCaveVacuumMode(modes, region.getCaveProfile());
}
}
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
if (biome != null) {
addCaveVacuumMode(modes, biome.getCaveProfile());
}
}
return modes;
}
private static void addCaveVacuumMode(Set<ObjectPlaceMode> modes, IrisCaveProfile profile) {
if (profile == null) {
return;
}
ObjectPlaceMode mode = profile.getDefaultObjectPlaceMode();
if (mode != null && IrisObjectVacuum.isVacuumMode(mode)) {
modes.add(mode);
}
}
}
@FunctionalInterface
interface SourceChunkConsumer {
void accept(int chunkX, int chunkZ);
@@ -0,0 +1,312 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.block.VectorMap;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.structure.object.IrisObjectTranslate;
import art.arcane.iris.structure.object.IrisObjectVacuum;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.Vector3i;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import java.util.Objects;
public record PlacementReach(int horizontal, int readMargin) {
public static final PlacementReach NONE = new PlacementReach(0, 0);
private static final int SPIN_PERIOD = 360;
private static final int MAXIMUM_ROTATION_KERNELS = 1024;
private static final double ROUNDING_SLACK = 1.0E-9D;
public PlacementReach {
if (horizontal < 0 || readMargin < 0) {
throw new IllegalArgumentException("Placement reach must be non-negative: " + horizontal + "+" + readMargin);
}
}
public static PlacementReach of(IrisObject object, IrisObjectPlacement placement, double scale) {
return of(Extent.of(object).scaled(scale), placement);
}
public static PlacementReach of(Extent extent, IrisObjectPlacement placement) {
return profile(placement).of(extent);
}
public static PlacementReach anchored(Extent extent, IrisObjectPlacement placement) {
return profile(placement).anchored(extent);
}
public static Profile profile(IrisObjectPlacement placement) {
return new Profile(Objects.requireNonNull(placement, "placement"));
}
public static int supportReadMargin(int surfaceSupportBuffer) {
return 1 + Math.max(0, surfaceSupportBuffer);
}
public int input() {
return saturate((long) horizontal + readMargin);
}
public PlacementReach union(PlacementReach other) {
int unionHorizontal = Math.max(horizontal, other.horizontal);
int unionInput = Math.max(input(), other.input());
return new PlacementReach(unionHorizontal, unionInput - unionHorizontal);
}
public PlacementReach withReadMargin(int minimumReadMargin) {
return minimumReadMargin <= readMargin ? this : new PlacementReach(horizontal, minimumReadMargin);
}
private static int roundedBound(double maximumMagnitude) {
return saturate((long) Math.floor(maximumMagnitude + 0.5D + ROUNDING_SLACK));
}
private static int saturate(long value) {
return value >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) Math.max(0L, value);
}
private static IrisBlockVector corner(Extent extent, int corner) {
return new IrisBlockVector(
(corner & 1) == 0 ? extent.x().minimum() : extent.x().maximum(),
(corner & 2) == 0 ? extent.y().minimum() : extent.y().maximum(),
(corner & 4) == 0 ? extent.z().minimum() : extent.z().maximum());
}
public static final class Profile {
private final IrisObjectRotation rotation;
private final IrisBlockVector translation;
private final boolean rotating;
private final boolean radial;
private final boolean vertical;
private final int[] spinsX;
private final int[] spinsY;
private final int[] spinsZ;
private final int additive;
private final int readMargin;
private Profile(IrisObjectPlacement placement) {
IrisObjectRotation configuredRotation = placement.getRotation();
IrisObjectTranslate translate = placement.getTranslate();
this.rotation = configuredRotation;
this.translation = translate != null && translate.canTranslate()
? new IrisBlockVector(translate.getX(), translate.getY(), translate.getZ())
: null;
this.rotating = configuredRotation != null && configuredRotation.canRotate();
this.spinsX = rotating ? distinctSpins(configuredRotation, Axis.X) : new int[]{0};
this.spinsY = rotating ? distinctSpins(configuredRotation, Axis.Y) : new int[]{0};
this.spinsZ = rotating ? distinctSpins(configuredRotation, Axis.Z) : new int[]{0};
long kernels = (long) spinsX.length * spinsY.length * spinsZ.length;
this.radial = placement.isRotateTowardsSlope() || (rotating && kernels > MAXIMUM_ROTATION_KERNELS);
this.vertical = placement.isRotateTowardsSlope()
|| (rotating && (configuredRotation.canRotateX() || configuredRotation.canRotateZ()));
this.additive = saturate((long) warpReach(placement.getWarp()) + vacuumReach(placement));
this.readMargin = supportReadMargin(placement.getSurfaceSupportBuffer());
}
public PlacementReach of(Extent extent) {
Objects.requireNonNull(extent, "extent");
return new PlacementReach(saturate((long) rotatedReach(extent) + additive), readMargin);
}
public PlacementReach anchored(Extent extent) {
Objects.requireNonNull(extent, "extent");
long span = Math.max(extent.x().span(), extent.z().span());
return new PlacementReach(saturate(span + additive), readMargin);
}
private int rotatedReach(Extent extent) {
if (radial) {
return radialReach(extent);
}
if (!rotating) {
return unrotatedReach(extent);
}
double maximum = 0D;
for (int spinX : spinsX) {
for (int spinY : spinsY) {
for (int spinZ : spinsZ) {
maximum = Math.max(maximum, rotatedCornerMaximum(extent, spinX, spinY, spinZ));
}
}
}
return roundedBound(maximum);
}
private double rotatedCornerMaximum(Extent extent, int spinX, int spinY, int spinZ) {
IrisBlockVector offset = translation == null ? null : rotation.rotate(translation, spinX, spinY, spinZ);
double maximum = 0D;
for (int corner = 0; corner < 8; corner++) {
IrisBlockVector rotated = rotation.rotate(corner(extent, corner), spinX, spinY, spinZ);
double x = offset == null ? rotated.getX() : rotated.getX() + offset.getX();
double z = offset == null ? rotated.getZ() : rotated.getZ() + offset.getZ();
maximum = Math.max(maximum, Math.max(Math.abs(x), Math.abs(z)));
}
return maximum;
}
private int unrotatedReach(Extent extent) {
double translateX = translation == null ? 0D : translation.getX();
double translateZ = translation == null ? 0D : translation.getZ();
double x = Math.max(Math.abs(extent.x().minimum() + translateX), Math.abs(extent.x().maximum() + translateX));
double z = Math.max(Math.abs(extent.z().minimum() + translateZ), Math.abs(extent.z().maximum() + translateZ));
return roundedBound(Math.max(x, z));
}
private int radialReach(Extent extent) {
double translateX = translation == null ? 0D : translation.getX();
double translateY = translation == null ? 0D : translation.getY();
double translateZ = translation == null ? 0D : translation.getZ();
double maximum = 0D;
for (int corner = 0; corner < 8; corner++) {
IrisBlockVector position = corner(extent, corner);
double x = position.getX() + translateX;
double y = vertical ? position.getY() + translateY : 0D;
double z = position.getZ() + translateZ;
maximum = Math.max(maximum, Math.sqrt((x * x) + (y * y) + (z * z)));
}
return roundedBound(maximum);
}
private static int warpReach(IrisGeneratorStyle warp) {
if (warp == null || warp.isFlat()) {
return 0;
}
return saturate((long) Math.ceil(Math.abs(warp.getMultiplier()) / 2D));
}
private static int vacuumReach(IrisObjectPlacement placement) {
if (!IrisObjectVacuum.isVacuumMode(placement.getMode())) {
return 0;
}
return IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings());
}
private static int[] distinctSpins(IrisObjectRotation rotation, Axis axis) {
boolean rotates = switch (axis) {
case X -> rotation.canRotateX();
case Y -> rotation.canRotateY();
case Z -> rotation.canRotateZ();
};
if (!rotates) {
return new int[]{0};
}
LongOpenHashSet angles = new LongOpenHashSet();
IntArrayList spins = new IntArrayList();
for (int spin = 0; spin < SPIN_PERIOD; spin++) {
double angle = switch (axis) {
case X -> rotation.getXRotation(spin);
case Y -> rotation.getYRotation(spin);
case Z -> rotation.getZRotation(spin);
};
if (angles.add(Double.doubleToLongBits(angle))) {
spins.add(spin);
}
}
return spins.toIntArray();
}
}
private enum Axis {
X,
Y,
Z
}
public record Extent(Span x, Span y, Span z) {
public Extent {
Objects.requireNonNull(x, "x");
Objects.requireNonNull(y, "y");
Objects.requireNonNull(z, "z");
}
public static Extent of(IrisObject object) {
Objects.requireNonNull(object, "object");
Vector3i center = object.getCenter();
int[] keys = keyBounds(object.getBlocks());
return new Extent(
Span.of(object.getW(), center.getBlockX(), keys[0], keys[3]),
Span.of(object.getH(), center.getBlockY(), keys[1], keys[4]),
Span.of(object.getD(), center.getBlockZ(), keys[2], keys[5]));
}
public Extent scaled(double scale) {
if (scale == 1D) {
return this;
}
return new Extent(x.scaled(scale), y.scaled(scale), z.scaled(scale));
}
private static int[] keyBounds(VectorMap<NativeBlockState> blocks) {
int[] bounds = {Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE,
Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE};
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
while (cursor.next()) {
IrisBlockVector key = cursor.key();
bounds[0] = Math.min(bounds[0], key.getBlockX());
bounds[1] = Math.min(bounds[1], key.getBlockY());
bounds[2] = Math.min(bounds[2], key.getBlockZ());
bounds[3] = Math.max(bounds[3], key.getBlockX());
bounds[4] = Math.max(bounds[4], key.getBlockY());
bounds[5] = Math.max(bounds[5], key.getBlockZ());
}
return bounds;
}
}
public record Span(int size, int center, int minimum, int maximum, boolean framed) {
public Span {
if (minimum > -center || maximum < size - 1 - center) {
throw new IllegalArgumentException("Span must cover the object frame: " + minimum + ".." + maximum
+ " for size " + size + " centered at " + center);
}
}
public static Span of(int size, int center, int keyMinimum, int keyMaximum) {
int frameMinimum = -center;
int frameMaximum = size - 1 - center;
boolean framed = keyMinimum >= frameMinimum && keyMaximum <= frameMaximum;
return new Span(size, center, Math.min(keyMinimum, frameMinimum), Math.max(keyMaximum, frameMaximum), framed);
}
public int span() {
return maximum - minimum;
}
public Span scaled(double scale) {
int scaledSize = (int) Math.ceil(size * scale);
int scaledCenter = scaledSize / 2;
int frameMinimum = -scaledCenter;
int frameMaximum = scaledSize - 1 - scaledCenter;
if (framed) {
return new Span(scaledSize, scaledCenter, frameMinimum, frameMaximum, true);
}
int scaledMinimum = (int) Math.floor((minimum + center) * scale) - scaledCenter;
double centerExcess = Math.max(0D, center - (size / 2D));
int scaledMaximum = (int) Math.ceil(((maximum + 1 + centerExcess) * scale) + 0.5D + ROUNDING_SLACK) - 1;
return new Span(scaledSize, scaledCenter, Math.min(scaledMinimum, frameMinimum),
Math.max(scaledMaximum, frameMaximum), false);
}
}
}
@@ -31,11 +31,6 @@ import art.arcane.iris.testsupport.PlatformBinding;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
@@ -45,22 +40,12 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
@Rule
public final PlatformBinding platform = PlatformBinding.mockPlatform();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
@SuppressWarnings("unchecked")
public void transformedFloatingPlacementExpandsOwnerRadius() throws Exception {
File objectFile = temporaryFolder.newFile("floating.iob");
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
output.writeInt(1);
output.writeInt(1);
output.writeInt(1);
}
public void transformedFloatingPlacementExpandsOwnerRadius() {
IrisObjectPlacement placement = new IrisObjectPlacement()
.setPlace(new KList<>("test/floating"))
.setTranslate(new IrisObjectTranslate().setX(32));
.setTranslate(new IrisObjectTranslate().setX(-32));
IrisFloatingChildBiomes floatingChild = new IrisFloatingChildBiomes()
.setFloatingObjects(new KList<>(placement));
IrisBiome biome = new IrisBiome().setFloatingChildBiomes(new KList<>(floatingChild));
@@ -69,7 +54,7 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
when(dimension.getReachableBiomes(org.mockito.ArgumentMatchers.any())).thenReturn(new KList<>(biome));
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
when(objectLoader.findFile("test/floating")).thenReturn(objectFile);
when(objectLoader.load("test/floating")).thenReturn(new IrisObject(1, 1, 1));
IrisData data = mock(IrisData.class);
when(data.getObjectLoader()).thenReturn(objectLoader);
@@ -79,8 +64,8 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
when(engineMantle.getEngine()).thenReturn(engine);
when(engineMantle.getData()).thenReturn(data);
assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius());
assertEquals(32, new MantleFloatingObjectComponent(engineMantle).getRadius());
when(dimension.getAllObjectScaleFactor()).thenReturn(2D);
assertEquals(34, new MantleFloatingObjectComponent(engineMantle).getRadius());
assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius());
}
}
@@ -24,7 +24,6 @@ import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
@@ -33,13 +32,8 @@ import art.arcane.volmlib.util.matter.Matter;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@@ -60,9 +54,6 @@ public class MantleObjectComponentBoundaryRadiusTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Before
public void bindPlatform() {
IrisPlatforms.unbind();
@@ -92,7 +83,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
.setRotation(IrisObjectRotation.of(90, 0, 0))
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
assertEquals(53, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 48, 7), placement, 1D));
assertEquals(27, PlacementReach.of(new IrisObject(5, 48, 7), placement, 1D).horizontal());
}
@Test
@@ -103,7 +94,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
.setMode(ObjectPlaceMode.VACUUM)
.setVacuumSettings(new IrisVacuumSettings().setRadius(12));
assertEquals(39, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 4, 3), placement, placement.getMaximumScale(null)));
assertEquals(22, PlacementReach.of(new IrisObject(5, 4, 3), placement, placement.getMaximumScale(null)).horizontal());
}
@Test
@@ -126,15 +117,12 @@ public class MantleObjectComponentBoundaryRadiusTest {
public void inheritedRadiusMatchesScaledFrameAndExplicitScaleOverridesIt() {
IrisDimension dimension = new IrisDimension().setAllObjectScaleFactor(2D);
IrisObjectPlacement inherited = new IrisObjectPlacement();
IrisBlockVector size = new IrisBlockVector(8, 8, 8);
assertEquals(16, MantleObjectComponent.calculatePlacementReach(size, inherited,
inherited.getMaximumScale(dimension)));
IrisObject object = new IrisObject(8, 8, 8);
assertEquals(8, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
dimension.setAllObjectScaleFactor(0.5D);
assertEquals(4, MantleObjectComponent.calculatePlacementReach(size, inherited,
inherited.getMaximumScale(dimension)));
assertEquals(2, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
inherited.setScale(new IrisObjectScale().setSize(1D));
assertEquals(8, MantleObjectComponent.calculatePlacementReach(size, inherited,
inherited.getMaximumScale(dimension)));
assertEquals(4, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
}
@Test
@@ -183,10 +171,11 @@ public class MantleObjectComponentBoundaryRadiusTest {
@Test
public void collisionInputRadiusCoversBothSourceTraversalLegs() {
assertEquals(82, MantleObjectComponent.calculateInputRadius(33, false));
assertEquals(130, MantleObjectComponent.calculateInputRadius(33, true));
assertEquals(84, MantleObjectComponent.calculateInputRadius(33, 35, false));
assertEquals(132, MantleObjectComponent.calculateInputRadius(33, 35, true));
assertEquals(82, MantleObjectComponent.calculateInputRadius(33, 0, false));
assertEquals(Integer.MAX_VALUE,
MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, true));
MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, Integer.MAX_VALUE, true));
}
@Test
@@ -199,7 +188,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
for (int radius = 0; radius < 300; radius++) {
for (boolean collisions : new boolean[]{false, true}) {
assertTrue(MantleObjectComponent.calculateEagerInputRadius(radius, collisions)
<= MantleObjectComponent.calculateInputRadius(radius, collisions));
<= MantleObjectComponent.calculateInputRadius(radius, radius, collisions));
}
}
assertEquals(Integer.MAX_VALUE,
@@ -231,27 +220,25 @@ public class MantleObjectComponentBoundaryRadiusTest {
when(engineMantle.getEngine()).thenReturn(engine);
when(engineMantle.getData()).thenReturn(data);
assertEquals(53, new MantleObjectComponent(engineMantle).getRadius());
assertEquals(27, new MantleObjectComponent(engineMantle).getRadius());
}
@Test
@SuppressWarnings("unchecked")
public void upperDimensionObjectsDriveRadiusAndCollisionMode() {
IrisObjectPlacement placement = new IrisObjectPlacement()
.setPlace(new KList<>("upper/tree"))
.setRotation(IrisObjectRotation.of(90, 0, 0))
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
placement.getForbiddenCollisions().add("upper/blocker");
IrisData baseData = mock(IrisData.class);
IrisData upperData = mock(IrisData.class);
IrisProceduralPlacement proceduralPlacement = mock(IrisProceduralPlacement.class);
when(proceduralPlacement.getVariantObjects(upperData))
.thenReturn(new KList<>(new IrisObject(5, 48, 7)));
when(proceduralPlacement.asPlacement()).thenReturn(placement);
IrisProceduralObjects proceduralObjects = mock(IrisProceduralObjects.class);
when(proceduralObjects.isEmpty()).thenReturn(false);
when(proceduralObjects.getAllPlacements()).thenReturn(new KList<>(proceduralPlacement));
ResourceLoader<IrisObject> upperObjects = mock(ResourceLoader.class);
when(upperObjects.load("upper/tree")).thenReturn(new IrisObject(5, 48, 7));
when(upperData.getObjectLoader()).thenReturn(upperObjects);
IrisRegion upperRegion = mock(IrisRegion.class);
when(upperRegion.getObjects()).thenReturn(new KList<>());
when(upperRegion.getProceduralObjects()).thenReturn(proceduralObjects);
when(upperRegion.getObjects()).thenReturn(new KList<>(placement));
when(upperRegion.getSurfaceObjects()).thenReturn(new KList<>(placement));
IrisDimension baseDimension = mock(IrisDimension.class);
when(baseDimension.getAllObjectScaleFactor()).thenReturn(1D);
when(baseDimension.isUpperDimensionObjects()).thenReturn(true);
@@ -273,20 +260,13 @@ public class MantleObjectComponentBoundaryRadiusTest {
MantleObjectComponent component = new MantleObjectComponent(engineMantle);
assertEquals(53, component.getRadius());
assertEquals(182, component.getInputRadius());
assertEquals(5, component.getRadius());
assertEquals(41, component.getInputRadius());
}
@Test
@SuppressWarnings("unchecked")
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() throws Exception {
File objectFile = temporaryFolder.newFile("tree.iob");
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
output.writeInt(1);
output.writeInt(1);
output.writeInt(1);
}
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() {
IrisObjectPlacement placement = new IrisObjectPlacement()
.setPlace(new KList<>("test/tree"))
.setTranslate(new IrisObjectTranslate().setX(32));
@@ -298,7 +278,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(biome));
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
when(objectLoader.findFile("test/tree")).thenReturn(objectFile);
when(objectLoader.load("test/tree")).thenReturn(new IrisObject(1, 1, 1));
IrisData data = mock(IrisData.class);
when(data.getObjectLoader()).thenReturn(objectLoader);
@@ -310,9 +290,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
when(engineMantle.getData()).thenReturn(data);
MantleObjectComponent component = spy(new MantleObjectComponent(engineMantle));
assertEquals(33, component.getRadius());
assertEquals(32, component.getRadius());
assertEquals(0, component.getOutputRadius());
assertEquals(82, component.getInputRadius());
assertEquals(68, component.getInputRadius());
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
@@ -341,9 +321,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
assertEquals(49, directOrigins.get());
assertEquals(25, directOrigins.get());
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
assertEquals(49, directOrigins.get());
assertEquals(25, directOrigins.get());
List<String> generatedChunks = new ArrayList<>();
doAnswer(invocation -> {
@@ -370,14 +350,14 @@ public class MantleObjectComponentBoundaryRadiusTest {
component.getRadius(),
(chunkX, chunkZ) -> replayedSources.add(chunkX + "," + chunkZ)
);
assertEquals(49, replayedSources.size());
assertEquals("-3,-3", replayedSources.getFirst());
assertEquals("-3,-2", replayedSources.get(1));
assertEquals("3,3", replayedSources.getLast());
assertEquals(25, replayedSources.size());
assertEquals("-2,-2", replayedSources.getFirst());
assertEquals("-2,-1", replayedSources.get(1));
assertEquals("2,2", replayedSources.getLast());
placement.getForbiddenCollisions().add("test/tree");
component.hotload();
assertEquals(130, component.getInputRadius());
assertEquals(100, component.getInputRadius());
}
private static final class TestMatterGenerator implements MatterGenerator {
@@ -0,0 +1,517 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisFloatingChildBiomes;
import art.arcane.iris.generation.decoration.IrisProceduralObjects;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.MeteredCache;
import art.arcane.iris.generation.runtime.PreservationRegistry;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.structure.object.FloatingObjectFootprint;
import art.arcane.iris.structure.object.IrisAxisRotationClamp;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.structure.object.IrisObjectScale;
import art.arcane.iris.structure.object.IrisObjectTranslate;
import art.arcane.iris.structure.object.IrisObjectPlacementScaleInterpolator;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.Vector3i;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import org.junit.After;
import org.junit.Assume;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.mockito.Answers;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
public class PlacementReachPackTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final int SPIN_PERIOD = 360;
private static final int MAXIMUM_CHECKED_KERNELS = 4096;
private static final String V2_HEADER = "Iris V2 IOB;";
private File pack;
private IrisData data;
private IrisDimension dimension;
@Before
public void loadPack() throws IOException {
String configured = System.getenv("IRIS_TEST_PACK");
Assume.assumeTrue(configured != null && !configured.isBlank());
pack = new File(configured).getAbsoluteFile();
Assume.assumeTrue(new File(pack, "objects").isDirectory());
File[] dimensions = new File(pack, "dimensions").listFiles((folder, name) -> name.endsWith(".json"));
Assume.assumeTrue(dimensions != null && dimensions.length > 0);
bindPlatform(Files.createTempDirectory("iris-placement-reach").toFile());
data = IrisData.get(pack);
String dimensionKey = System.getenv().getOrDefault("IRIS_TEST_PACK_DIMENSION",
dimensions[0].getName().substring(0, dimensions[0].getName().length() - 5));
dimension = data.getDimensionLoader().load(dimensionKey);
assertTrue("dimension " + dimensionKey + " must load", dimension != null);
}
@After
public void unbindPlatform() {
IrisServices.remove(PreservationRegistry.class);
IrisPlatforms.unbind();
}
@Test
public void everyObjectFileStaysInsideItsReachUnderRightAngleRotations() throws IOException {
List<IrisObjectPlacement> placements = List.of(
new IrisObjectPlacement(),
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D),
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp())),
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp(true, false, 0D, 0D, 90D))),
new IrisObjectPlacement().setRotation(IrisObjectRotation.of(0D, 0D, 0D)),
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp()))
.setTranslate(new IrisObjectTranslate().setX(7).setZ(-3)));
List<Path> files;
try (Stream<Path> walk = Files.walk(new File(pack, "objects").toPath())) {
files = walk.filter(path -> path.toString().endsWith(".iob")).sorted().toList();
}
assertTrue(!files.isEmpty());
Statistics statistics = new Statistics();
NativeBlockState marker = mock(NativeBlockState.class, withSettings().stubOnly());
for (Path file : files) {
IrisObject object = readPositions(file, marker);
for (IrisObjectPlacement placement : placements) {
verifyCentered(file.toString(), object, placement, PlacementReach.of(object, placement, 1D), statistics);
}
}
System.out.println("PLACEMENT_REACH_FILES objects=" + files.size() + " " + statistics);
}
@Test
public void everyPackPlacementStaysInsideItsReach() {
Statistics statistics = new Statistics();
Set<IrisObjectPlacement> placements = new LinkedHashSet<>();
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
placements.addAll(region.getObjects());
}
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
placements.addAll(biome.getObjects());
}
for (IrisObjectPlacement placement : placements) {
for (String key : placement.getPlace()) {
IrisObject object = data.getObjectLoader().load(key);
if (object == null) {
continue;
}
PlacementReach reach = PlacementReach.of(object, placement, placement.getMaximumScale(dimension));
forEachVariant(object, placement, 1D, variant -> verifyCentered(key, variant, placement, reach, statistics));
}
}
for (IrisProceduralPlacement procedural : proceduralPlacements()) {
KList<IrisObject> variants = procedural.getVariantObjects(data);
if (variants == null) {
continue;
}
IrisObjectPlacement placement = procedural.asPlacement();
for (IrisObject variant : variants) {
PlacementReach reach = PlacementReach.of(variant, placement, placement.getMaximumScale(dimension));
forEachVariant(variant, placement, 1D,
scaled -> verifyCentered(variant.getLoadKey(), scaled, placement, reach, statistics));
}
}
assertTrue(statistics.checked > 0);
System.out.println("PLACEMENT_REACH_PLACEMENTS placements=" + placements.size() + " " + statistics);
}
@Test
public void floatingPlacementsStayInsideTheirReach() {
Statistics statistics = new Statistics();
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
KList<IrisFloatingChildBiomes> entries = biome.getFloatingChildBiomes();
if (entries == null) {
continue;
}
for (IrisFloatingChildBiomes entry : entries) {
double shrink = entry.hasObjectShrink() ? entry.getShrinkScale().getMaxScale() : 1D;
IrisBiome target = entry.getRealBiome(biome, data);
for (IrisObjectPlacement placement : listOrEmpty(entry.getFloatingObjects())) {
verifyFloating(placement, shrink, false, statistics);
}
List<IrisObjectPlacement> anchored = new ArrayList<>();
anchored.addAll(listOrEmpty(entry.getExtraObjects()));
anchored.addAll(listOrEmpty(entry.getTopObjectOverrides()));
anchored.addAll(listOrEmpty(entry.getBottomObjectOverrides()));
if (target != null) {
anchored.addAll(listOrEmpty(entry.resolveTopObjects(target)));
anchored.addAll(listOrEmpty(entry.resolveBottomObjects(target)));
}
for (IrisObjectPlacement placement : anchored) {
verifyFloating(placement, shrink, true, statistics);
}
}
}
System.out.println("PLACEMENT_REACH_FLOATING " + statistics);
}
@Test
public void reportsTheMantleWindow() {
Engine engine = mock(Engine.class);
when(engine.getDimension()).thenReturn(dimension);
EngineMantle engineMantle = mock(EngineMantle.class);
when(engineMantle.getEngine()).thenReturn(engine);
when(engineMantle.getData()).thenReturn(data);
MantleObjectComponent objects = new MantleObjectComponent(engineMantle);
MantleFloatingObjectComponent floating = new MantleFloatingObjectComponent(engineMantle);
int objectRadius = objects.getRadius();
int objectInput = objects.getInputRadius();
int objectEager = objects.getEagerInputRadius(objectInput);
int floatingRadius = floating.getRadius();
int floatingPassRadius = chunkRadius(floatingRadius);
int objectPassRadius = chunkRadius(floatingRadius);
int terrainRadius = chunkRadius(floatingRadius + objectEager + 1);
int writerRadius = 2 * chunkRadius(floatingRadius + objectInput + 1);
System.out.println("PLACEMENT_REACH_WINDOW objectReach=" + objectRadius
+ " objectInput=" + objectInput
+ " objectEager=" + objectEager
+ " sourceChunkRadius=" + MantleObjectComponent.sourceChunkRadius(objectRadius)
+ " floatingReach=" + floatingRadius
+ " floatingPassRadius=" + floatingPassRadius
+ " objectPassRadius=" + objectPassRadius
+ " terrainRadius=" + terrainRadius
+ " terrainWindow=" + ((2 * terrainRadius) + 1) + "x" + ((2 * terrainRadius) + 1)
+ " writerRadius=" + writerRadius);
assertTrue(objectRadius > 0);
}
private void verifyFloating(IrisObjectPlacement placement, double shrink, boolean anchored, Statistics statistics) {
if (placement == null || placement.getPlace() == null) {
return;
}
for (String key : placement.getPlace()) {
IrisObject object = data.getObjectLoader().load(key);
if (object == null) {
continue;
}
PlacementReach.Extent extent = PlacementReach.Extent.of(object)
.scaled(placement.getMaximumScale(dimension)).scaled(shrink);
if (!anchored) {
PlacementReach reach = PlacementReach.of(extent, placement);
forEachVariant(object, placement, shrink, variant -> verifyCentered(key, variant, placement, reach, statistics));
continue;
}
PlacementReach reach = PlacementReach.anchored(extent, placement);
forEachVariant(object, placement, shrink, variant -> verifyAnchored(key, variant, reach, statistics));
}
}
private void verifyAnchored(String key, IrisObject variant, PlacementReach reach, Statistics statistics) {
FloatingObjectFootprint footprint = FloatingObjectFootprint.compute(variant);
List<IrisBlockVector> positions = positions(variant);
List<IrisObjectRotation> rotations = List.of(IrisObjectRotation.of(0D, 0D, 0D),
IrisObjectRotation.xFlip180WithY(0D), IrisObjectRotation.xFlip180WithY(90D),
IrisObjectRotation.xFlip180WithY(180D), IrisObjectRotation.xFlip180WithY(270D));
for (IrisObjectRotation rotation : rotations) {
IrisBlockVector anchor = rotation.rotate(new IrisBlockVector(footprint.getTallestKx(), 0, footprint.getTallestKz()), 0, 0, 0);
int actual = 0;
for (IrisBlockVector position : positions) {
IrisBlockVector rotated = rotation.rotate(position, 0, 0, 0);
actual = Math.max(actual, Math.abs((int) Math.round(rotated.getX()) - anchor.getBlockX()));
actual = Math.max(actual, Math.abs((int) Math.round(rotated.getZ()) - anchor.getBlockZ()));
}
record(key, actual, reach.horizontal(), statistics);
}
}
private void verifyCentered(String key, IrisObject object, IrisObjectPlacement placement, PlacementReach reach,
Statistics statistics) {
IrisObjectRotation rotation = placement.getRotation();
IrisObjectTranslate translate = placement.getTranslate();
boolean translating = translate != null && translate.canTranslate();
int[] spinsX = spins(rotation, 0);
int[] spinsY = spins(rotation, 1);
int[] spinsZ = spins(rotation, 2);
List<IrisBlockVector> positions = positions(object);
long kernels = (long) spinsX.length * spinsY.length * spinsZ.length;
if (kernels > MAXIMUM_CHECKED_KERNELS) {
fail("rotation for " + key + " has " + kernels + " kernels");
}
for (int spinX : spinsX) {
for (int spinY : spinsY) {
for (int spinZ : spinsZ) {
IrisBlockVector offset = translating
? rotation.rotate(new IrisBlockVector(translate.getX(), translate.getY(), translate.getZ()), spinX, spinY, spinZ)
: null;
int actual = 0;
for (IrisBlockVector position : positions) {
IrisBlockVector moved = rotation.rotate(position, spinX, spinY, spinZ).clone();
if (offset != null) {
moved.add(offset);
}
actual = Math.max(actual, Math.abs((int) Math.round(moved.getX())));
actual = Math.max(actual, Math.abs((int) Math.round(moved.getZ())));
}
record(key, actual, reach.horizontal(), statistics);
}
}
}
}
private static IrisObject readPositions(Path file, NativeBlockState marker) throws IOException {
try (DataInputStream input = new DataInputStream(new BufferedInputStream(Files.newInputStream(file)))) {
IrisObject object = new IrisObject(input.readInt(), input.readInt(), input.readInt());
input.mark(Short.BYTES + V2_HEADER.length());
int headerLength = input.readUnsignedShort();
boolean v2 = headerLength == V2_HEADER.length()
&& V2_HEADER.equals(new String(input.readNBytes(headerLength), StandardCharsets.US_ASCII));
if (!v2) {
input.reset();
}
int paletteSize = v2 ? input.readShort() : 0;
for (int index = 0; index < paletteSize; index++) {
input.readUTF();
}
int blocks = input.readInt();
for (int index = 0; index < blocks; index++) {
IrisBlockVector position = new IrisBlockVector(input.readShort(), input.readShort(), input.readShort());
if (v2) {
input.readShort();
} else {
input.readUTF();
}
object.getBlocks().put(position, marker);
}
return object;
}
}
private static void record(String key, int actual, int reach, Statistics statistics) {
statistics.checked++;
statistics.maximumActual = Math.max(statistics.maximumActual, actual);
statistics.maximumReach = Math.max(statistics.maximumReach, reach);
statistics.slack += reach - actual;
if (actual > reach) {
fail(key + " places a block " + actual + " blocks from its origin, beyond its reach " + reach);
}
}
private static List<IrisBlockVector> positions(IrisObject object) {
List<IrisBlockVector> positions = new ArrayList<>(object.getBlocks().size() + 8);
object.getBlocks().forEach((position, state) -> positions.add(position));
Vector3i center = object.getCenter();
int[] xs = {-center.getBlockX(), object.getW() - center.getBlockX() - 1};
int[] ys = {-center.getBlockY(), object.getH() - center.getBlockY() - 1};
int[] zs = {-center.getBlockZ(), object.getD() - center.getBlockZ() - 1};
for (int x : xs) {
for (int y : ys) {
for (int z : zs) {
positions.add(new IrisBlockVector(x, y, z));
}
}
}
return positions;
}
private void forEachVariant(IrisObject object, IrisObjectPlacement placement, double shrink, Consumer<IrisObject> consumer) {
IrisObjectScale scale = placement.getScale();
IrisObjectPlacementScaleInterpolator interpolation = scale == null || scale.getInterpolation() == null
? IrisObjectPlacementScaleInterpolator.NONE : scale.getInterpolation();
for (double factor : scales(placement)) {
IrisObject variant = factor == 1D && (scale == null || !scale.shouldScale())
? object
: scale == null ? IrisObjectScale.getFixed(object, factor) : object.scaled(factor, interpolation);
consumer.accept(shrink < 1D ? variant.scaled(shrink, IrisObjectPlacementScaleInterpolator.NONE) : variant);
}
}
private double[] scales(IrisObjectPlacement placement) {
IrisObjectScale scale = placement.getScale();
if (scale == null) {
return new double[]{dimension.getAllObjectScaleFactor()};
}
if (!scale.shouldScale()) {
return new double[]{1D};
}
if (scale.getSize() != 1D) {
return new double[]{scale.getSize()};
}
if (scale.getMinimumScale() == scale.getMaximumScale()) {
return new double[]{scale.getMinimumScale()};
}
int count = Math.max(1, Math.min(scale.getVariations(), 32));
double[] scales = new double[count];
for (int index = 0; index < count; index++) {
scales[index] = scale.getMinimumScale() + (((scale.getMaximumScale() - scale.getMinimumScale()) / count) * index);
}
return scales;
}
private List<IrisProceduralPlacement> proceduralPlacements() {
List<IrisProceduralPlacement> placements = new ArrayList<>();
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
addProcedural(placements, region.getProceduralObjects());
}
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
addProcedural(placements, biome.getProceduralObjects());
}
return placements;
}
private static void addProcedural(List<IrisProceduralPlacement> placements, IrisProceduralObjects procedural) {
if (procedural != null && !procedural.isEmpty()) {
placements.addAll(procedural.getAllPlacements());
}
}
private static int[] spins(IrisObjectRotation rotation, int axis) {
if (rotation == null || !rotation.canRotate()) {
return new int[]{0};
}
boolean rotates = switch (axis) {
case 0 -> rotation.canRotateX();
case 1 -> rotation.canRotateY();
default -> rotation.canRotateZ();
};
if (!rotates) {
return new int[]{0};
}
LongOpenHashSet angles = new LongOpenHashSet();
IntArrayList spins = new IntArrayList();
for (int spin = -SPIN_PERIOD + 1; spin < SPIN_PERIOD; spin++) {
double angle = switch (axis) {
case 0 -> rotation.getXRotation(spin);
case 1 -> rotation.getYRotation(spin);
default -> rotation.getZRotation(spin);
};
if (angles.add(Double.doubleToLongBits(angle))) {
spins.add(spin);
}
}
return spins.toIntArray();
}
private static <T> List<T> listOrEmpty(KList<T> list) {
return list == null ? List.of() : list;
}
private static IrisObjectRotation rotation(IrisAxisRotationClamp x, IrisAxisRotationClamp y, IrisAxisRotationClamp z) {
IrisObjectRotation rotation = new IrisObjectRotation();
rotation.setEnabled(true);
rotation.setXAxis(x);
rotation.setYAxis(y);
rotation.setZAxis(z);
return rotation;
}
private static int chunkRadius(int blockRadius) {
return blockRadius > 0 ? Math.ceilDiv(blockRadius, 16) : 0;
}
private static void bindPlatform(File dataFolder) {
IrisPlatforms.unbind();
Map<String, NativeBlockState> states = new ConcurrentHashMap<>();
PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly());
when(registries.block(anyString())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
when(registries.blockOrNull(anyString())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
when(registries.blockOrNull(anyString(), anyBoolean())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
when(registries.air()).thenAnswer(invocation -> state(states, "minecraft:air"));
IrisPlatform platform = mock(IrisPlatform.class, Answers.CALLS_REAL_METHODS);
when(platform.dataFolder()).thenReturn(dataFolder);
when(platform.dataFile(any(String[].class))).thenAnswer(invocation -> {
Object[] path = invocation.getRawArguments();
return new File(dataFolder, String.join(File.separator, (String[]) path[0]));
});
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
IrisServices.register(PreservationRegistry.class, new IgnoredPreservation());
}
private static NativeBlockState state(Map<String, NativeBlockState> states, String key) {
return states.computeIfAbsent(key, ignored -> {
String material = materialOf(key);
boolean air = material.endsWith(":air") || material.endsWith("cave_air") || material.endsWith("void_air");
NativeBlockState state = mock(NativeBlockState.class, withSettings().stubOnly());
when(state.key()).thenReturn(key);
when(state.materialKey()).thenReturn(material);
when(state.isAir()).thenReturn(air);
when(state.isSolid()).thenReturn(!air);
when(state.isOccluding()).thenReturn(!air);
return state;
});
}
private static String materialOf(String key) {
String trimmed = key.trim().toLowerCase(Locale.ROOT);
int properties = trimmed.indexOf('[');
String head = properties < 0 ? trimmed : trimmed.substring(0, properties);
return head.indexOf(':') < 0 ? "minecraft:" + head : head;
}
private static final class IgnoredPreservation implements PreservationRegistry {
@Override
public void register(Thread thread) {
}
@Override
public void register(ExecutorService service) {
}
@Override
public void registerCache(MeteredCache cache) {
}
@Override
public void dereference() {
}
}
private static final class Statistics {
private long checked;
private long slack;
private int maximumActual;
private int maximumReach;
@Override
public String toString() {
return "checked=" + checked + " maxActual=" + maximumActual + " maxReach=" + maximumReach
+ " meanSlack=" + (checked == 0 ? 0D : (double) slack / checked);
}
}
}
@@ -0,0 +1,416 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.structure.object.FloatingObjectFootprint;
import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.iris.structure.object.IrisAxisRotationClamp;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.structure.object.IrisObjectPlacementScaleInterpolator;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.structure.object.IrisObjectScale;
import art.arcane.iris.structure.object.IrisObjectTranslate;
import art.arcane.iris.structure.object.ObjectPlaceMode;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
public class PlacementReachTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final int ORIGIN_X = 1000;
private static final int ORIGIN_Y = 90;
private static final int ORIGIN_Z = -2000;
private static final int GROUND_Y = 64;
private final Map<String, NativeBlockState> states = new ConcurrentHashMap<>();
private IrisData data;
private IrisData objectData;
private Engine engine;
@Before
public void bindPlatform() {
IrisPlatforms.unbind();
PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly());
when(registries.block(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
when(registries.blockOrNull(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
when(registries.blockOrNull(anyString(), anyBoolean())).thenAnswer(invocation -> state(invocation.getArgument(0)));
when(registries.air()).thenAnswer(invocation -> state("minecraft:air"));
IrisPlatform platform = mock(IrisPlatform.class, withSettings().stubOnly());
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
ProceduralStream<NativeBlockState> rock = mock(ProceduralStream.class, withSettings().stubOnly());
IrisComplex complex = mock(IrisComplex.class, withSettings().stubOnly());
when(complex.getRockStream()).thenReturn(rock);
engine = mock(Engine.class, withSettings().stubOnly());
when(engine.getHeight()).thenReturn(256);
when(engine.getMinHeight()).thenReturn(0);
when(engine.getComplex()).thenReturn(complex);
data = mock(IrisData.class, withSettings().stubOnly());
when(data.getEngine()).thenReturn(engine);
objectData = mock(IrisData.class, withSettings().stubOnly());
}
@After
public void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
public void halfExtentReachIsTheCenteredFrameNotTheFullWidth() {
IrisObject object = singleBlockObject(103, 75, 73);
IrisObjectPlacement placement = new IrisObjectPlacement();
assertEquals(51, PlacementReach.of(object, placement, 1D).horizontal());
assertEquals(54, PlacementReach.of(object, placement, 1D).input());
assertEquals(51, PlacementReach.of(object, placement.setRotation(IrisObjectRotation.of(0D, 0D, 0D)), 1D).horizontal());
}
@Test
public void arbitraryAngleRotationUsesTheRotatedFrame() {
IrisObject object = singleBlockObject(21, 5, 21);
IrisObjectPlacement placement = new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp()));
assertEquals(14, PlacementReach.of(object, placement, 1D).horizontal());
}
@Test
public void slopeRotationIsBoundedEvenWhenTheConfiguredRotationIsDisabled() {
IrisObject object = singleBlockObject(21, 9, 21);
IrisObjectPlacement placement = new IrisObjectPlacement().setRotation(IrisObjectRotation.of(0D, 0D, 0D));
assertEquals(10, PlacementReach.of(object, placement, 1D).horizontal());
assertEquals(15, PlacementReach.of(object, placement.setRotateTowardsSlope(true), 1D).horizontal());
}
@Test
public void unionKeepsTheLargestReachAndTheLargestInput() {
PlacementReach union = new PlacementReach(10, 2).union(new PlacementReach(4, 12));
assertEquals(10, union.horizontal());
assertEquals(16, union.input());
assertEquals(19, union.withReadMargin(9).input());
assertEquals(16, union.withReadMargin(1).input());
}
@Test
public void scaledSpanBoundsEveryScaledVariant() {
Random random = new Random(91L);
double[] scales = {0.01D, 0.25D, 0.45D, 0.5D, 0.75D, 0.95D, 1D, 1.3D, 1.5D, 2D, 2.6D, 3.7D, 4D};
for (int trial = 0; trial < 120; trial++) {
int outside = trial % 3 == 0 ? 1 + random.nextInt(3) : 0;
IrisObject object = randomObject(random, outside);
PlacementReach.Extent extent = PlacementReach.Extent.of(object);
for (double scale : scales) {
for (IrisObjectPlacementScaleInterpolator interpolation : List.of(IrisObjectPlacementScaleInterpolator.NONE,
IrisObjectPlacementScaleInterpolator.TRILINEAR)) {
if (interpolation != IrisObjectPlacementScaleInterpolator.NONE && scale > 2D) {
continue;
}
IrisObject scaled = object.scaled(scale, interpolation);
for (double maximum : new double[]{scale, Math.min(IrisObjectScale.MAXIMUM_FACTOR, scale * 1.4D)}) {
assertContains(extent.scaled(maximum), scaled, "scale " + scale + " bounded at " + maximum);
for (double shrink : new double[]{0.45D, 0.8D}) {
assertContains(extent.scaled(maximum).scaled(shrink),
scaled.scaled(shrink, IrisObjectPlacementScaleInterpolator.NONE),
"scale " + scale + " shrink " + shrink);
}
}
}
}
}
}
@Test
public void runnerWritesAndReadsStayInsideTheReach() {
Random random = new Random(1337L);
for (int trial = 0; trial < 260; trial++) {
IrisObject object = randomObject(random, trial % 5 == 0 ? 2 : 0);
object.setLoader(objectData);
IrisObjectPlacement placement = randomPlacement(random);
PlacementReach reach = PlacementReach.of(object, placement, placement.getMaximumScale(null));
for (int seed = 0; seed < 12; seed++) {
RNG rng = new RNG((trial * 7919L) + seed);
IrisObject variant = placement.getScale() == null ? object : placement.getScale().get(rng, object);
RecordingPlacer placer = new RecordingPlacer(engine);
variant.place(ORIGIN_X, ORIGIN_Y, ORIGIN_Z, placer, placement, rng, null, null, data);
String context = "trial " + trial + " seed " + seed + " mode " + placement.getMode()
+ " rotation " + placement.getRotation() + " translate " + placement.getTranslate()
+ " scale " + placement.getScale() + " warp " + placement.getWarp().getMultiplier();
assertTrue(context + " writes " + placer.writeReach + " > " + reach.horizontal(),
placer.writeReach <= reach.horizontal());
assertTrue(context + " reads " + placer.readReach + " > " + reach.input(),
placer.readReach <= reach.input());
}
}
}
@Test
public void anchoredReachCoversTheFloatingFootprintAnchor() {
Random random = new Random(7L);
for (int trial = 0; trial < 80; trial++) {
IrisObject object = randomObject(random, trial % 4 == 0 ? 1 : 0);
PlacementReach reach = PlacementReach.anchored(PlacementReach.Extent.of(object), new IrisObjectPlacement());
FloatingObjectFootprint footprint = FloatingObjectFootprint.compute(object);
for (IrisObjectRotation rotation : List.of(IrisObjectRotation.of(0D, 0D, 0D),
IrisObjectRotation.xFlip180WithY(0D), IrisObjectRotation.xFlip180WithY(90D),
IrisObjectRotation.xFlip180WithY(180D), IrisObjectRotation.xFlip180WithY(270D))) {
IrisBlockVector anchor = rotation.rotate(new IrisBlockVector(footprint.getTallestKx(), 0, footprint.getTallestKz()), 0, 0, 0);
object.getBlocks().forEach((position, state) -> {
IrisBlockVector rotated = rotation.rotate(position, 0, 0, 0);
assertTrue(Math.abs((int) Math.round(rotated.getX()) - anchor.getBlockX()) <= reach.horizontal());
assertTrue(Math.abs((int) Math.round(rotated.getZ()) - anchor.getBlockZ()) <= reach.horizontal());
});
}
}
}
private IrisObjectPlacement randomPlacement(Random random) {
IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setRotation(randomRotation(random));
if (random.nextInt(3) > 0) {
placement.setTranslate(new IrisObjectTranslate()
.setX(random.nextInt(41) - 20)
.setY(random.nextInt(21) - 10)
.setZ(random.nextInt(41) - 20));
}
if (random.nextInt(3) == 0) {
placement.setWarp(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setMultiplier(1D + random.nextInt(9)));
}
List<ObjectPlaceMode> modes = List.of(ObjectPlaceMode.CENTER_HEIGHT, ObjectPlaceMode.STILT, ObjectPlaceMode.FAST_STILT,
ObjectPlaceMode.CENTER_STILT, ObjectPlaceMode.ERODE_STILT, ObjectPlaceMode.ORGANIC_STILT, ObjectPlaceMode.CEILING_HANG,
ObjectPlaceMode.VACUUM, ObjectPlaceMode.VACUUM_HIGH, ObjectPlaceMode.VACUUM_FAST, ObjectPlaceMode.VACUUM_ORGANIC,
ObjectPlaceMode.VACUUM_WAVY, ObjectPlaceMode.STRUCTURE_PIECE);
placement.setMode(modes.get(random.nextInt(modes.size())));
if (random.nextInt(4) == 0) {
placement.setVacuumSettings(new IrisVacuumSettings().setRadius(1 + random.nextInt(16)));
}
if (random.nextInt(4) == 0) {
placement.setStiltSettings(new IrisStiltSettings());
}
placement.setBore(random.nextInt(4) == 0);
placement.setBoreExtendMaxY(random.nextInt(3));
placement.setSnow(random.nextInt(4) == 0 ? 0.6D : 0D);
placement.setSurfaceSupportBuffer(random.nextInt(5));
switch (random.nextInt(5)) {
case 0 -> placement.setScale(new IrisObjectScale().setSize(0.3D + (random.nextDouble() * 1.6D)));
case 1 -> placement.setScale(new IrisObjectScale().setMinimumScale(0.5D).setMaximumScale(2.3D)
.setVariations(5).setInterpolation(IrisObjectPlacementScaleInterpolator.TRILINEAR));
case 2 -> placement.setScale(new IrisObjectScale().setMinimumScale(0.4D).setMaximumScale(0.9D).setVariations(3));
default -> {
}
}
return placement;
}
private static IrisObjectRotation randomRotation(Random random) {
return switch (random.nextInt(9)) {
case 0 -> new IrisObjectRotation();
case 1 -> IrisObjectRotation.of(0D, 0D, 0D);
case 2 -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp());
case 3 -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 0D, 0D, 0D), new IrisAxisRotationClamp());
case 4 -> IrisObjectRotation.of(90D, 0D, 0D);
case 5 -> rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D),
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp(true, false, 0D, 0D, 90D));
case 6 -> rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 0D),
new IrisAxisRotationClamp(true, false, 0D, 0D, 0D), new IrisAxisRotationClamp(true, false, 0D, 0D, 30D));
case 7 -> IrisObjectRotation.of(30D, 135D, 0D);
default -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 20D, 110D, 15D),
new IrisAxisRotationClamp(true, true, 180D, 180D, 0D));
};
}
private IrisObject randomObject(Random random, int outside) {
int width = 1 + random.nextInt(18);
int height = 1 + random.nextInt(14);
int depth = 1 + random.nextInt(18);
IrisObject object = new IrisObject(width, height, depth);
int count = 1 + random.nextInt(Math.max(1, (width * height * depth) / 3));
for (int index = 0; index < count; index++) {
int x = random.nextInt(width) - (width / 2);
int y = random.nextInt(height) - (height / 2);
int z = random.nextInt(depth) - (depth / 2);
object.getBlocks().put(new IrisBlockVector(x, y, z), state(random.nextInt(5) == 0 ? "minecraft:oak_leaves" : "minecraft:stone"));
}
for (int index = 0; index < outside; index++) {
int x = random.nextInt(width + 10) - ((width + 10) / 2);
int y = random.nextInt(height + 6) - ((height + 6) / 2);
int z = random.nextInt(depth + 10) - ((depth + 10) / 2);
object.getBlocks().put(new IrisBlockVector(x, y, z), state("minecraft:stone"));
}
return object;
}
private IrisObject singleBlockObject(int width, int height, int depth) {
IrisObject object = new IrisObject(width, height, depth);
object.getBlocks().put(new IrisBlockVector(0, 0, 0), state("minecraft:stone"));
return object;
}
private static void assertContains(PlacementReach.Extent extent, IrisObject object, String context) {
object.getBlocks().forEach((position, state) -> {
assertTrue(context + " x " + position.getBlockX() + " outside " + extent.x(),
position.getBlockX() >= extent.x().minimum() && position.getBlockX() <= extent.x().maximum());
assertTrue(context + " y " + position.getBlockY() + " outside " + extent.y(),
position.getBlockY() >= extent.y().minimum() && position.getBlockY() <= extent.y().maximum());
assertTrue(context + " z " + position.getBlockZ() + " outside " + extent.z(),
position.getBlockZ() >= extent.z().minimum() && position.getBlockZ() <= extent.z().maximum());
});
int centerX = object.getCenter().getBlockX();
int centerZ = object.getCenter().getBlockZ();
assertTrue(context + " frame x", -centerX >= extent.x().minimum() && object.getW() - 1 - centerX <= extent.x().maximum());
assertTrue(context + " frame z", -centerZ >= extent.z().minimum() && object.getD() - 1 - centerZ <= extent.z().maximum());
}
private static IrisObjectRotation rotation(IrisAxisRotationClamp x, IrisAxisRotationClamp y, IrisAxisRotationClamp z) {
IrisObjectRotation rotation = new IrisObjectRotation();
rotation.setEnabled(true);
rotation.setXAxis(x);
rotation.setYAxis(y);
rotation.setZAxis(z);
return rotation;
}
private NativeBlockState state(String key) {
return states.computeIfAbsent(key, ignored -> {
String material = key.toLowerCase(Locale.ROOT);
boolean air = material.endsWith("air");
NativeBlockState state = mock(NativeBlockState.class, withSettings().stubOnly());
when(state.key()).thenReturn(key);
when(state.materialKey()).thenReturn(material);
when(state.isAir()).thenReturn(air);
when(state.isSolid()).thenReturn(!air);
when(state.isOccluding()).thenReturn(!air);
return state;
});
}
private static final class RecordingPlacer implements IObjectPlacer {
private final Engine engine;
private int writeReach;
private int readReach;
private RecordingPlacer(Engine engine) {
this.engine = engine;
}
private void write(int x, int z) {
writeReach = Math.max(writeReach, Math.max(Math.abs(x - ORIGIN_X), Math.abs(z - ORIGIN_Z)));
read(x, z);
}
private void read(int x, int z) {
readReach = Math.max(readReach, Math.max(Math.abs(x - ORIGIN_X), Math.abs(z - ORIGIN_Z)));
}
@Override
public int getHighest(int x, int z, IrisData data) {
read(x, z);
return GROUND_Y;
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
read(x, z);
return GROUND_Y;
}
@Override
public void set(int x, int y, int z, NativeBlockState d) {
write(x, z);
}
@Override
public NativeBlockState get(int x, int y, int z) {
read(x, z);
return null;
}
@Override
public boolean isPreventingDecay() {
return false;
}
@Override
public boolean isCarved(int x, int y, int z) {
read(x, z);
return false;
}
@Override
public boolean isSolid(int x, int y, int z) {
read(x, z);
return y < GROUND_Y;
}
@Override
public boolean isUnderwater(int x, int z) {
read(x, z);
return false;
}
@Override
public int getFluidHeight() {
return GROUND_Y - 1;
}
@Override
public boolean isDebugSmartBore() {
return false;
}
@Override
public void setTile(int xx, int yy, int zz, TileData tile) {
write(xx, zz);
}
@Override
public <T> void setData(int xx, int yy, int zz, T data) {
write(xx, zz);
}
@Override
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
read(xx, zz);
return null;
}
@Override
public Engine getEngine() {
return engine;
}
}
}