Whoops Core

This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-08 16:15:19 -04:00
1 parent 98e51cba8d
commit 0782bbf0b4
31 files changed
+1178 -190

No files matched your search

@@ -38,6 +38,7 @@ import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement; import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectPlacementScaleInterpolator; import art.arcane.iris.engine.object.IrisObjectPlacementScaleInterpolator;
import art.arcane.iris.engine.object.IrisObjectRotation; import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.IrisObjectScale;
import art.arcane.iris.engine.object.TileData; import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.platform.bukkit.BukkitBlockState; import art.arcane.iris.platform.bukkit.BukkitBlockState;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
@@ -51,6 +52,7 @@ import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param; import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler; import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
import art.arcane.iris.util.common.director.specialhandlers.ObjectScaleHandler;
import art.arcane.iris.util.common.director.specialhandlers.ObjectTargetHandler; import art.arcane.iris.util.common.director.specialhandlers.ObjectTargetHandler;
import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.math.Direction; import art.arcane.iris.util.common.math.Direction;
@@ -551,17 +553,21 @@ public class CommandObject implements DirectorExecutor {
boolean edit, boolean edit,
@Param(description = "The amount of degrees to rotate by", descriptionKey = "iris.director.commandobject.param.amount_degrees_rotate_by", defaultValue = "0") @Param(description = "The amount of degrees to rotate by", descriptionKey = "iris.director.commandobject.param.amount_degrees_rotate_by", defaultValue = "0")
int rotate, int rotate,
@Param(description = "The factor by which to scale the object placement", descriptionKey = "iris.director.commandobject.param.factor_by_which_scale_object_placement", defaultValue = "1") @Param(description = "Explicit scale factor, or dimension to inherit the current Iris world's factor", descriptionKey = "iris.director.commandobject.param.factor_by_which_scale_object_placement", defaultValue = "dimension", customHandler = ObjectScaleHandler.class)
double scale Double scale
// , // ,
// @Param(description = "The scale interpolator to use", descriptionKey = "iris.director.commandobject.param.scale_interpolator_use", defaultValue = "none") // @Param(description = "The scale interpolator to use", descriptionKey = "iris.director.commandobject.param.scale_interpolator_use", defaultValue = "none")
// IrisObjectPlacementScaleInterpolator interpolator // IrisObjectPlacementScaleInterpolator interpolator
) { ) {
IrisObject o = IrisData.loadAnyObject(object, data()); IrisObject o = IrisData.loadAnyObject(object, data());
Engine placementEngine = engine();
double factor = scale == null
? placementEngine == null ? 1D : placementEngine.getDimension().getAllObjectScaleFactor()
: IrisObjectScale.requireValidFactor(scale, "scale");
double maxScale = Double.max(10 - o.getBlocks().size() / 10000d, 1); double maxScale = Double.max(10 - o.getBlocks().size() / 10000d, 1);
if (scale > maxScale) { if (factor > maxScale) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_INDICATED_SCALE_EXCEEDS_MAXIMUM_DOWNSCALED_MAXIMUM, MessageArgument.untrusted("maxScale", maxScale))); sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_INDICATED_SCALE_EXCEEDS_MAXIMUM_DOWNSCALED_MAXIMUM, MessageArgument.untrusted("maxScale", maxScale)));
scale = maxScale; factor = maxScale;
} }
IrisObjectPlacement placement = new IrisObjectPlacement(); IrisObjectPlacement placement = new IrisObjectPlacement();
@@ -581,14 +587,16 @@ public class CommandObject implements DirectorExecutor {
Map<Block, BlockData> futureChanges = new HashMap<>(); Map<Block, BlockData> futureChanges = new HashMap<>();
if (scale != 1) { if (factor != 1D) {
o = o.scaled(scale, IrisObjectPlacementScaleInterpolator.TRICUBIC); o = scale == null ? IrisObjectScale.getFixed(o, factor)
: new IrisObjectScale().setSize(factor)
.setInterpolation(IrisObjectPlacementScaleInterpolator.TRICUBIC).get(new RNG(), o);
} }
// Block writes must run on the thread owning the target chunk; the undo log stays global. // Block writes must run on the thread owning the target chunk; the undo log stays global.
final IrisObject placed = o; final IrisObject placed = o;
if (!J.runAt(block, () -> { if (!J.runAt(block, () -> {
placed.place(block.getBlockX(), block.getBlockY() + (int) placed.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges, null), placement, new RNG(), null); placed.place(block.getBlockX(), block.getBlockY() + (int) placed.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges, null), placement, new RNG(), placed.getLoader());
J.runGlobal(() -> Iris.service(ObjectSVC.class).addChanges(futureChanges)); J.runGlobal(() -> Iris.service(ObjectSVC.class).addChanges(futureChanges));
if (!edit) { if (!edit) {
@@ -415,7 +415,8 @@ public final class ModdedObjectCommands {
placement.setRotation(IrisObjectRotation.of(0, rotation, 0)); placement.setRotation(IrisObjectRotation.of(0, rotation, 0));
ModdedObjectPlacer placer = new ModdedObjectPlacer(level, engine); ModdedObjectPlacer placer = new ModdedObjectPlacer(level, engine);
try { try {
object.place(target.getX(), target.getY() + object.getCenter().getY(), target.getZ(), placer, placement, new RNG(), null); object = placement.scaleObject(new RNG(), object, engine == null ? null : engine.getDimension());
object.place(target.getX(), target.getY() + object.getCenter().getY(), target.getZ(), placer, placement, new RNG(), object.getLoader());
} catch (Throwable e) { } catch (Throwable e) {
ModdedIrisLog.error("Iris paste failed for {}", key, e); ModdedIrisLog.error("Iris paste failed for {}", key, e);
ModdedObjectUndo.record(player == null ? ModdedObjectUndo.CONSOLE : player.getUUID(), level, placer.undoSnapshot()); ModdedObjectUndo.record(player == null ? ModdedObjectUndo.CONSOLE : player.getUUID(), level, placer.undoSnapshot());
@@ -418,7 +418,7 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
JSONObject parsed = new JSONObject(rawText); JSONObject parsed = new JSONObject(rawText);
JsonSchemaValidator.validateTopLevelKeys(parsed, rawText, j, resourceTypeName, objectClass); JsonSchemaValidator.validateTopLevelKeys(parsed, rawText, j, resourceTypeName, objectClass);
if (objectClass == IrisDimension.class) { if (objectClass == IrisDimension.class) {
PackValidator.requireValidStaticObjects(manager.getDataFolder(), name, parsed); PackValidator.requireValidObjectSettings(manager.getDataFolder(), name, parsed);
} }
T t = getManager().getGson() T t = getManager().getGson()
.fromJson(preprocess(parsed).toString(0), objectClass); .fromJson(preprocess(parsed).toString(0), objectClass);
@@ -19,6 +19,7 @@
package art.arcane.iris.core.pack; package art.arcane.iris.core.pack;
import art.arcane.iris.engine.object.IrisDimensionType; import art.arcane.iris.engine.object.IrisDimensionType;
import art.arcane.iris.engine.object.IrisObjectScale;
import art.arcane.iris.engine.object.IrisWorldBoundary; import art.arcane.iris.engine.object.IrisWorldBoundary;
import art.arcane.volmlib.util.json.JSONArray; import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject; import art.arcane.volmlib.util.json.JSONObject;
@@ -57,6 +58,7 @@ final class PackDimensionValidator {
validateDimensionHeights(packFolder, dimensionKey, dimJson, blockingErrors); validateDimensionHeights(packFolder, dimensionKey, dimJson, blockingErrors);
validateWorldBoundary(dimensionKey, dimJson, blockingErrors); validateWorldBoundary(dimensionKey, dimJson, blockingErrors);
validateStaticObjects(packFolder, dimensionKey, dimJson, blockingErrors); validateStaticObjects(packFolder, dimensionKey, dimJson, blockingErrors);
validateObjectScaleFactor(dimensionKey, dimJson, blockingErrors);
validateDimensionStack(packFolder, dimensionKey, dimJson, dimensionKeys, blockingErrors); validateDimensionStack(packFolder, dimensionKey, dimJson, dimensionKeys, blockingErrors);
JSONArray regionsArray = dimJson.optJSONArray("regions"); JSONArray regionsArray = dimJson.optJSONArray("regions");
@@ -102,6 +104,11 @@ final class PackDimensionValidator {
} }
} }
static void validateObjectScaleFactor(String dimensionKey, JSONObject dimension, List<String> errors) {
validateFiniteNumber(dimension, "allObjectScaleFactor", IrisObjectScale.MINIMUM_FACTOR,
IrisObjectScale.MAXIMUM_FACTOR, "Dimension '" + dimensionKey + "'", errors);
}
static void validateDimensionStack(File packFolder, String dimensionKey, JSONObject dimension, Set<String> dimensionKeys, static void validateDimensionStack(File packFolder, String dimensionKey, JSONObject dimension, Set<String> dimensionKeys,
List<String> blockingErrors) { List<String> blockingErrors) {
if (!dimension.has("dimensionStack")) { if (!dimension.has("dimensionStack")) {
@@ -86,9 +86,10 @@ public final class PackValidator {
return validate(packFolder, false); return validate(packFolder, false);
} }
public static void requireValidStaticObjects(File packFolder, String dimensionKey, JSONObject dimension) { public static void requireValidObjectSettings(File packFolder, String dimensionKey, JSONObject dimension) {
List<String> errors = new ArrayList<>(); List<String> errors = new ArrayList<>();
PackDimensionValidator.validateStaticObjects(packFolder, dimensionKey, dimension, errors); PackDimensionValidator.validateStaticObjects(packFolder, dimensionKey, dimension, errors);
PackDimensionValidator.validateObjectScaleFactor(dimensionKey, dimension, errors);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
throw new IllegalArgumentException(String.join("; ", errors)); throw new IllegalArgumentException(String.join("; ", errors));
} }
@@ -150,7 +150,8 @@ public class TreeSVC implements IrisService {
} }
saplingPlane.forEach(block -> block.setType(Material.AIR)); saplingPlane.forEach(block -> block.setType(Material.AIR));
IrisObject object = worldAccess.getData().getObjectLoader().load(pool.getRandom(RNG.r)); IrisObject object = placement.scaleObject(RNG.r,
worldAccess.getData().getObjectLoader().load(pool.getRandom(RNG.r)), dimension);
if (object == null) { if (object == null) {
return; return;
@@ -477,8 +477,9 @@ public class IrisStructureComponent extends IrisMantleComponent {
if (object == null) { if (object == null) {
return; return;
} }
int horizontalSpan = Math.max(1, Math.max(object.getW(), object.getD())); double scale = Math.max(1D, getDimension().getAllObjectScaleFactor());
int verticalSpan = Math.max(1, object.getH()); int horizontalSpan = scaledMarkerSpan(Math.max(object.getW(), object.getD()), scale);
int verticalSpan = scaledMarkerSpan(object.getH(), scale);
ObjectMarkerBounds search = intersections.expand(horizontalSpan - 1, verticalSpan - 1, worldHeight); ObjectMarkerBounds search = intersections.expand(horizontalSpan - 1, verticalSpan - 1, worldHeight);
KList<ObjectBlockPosition> positions = new KList<>(); KList<ObjectBlockPosition> positions = new KList<>();
int minChunkX = Math.floorDiv(search.minX, 16); int minChunkX = Math.floorDiv(search.minX, 16);
@@ -516,6 +517,11 @@ public class IrisStructureComponent extends IrisMantleComponent {
return decoded != null && !decoded.structureAware(); return decoded != null && !decoded.structureAware();
} }
static int scaledMarkerSpan(int dimension, double scale) {
return scale <= 1D ? Math.max(1, dimension)
: (int) Math.ceil(Math.max(1, dimension) * scale);
}
static ObjectMarkerBounds markerSearchBounds(int minX, int minY, int minZ, static ObjectMarkerBounds markerSearchBounds(int minX, int minY, int minZ,
int maxX, int maxY, int maxZ, int maxX, int maxY, int maxZ,
int horizontalSpan, int verticalSpan, int horizontalSpan, int verticalSpan,
@@ -188,7 +188,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (raw == null) { if (raw == null) {
continue; continue;
} }
IrisObject obj0 = placement.getScale().get(rng, raw); IrisObject obj0 = placement.scaleObject(rng, raw, getDimension());
if (obj0 == null) { if (obj0 == null) {
continue; continue;
} }
@@ -248,7 +248,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (raw == null) { if (raw == null) {
continue; continue;
} }
IrisObject obj0 = placement.getScale().get(rng, raw); IrisObject obj0 = placement.scaleObject(rng, raw, getDimension());
if (obj0 == null) { if (obj0 == null) {
continue; continue;
} }
@@ -340,7 +340,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (raw == null) { if (raw == null) {
continue; continue;
} }
IrisObject obj0 = placement.getScale().get(rng, raw); IrisObject obj0 = placement.scaleObject(rng, raw, getDimension());
if (obj0 == null) { if (obj0 == null) {
continue; continue;
} }
@@ -628,7 +628,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
size = IrisObject.sampleSize(file); size = IrisObject.sampleSize(file);
sizeCache.put(objectKey, size); sizeCache.put(objectKey, size);
} }
int reach = MantleObjectComponent.calculatePlacementReach(size, placement); int reach = MantleObjectComponent.calculatePlacementReach(size, placement, placement.getMaximumScale(getDimension()));
if (reach > 128 && warnedLargeObjects.add(objectKey)) { if (reach > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Floating object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!"); IrisLogging.warn("Floating object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
} }
@@ -622,7 +622,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
IObjectPlacer placer = golden ? new GoldenDebugObjectPlacer(basePlacer, scope + "/" + p.getName()) : basePlacer; IObjectPlacer placer = golden ? new GoldenDebugObjectPlacer(basePlacer, scope + "/" + p.getName()) : basePlacer;
int density = Math.max(1, p.getDensity()); int density = Math.max(1, p.getDensity());
for (int i = 0; i < density; i++) { for (int i = 0; i < density; i++) {
IrisObject variant = p.getVariantObject(getData(), rng); IrisObject variant = placement.scaleObject(rng, p.getVariantObject(getData(), rng), getDimension());
if (variant == null) { if (variant == null) {
if (golden) { if (golden) {
IrisLogging.debug("Goldendebug procedural pick: chunk=" + x + "," + z IrisLogging.debug("Goldendebug procedural pick: chunk=" + x + "," + z
@@ -887,7 +887,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
for (int i = 0; i < density; i++) { for (int i = 0; i < density; i++) {
attempts++; attempts++;
IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(complex, rng)); IrisObject v = objectPlacement.scaleObject(rng, objectPlacement.getObject(complex, rng), getDimension());
if (v == null) { if (v == null) {
nullObjects++; nullObjects++;
if (traceRegen) { if (traceRegen) {
@@ -1030,7 +1030,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
for (int i = 0; i < density; i++) { for (int i = 0; i < density; i++) {
attempts++; attempts++;
IrisObject object = objectPlacement.getScale().get(rng, objectPlacement.getObject(complex, rng)); IrisObject object = objectPlacement.scaleObject(rng, objectPlacement.getObject(complex, rng), getDimension());
if (object == null) { if (object == null) {
nullObjects++; nullObjects++;
if (traceRegen) { if (traceRegen) {
@@ -1261,7 +1261,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int density = objectPlacement.getDensity(rng, minX, minZ, getData()); int density = objectPlacement.getDensity(rng, minX, minZ, getData());
for (int i = 0; i < density; i++) { for (int i = 0; i < density; i++) {
IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(complex, rng)); IrisObject v = objectPlacement.scaleObject(rng, objectPlacement.getObject(upperCtx, rng), upperCtx.getDimension());
if (v == null) { if (v == null) {
continue; continue;
} }
@@ -1647,7 +1647,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
private Set<String> guessPlacedKeys(RNG rng, int x, int z, IrisObjectPlacement objectPlacement) { private Set<String> guessPlacedKeys(RNG rng, int x, int z, IrisObjectPlacement objectPlacement) {
Set<String> f = new KSet<>(); Set<String> f = new KSet<>();
for (int i = 0; i < objectPlacement.getDensity(rng, x, z, getData()); i++) { for (int i = 0; i < objectPlacement.getDensity(rng, x, z, getData()); i++) {
IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(getComplex(), rng)); IrisObject v = objectPlacement.scaleObject(rng, objectPlacement.getObject(getComplex(), rng), getDimension());
if (v == null) { if (v == null) {
continue; continue;
} }
@@ -1708,11 +1708,12 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
radius = Math.max(radius, computePlacementRadius( radius = Math.max(radius, computePlacementRadius(
region.getObjects(), region.getObjects(),
dimension,
dataProvider, dataProvider,
sizeCache, sizeCache,
warnedLargeObjects warnedLargeObjects
)); ));
radius = Math.max(radius, computeProceduralRadius(region.getProceduralObjects(), dataProvider)); radius = Math.max(radius, computeProceduralRadius(region.getProceduralObjects(), dimension, dataProvider));
} }
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) { for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
if (biome == null) { if (biome == null) {
@@ -1720,17 +1721,19 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
radius = Math.max(radius, computePlacementRadius( radius = Math.max(radius, computePlacementRadius(
biome.getObjects(), biome.getObjects(),
dimension,
dataProvider, dataProvider,
sizeCache, sizeCache,
warnedLargeObjects warnedLargeObjects
)); ));
radius = Math.max(radius, computeProceduralRadius(biome.getProceduralObjects(), dataProvider)); radius = Math.max(radius, computeProceduralRadius(biome.getProceduralObjects(), dimension, dataProvider));
} }
return radius; return radius;
} }
private int computePlacementRadius( private int computePlacementRadius(
KList<IrisObjectPlacement> placements, KList<IrisObjectPlacement> placements,
IrisDimension dimension,
DataProvider dataProvider, DataProvider dataProvider,
KMap<String, IrisBlockVector> sizeCache, KMap<String, IrisBlockVector> sizeCache,
KSet<String> warnedLargeObjects KSet<String> warnedLargeObjects
@@ -1746,7 +1749,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (size == null) { if (size == null) {
continue; continue;
} }
int reach = calculatePlacementReach(size, placement); int reach = calculatePlacementReach(size, placement, placement.getMaximumScale(dimension));
if (reach > 128 && warnedLargeObjects.add(objectKey)) { if (reach > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!"); IrisLogging.warn("Object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
} }
@@ -1759,7 +1762,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
return radius; return radius;
} }
private int computeProceduralRadius(IrisProceduralObjects procedural, DataProvider dataProvider) { private int computeProceduralRadius(IrisProceduralObjects procedural, IrisDimension dimension, DataProvider dataProvider) {
if (procedural == null || procedural.isEmpty()) { if (procedural == null || procedural.isEmpty()) {
return 0; return 0;
} }
@@ -1778,13 +1781,13 @@ public class MantleObjectComponent extends IrisMantleComponent {
continue; continue;
} }
IrisBlockVector size = new IrisBlockVector(variant.getW(), variant.getH(), variant.getD()); IrisBlockVector size = new IrisBlockVector(variant.getW(), variant.getH(), variant.getD());
radius = Math.max(radius, calculatePlacementReach(size, objectPlacement)); radius = Math.max(radius, calculatePlacementReach(size, objectPlacement, objectPlacement.getMaximumScale(dimension)));
} }
} }
return radius; return radius;
} }
static int calculatePlacementReach(IrisBlockVector size, IrisObjectPlacement placement) { static int calculatePlacementReach(IrisBlockVector size, IrisObjectPlacement placement, double scale) {
if (size == null) { if (size == null) {
return 0; return 0;
} }
@@ -1792,7 +1795,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
return Math.max(Math.abs(size.getBlockX()), Math.abs(size.getBlockZ())); return Math.max(Math.abs(size.getBlockX()), Math.abs(size.getBlockZ()));
} }
double scale = placement.getScale() != null ? Math.max(1D, placement.getScale().getMaxScale()) : 1D;
int width = scaledDimension(size.getBlockX(), scale); int width = scaledDimension(size.getBlockX(), scale);
int height = scaledDimension(size.getBlockY(), scale); int height = scaledDimension(size.getBlockY(), scale);
int depth = scaledDimension(size.getBlockZ(), scale); int depth = scaledDimension(size.getBlockZ(), scale);
@@ -1823,7 +1825,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
int reach = calculatePlacementReach( int reach = calculatePlacementReach(
new IrisBlockVector(object.getW(), object.getH(), object.getD()), new IrisBlockVector(object.getW(), object.getH(), object.getD()),
placement); placement, 1D);
return complex.allowsNewGenerationFootprint( return complex.allowsNewGenerationFootprint(
saturatedOffset(anchorX, -reach), saturatedOffset(anchorX, -reach),
saturatedOffset(anchorZ, -reach), saturatedOffset(anchorZ, -reach),
@@ -1838,10 +1840,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
private static int scaledDimension(int dimension, double scale) { private static int scaledDimension(int dimension, double scale) {
int absoluteDimension = Math.abs(dimension); int absoluteDimension = Math.abs(dimension);
if (scale <= 1D) { if (scale == 1D) {
return absoluteDimension; return absoluteDimension;
} }
return (int) Math.ceil((absoluteDimension * scale) + (scale * 2D)); return (int) Math.ceil(absoluteDimension * scale);
} }
private static int calculateTranslationReach(IrisObjectTranslate translate, IrisObjectRotation rotation, boolean rotateVertically) { private static int calculateTranslationReach(IrisObjectTranslate translate, IrisObjectRotation rotation, boolean rotateVertically) {
@@ -186,6 +186,10 @@ public class IrisDimension extends IrisRegistrant {
@MaxNumber(16) @MaxNumber(16)
@Desc("Minimum surface-support buffer, in blocks, applied to every surface object placement in this dimension. A placement may ask for more but never less.") @Desc("Minimum surface-support buffer, in blocks, applied to every surface object placement in this dimension. A placement may ask for more but never less.")
private int objectSurfaceSupportBuffer = 2; private int objectSurfaceSupportBuffer = 2;
@MinNumber(IrisObjectScale.MINIMUM_FACTOR)
@MaxNumber(IrisObjectScale.MAXIMUM_FACTOR)
@Desc("Default size factor for objects in this dimension. 0.5 halves their size and 2 doubles it. Any explicit object or placement scale, including 1, overrides this factor. Jigsaw structures are excluded.")
private double allObjectScaleFactor = 1D;
@Desc("The world environment") @Desc("The world environment")
private IrisEnvironment environment = IrisEnvironment.NORMAL; private IrisEnvironment environment = IrisEnvironment.NORMAL;
@RegistryListResource(IrisRegion.class) @RegistryListResource(IrisRegion.class)
@@ -319,6 +323,16 @@ public class IrisDimension extends IrisRegistrant {
return staticObjectLayer.aquire(() -> IrisStaticObjectLayer.compile(this, data)); return staticObjectLayer.aquire(() -> IrisStaticObjectLayer.compile(this, data));
} }
public double getAllObjectScaleFactor() {
return IrisObjectScale.requireValidFactor(allObjectScaleFactor, "allObjectScaleFactor");
}
public IrisDimension setAllObjectScaleFactor(double factor) {
allObjectScaleFactor = IrisObjectScale.requireValidFactor(factor, "allObjectScaleFactor");
staticObjectLayer.reset();
return this;
}
/** /**
* Runs the version-content gate over every static object so the pack report lists the entries this server cannot * Runs the version-content gate over every static object so the pack report lists the entries this server cannot
* place. The static layer makes the same decision when it compiles; validation calls this on its detached loader * place. The static layer makes the same decision when it compiles; validation calls this on its detached loader
@@ -162,8 +162,8 @@ public class IrisObjectPlacement {
private KList<IrisObjectReplace> edit = new KList<>(); private KList<IrisObjectReplace> edit = new KList<>();
@Desc("Translate this object's placement") @Desc("Translate this object's placement")
private IrisObjectTranslate translate = new IrisObjectTranslate(); private IrisObjectTranslate translate = new IrisObjectTranslate();
@Desc("Scale Objects") @Desc("Explicit object scale. When omitted, the dimension's allObjectScaleFactor applies. Any authored scale, including size 1, overrides that factor.")
private IrisObjectScale scale = new IrisObjectScale(); private IrisObjectScale scale;
@ArrayType(min = 1, type = IrisObjectLoot.class) @ArrayType(min = 1, type = IrisObjectLoot.class)
@Desc("The loot tables to apply to these objects") @Desc("The loot tables to apply to these objects")
private KList<IrisObjectLoot> loot = new KList<>(); private KList<IrisObjectLoot> loot = new KList<>();
@@ -363,6 +363,18 @@ public class IrisObjectPlacement {
return data.getObjectLoader().load(pool.get(random.nextInt(pool.size()))); return data.getObjectLoader().load(pool.get(random.nextInt(pool.size())));
} }
public IrisObject scaleObject(RNG random, IrisObject object, IrisDimension dimension) {
return scale == null
? IrisObjectScale.getFixed(object, dimension == null ? 1D : dimension.getAllObjectScaleFactor())
: scale.get(random, object);
}
public double getMaximumScale(IrisDimension dimension) {
return scale == null
? dimension == null ? 1D : dimension.getAllObjectScaleFactor()
: scale.getMaxScale();
}
public boolean matches(IrisTreeSize size, TreeType type) { public boolean matches(IrisTreeSize size, TreeType type) {
for (IrisTree i : getTrees()) { for (IrisTree i : getTrees()) {
if (i.matches(size, type)) { if (i.matches(size, type)) {
@@ -125,6 +125,7 @@ final class IrisObjectPlacementRunner {
boolean preventingDecay = placer.isPreventingDecay(); boolean preventingDecay = placer.isPreventingDecay();
boolean waterlogCandidate = config.isWaterloggable() || config.isUnderwater(); boolean waterlogCandidate = config.isWaterloggable() || config.isUnderwater();
boolean rawStructurePiece = config.getMode() == ObjectPlaceMode.STRUCTURE_PIECE; boolean rawStructurePiece = config.getMode() == ObjectPlaceMode.STRUCTURE_PIECE;
boolean paint = yv < 0 && config.getMode() == ObjectPlaceMode.PAINT;
boolean organicFloor = config.getMode() == ObjectPlaceMode.ORGANIC_STILT; boolean organicFloor = config.getMode() == ObjectPlaceMode.ORGANIC_STILT;
boolean ceilingHang = config.getMode() == ObjectPlaceMode.CEILING_HANG; boolean ceilingHang = config.getMode() == ObjectPlaceMode.CEILING_HANG;
boolean organic = organicFloor || ceilingHang; boolean organic = organicFloor || ceilingHang;
@@ -257,7 +258,10 @@ final class IrisObjectPlacementRunner {
} }
} }
} else if (config.getMode().equals(ObjectPlaceMode.PAINT)) { } else if (config.getMode().equals(ObjectPlaceMode.PAINT)) {
y = placer.getHighest(x, z, self.getLoader(), config.isUnderwater()) + rty; int anchorX = x + (translating ? (int) Math.round(translateOffset.getX()) : 0);
int anchorZ = z + (translating ? (int) Math.round(translateOffset.getZ()) : 0);
y = (c != null ? c.getSurface()
: placer.getHighest(anchorX, anchorZ, self.getLoader(), config.isUnderwater())) + rty;
if (!config.isForcePlace()) { if (!config.isForcePlace()) {
if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { if (shouldBailForCarvingAnchor(placer, config, x, y, z)) {
bail = true; bail = true;
@@ -302,12 +306,30 @@ final class IrisObjectPlacementRunner {
return -1; return -1;
} }
IrisPaintSurfaceProjection paintProjection = null;
KList<IrisBlockVector> paintSupport = null;
if (paint) {
int buffer = config.isRequireSurfaceSupport() ? Math.max(0, config.getSurfaceSupportBuffer()) : 0;
int anchorX = x + (translating ? (int) Math.round(translateOffset.getX()) : 0);
int anchorZ = z + (translating ? (int) Math.round(translateOffset.getZ()) : 0);
paintProjection = IrisPaintSurfaceProjection.create(placer, new IrisPaintSurfaceProjection.Request(
self.getLoader(), x + placementBounds.minX() - buffer, x + placementBounds.maxX() + buffer,
z + placementBounds.minZ() - buffer, z + placementBounds.maxZ() + buffer,
anchorX, y - rty, anchorZ, config.isUnderwater(), config.getHeightmap() != null));
if (paintProjection == null) {
return -1;
}
paintSupport = transformedPaintSupport(spin, translateOffset, surfaceWarp, warpHalf, x, y + yrand, z);
}
if (yv < 0 if (yv < 0
&& config.getCarvingSupport().supportsSurface() && config.getCarvingSupport().supportsSurface()
&& config.getMode() != ObjectPlaceMode.FLOATING && config.getMode() != ObjectPlaceMode.FLOATING
&& !rawStructurePiece && !rawStructurePiece
&& IrisSurfaceSupport.intersectsHydrology(oplacer, x, z, config.getTranslate(), && (paint ? IrisSurfaceSupport.intersectsHydrology(oplacer, x, z, null, null,
config.getRotation(), spinx, spiny, spinz, self.getSurfaceSupportOffsets())) { 0, 0, 0, paintSupport)
: IrisSurfaceSupport.intersectsHydrology(oplacer, x, z, config.getTranslate(),
config.getRotation(), spinx, spiny, spinz, self.getSurfaceSupportOffsets()))) {
return -1; return -1;
} }
@@ -325,9 +347,12 @@ final class IrisObjectPlacementRunner {
&& !config.isUnderwater() && !config.isUnderwater()
&& !config.isOnwater() && !config.isOnwater()
&& config.isRequireSurfaceSupport() && config.isRequireSurfaceSupport()
&& IrisSurfaceSupport.isUnsupported(oplacer, self.getLoader(), x, z, config.getTranslate(), && (paint ? IrisSurfaceSupport.isUnsupported(oplacer, self.getLoader(), x, z, null, null,
0, 0, 0, paintSupport, config.getSurfaceSupportBuffer(), config.getSurfaceSupportDepth(),
paintProjection)
: IrisSurfaceSupport.isUnsupported(oplacer, self.getLoader(), x, z, config.getTranslate(),
config.getRotation(), spinx, spiny, spinz, self.getSurfaceSupportOffsets(), config.getRotation(), spinx, spiny, spinz, self.getSurfaceSupportOffsets(),
config.getSurfaceSupportBuffer(), config.getSurfaceSupportDepth())) { config.getSurfaceSupportBuffer(), config.getSurfaceSupportDepth()))) {
return -1; return -1;
} }
@@ -351,22 +376,27 @@ final class IrisObjectPlacementRunner {
} }
if (!rawStructurePiece && nativeStructureVetoes(placer, config, spin, placementBounds, translating, translateOffset, ceilingHang, if (!rawStructurePiece && nativeStructureVetoes(placer, config, spin, placementBounds, translating, translateOffset, ceilingHang,
yv < 0 && config.getMode() == ObjectPlaceMode.PAINT, warpMargin, x, y + yrand, z)) { paintProjection, surfaceWarp, warpHalf, yrand, warpMargin, x, y + yrand, z)) {
return -1; return -1;
} }
boolean paint = yv < 0 && config.getMode() == ObjectPlaceMode.PAINT;
WorldBounds worldBounds = null; WorldBounds worldBounds = null;
if (config.isBore() || (!config.isForcePlace() && !rawStructurePiece if (config.isBore() || (!config.isForcePlace() && !rawStructurePiece
&& (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty()))) { && (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty()))) {
worldBounds = resolveWorldBounds(placer, config, placementBounds, paint, x, y + yrand, z); worldBounds = resolveWorldBounds(placementBounds, paintProjection, yrand, x, y + yrand, z);
} }
if (!config.isForcePlace() && !rawStructurePiece && (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty())) { if (!config.isForcePlace() && !rawStructurePiece && (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty())) {
Engine engine = requireDataEngine(rdata, "collision settings"); Engine engine = requireDataEngine(rdata, "collision settings");
for (int i = worldBounds.minX(); i <= worldBounds.maxX(); i++) { for (int i = worldBounds.minX(); i <= worldBounds.maxX(); i++) {
for (int j = worldBounds.minY(); j <= worldBounds.maxY(); j++) { for (int k = worldBounds.minZ(); k <= worldBounds.maxZ(); k++) {
for (int k = worldBounds.minZ(); k <= worldBounds.maxZ(); k++) { int surface = paint ? paintProjection.surfaceY(i, k) : 0;
if (surface == IrisPaintSurfaceProjection.MISSING) {
continue;
}
int minimumY = paint ? surface + placementBounds.minY() + Math.floorDiv(self.h, 2) + yrand : worldBounds.minY();
int maximumY = paint ? surface + placementBounds.maxY() + Math.floorDiv(self.h, 2) + yrand : worldBounds.maxY();
for (int j = minimumY; j <= maximumY; j++) {
String placementMarker = placer.getData(i, j, k, String.class); String placementMarker = placer.getData(i, j, k, String.class);
PlacedObject p = engine.resolveObjectPlacementMarker(i, k, placementMarker); PlacedObject p = engine.resolveObjectPlacementMarker(i, k, placementMarker);
if (p == null) continue; if (p == null) continue;
@@ -387,8 +417,14 @@ final class IrisObjectPlacementRunner {
if (config.isBore()) { if (config.isBore()) {
for (int i = worldBounds.minX(); i <= worldBounds.maxX(); i++) { for (int i = worldBounds.minX(); i <= worldBounds.maxX(); i++) {
for (int j = worldBounds.minY() - config.getBoreExtendMinY(); j <= worldBounds.maxY() + config.getBoreExtendMaxY(); j++) { for (int k = worldBounds.minZ(); k <= worldBounds.maxZ(); k++) {
for (int k = worldBounds.minZ(); k <= worldBounds.maxZ(); k++) { int surface = paint ? paintProjection.surfaceY(i, k) : 0;
if (surface == IrisPaintSurfaceProjection.MISSING) {
continue;
}
int minimumY = paint ? surface + placementBounds.minY() + Math.floorDiv(self.h, 2) + yrand : worldBounds.minY();
int maximumY = paint ? surface + placementBounds.maxY() + Math.floorDiv(self.h, 2) + yrand : worldBounds.maxY();
for (int j = minimumY - config.getBoreExtendMinY(); j <= maximumY + config.getBoreExtendMaxY(); j++) {
placer.set(i, j, k, IrisObject.States.air()); placer.set(i, j, k, IrisObject.States.air());
} }
} }
@@ -546,8 +582,14 @@ final class IrisObjectPlacementRunner {
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x); zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x);
} }
if (yv < 0 && (config.getMode().equals(ObjectPlaceMode.PAINT)) && !B.isVineBlock(data)) { if (paint) {
yy = (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + placer.getHighest(xx, zz, self.getLoader(), config.isUnderwater()); int surface = paintProjection.surfaceY(xx, zz);
if (surface == IrisPaintSurfaceProjection.MISSING) {
continue;
}
if (!B.isVineBlock(data)) {
yy = (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + surface + yrand;
}
} }
if (config.isMeld() && !rawStructurePiece && !placer.isSolid(xx, yy, zz)) { if (config.isMeld() && !rawStructurePiece && !placer.isSolid(xx, yy, zz)) {
@@ -864,6 +906,26 @@ final class IrisObjectPlacementRunner {
return !wouldReplace && (rawStructurePiece || !air); return !wouldReplace && (rawStructurePiece || !air);
} }
private KList<IrisBlockVector> transformedPaintSupport(SpinKernel spin, IrisBlockVector translateOffset,
CNG warp, double half, int x, int y, int z) {
KList<IrisBlockVector> offsets = new KList<>();
for (IrisBlockVector source : self.getSurfaceSupportOffsets()) {
IrisBlockVector offset = source.clone();
spin.rotate(offset);
if (translateOffset != null) {
offset.add(translateOffset);
}
int worldX = x + (int) Math.round(offset.getX());
int worldZ = z + (int) Math.round(offset.getZ());
if (warp != null) {
worldX += warp.fitDouble(-half, half, offset.getX() + x, offset.getY() + y, offset.getZ() + z);
worldZ += warp.fitDouble(-half, half, offset.getZ() + z, offset.getY() + y, offset.getX() + x);
}
offsets.add(new IrisBlockVector(worldX - x, 0, worldZ - z));
}
return offsets;
}
private TransformedBounds transformedBounds(SpinKernel spin, boolean translating, IrisBlockVector translateOffset, private TransformedBounds transformedBounds(SpinKernel spin, boolean translating, IrisBlockVector translateOffset,
boolean ceilingHang, int margin) { boolean ceilingHang, int margin) {
int sourceMinX = -self.getCenter().getBlockX(); int sourceMinX = -self.getCenter().getBlockX();
@@ -909,13 +971,13 @@ final class IrisObjectPlacementRunner {
return new TransformedBounds(minX - margin, maxX + margin, minY, maxY, minZ - margin, maxZ + margin); return new TransformedBounds(minX - margin, maxX + margin, minY, maxY, minZ - margin, maxZ + margin);
} }
private WorldBounds resolveWorldBounds(IObjectPlacer placer, IrisObjectPlacement config, TransformedBounds bounds, private WorldBounds resolveWorldBounds(TransformedBounds bounds, IrisPaintSurfaceProjection projection,
boolean paint, int x, int y, int z) { int randomY, int x, int y, int z) {
int minX = x + bounds.minX(); int minX = x + bounds.minX();
int maxX = x + bounds.maxX(); int maxX = x + bounds.maxX();
int minZ = z + bounds.minZ(); int minZ = z + bounds.minZ();
int maxZ = z + bounds.maxZ(); int maxZ = z + bounds.maxZ();
if (!paint) { if (projection == null) {
return new WorldBounds(minX, maxX, y + bounds.minY(), y + bounds.maxY(), minZ, maxZ); return new WorldBounds(minX, maxX, y + bounds.minY(), y + bounds.maxY(), minZ, maxZ);
} }
@@ -923,12 +985,15 @@ final class IrisObjectPlacementRunner {
int maximumSurface = Integer.MIN_VALUE; int maximumSurface = Integer.MIN_VALUE;
for (int worldX = minX; worldX <= maxX; worldX++) { for (int worldX = minX; worldX <= maxX; worldX++) {
for (int worldZ = minZ; worldZ <= maxZ; worldZ++) { for (int worldZ = minZ; worldZ <= maxZ; worldZ++) {
int surface = placer.getHighest(worldX, worldZ, self.getLoader(), config.isUnderwater()); int surface = projection.surfaceY(worldX, worldZ);
if (surface == IrisPaintSurfaceProjection.MISSING) {
continue;
}
minimumSurface = Math.min(minimumSurface, surface); minimumSurface = Math.min(minimumSurface, surface);
maximumSurface = Math.max(maximumSurface, surface); maximumSurface = Math.max(maximumSurface, surface);
} }
} }
int paintOffset = Math.floorDiv(self.h, 2); int paintOffset = Math.floorDiv(self.h, 2) + randomY;
return new WorldBounds(minX, maxX, minimumSurface + bounds.minY() + paintOffset, return new WorldBounds(minX, maxX, minimumSurface + bounds.minY() + paintOffset,
maximumSurface + bounds.maxY() + paintOffset, minZ, maxZ); maximumSurface + bounds.maxY() + paintOffset, minZ, maxZ);
} }
@@ -960,7 +1025,8 @@ final class IrisObjectPlacementRunner {
*/ */
private boolean nativeStructureVetoes(IObjectPlacer placer, IrisObjectPlacement config, SpinKernel spin, TransformedBounds bounds, private boolean nativeStructureVetoes(IObjectPlacer placer, IrisObjectPlacement config, SpinKernel spin, TransformedBounds bounds,
boolean translating, IrisBlockVector translateOffset, boolean ceilingHang, boolean translating, IrisBlockVector translateOffset, boolean ceilingHang,
boolean paint, int warpMargin, int x, int y, int z) { IrisPaintSurfaceProjection projection, CNG surfaceWarp, double warpHalf,
int randomY, int warpMargin, int x, int y, int z) {
Engine engine = placer.getEngine(); Engine engine = placer.getEngine();
if (engine == null) { if (engine == null) {
return false; return false;
@@ -976,8 +1042,8 @@ final class IrisObjectPlacementRunner {
} }
int worldY = y + engine.getMinHeight(); int worldY = y + engine.getMinHeight();
int envelopeMinY = paint ? Integer.MIN_VALUE : worldY + bounds.minY() - warpMargin; int envelopeMinY = projection != null ? Integer.MIN_VALUE : worldY + bounds.minY() - warpMargin;
int envelopeMaxY = paint ? Integer.MAX_VALUE : worldY + bounds.maxY() + warpMargin; int envelopeMaxY = projection != null ? Integer.MAX_VALUE : worldY + bounds.maxY() + warpMargin;
boolean envelopeMeetsPiece = false; boolean envelopeMeetsPiece = false;
for (NativeStructureVolume volume : volumes) { for (NativeStructureVolume volume : volumes) {
if (volume.intersects(minX, envelopeMinY, minZ, maxX, envelopeMaxY, maxZ)) { if (volume.intersects(minX, envelopeMinY, minZ, maxX, envelopeMaxY, maxZ)) {
@@ -1010,14 +1076,24 @@ final class IrisObjectPlacementRunner {
int xx = x + (int) Math.round(i.getX()); int xx = x + (int) Math.round(i.getX());
int zz = z + (int) Math.round(i.getZ()); int zz = z + (int) Math.round(i.getZ());
int yy = paint int yy = y + (int) Math.round(i.getY());
? (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) if (projection != null) {
+ placer.getHighest(xx, zz, self.getLoader(), config.isUnderwater()) if (surfaceWarp != null) {
: y + (int) Math.round(i.getY()); xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z);
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x);
}
int surface = projection.surfaceY(xx, zz);
if (surface == IrisPaintSurfaceProjection.MISSING) {
continue;
}
if (!B.isVineBlock(state)) {
yy = (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + surface + randomY;
}
}
int worldBlockY = yy + engine.getMinHeight(); int worldBlockY = yy + engine.getMinHeight();
for (NativeStructureVolume volume : volumes) { for (NativeStructureVolume volume : volumes) {
if (volume.containsWithin(xx, worldBlockY, zz, warpMargin)) { if (volume.containsWithin(xx, worldBlockY, zz, projection == null ? warpMargin : 0)) {
return true; return true;
} }
} }
@@ -43,6 +43,8 @@ import java.util.Map;
@Desc("Scale objects") @Desc("Scale objects")
@Data @Data
public class IrisObjectScale { public class IrisObjectScale {
public static final double MINIMUM_FACTOR = 0.01D;
public static final double MAXIMUM_FACTOR = 50D;
private static final long CACHE_MIN_ESTIMATED_BYTES = 16L * 1024L * 1024L; private static final long CACHE_MIN_ESTIMATED_BYTES = 16L * 1024L * 1024L;
private static final long CACHE_MAX_ESTIMATED_BYTES = 64L * 1024L * 1024L; private static final long CACHE_MAX_ESTIMATED_BYTES = 64L * 1024L * 1024L;
private static final long CACHE_ENTRY_ESTIMATED_BYTES = 512L; private static final long CACHE_ENTRY_ESTIMATED_BYTES = 512L;
@@ -88,15 +90,32 @@ public class IrisObjectScale {
return minimumScale != 1 || maximumScale != 1; return minimumScale != 1 || maximumScale != 1;
} }
public IrisObjectScale setSize(double size) {
this.size = requireValidFactor(size, "size");
return this;
}
public IrisObjectScale setMinimumScale(double minimumScale) {
this.minimumScale = requireValidFactor(minimumScale, "minimumScale");
return this;
}
public IrisObjectScale setMaximumScale(double maximumScale) {
this.maximumScale = requireValidFactor(maximumScale, "maximumScale");
return this;
}
public int getMaxSizeFor(int indim) { public int getMaxSizeFor(int indim) {
return (int) Math.ceil(getMaxScale() * indim); return (int) Math.ceil(getMaxScale() * indim);
} }
public double getMaxScale() { public double getMaxScale() {
if (size != 1) { ScaleRequest request = snapshotRequest();
return size; if (!request.shouldScale()) {
return 1D;
} }
return Math.max(minimumScale, maximumScale); return request.size() != 1D ? request.size()
: Math.max(request.minimumScale(), request.maximumScale());
} }
public IrisObject get(RNG rng, IrisObject origin) { public IrisObject get(RNG rng, IrisObject origin) {
@@ -109,7 +128,27 @@ public class IrisObjectScale {
return origin; return origin;
} }
int variantIndex = request.selectVariant(rng); return get(origin, request, request.selectVariant(rng));
}
public static IrisObject getFixed(IrisObject origin, double factor) {
requireValidFactor(factor, "Object scale");
if (origin == null || factor == 1D) {
return origin;
}
return get(origin, new ScaleRequest(factor, 1D, 1D, 7,
IrisObjectPlacementScaleInterpolator.NONE), 0);
}
public static double requireValidFactor(double factor, String name) {
if (!Double.isFinite(factor) || factor < MINIMUM_FACTOR || factor > MAXIMUM_FACTOR) {
throw new IllegalArgumentException(name + " must be finite and between "
+ MINIMUM_FACTOR + " and " + MAXIMUM_FACTOR + ".");
}
return factor;
}
private static IrisObject get(IrisObject origin, ScaleRequest request, int variantIndex) {
CacheKey key = CACHE.key(origin, request, variantIndex); CacheKey key = CACHE.key(origin, request, variantIndex);
CacheLookup lookup = CACHE.lookup(key); CacheLookup lookup = CACHE.lookup(key);
if (lookup.variant() != null) { if (lookup.variant() != null) {
@@ -145,6 +184,9 @@ public class IrisObjectScale {
} }
private ScaleRequest snapshotRequest() { private ScaleRequest snapshotRequest() {
requireValidFactor(size, "size");
requireValidFactor(minimumScale, "minimumScale");
requireValidFactor(maximumScale, "maximumScale");
IrisObjectPlacementScaleInterpolator configuredInterpolation = interpolation == null IrisObjectPlacementScaleInterpolator configuredInterpolation = interpolation == null
? IrisObjectPlacementScaleInterpolator.NONE ? IrisObjectPlacementScaleInterpolator.NONE
: interpolation; : interpolation;
@@ -88,130 +88,99 @@ final class IrisObjectTransforms {
} }
static IrisObject scaledAroundOrigin(IrisObject self, double scale, IrisObjectPlacementScaleInterpolator interpolation) { static IrisObject scaledAroundOrigin(IrisObject self, double scale, IrisObjectPlacementScaleInterpolator interpolation) {
if (!Double.isFinite(scale) || scale <= 0 || scale > 50) {
throw new IllegalArgumentException("Object scale must be finite, greater than zero, and at most 50");
}
return scaled(self, scale, interpolation, ScaleOrigin.SAVED); return scaled(self, scale, interpolation, ScaleOrigin.SAVED);
} }
private static IrisObject scaled(IrisObject self, double scale, IrisObjectPlacementScaleInterpolator interpolation, private static IrisObject scaled(IrisObject self, double scale, IrisObjectPlacementScaleInterpolator interpolation,
ScaleOrigin origin) { ScaleOrigin origin) {
IrisObjectScale.requireValidFactor(scale, "Object scale");
if (interpolation == null) { if (interpolation == null) {
interpolation = IrisObjectPlacementScaleInterpolator.NONE; interpolation = IrisObjectPlacementScaleInterpolator.NONE;
} }
boolean savedOrigin = origin == ScaleOrigin.SAVED; boolean savedOrigin = origin == ScaleOrigin.SAVED;
IrisVector sm1 = new IrisVector(scale - 1, scale - 1, scale - 1); List<ScaledVoxel> voxels;
scale = Math.max(0.001, Math.min(50, scale)); IrisObject output;
if (!savedOrigin && scale < 1) {
scale = scale - 0.0001;
}
VectorMap<PlatformBlockState> placeBlock = new VectorMap<>();
VectorMap<TileData> placeTile = new VectorMap<>();
VectorMap<IrisBlockVector> placeMax = savedOrigin && scale > 1 ? new VectorMap<>() : null;
IrisVector center;
IrisObject oo;
boolean hasTiles;
self.readLock.lock(); self.readLock.lock();
try { try {
center = new IrisVector(self.getCenter().getX(), self.getCenter().getY(), self.getCenter().getZ()); output = savedOrigin ? createOriginScaledObject(self, scale)
if (self.getH() == 2) { : createCenteredScaledObject(self, scale);
center = center.setY(center.getBlockY() + 0.5); output.setLoadKey(self.getLoadKey());
} output.setLoader(self.getLoader());
if (self.getW() == 2) { output.setLoadFile(self.getLoadFile());
center = center.setX(center.getBlockX() + 0.5); voxels = new ArrayList<>(self.blocks.size());
} Vector3i sourceCenter = self.getCenter();
if (self.getD() == 2) { IrisBlockVector targetCenter = new IrisBlockVector(output.getCenter().getX(),
center = center.setZ(center.getBlockZ() + 0.5); output.getCenter().getY(), output.getCenter().getZ());
}
oo = savedOrigin ? createOriginScaledObject(self, scale)
: new IrisObject((int) Math.ceil((self.w * scale) + (scale * 2)), (int) Math.ceil((self.h * scale) + (scale * 2)), (int) Math.ceil((self.d * scale) + (scale * 2)));
oo.setLoadKey(self.getLoadKey());
oo.setLoader(self.getLoader());
oo.setLoadFile(self.getLoadFile());
hasTiles = !self.states.isEmpty();
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : self.blocks) { for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : self.blocks) {
PlatformBlockState bd = entry.getValue();
IrisBlockVector sourcePosition = entry.getKey(); IrisBlockVector sourcePosition = entry.getKey();
IrisBlockVector position = savedOrigin IrisBlockVector position = savedOrigin ? sourcePosition : new IrisBlockVector(
? scaledMinimum(sourcePosition, scale) sourcePosition.getBlockX() + sourceCenter.getX(),
: sourcePosition.clone().add(IrisObject.HALF).subtract(center) sourcePosition.getBlockY() + sourceCenter.getY(),
.multiply(scale).add(sm1).toBlockVector(); sourcePosition.getBlockZ() + sourceCenter.getZ());
placeBlock.put(position, bd); IrisBlockVector minimum = scaledMinimum(position, scale);
if (placeMax != null) { IrisBlockVector maximum = scaledMaximum(position, scale);
placeMax.put(position, scaledMaximum(sourcePosition, scale)); if (!savedOrigin) {
} minimum.subtract(targetCenter);
if (hasTiles) { maximum.subtract(targetCenter);
TileData tile = self.states.get(entry.getKey());
if (tile == null) {
placeTile.remove(position);
} else {
placeTile.put(position, tile);
}
} }
TileData tile = self.states.get(sourcePosition);
voxels.add(new ScaledVoxel(sourcePosition.clone(), minimum, maximum, entry.getValue(),
tile == null ? null : tile.clone()));
} }
} finally { } finally {
self.readLock.unlock(); self.readLock.unlock();
} }
if (scale != Math.rint(scale)) {
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : placeBlock) { voxels.sort(Comparator.comparingInt((ScaledVoxel voxel) -> voxel.source().getBlockX())
IrisBlockVector v = entry.getKey(); .thenComparingInt(voxel -> voxel.source().getBlockY())
TileData tile = hasTiles ? placeTile.get(v) : null; .thenComparingInt(voxel -> voxel.source().getBlockZ()));
if (scale > 1) { }
IrisVector minimum = savedOrigin ? v : v.clone().add(center); for (ScaledVoxel voxel : voxels) {
IrisVector maximum = savedOrigin ? placeMax.get(v) : v.clone().add(center).add(sm1); writeScaledVoxel(output, voxel);
int minX = Math.min(minimum.getBlockX(), maximum.getBlockX());
int maxX = Math.max(minimum.getBlockX(), maximum.getBlockX());
int minY = Math.min(minimum.getBlockY(), maximum.getBlockY());
int maxY = Math.max(minimum.getBlockY(), maximum.getBlockY());
int minZ = Math.min(minimum.getBlockZ(), maximum.getBlockZ());
int maxZ = Math.max(minimum.getBlockZ(), maximum.getBlockZ());
IrisBlockVector position = new IrisBlockVector(minX, minY, minZ);
for (int x = minX; x <= maxX; x++) {
position.setX(x);
for (int z = minZ; z <= maxZ; z++) {
position.setZ(z);
for (int y = minY; y <= maxY; y++) {
position.setY(y);
oo.blocks.put(position, entry.getValue());
if (hasTiles) {
if (tile == null) {
oo.states.remove(position);
} else {
oo.states.put(position, tile.clone());
}
}
}
}
}
} else {
IrisBlockVector position = savedOrigin
? v
: oo.getSigned(v.getBlockX(), v.getBlockY(), v.getBlockZ());
oo.blocks.put(position, entry.getValue());
if (hasTiles) {
if (tile == null) {
oo.states.remove(position);
} else {
oo.states.put(position, tile.clone());
}
}
}
} }
VectorMap<PlatformBlockState> scaledBlocks = oo.blocks; VectorMap<PlatformBlockState> scaledBlocks = output.blocks;
if (scale > 1) { if (scale > 1) {
switch (interpolation) { switch (interpolation) {
case TRILINEAR -> trilinear(oo, (int) Math.round(scale)); case TRILINEAR -> trilinear(output, (int) Math.round(scale));
case TRICUBIC -> tricubic(oo, (int) Math.round(scale)); case TRICUBIC -> tricubic(output, (int) Math.round(scale));
case TRIHERMITE -> trihermite(oo, (int) Math.round(scale)); case TRIHERMITE -> trihermite(output, (int) Math.round(scale));
} }
} }
removeInapplicableTiles(output, scaledBlocks);
return output;
}
removeInapplicableTiles(oo, scaledBlocks); private static void writeScaledVoxel(IrisObject output, ScaledVoxel voxel) {
return oo; IrisBlockVector minimum = voxel.minimum();
IrisBlockVector maximum = voxel.maximum();
IrisBlockVector position = minimum.clone();
for (int x = minimum.getBlockX(); x <= maximum.getBlockX(); x++) {
position.setX(x);
for (int z = minimum.getBlockZ(); z <= maximum.getBlockZ(); z++) {
position.setZ(z);
for (int y = minimum.getBlockY(); y <= maximum.getBlockY(); y++) {
position.setY(y);
output.blocks.put(position, voxel.block());
if (voxel.tile() == null) {
output.states.remove(position);
} else {
output.states.put(position, voxel.tile().clone());
}
}
}
}
}
private static IrisObject createCenteredScaledObject(IrisObject source, double scale) {
IrisObject object = new IrisObject((int) Math.ceil(source.w * scale),
(int) Math.ceil(source.h * scale), (int) Math.ceil(source.d * scale));
Vector3i center = object.getCenter();
object.aabb.aquire(() -> new AxisAlignedBB(
new IrisPosition(-center.getX(), -center.getY(), -center.getZ()),
new IrisPosition(object.w - center.getX() - 1, object.h - center.getY() - 1,
object.d - center.getZ() - 1)));
return object;
} }
private static IrisObject createOriginScaledObject(IrisObject source, double scale) { private static IrisObject createOriginScaledObject(IrisObject source, double scale) {
@@ -499,6 +468,10 @@ final class IrisObjectTransforms {
private record NearestBlock(int x, int y, int z, int rank, PlatformBlockState state) { private record NearestBlock(int x, int y, int z, int rank, PlatformBlockState state) {
} }
private record ScaledVoxel(IrisBlockVector source, IrisBlockVector minimum, IrisBlockVector maximum,
PlatformBlockState block, TileData tile) {
}
private enum ScaleOrigin { private enum ScaleOrigin {
DEFAULT, DEFAULT,
SAVED SAVED
@@ -0,0 +1,138 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import java.util.Arrays;
final class IrisPaintSurfaceProjection {
static final int MISSING = Integer.MIN_VALUE;
private static final int MAXIMUM_STEP = 4;
private static final int MAXIMUM_CELLS = 1 << 22;
private final Request request;
private final int depth;
private final int[] surfaces;
private IrisPaintSurfaceProjection(Request request, int[] surfaces) {
this.request = request;
depth = request.maxZ() - request.minZ() + 1;
this.surfaces = surfaces;
}
static IrisPaintSurfaceProjection create(IObjectPlacer placer, Request request) {
long width = (long) request.maxX() - request.minX() + 1;
long depth = (long) request.maxZ() - request.minZ() + 1;
long cells = width * depth;
if (width <= 0 || depth <= 0 || width > MAXIMUM_CELLS || depth > MAXIMUM_CELLS
|| cells > MAXIMUM_CELLS || request.anchorX() < request.minX()
|| request.anchorX() > request.maxX() || request.anchorZ() < request.minZ()
|| request.anchorZ() > request.maxZ()) {
return null;
}
int[] surfaces = new int[(int) cells];
Arrays.fill(surfaces, MISSING);
IrisPaintSurfaceProjection projection = new IrisPaintSurfaceProjection(request, surfaces);
return projection.resolve(placer) ? projection : null;
}
int surfaceY(int x, int z) {
if (x < request.minX() || x > request.maxX() || z < request.minZ() || z > request.maxZ()) {
return MISSING;
}
return surfaces[index(x, z)];
}
private boolean resolve(IObjectPlacer placer) {
int[] highest = new int[surfaces.length];
Arrays.fill(highest, MISSING);
int[] pending = new int[surfaces.length];
int anchor = index(request.anchorX(), request.anchorZ());
highest[anchor] = placer.getHighest(request.anchorX(), request.anchorZ(), request.data(), request.ignoreFluid());
if (!accepts(placer, request.anchorX(), request.anchorZ(), request.anchorY(), highest[anchor])) {
return false;
}
surfaces[anchor] = request.anchorY();
pending[0] = anchor;
int count = 1;
for (int cursor = 0; cursor < count; cursor++) {
int cell = pending[cursor];
int x = request.minX() + cell / depth;
int z = request.minZ() + cell % depth;
int reference = surfaces[cell];
count = visit(placer, (long) x - 1, z, reference, highest, pending, count);
count = visit(placer, (long) x + 1, z, reference, highest, pending, count);
count = visit(placer, x, (long) z - 1, reference, highest, pending, count);
count = visit(placer, x, (long) z + 1, reference, highest, pending, count);
}
return true;
}
private int visit(IObjectPlacer placer, long candidateX, long candidateZ, int reference,
int[] highest, int[] pending, int count) {
if (candidateX < request.minX() || candidateX > request.maxX()
|| candidateZ < request.minZ() || candidateZ > request.maxZ()) {
return count;
}
int x = (int) candidateX;
int z = (int) candidateZ;
int cell = index(x, z);
if (surfaces[cell] != MISSING) {
return count;
}
if (highest[cell] == MISSING) {
highest[cell] = placer.getHighest(x, z, request.data(), request.ignoreFluid());
}
for (int distance = 0; distance <= MAXIMUM_STEP; distance++) {
long lower = (long) reference - distance;
if (accepts(placer, x, z, lower, highest[cell])) {
surfaces[cell] = (int) lower;
pending[count++] = cell;
return count;
}
long upper = (long) reference + distance;
if (distance > 0 && accepts(placer, x, z, upper, highest[cell])) {
surfaces[cell] = (int) upper;
pending[count++] = cell;
return count;
}
}
return count;
}
private boolean accepts(IObjectPlacer placer, int x, int z, long y, int highest) {
if (y <= Integer.MIN_VALUE || y >= Integer.MAX_VALUE || y > highest
|| !matchesNeighbour((long) x - 1, z, y) || !matchesNeighbour((long) x + 1, z, y)
|| !matchesNeighbour(x, (long) z - 1, y) || !matchesNeighbour(x, (long) z + 1, y)) {
return false;
}
if (y == highest) {
return request.virtualHeightmap()
|| solid(placer, x, (int) y, z)
|| !request.ignoreFluid() && y == placer.getFluidHeight(x, z);
}
if (request.virtualHeightmap()) {
return false;
}
return solid(placer, x, (int) y, z) && !solid(placer, x, (int) y + 1, z);
}
private boolean matchesNeighbour(long x, long z, long y) {
if (x < request.minX() || x > request.maxX() || z < request.minZ() || z > request.maxZ()) {
return true;
}
int neighbour = surfaceY((int) x, (int) z);
return neighbour == MISSING || Math.abs(y - neighbour) <= MAXIMUM_STEP;
}
private boolean solid(IObjectPlacer placer, int x, int y, int z) {
return placer.isSurfaceSolid(x, y, z) && !placer.isCarved(x, y, z);
}
private int index(int x, int z) {
return (x - request.minX()) * depth + z - request.minZ();
}
record Request(IrisData data, int minX, int maxX, int minZ, int maxZ,
int anchorX, int anchorY, int anchorZ, boolean ignoreFluid, boolean virtualHeightmap) {
}
}
@@ -40,8 +40,8 @@ public class IrisStaticObject {
private IrisStaticObjectRotation rotation = new IrisStaticObjectRotation(); private IrisStaticObjectRotation rotation = new IrisStaticObjectRotation();
@MinNumber(MINIMUM_SCALE) @MinNumber(MINIMUM_SCALE)
@MaxNumber(MAXIMUM_SCALE) @MaxNumber(MAXIMUM_SCALE)
@Desc("Fixed size multiplier. 1 preserves the saved size, 0.5 halves it and 2 doubles it.") @Desc("Explicit fixed size multiplier. When omitted, the dimension's allObjectScaleFactor applies. An explicit 1 preserves the saved size.")
private double scale = 1D; private Double scale;
@Desc("Interpolation used when enlarging the object. NONE preserves blocky shapes.") @Desc("Interpolation used when enlarging the object. NONE preserves blocky shapes.")
private IrisObjectPlacementScaleInterpolator scaleInterpolation = IrisObjectPlacementScaleInterpolator.NONE; private IrisObjectPlacementScaleInterpolator scaleInterpolation = IrisObjectPlacementScaleInterpolator.NONE;
@ArrayType(type = IrisObjectReplace.class) @ArrayType(type = IrisObjectReplace.class)
@@ -74,9 +74,8 @@ public class IrisStaticObject {
throw new IllegalArgumentException("rotation must be an object."); throw new IllegalArgumentException("rotation must be an object.");
} }
rotation.validate(); rotation.validate();
if (!Double.isFinite(scale) || scale < MINIMUM_SCALE || scale > MAXIMUM_SCALE) { if (scale != null) {
throw new IllegalArgumentException("scale must be finite and between " IrisObjectScale.requireValidFactor(scale, "scale");
+ MINIMUM_SCALE + " and " + MAXIMUM_SCALE + ".");
} }
if (scaleInterpolation == null) { if (scaleInterpolation == null) {
throw new IllegalArgumentException("scaleInterpolation must be a supported interpolation mode."); throw new IllegalArgumentException("scaleInterpolation must be a supported interpolation mode.");
@@ -141,12 +140,23 @@ public class IrisStaticObject {
.setMode(ObjectPlaceMode.STRUCTURE_PIECE) .setMode(ObjectPlaceMode.STRUCTURE_PIECE)
.setForcePlace(true) .setForcePlace(true)
.setRotation(rotation.toRotation()) .setRotation(rotation.toRotation())
.setScale(new IrisObjectScale().setSize(scale).setInterpolation(scaleInterpolation)) .setScale(scale == null ? null
: new IrisObjectScale().setSize(scale).setInterpolation(scaleInterpolation))
.setEdit(new KList<>(edit)) .setEdit(new KList<>(edit))
.setBore(bore) .setBore(bore)
.setSmartBore(smartBore); .setSmartBore(smartBore);
} }
public IrisStaticObject setScale(Double scale) {
this.scale = scale == null ? null : IrisObjectScale.requireValidFactor(scale, "scale");
return this;
}
public double resolveScale(IrisDimension dimension) {
return scale == null ? dimension.getAllObjectScaleFactor()
: IrisObjectScale.requireValidFactor(scale, "scale");
}
private static void validateEdit(IrisObjectReplace replacement, int index) { private static void validateEdit(IrisObjectReplace replacement, int index) {
if (replacement.getFind() == null || replacement.getFind().isEmpty() if (replacement.getFind() == null || replacement.getFind().isEmpty()
|| replacement.getFind().contains(null)) { || replacement.getFind().contains(null)) {
@@ -55,8 +55,9 @@ public final class IrisStaticObjectLayer {
if (object == null) { if (object == null) {
throw new IllegalArgumentException("Object '" + entry.getObject() + "' could not be loaded"); throw new IllegalArgumentException("Object '" + entry.getObject() + "' could not be loaded");
} }
if (entry.getScale() != 1D) { double scale = entry.resolveScale(dimension);
object = object.scaledAroundOrigin(entry.getScale(), entry.getScaleInterpolation()); if (scale != 1D) {
object = object.scaledAroundOrigin(scale, entry.getScaleInterpolation());
} else if (entry.isSmartBore()) { } else if (entry.isSmartBore()) {
object = object.copy(); object = object.copy();
} }
@@ -24,11 +24,20 @@ public final class IrisSurfaceSupport {
IrisObjectTranslate translate, IrisObjectRotation rotation, IrisObjectTranslate translate, IrisObjectRotation rotation,
int spinX, int spinY, int spinZ, int spinX, int spinY, int spinZ,
List<IrisBlockVector> supportOffsets, int buffer, int minSolidDepth) { List<IrisBlockVector> supportOffsets, int buffer, int minSolidDepth) {
return isUnsupported(placer, data, x, z, translate, rotation, spinX, spinY, spinZ,
supportOffsets, buffer, minSolidDepth, null);
}
static boolean isUnsupported(IObjectPlacer placer, IrisData data, int x, int z,
IrisObjectTranslate translate, IrisObjectRotation rotation,
int spinX, int spinY, int spinZ,
List<IrisBlockVector> supportOffsets, int buffer, int minSolidDepth,
IrisPaintSurfaceProjection projection) {
Stencil stencil = STENCIL.get(); Stencil stencil = STENCIL.get();
if (!stencil.build(supportOffsets, translate, rotation, spinX, spinY, spinZ, Math.max(0, buffer))) { if (!stencil.build(supportOffsets, translate, rotation, spinX, spinY, spinZ, Math.max(0, buffer))) {
return true; return true;
} }
return stencil.anyColumnUnsupported(placer, data, x, z, Math.max(1, minSolidDepth)); return stencil.anyColumnUnsupported(placer, data, x, z, Math.max(1, minSolidDepth), projection);
} }
public static boolean intersectsHydrology(IObjectPlacer placer, int x, int z, public static boolean intersectsHydrology(IObjectPlacer placer, int x, int z,
@@ -165,13 +174,14 @@ public final class IrisSurfaceSupport {
} }
/** Centre-outward so the common failure, a hole right under the anchor, costs one column. */ /** Centre-outward so the common failure, a hole right under the anchor, costs one column. */
private boolean anyColumnUnsupported(IObjectPlacer placer, IrisData data, int x, int z, int minSolidDepth) { private boolean anyColumnUnsupported(IObjectPlacer placer, IrisData data, int x, int z, int minSolidDepth,
IrisPaintSurfaceProjection projection) {
int maxRing = Math.max( int maxRing = Math.max(
Math.max(centerX, width - 1 - centerX), Math.max(centerX, width - 1 - centerX),
Math.max(centerZ, depth - 1 - centerZ)); Math.max(centerZ, depth - 1 - centerZ));
for (int ring = 0; ring <= maxRing; ring++) { for (int ring = 0; ring <= maxRing; ring++) {
if (ring == 0) { if (ring == 0) {
if (isColumnUnsupported(placer, data, x, z, centerX, centerZ, minSolidDepth)) { if (isColumnUnsupported(placer, data, x, z, centerX, centerZ, minSolidDepth, projection)) {
return true; return true;
} }
continue; continue;
@@ -182,14 +192,14 @@ public final class IrisSurfaceSupport {
int lowZ = centerZ - ring; int lowZ = centerZ - ring;
int highZ = centerZ + ring; int highZ = centerZ + ring;
for (int sx = lowX; sx <= highX; sx++) { for (int sx = lowX; sx <= highX; sx++) {
if (isColumnUnsupported(placer, data, x, z, sx, lowZ, minSolidDepth) if (isColumnUnsupported(placer, data, x, z, sx, lowZ, minSolidDepth, projection)
|| isColumnUnsupported(placer, data, x, z, sx, highZ, minSolidDepth)) { || isColumnUnsupported(placer, data, x, z, sx, highZ, minSolidDepth, projection)) {
return true; return true;
} }
} }
for (int sz = lowZ + 1; sz < highZ; sz++) { for (int sz = lowZ + 1; sz < highZ; sz++) {
if (isColumnUnsupported(placer, data, x, z, lowX, sz, minSolidDepth) if (isColumnUnsupported(placer, data, x, z, lowX, sz, minSolidDepth, projection)
|| isColumnUnsupported(placer, data, x, z, highX, sz, minSolidDepth)) { || isColumnUnsupported(placer, data, x, z, highX, sz, minSolidDepth, projection)) {
return true; return true;
} }
} }
@@ -214,7 +224,8 @@ public final class IrisSurfaceSupport {
} }
private boolean isColumnUnsupported(IObjectPlacer placer, IrisData data, int x, int z, private boolean isColumnUnsupported(IObjectPlacer placer, IrisData data, int x, int z,
int stencilX, int stencilZ, int minSolidDepth) { int stencilX, int stencilZ, int minSolidDepth,
IrisPaintSurfaceProjection projection) {
if (stencilX < 0 || stencilX >= width || stencilZ < 0 || stencilZ >= depth if (stencilX < 0 || stencilX >= width || stencilZ < 0 || stencilZ >= depth
|| !cells[(stencilX * depth) + stencilZ]) { || !cells[(stencilX * depth) + stencilZ]) {
return false; return false;
@@ -222,9 +233,14 @@ public final class IrisSurfaceSupport {
int columnX = x + minX + stencilX; int columnX = x + minX + stencilX;
int columnZ = z + minZ + stencilZ; int columnZ = z + minZ + stencilZ;
int surfaceY = placer.getHighest(columnX, columnZ, data, true); int surfaceY = projection == null ? placer.getHighest(columnX, columnZ, data, true)
: projection.surfaceY(columnX, columnZ);
if (surfaceY == IrisPaintSurfaceProjection.MISSING) {
return false;
}
for (int below = 0; below < minSolidDepth; below++) { for (int below = 0; below < minSolidDepth; below++) {
if (placer.isCarved(columnX, surfaceY - below, columnZ)) { if (placer.isCarved(columnX, surfaceY - below, columnZ)
|| projection != null && !placer.isSurfaceSolid(columnX, surfaceY - below, columnZ)) {
return true; return true;
} }
} }
@@ -0,0 +1,30 @@
package art.arcane.iris.util.common.director.specialhandlers;
import art.arcane.iris.engine.object.IrisObjectScale;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import art.arcane.volmlib.util.director.handlers.base.DoubleHandlerBase;
public final class ObjectScaleHandler extends DoubleHandlerBase {
@Override
public Double parse(String input, boolean force) throws DirectorParsingException {
if (input.trim().equalsIgnoreCase("dimension")) {
return null;
}
double factor = super.parse(input, force);
try {
return IrisObjectScale.requireValidFactor(factor, "scale");
} catch (IllegalArgumentException failure) {
throw new DirectorParsingException(failure.getMessage());
}
}
@Override
public String toString(Double value) {
return value == null ? "dimension" : super.toString(value);
}
@Override
public String getRandomDefault() {
return "dimension";
}
}
@@ -0,0 +1,35 @@
package art.arcane.iris.core.pack;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class PackObjectScaleFactorTest {
@Test
public void acceptsOmissionAndFiniteFactorsWithinResamplerLimits() {
for (String json : List.of("{}", "{\"allObjectScaleFactor\":0.01}",
"{\"allObjectScaleFactor\":0.5}", "{\"allObjectScaleFactor\":1}",
"{\"allObjectScaleFactor\":2}", "{\"allObjectScaleFactor\":50}")) {
List<String> errors = new ArrayList<>();
PackDimensionValidator.validateObjectScaleFactor("main", new JSONObject(json), errors);
assertEquals(json, List.of(), errors);
}
}
@Test
public void blocksInvalidTypesAndFactorsInPackAndRuntimeValidation() {
for (String value : List.of("null", "true", "\"2\"", "[]", "{}", "0", "-1", "0.009", "50.001")) {
JSONObject json = new JSONObject("{\"allObjectScaleFactor\":" + value + "}");
List<String> errors = new ArrayList<>();
PackDimensionValidator.validateObjectScaleFactor("main", json, errors);
assertEquals(value, 1, errors.size());
assertThrows(IllegalArgumentException.class,
() -> PackValidator.requireValidObjectSettings(null, "main", json));
}
}
}
@@ -78,7 +78,7 @@ public class SchemaBuilderIdentityTest {
@ClassRule @ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean(); public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final String EXPECTED_SCHEMA_DIGEST = "2f47b30c04bf90a5342df92d7335670679176665520527913f3b90ed0cda65d5"; private static final String EXPECTED_SCHEMA_DIGEST = "ddfc51ba19c02dd50737a0d1ba30ee24cdb6cbbe5fed40c875adac69960ad81d";
private static final List<Class<?>> SCHEMA_ROOTS = List.of( private static final List<Class<?>> SCHEMA_ROOTS = List.of(
IrisDimension.class, IrisDimension.class,
@@ -7,6 +7,14 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public class IrisStructureComponentOverboreTest { public class IrisStructureComponentOverboreTest {
@Test
public void enlargedTreeSearchCoversPaddedScaleWithoutEnlargingShrunkTrees() {
assertEquals(4, IrisStructureComponent.scaledMarkerSpan(4, 1D));
assertEquals(4, IrisStructureComponent.scaledMarkerSpan(4, 0.5D));
assertEquals(8, IrisStructureComponent.scaledMarkerSpan(4, 2D));
assertEquals(20, IrisStructureComponent.scaledMarkerSpan(10, 2D));
}
@Test @Test
public void ordinaryMarkersAreSeparatedFromStructureMarkers() { public void ordinaryMarkersAreSeparatedFromStructureMarkers() {
assertTrue(IrisStructureComponent.isOrdinaryObjectMarker("trees/oak@42")); assertTrue(IrisStructureComponent.isOrdinaryObjectMarker("trees/oak@42"));
@@ -66,6 +66,7 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
.setFloatingObjects(new KList<>(placement)); .setFloatingObjects(new KList<>(placement));
IrisBiome biome = new IrisBiome().setFloatingChildBiomes(new KList<>(floatingChild)); IrisBiome biome = new IrisBiome().setFloatingChildBiomes(new KList<>(floatingChild));
IrisDimension dimension = mock(IrisDimension.class); IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getAllObjectScaleFactor()).thenReturn(1D);
when(dimension.getReachableBiomes(org.mockito.ArgumentMatchers.any())).thenReturn(new KList<>(biome)); when(dimension.getReachableBiomes(org.mockito.ArgumentMatchers.any())).thenReturn(new KList<>(biome));
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class); ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
@@ -80,5 +81,7 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
when(engineMantle.getData()).thenReturn(data); when(engineMantle.getData()).thenReturn(data);
assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius()); assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius());
when(dimension.getAllObjectScaleFactor()).thenReturn(2D);
assertEquals(34, new MantleFloatingObjectComponent(engineMantle).getRadius());
} }
} }
@@ -96,7 +96,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
.setRotation(IrisObjectRotation.of(90, 0, 0)) .setRotation(IrisObjectRotation.of(90, 0, 0))
.setTranslate(new IrisObjectTranslate().setX(3).setY(4)); .setTranslate(new IrisObjectTranslate().setX(3).setY(4));
assertEquals(53, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 48, 7), placement)); assertEquals(53, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 48, 7), placement, 1D));
} }
@Test @Test
@@ -107,7 +107,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
.setMode(ObjectPlaceMode.VACUUM) .setMode(ObjectPlaceMode.VACUUM)
.setVacuumSettings(new IrisVacuumSettings().setRadius(12)); .setVacuumSettings(new IrisVacuumSettings().setRadius(12));
assertEquals(43, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 4, 3), placement)); assertEquals(39, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 4, 3), placement, placement.getMaximumScale(null)));
} }
@Test @Test
@@ -126,6 +126,30 @@ public class MantleObjectComponentBoundaryRadiusTest {
verify(complex).allowsNewGenerationFootprint(95, 195, 105, 205); verify(complex).allowsNewGenerationFootprint(95, 195, 105, 205);
} }
@Test
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)));
dimension.setAllObjectScaleFactor(0.5D);
assertEquals(4, MantleObjectComponent.calculatePlacementReach(size, inherited,
inherited.getMaximumScale(dimension)));
inherited.setScale(new IrisObjectScale().setSize(1D));
assertEquals(8, MantleObjectComponent.calculatePlacementReach(size, inherited,
inherited.getMaximumScale(dimension)));
}
@Test
public void alreadyScaledObjectFootprintDoesNotApplyScaleTwice() {
IrisComplex complex = mock(IrisComplex.class);
IrisObject object = new IrisObject(20, 20, 20);
IrisObjectPlacement placement = new IrisObjectPlacement().setScale(new IrisObjectScale().setSize(2D));
MantleObjectComponent.allowsObjectPlacement(complex, object, placement, 100, 200);
verify(complex).allowsNewGenerationFootprint(80, 180, 120, 220);
}
@Test @Test
public void sourceReplayUsesAscendingSignedCoordinates() { public void sourceReplayUsesAscendingSignedCoordinates() {
List<String> replayedSources = new ArrayList<>(); List<String> replayedSources = new ArrayList<>();
@@ -185,6 +209,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
when(region.getObjects()).thenReturn(new KList<>()); when(region.getObjects()).thenReturn(new KList<>());
when(region.getProceduralObjects()).thenReturn(proceduralObjects); when(region.getProceduralObjects()).thenReturn(proceduralObjects);
IrisDimension dimension = mock(IrisDimension.class); IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getAllObjectScaleFactor()).thenReturn(1D);
when(dimension.getAllRegions(any())).thenReturn(new KList<>(region)); when(dimension.getAllRegions(any())).thenReturn(new KList<>(region));
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>()); when(dimension.getReachableBiomes(any())).thenReturn(new KList<>());
Engine engine = mock(Engine.class); Engine engine = mock(Engine.class);
@@ -215,10 +240,12 @@ public class MantleObjectComponentBoundaryRadiusTest {
when(upperRegion.getObjects()).thenReturn(new KList<>()); when(upperRegion.getObjects()).thenReturn(new KList<>());
when(upperRegion.getProceduralObjects()).thenReturn(proceduralObjects); when(upperRegion.getProceduralObjects()).thenReturn(proceduralObjects);
IrisDimension baseDimension = mock(IrisDimension.class); IrisDimension baseDimension = mock(IrisDimension.class);
when(baseDimension.getAllObjectScaleFactor()).thenReturn(1D);
when(baseDimension.isUpperDimensionObjects()).thenReturn(true); when(baseDimension.isUpperDimensionObjects()).thenReturn(true);
when(baseDimension.getAllRegions(any())).thenReturn(new KList<>()); when(baseDimension.getAllRegions(any())).thenReturn(new KList<>());
when(baseDimension.getReachableBiomes(any())).thenReturn(new KList<>()); when(baseDimension.getReachableBiomes(any())).thenReturn(new KList<>());
IrisDimension upperDimension = mock(IrisDimension.class); IrisDimension upperDimension = mock(IrisDimension.class);
when(upperDimension.getAllObjectScaleFactor()).thenReturn(1D);
when(upperDimension.getAllRegions(any())).thenReturn(new KList<>(upperRegion)); when(upperDimension.getAllRegions(any())).thenReturn(new KList<>(upperRegion));
when(upperDimension.getReachableBiomes(any())).thenReturn(new KList<>()); when(upperDimension.getReachableBiomes(any())).thenReturn(new KList<>());
UpperDimensionContext upperContext = mock(UpperDimensionContext.class); UpperDimensionContext upperContext = mock(UpperDimensionContext.class);
@@ -252,6 +279,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
.setTranslate(new IrisObjectTranslate().setX(32)); .setTranslate(new IrisObjectTranslate().setX(32));
IrisBiome biome = new IrisBiome().setObjects(new KList<>(placement)); IrisBiome biome = new IrisBiome().setObjects(new KList<>(placement));
IrisDimension dimension = mock(IrisDimension.class); IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getAllObjectScaleFactor()).thenReturn(1D);
when(dimension.isUseMantle()).thenReturn(true); when(dimension.isUseMantle()).thenReturn(true);
when(dimension.getAllRegions(any())).thenReturn(new KList<>()); when(dimension.getAllRegions(any())).thenReturn(new KList<>());
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(biome)); when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(biome));
@@ -0,0 +1,195 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.volmlib.util.math.RNG;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.junit.After;
import org.junit.Test;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisObjectGlobalScaleTest {
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
private final Gson gson = new Gson();
@After
public void clearCache() {
IrisObjectScale.invalidate(null);
}
@Test
public void defaultFactorPreservesObjectAndRandomSequence() {
IrisObject origin = new IrisObject(8, 8, 8);
IrisObjectPlacement placement = gson.fromJson("{}", IrisObjectPlacement.class);
RNG actual = new RNG(731L);
RNG expected = new RNG(731L);
assertNull(placement.getScale());
assertSame(origin, placement.scaleObject(actual, origin, new IrisDimension()));
assertSame(origin, placement.scaleObject(actual, origin, null));
assertEquals(expected.nextLong(), actual.nextLong());
assertFalse(gson.toJson(placement).contains("\"scale\""));
assertNull(placement.toPlacement("trees/oak").getScale());
}
@Test
public void dimensionFactorsUseSeparateCachedVariantsWithoutChangingSourceOrRandomness() {
IrisObject origin = new IrisObject(8, 8, 8);
IrisObjectPlacement placement = new IrisObjectPlacement();
IrisDimension large = new IrisDimension().setAllObjectScaleFactor(2D);
IrisDimension small = new IrisDimension().setAllObjectScaleFactor(0.5D);
RNG actual = new RNG(731L);
RNG expected = new RNG(731L);
IrisObject doubled = placement.scaleObject(actual, origin, large);
IrisObject halved = placement.scaleObject(actual, origin, small);
assertEquals(16, doubled.getW());
assertEquals(4, halved.getW());
assertEquals(8, origin.getW());
assertNotSame(origin, doubled);
assertNotSame(doubled, halved);
assertSame(doubled, placement.scaleObject(actual, origin, large));
assertSame(halved, placement.scaleObject(actual, origin, small));
assertEquals(expected.nextLong(), actual.nextLong());
}
@Test
public void globalAndExplicitFactorsScaleOccupiedCubeBoundsExactly() {
IrisObject origin = new IrisObject(4, 4, 4);
PlatformBlockState stone = mock(PlatformBlockState.class);
for (int x = 0; x < 4; x++) {
for (int y = 0; y < 4; y++) {
for (int z = 0; z < 4; z++) {
origin.setUnsigned(x, y, z, stone);
}
}
}
IrisObjectPlacement inherited = new IrisObjectPlacement();
IrisDimension doubled = new IrisDimension().setAllObjectScaleFactor(2D);
assertOccupiedCube(inherited.scaleObject(new RNG(1), origin, doubled), 8);
assertOccupiedCube(inherited.scaleObject(new RNG(1), origin,
new IrisDimension().setAllObjectScaleFactor(0.5D)), 2);
assertOccupiedCube(new IrisObjectPlacement().setScale(new IrisObjectScale().setSize(0.5D))
.scaleObject(new RNG(1), origin, doubled), 2);
assertOccupiedCube(new IrisObjectPlacement().setScale(new IrisObjectScale())
.scaleObject(new RNG(1), origin, doubled), 4);
assertOccupiedCube(origin, 4);
}
@Test
public void disabledVariationRangeReportsTheUnscaledFootprint() {
IrisObject origin = new IrisObject(4, 4, 4);
IrisObjectScale scale = new IrisObjectScale().setMinimumScale(0.25D)
.setMaximumScale(0.25D).setVariations(0);
assertSame(origin, scale.get(new RNG(1), origin));
assertEquals(1D, scale.getMaxScale(), 0D);
}
@Test
public void explicitUnitAndEmptyScaleOverrideDimensionFactorAcrossJsonCopies() {
IrisObject origin = new IrisObject(8, 8, 8);
IrisDimension dimension = new IrisDimension().setAllObjectScaleFactor(2D);
for (String json : new String[]{"{\"scale\":{}}", "{\"scale\":{\"size\":1.0}}"}) {
IrisObjectPlacement placement = gson.fromJson(json, IrisObjectPlacement.class);
IrisObjectPlacement copy = gson.fromJson(gson.toJson(placement.toPlacement("tree")), IrisObjectPlacement.class);
assertSame(origin, copy.scaleObject(new RNG(1), origin, dimension));
assertEquals(1D, copy.getMaximumScale(dimension), 0D);
}
}
@Test
public void referencedScaleSnippetRemainsAnExplicitOverride() throws Exception {
Path root = temporaryFolder.getRoot().toPath();
Path snippets = Files.createDirectories(root.resolve("snippet/object-scale"));
Files.writeString(snippets.resolve("unit.json"), "{}");
Files.writeString(snippets.resolve("half.json"), "{\"size\":0.5}");
IrisData data = mock(IrisData.class, CALLS_REAL_METHODS);
when(data.getDataFolder()).thenReturn(root.toFile());
Gson loaderGson = new GsonBuilder().registerTypeAdapterFactory(data).create();
IrisDimension dimension = new IrisDimension().setAllObjectScaleFactor(2D);
IrisObject origin = new IrisObject(4, 4, 4);
IrisObjectPlacement unit = loaderGson.fromJson(
"{\"scale\":\"snippet/object-scale/unit\"}", IrisObjectPlacement.class);
IrisObjectPlacement half = loaderGson.fromJson(
"{\"scale\":\"snippet/object-scale/half\"}", IrisObjectPlacement.class);
assertNotNull(unit.getScale());
assertSame(origin, unit.scaleObject(new RNG(1), origin, dimension));
assertEquals(2, half.scaleObject(new RNG(1), origin, dimension).getW());
IrisObjectPlacement copied = loaderGson.fromJson(loaderGson.toJson(half.toPlacement("tree")),
IrisObjectPlacement.class);
assertEquals(0.5D, copied.getMaximumScale(dimension), 0D);
}
@Test
public void explicitRangeKeepsItsExistingSelectionSequenceWithoutMultiplying() {
IrisObject origin = new IrisObject(8, 8, 8);
IrisObjectScale authored = new IrisObjectScale().setMinimumScale(0.5D)
.setMaximumScale(1.5D).setVariations(4);
IrisObjectPlacement placement = new IrisObjectPlacement().setScale(authored);
IrisDimension dimension = new IrisDimension().setAllObjectScaleFactor(3D);
RNG actual = new RNG(1337);
RNG expected = new RNG(1337);
for (int index = 0; index < 12; index++) {
assertSame(authored.get(expected, origin), placement.scaleObject(actual, origin, dimension));
}
assertEquals(1.5D, placement.getMaximumScale(dimension), 0D);
assertEquals(expected.nextLong(), actual.nextLong());
}
@Test
public void explicitOppositeFactorWinsAndDirectSettersRejectUnsafeValues() {
IrisObject origin = new IrisObject(8, 8, 8);
IrisObjectPlacement placement = new IrisObjectPlacement().setScale(new IrisObjectScale().setSize(0.5D));
assertSame(IrisObjectScale.getFixed(origin, 0.5D), placement.scaleObject(new RNG(1), origin,
new IrisDimension().setAllObjectScaleFactor(2D)));
for (double invalid : new double[]{Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY,
0D, -1D, 0.009D, 50.001D}) {
assertThrows(IllegalArgumentException.class, () -> new IrisDimension().setAllObjectScaleFactor(invalid));
assertThrows(IllegalArgumentException.class, () -> new IrisObjectScale().setSize(invalid));
assertThrows(IllegalArgumentException.class, () -> new IrisStaticObject().setScale(invalid));
}
assertThrows(IllegalArgumentException.class, () -> gson.fromJson(
"{\"allObjectScaleFactor\":0}", IrisDimension.class).getAllObjectScaleFactor());
}
private static void assertOccupiedCube(IrisObject object, int width) {
assertEquals(width, object.getW());
assertEquals(width, object.getH());
assertEquals(width, object.getD());
assertEquals(width * width * width, object.getBlocks().size());
int minimum = -(width / 2);
for (int x = minimum; x < minimum + width; x++) {
for (int y = minimum; y < minimum + width; y++) {
for (int z = minimum; z < minimum + width; z++) {
assertNotNull(object.getBlocks().get(new IrisBlockVector(x, y, z)));
}
}
}
assertEquals(minimum, object.getAABB().min().getX());
assertEquals(minimum + width - 1, object.getAABB().max().getX());
}
}
@@ -2,12 +2,14 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeStructureVolume;
import art.arcane.iris.engine.framework.PlacedObject; import art.arcane.iris.engine.framework.PlacedObject;
import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries; import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard; import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
@@ -37,7 +39,9 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -279,6 +283,133 @@ public class IrisObjectPlacementRunnerRegressionTest {
assertEquals(1, object.getStates().size()); assertEquals(1, object.getStates().size());
} }
@Test
public void paintFollowsTheLowerLedgeBeneathADistantRoof() {
RecordingPlacer placer = new RecordingPlacer(null);
placer.terrain(1, 0, 0, 80, 150, 160);
placer.terrain(2, 0, 0, 80, 150, 160);
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.PAINT)
.setRequireSurfaceSupport(true).setSurfaceSupportBuffer(0);
assertEquals(80, lineObject(5).place(0, -1, 0, placer, placement, new RNG(2L), data));
assertEquals(5, placer.writes().size());
assertTrue(placer.writes().stream().allMatch(write -> write.y() == 80));
}
@Test
public void paintClipsAtACliffWithoutReachingThePatchBeyondIt() {
RecordingPlacer placer = new RecordingPlacer(null);
placer.terrain(1, 0, 0, 20);
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.PAINT);
lineObject(5).place(0, -1, 0, placer, placement, new RNG(2L), data);
assertEquals(3, placer.writes().size());
assertTrue(placer.writes().stream().allMatch(write -> write.x() <= 0 && write.y() == 80));
}
@Test
public void paintAccumulatesNormalAscendingAndDescendingSlopeSteps() {
RecordingPlacer placer = new RecordingPlacer(null);
for (int x = -6; x <= 6; x++) {
placer.terrain(x, 0, 0, 80 + x * 3);
}
lineObject(13).place(0, -1, 0, placer, placement().setMode(ObjectPlaceMode.PAINT), new RNG(2L), data);
assertEquals(13, placer.writes().size());
assertTrue(placer.writes().stream().allMatch(write -> write.y() == 80 + write.x() * 3));
}
@Test
public void paintingAndBoringCannotFeedBackIntoTheSurfaceProjection() {
RecordingPlacer placer = new RecordingPlacer(null);
placer.highestFollowsWrites = true;
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.PAINT).setBore(true);
boxObject(1, 3, 1).place(0, -1, 0, placer, placement, new RNG(2L), data);
assertEquals(3, placer.writesOf(solid).size());
assertTrue(placer.writes().stream().allMatch(write -> write.y() >= 80 && write.y() <= 82));
}
@Test
public void paintUsesTranslatedAnchorAndMatchingRandomizedBoreBounds() {
RecordingPlacer placer = new RecordingPlacer(null);
placer.terrain(0, -3, 0, 108);
placer.terrain(0, -4, 0, 110);
placer.terrain(0, -5, 0, 112);
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.PAINT).setBore(true)
.setRotation(IrisObjectRotation.of(0, 90, 0))
.setTranslate(new IrisObjectTranslate().setX(4).setY(5).setYRandom(3));
RNG rng = mock(RNG.class);
when(rng.i(0, 3)).thenReturn(3);
assertEquals(113, lineObject(3).place(0, -1, 0, placer, placement, rng, data));
assertEquals(3, placer.writesOf(solid).size());
assertEquals(3, placer.writesOf(IrisObject.States.air()).size());
assertTrue(placer.writes().stream().allMatch(write -> write.x() == 0
&& write.z() >= -5 && write.z() <= -3 && write.y() == 110 - (write.z() + 4) * 2 + 8));
}
@Test
public void nativeVetoUsesTheSelectedLedgeAndRandomTranslationOffset() {
RecordingPlacer placer = new RecordingPlacer(engine);
placer.terrain(1, 0, 0, 80, 150, 160);
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getNativeStructureVolumes(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(
new KList<>(NativeStructureVolume.of("test", 1, 21, 0, 1, 21, 0)));
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.PAINT).setForcePlace(true)
.setTranslate(new IrisObjectTranslate().setY(2).setYRandom(3));
RNG rng = mock(RNG.class);
when(rng.i(0, 3)).thenReturn(3);
assertEquals(-1, lineObject(3).place(0, -1, 0, placer, placement, rng, data));
assertTrue(placer.writes().isEmpty());
when(engine.getNativeStructureVolumes(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(
new KList<>(NativeStructureVolume.of("test", 1, 101, 0, 1, 101, 0)));
assertEquals(83, lineObject(3).place(0, -1, 0, placer, placement, rng, data));
assertTrue(placer.writes().stream().allMatch(write -> write.y() == 85));
}
@Test
public void warpedPaintAndNativeVetoUseTheSameFinalColumns() {
RecordingPlacer placer = new RecordingPlacer(engine);
for (int x = 1; x <= 3; x++) {
placer.terrain(x, 2, 0, 80, 150, 160);
}
IrisObjectPlacement placement = spy(placement().setMode(ObjectPlaceMode.PAINT).setForcePlace(true)
.setWarp(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setMultiplier(4)));
CNG warp = mock(CNG.class);
when(warp.fitDouble(anyDouble(), anyDouble(), anyDouble(), anyDouble(), anyDouble())).thenReturn(2D);
doReturn(warp).when(placement).getSurfaceWarp(any(RNG.class), any());
when(engine.getNativeStructureVolumes(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(
new KList<>(NativeStructureVolume.of("test", 3, 80, 2, 3, 80, 2)));
assertEquals(-1, lineObject(3).place(0, -1, 0, placer, placement, new RNG(2L), data));
assertTrue(placer.writes().isEmpty());
when(engine.getNativeStructureVolumes(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(NativeStructureVolume.NONE);
lineObject(3).place(0, -1, 0, placer, placement, new RNG(2L), data);
assertEquals(3, placer.writes().size());
assertTrue(placer.writes().stream().allMatch(write -> write.x() >= 1 && write.x() <= 3
&& write.z() == 2 && write.y() == 80));
}
@Test
public void paintSupportChecksTheThicknessOfTheSelectedLowerLedge() {
RecordingPlacer placer = new RecordingPlacer(null);
placer.terrain(1, 0, 0, 20, 80, 80, 150, 160);
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.PAINT)
.setRequireSurfaceSupport(true).setSurfaceSupportBuffer(0).setSurfaceSupportDepth(2);
assertEquals(-1, lineObject(3).place(0, -1, 0, placer, placement, new RNG(2L), data));
assertTrue(placer.writes().isEmpty());
}
private IrisObjectPlacement placement() { private IrisObjectPlacement placement() {
IrisObjectPlacement placement = new IrisObjectPlacement(); IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT); placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
@@ -326,9 +457,11 @@ public class IrisObjectPlacementRunnerRegressionTest {
private final Map<String, PlatformBlockState> world = new HashMap<>(); private final Map<String, PlatformBlockState> world = new HashMap<>();
private final Map<String, Object> data = new HashMap<>(); private final Map<String, Object> data = new HashMap<>();
private final List<String> sampledColumns = new ArrayList<>(); private final List<String> sampledColumns = new ArrayList<>();
private final Map<String, int[]> terrain = new HashMap<>();
private final Engine engine; private final Engine engine;
private int failAfterWrites = Integer.MAX_VALUE; private int failAfterWrites = Integer.MAX_VALUE;
private boolean debugSmartBore; private boolean debugSmartBore;
private boolean highestFollowsWrites;
private RecordingPlacer(Engine engine) { private RecordingPlacer(Engine engine) {
this.engine = engine; this.engine = engine;
@@ -342,6 +475,16 @@ public class IrisObjectPlacementRunnerRegressionTest {
this.debugSmartBore = debugSmartBore; this.debugSmartBore = debugSmartBore;
} }
private void terrain(int x, int z, int... spans) {
terrain.put(x + ":" + z, spans);
}
private int highest(int x, int z) {
int[] spans = terrain.get(x + ":" + z);
return (spans == null ? ANCHOR_Y : spans[spans.length - 1])
+ (highestFollowsWrites && !writes.isEmpty() ? 1000 : 0);
}
private List<BlockWrite> writes() { private List<BlockWrite> writes() {
return writes; return writes;
} }
@@ -357,13 +500,13 @@ public class IrisObjectPlacementRunnerRegressionTest {
@Override @Override
public int getHighest(int x, int z, IrisData data) { public int getHighest(int x, int z, IrisData data) {
sampledColumns.add(x + ":" + z); sampledColumns.add(x + ":" + z);
return ANCHOR_Y; return highest(x, z);
} }
@Override @Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
sampledColumns.add(x + ":" + z); sampledColumns.add(x + ":" + z);
return ANCHOR_Y; return highest(x, z);
} }
@Override @Override
@@ -390,6 +533,20 @@ public class IrisObjectPlacementRunnerRegressionTest {
return false; return false;
} }
@Override
public boolean isSurfaceSolid(int x, int y, int z) {
int[] spans = terrain.get(x + ":" + z);
if (spans == null) {
return y >= 0 && y <= ANCHOR_Y;
}
for (int index = 0; index < spans.length; index += 2) {
if (y >= spans[index] && y <= spans[index + 1]) {
return true;
}
}
return false;
}
@Override @Override
public boolean isSolid(int x, int y, int z) { public boolean isSolid(int x, int y, int z) {
PlatformBlockState state = get(x, y, z); PlatformBlockState state = get(x, y, z);
@@ -215,6 +215,56 @@ public class IrisObjectTransformsTileTest {
assertEquals("minecraft:barrel", scaled.getBlocks().get(new IrisBlockVector(4, 0, 0)).materialKey()); assertEquals("minecraft:barrel", scaled.getBlocks().get(new IrisBlockVector(4, 0, 0)).materialKey());
} }
@Test
public void fractionalReductionIncludesTheOuterSavedVoxelEdge() {
IrisObject source = new IrisObject(4, 1, 1);
source.setUnsigned(3, 0, 0, state("minecraft:chest"));
source.setUnsignedTile(3, 0, 0, tile("minecraft:chest", "edge"));
IrisObject scaled = source.scaledAroundOrigin(0.75D, IrisObjectPlacementScaleInterpolator.NONE);
assertEquals(2, scaled.getBlocks().size());
assertEquals("minecraft:chest", scaled.getBlocks().get(new IrisBlockVector(1, 0, 0)).materialKey());
assertEquals("edge", scaled.getStates().get(new IrisBlockVector(1, 0, 0)).getProperties().get("name"));
assertEquals(1, scaled.getAABB().max().getX());
}
@Test
public void fractionalScaleWinnersDoNotDependOnSourceInsertionOrder() {
for (boolean savedOrigin : new boolean[]{false, true}) {
IrisObject ascending = collisionSource(false);
IrisObject descending = collisionSource(true);
IrisObject first = savedOrigin
? ascending.scaledAroundOrigin(0.5D, IrisObjectPlacementScaleInterpolator.NONE)
: ascending.scaled(0.5D, IrisObjectPlacementScaleInterpolator.NONE);
IrisObject second = savedOrigin
? descending.scaledAroundOrigin(0.5D, IrisObjectPlacementScaleInterpolator.NONE)
: descending.scaled(0.5D, IrisObjectPlacementScaleInterpolator.NONE);
assertEquals(2, first.getBlocks().size());
assertEquals(1, first.getStates().size());
assertEquals("minecraft:stone", first.getBlocks().get(new IrisBlockVector(-1, 0, 0)).materialKey());
assertEquals("winner", first.getStates().get(new IrisBlockVector(0, 0, 0)).getProperties().get("name"));
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : first.getBlocks()) {
assertEquals(entry.getValue().materialKey(), second.getBlocks().get(entry.getKey()).materialKey());
assertEquals(first.getStates().get(entry.getKey()), second.getStates().get(entry.getKey()));
}
}
}
private static IrisObject collisionSource(boolean reverse) {
IrisObject source = new IrisObject(4, 1, 1);
for (int index = 0; index < 4; index++) {
int x = reverse ? 3 - index : index;
String material = x == 1 ? "minecraft:stone" : "minecraft:chest";
source.setUnsigned(x, 0, 0, state(material));
if (x != 1) {
source.setUnsignedTile(x, 0, 0, tile(material, x == 3 ? "winner" : "overwritten"));
}
}
return source;
}
@Test @Test
public void untypedTilesRemainAttachedToTheirScaledSourceMaterial() { public void untypedTilesRemainAttachedToTheirScaledSourceMaterial() {
IrisObject source = new IrisObject(1, 1, 1); IrisObject source = new IrisObject(1, 1, 1);
@@ -0,0 +1,109 @@
package art.arcane.iris.engine.object;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisPaintSurfaceProjectionTest {
@Test
public void positiveAndNegativeChunkSeamsHaveIdenticalRelativeSurfaces() {
IrisPaintSurfaceProjection positive = projection(15);
IrisPaintSurfaceProjection negative = projection(-17);
assertNotNull(positive);
assertNotNull(negative);
for (int x = -5; x <= 5; x++) {
for (int z = -1; z <= 1; z++) {
assertEquals(80 + x * 2, positive.surfaceY(15 + x, z));
assertEquals(positive.surfaceY(15 + x, z), negative.surfaceY(-17 + x, z));
}
}
}
@Test
public void exposedFloorsMustExistAtTheRequestedAnchor() {
IObjectPlacer placer = mock(IObjectPlacer.class);
when(placer.getHighest(anyInt(), anyInt(), any(), anyBoolean())).thenReturn(140);
when(placer.isSurfaceSolid(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
int y = invocation.getArgument(1);
return y >= 0 && y <= 100;
});
assertNull(IrisPaintSurfaceProjection.create(placer, request(80, false)));
assertNotNull(IrisPaintSurfaceProjection.create(placer, request(100, false)));
assertNull(IrisPaintSurfaceProjection.create(placer, request(140, false)));
}
@Test
public void aVirtualHeightmapDoesNotFallBackToPhysicalLedges() {
IObjectPlacer placer = mock(IObjectPlacer.class);
when(placer.getHighest(anyInt(), anyInt(), any(), anyBoolean()))
.thenAnswer(invocation -> (int) invocation.getArgument(0) == 0 ? 80 : 140);
when(placer.isSurfaceSolid(anyInt(), anyInt(), anyInt()))
.thenAnswer(invocation -> (int) invocation.getArgument(1) <= 80);
IrisPaintSurfaceProjection projection = IrisPaintSurfaceProjection.create(placer, request(80, true));
assertNotNull(projection);
assertEquals(80, projection.surfaceY(0, 0));
assertEquals(IrisPaintSurfaceProjection.MISSING, projection.surfaceY(1, 0));
}
@Test
public void surfaceChoicesStayCompatibleWhenTwoTraversalBranchesMeet() {
IObjectPlacer placer = mock(IObjectPlacer.class);
when(placer.getHighest(anyInt(), anyInt(), any(), anyBoolean())).thenAnswer(invocation -> {
int x = invocation.getArgument(0);
int z = invocation.getArgument(1);
return 80 + x * z;
});
when(placer.isSurfaceSolid(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
int x = invocation.getArgument(0);
int y = invocation.getArgument(1);
int z = invocation.getArgument(2);
return y <= 80 + x * z;
});
IrisPaintSurfaceProjection projection = IrisPaintSurfaceProjection.create(placer,
new IrisPaintSurfaceProjection.Request(null, -6, 6, -6, 6, 0, 80, 0, true, false));
assertNotNull(projection);
for (int x = -6; x < 6; x++) {
for (int z = -6; z < 6; z++) {
int surface = projection.surfaceY(x, z);
if (surface == IrisPaintSurfaceProjection.MISSING) {
continue;
}
int east = projection.surfaceY(x + 1, z);
int south = projection.surfaceY(x, z + 1);
assertTrue(east == IrisPaintSurfaceProjection.MISSING || Math.abs(surface - east) <= 4);
assertTrue(south == IrisPaintSurfaceProjection.MISSING || Math.abs(surface - south) <= 4);
}
}
}
private IrisPaintSurfaceProjection projection(int anchorX) {
IObjectPlacer placer = mock(IObjectPlacer.class);
when(placer.getHighest(anyInt(), anyInt(), any(), anyBoolean())).thenAnswer(invocation -> {
int x = invocation.getArgument(0);
return x > anchorX ? 200 : 80 + (x - anchorX) * 2;
});
when(placer.isSurfaceSolid(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
int x = invocation.getArgument(0);
int y = invocation.getArgument(1);
return y <= 80 + (x - anchorX) * 2 || x > anchorX && y >= 190 && y <= 200;
});
return IrisPaintSurfaceProjection.create(placer, new IrisPaintSurfaceProjection.Request(null,
anchorX - 5, anchorX + 5, -1, 1, anchorX, 80, 0, true, false));
}
private IrisPaintSurfaceProjection.Request request(int anchorY, boolean virtual) {
return new IrisPaintSurfaceProjection.Request(null, 0, 1, 0, 0, 0, anchorY, 0, true, virtual);
}
}
@@ -81,6 +81,41 @@ public class IrisStaticObjectLayerTest {
verifyNoInteractions(engine, loader); verifyNoInteractions(engine, loader);
} }
@Test
public void globalScalingKeepsSavedOriginsAndExplicitEntrySizes() {
IrisObject cube = object("cube", 4, 4, 4);
for (int x = -2; x < 2; x++) {
for (int y = -2; y < 2; y++) {
for (int z = -2; z < 2; z++) {
cube.getBlocks().put(new IrisBlockVector(x, y, z), state("minecraft:stone"));
}
}
}
for (double factor : new double[]{2D, 0.5D}) {
IrisDimension dimension = new IrisDimension().setAllObjectScaleFactor(factor)
.setStaticObjects(new KList<>(entry("cube", 16, 100, 16),
entry("cube", 48, 100, 16).setScale(1D),
entry("cube", 80, 100, 16).setScale(1D / factor)));
IrisStaticObjectLayer layer = dimension.getStaticObjectLayer(data);
int[] counts = new int[3];
for (int chunkX = 0; chunkX < 6; chunkX++) {
for (int chunkZ = 0; chunkZ < 2; chunkZ++) {
counts[chunkX / 2] += layer.blocks(chunkX, chunkZ).size();
}
}
assertEquals(factor == 2D ? 512 : 8, counts[0]);
assertEquals(64, counts[1]);
assertEquals(factor == 2D ? 8 : 512, counts[2]);
int halfWidth = factor == 2D ? 4 : 1;
assertTrue(layer.contains(16 - halfWidth, 164 - halfWidth, 16 - halfWidth));
assertFalse(layer.contains(15 - halfWidth, 164 - halfWidth, 16 - halfWidth));
dimension.setAllObjectScaleFactor(1D);
assertNotSame(layer, dimension.getStaticObjectLayer(data));
}
assertEquals(64, cube.getBlocks().size());
assertEquals(4, cube.getW());
}
@Test @Test
public void rotatedNegativeCoordinatesAndMinusOneYUseExactWorldOrigin() { public void rotatedNegativeCoordinatesAndMinusOneYUseExactWorldOrigin() {
IrisObject object = object("cross", 5, 3, 5); IrisObject object = object("cross", 5, 3, 5);
@@ -9,6 +9,7 @@ import java.util.List;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@@ -23,7 +24,7 @@ public class IrisStaticObjectTest {
assertEquals(ObjectPlaceMode.STRUCTURE_PIECE, placement.getMode()); assertEquals(ObjectPlaceMode.STRUCTURE_PIECE, placement.getMode());
assertTrue(placement.isForcePlace()); assertTrue(placement.isForcePlace());
assertFalse(placement.getRotation().isEnabled()); assertFalse(placement.getRotation().isEnabled());
assertFalse(placement.getScale().shouldScale()); assertNull(placement.getScale());
assertEquals(new IrisPosition(100, 100, -100), object.getPosition()); assertEquals(new IrisPosition(100, 100, -100), object.getPosition());
} }
@@ -69,8 +70,8 @@ public class IrisStaticObjectTest {
assertThrows(IllegalArgumentException.class, () -> new IrisStaticObject().validate(-64, 320)); assertThrows(IllegalArgumentException.class, () -> new IrisStaticObject().validate(-64, 320));
assertThrows(IllegalArgumentException.class, () -> validObject().setPosition(null).validate(-64, 320)); assertThrows(IllegalArgumentException.class, () -> validObject().setPosition(null).validate(-64, 320));
assertThrows(IllegalArgumentException.class, () -> validObject().setScale(Double.NaN).validate(-64, 320)); assertThrows(IllegalArgumentException.class, () -> validObject().setScale(Double.NaN).validate(-64, 320));
assertThrows(IllegalArgumentException.class, () -> validObject().setScale(0).validate(-64, 320)); assertThrows(IllegalArgumentException.class, () -> validObject().setScale(0D).validate(-64, 320));
assertThrows(IllegalArgumentException.class, () -> validObject().setScale(51).validate(-64, 320)); assertThrows(IllegalArgumentException.class, () -> validObject().setScale(51D).validate(-64, 320));
assertThrows(IllegalArgumentException.class, () -> validObject().setRotation(null).validate(-64, 320)); assertThrows(IllegalArgumentException.class, () -> validObject().setRotation(null).validate(-64, 320));
assertThrows(IllegalArgumentException.class, assertThrows(IllegalArgumentException.class,
() -> validObject().setRotation(new IrisStaticObjectRotation().setY(-361)).validate(-64, 320)); () -> validObject().setRotation(new IrisStaticObjectRotation().setY(-361)).validate(-64, 320));
@@ -0,0 +1,29 @@
package art.arcane.iris.util.common.director.specialhandlers;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
public class ObjectScaleHandlerTest {
@Test
public void distinguishesInheritedScaleFromExplicitOne() throws Exception {
ObjectScaleHandler handler = new ObjectScaleHandler();
assertNull(handler.parse("dimension", true));
assertNull(handler.parse(" DIMENSION ", false));
assertEquals(1D, handler.parse("1.0", false), 0D);
assertEquals(0.5D, handler.parse("0.5", false), 0D);
assertEquals(2D, handler.parse("2", false), 0D);
assertEquals("dimension", handler.toString(null));
}
@Test
public void rejectsInvalidManualFactors() {
ObjectScaleHandler handler = new ObjectScaleHandler();
for (String invalid : new String[]{"NaN", "Infinity", "-Infinity", "0", "-1", "0.009", "50.001"}) {
assertThrows(invalid, DirectorParsingException.class, () -> handler.parse(invalid, false));
}
}
}