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.IrisObjectPlacementScaleInterpolator;
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.platform.bukkit.BukkitBlockState;
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.Param;
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.format.C;
import art.arcane.iris.util.common.math.Direction;
@@ -551,17 +553,21 @@ public class CommandObject implements DirectorExecutor {
boolean edit,
@Param(description = "The amount of degrees to rotate by", descriptionKey = "iris.director.commandobject.param.amount_degrees_rotate_by", defaultValue = "0")
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")
double scale
@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
// ,
// @Param(description = "The scale interpolator to use", descriptionKey = "iris.director.commandobject.param.scale_interpolator_use", defaultValue = "none")
// IrisObjectPlacementScaleInterpolator interpolator
) {
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);
if (scale > maxScale) {
if (factor > 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();
@@ -581,14 +587,16 @@ public class CommandObject implements DirectorExecutor {
Map<Block, BlockData> futureChanges = new HashMap<>();
if (scale != 1) {
o = o.scaled(scale, IrisObjectPlacementScaleInterpolator.TRICUBIC);
if (factor != 1D) {
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.
final IrisObject placed = o;
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));
if (!edit) {
@@ -415,7 +415,8 @@ public final class ModdedObjectCommands {
placement.setRotation(IrisObjectRotation.of(0, rotation, 0));
ModdedObjectPlacer placer = new ModdedObjectPlacer(level, engine);
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) {
ModdedIrisLog.error("Iris paste failed for {}", key, e);
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);
JsonSchemaValidator.validateTopLevelKeys(parsed, rawText, j, resourceTypeName, objectClass);
if (objectClass == IrisDimension.class) {
PackValidator.requireValidStaticObjects(manager.getDataFolder(), name, parsed);
PackValidator.requireValidObjectSettings(manager.getDataFolder(), name, parsed);
}
T t = getManager().getGson()
.fromJson(preprocess(parsed).toString(0), objectClass);
@@ -19,6 +19,7 @@
package art.arcane.iris.core.pack;
import art.arcane.iris.engine.object.IrisDimensionType;
import art.arcane.iris.engine.object.IrisObjectScale;
import art.arcane.iris.engine.object.IrisWorldBoundary;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
@@ -57,6 +58,7 @@ final class PackDimensionValidator {
validateDimensionHeights(packFolder, dimensionKey, dimJson, blockingErrors);
validateWorldBoundary(dimensionKey, dimJson, blockingErrors);
validateStaticObjects(packFolder, dimensionKey, dimJson, blockingErrors);
validateObjectScaleFactor(dimensionKey, dimJson, blockingErrors);
validateDimensionStack(packFolder, dimensionKey, dimJson, dimensionKeys, blockingErrors);
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,
List<String> blockingErrors) {
if (!dimension.has("dimensionStack")) {
@@ -86,9 +86,10 @@ public final class PackValidator {
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<>();
PackDimensionValidator.validateStaticObjects(packFolder, dimensionKey, dimension, errors);
PackDimensionValidator.validateObjectScaleFactor(dimensionKey, dimension, errors);
if (!errors.isEmpty()) {
throw new IllegalArgumentException(String.join("; ", errors));
}
@@ -150,7 +150,8 @@ public class TreeSVC implements IrisService {
}
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) {
return;
@@ -477,8 +477,9 @@ public class IrisStructureComponent extends IrisMantleComponent {
if (object == null) {
return;
}
int horizontalSpan = Math.max(1, Math.max(object.getW(), object.getD()));
int verticalSpan = Math.max(1, object.getH());
double scale = Math.max(1D, getDimension().getAllObjectScaleFactor());
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);
KList<ObjectBlockPosition> positions = new KList<>();
int minChunkX = Math.floorDiv(search.minX, 16);
@@ -516,6 +517,11 @@ public class IrisStructureComponent extends IrisMantleComponent {
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,
int maxX, int maxY, int maxZ,
int horizontalSpan, int verticalSpan,
@@ -188,7 +188,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (raw == null) {
continue;
}
IrisObject obj0 = placement.getScale().get(rng, raw);
IrisObject obj0 = placement.scaleObject(rng, raw, getDimension());
if (obj0 == null) {
continue;
}
@@ -248,7 +248,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (raw == null) {
continue;
}
IrisObject obj0 = placement.getScale().get(rng, raw);
IrisObject obj0 = placement.scaleObject(rng, raw, getDimension());
if (obj0 == null) {
continue;
}
@@ -340,7 +340,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (raw == null) {
continue;
}
IrisObject obj0 = placement.getScale().get(rng, raw);
IrisObject obj0 = placement.scaleObject(rng, raw, getDimension());
if (obj0 == null) {
continue;
}
@@ -628,7 +628,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
size = IrisObject.sampleSize(file);
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)) {
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;
int density = Math.max(1, p.getDensity());
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 (golden) {
IrisLogging.debug("Goldendebug procedural pick: chunk=" + x + "," + z
@@ -887,7 +887,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
for (int i = 0; i < density; i++) {
attempts++;
IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(complex, rng));
IrisObject v = objectPlacement.scaleObject(rng, objectPlacement.getObject(complex, rng), getDimension());
if (v == null) {
nullObjects++;
if (traceRegen) {
@@ -1030,7 +1030,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
for (int i = 0; i < density; i++) {
attempts++;
IrisObject object = objectPlacement.getScale().get(rng, objectPlacement.getObject(complex, rng));
IrisObject object = objectPlacement.scaleObject(rng, objectPlacement.getObject(complex, rng), getDimension());
if (object == null) {
nullObjects++;
if (traceRegen) {
@@ -1261,7 +1261,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int density = objectPlacement.getDensity(rng, minX, minZ, getData());
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) {
continue;
}
@@ -1647,7 +1647,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
private Set<String> guessPlacedKeys(RNG rng, int x, int z, IrisObjectPlacement objectPlacement) {
Set<String> f = new KSet<>();
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) {
continue;
}
@@ -1708,11 +1708,12 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
radius = Math.max(radius, computePlacementRadius(
region.getObjects(),
dimension,
dataProvider,
sizeCache,
warnedLargeObjects
));
radius = Math.max(radius, computeProceduralRadius(region.getProceduralObjects(), dataProvider));
radius = Math.max(radius, computeProceduralRadius(region.getProceduralObjects(), dimension, dataProvider));
}
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
if (biome == null) {
@@ -1720,17 +1721,19 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
radius = Math.max(radius, computePlacementRadius(
biome.getObjects(),
dimension,
dataProvider,
sizeCache,
warnedLargeObjects
));
radius = Math.max(radius, computeProceduralRadius(biome.getProceduralObjects(), dataProvider));
radius = Math.max(radius, computeProceduralRadius(biome.getProceduralObjects(), dimension, dataProvider));
}
return radius;
}
private int computePlacementRadius(
KList<IrisObjectPlacement> placements,
IrisDimension dimension,
DataProvider dataProvider,
KMap<String, IrisBlockVector> sizeCache,
KSet<String> warnedLargeObjects
@@ -1746,7 +1749,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (size == null) {
continue;
}
int reach = calculatePlacementReach(size, placement);
int reach = calculatePlacementReach(size, placement, placement.getMaximumScale(dimension));
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
}
@@ -1759,7 +1762,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
return radius;
}
private int computeProceduralRadius(IrisProceduralObjects procedural, DataProvider dataProvider) {
private int computeProceduralRadius(IrisProceduralObjects procedural, IrisDimension dimension, DataProvider dataProvider) {
if (procedural == null || procedural.isEmpty()) {
return 0;
}
@@ -1778,13 +1781,13 @@ public class MantleObjectComponent extends IrisMantleComponent {
continue;
}
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;
}
static int calculatePlacementReach(IrisBlockVector size, IrisObjectPlacement placement) {
static int calculatePlacementReach(IrisBlockVector size, IrisObjectPlacement placement, double scale) {
if (size == null) {
return 0;
}
@@ -1792,7 +1795,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
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 height = scaledDimension(size.getBlockY(), scale);
int depth = scaledDimension(size.getBlockZ(), scale);
@@ -1823,7 +1825,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
int reach = calculatePlacementReach(
new IrisBlockVector(object.getW(), object.getH(), object.getD()),
placement);
placement, 1D);
return complex.allowsNewGenerationFootprint(
saturatedOffset(anchorX, -reach),
saturatedOffset(anchorZ, -reach),
@@ -1838,10 +1840,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
private static int scaledDimension(int dimension, double scale) {
int absoluteDimension = Math.abs(dimension);
if (scale <= 1D) {
if (scale == 1D) {
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) {
@@ -186,6 +186,10 @@ public class IrisDimension extends IrisRegistrant {
@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.")
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")
private IrisEnvironment environment = IrisEnvironment.NORMAL;
@RegistryListResource(IrisRegion.class)
@@ -319,6 +323,16 @@ public class IrisDimension extends IrisRegistrant {
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
* 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<>();
@Desc("Translate this object's placement")
private IrisObjectTranslate translate = new IrisObjectTranslate();
@Desc("Scale Objects")
private IrisObjectScale scale = new IrisObjectScale();
@Desc("Explicit object scale. When omitted, the dimension's allObjectScaleFactor applies. Any authored scale, including size 1, overrides that factor.")
private IrisObjectScale scale;
@ArrayType(min = 1, type = IrisObjectLoot.class)
@Desc("The loot tables to apply to these objects")
private KList<IrisObjectLoot> loot = new KList<>();
@@ -363,6 +363,18 @@ public class IrisObjectPlacement {
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) {
for (IrisTree i : getTrees()) {
if (i.matches(size, type)) {
@@ -125,6 +125,7 @@ final class IrisObjectPlacementRunner {
boolean preventingDecay = placer.isPreventingDecay();
boolean waterlogCandidate = config.isWaterloggable() || config.isUnderwater();
boolean rawStructurePiece = config.getMode() == ObjectPlaceMode.STRUCTURE_PIECE;
boolean paint = yv < 0 && config.getMode() == ObjectPlaceMode.PAINT;
boolean organicFloor = config.getMode() == ObjectPlaceMode.ORGANIC_STILT;
boolean ceilingHang = config.getMode() == ObjectPlaceMode.CEILING_HANG;
boolean organic = organicFloor || ceilingHang;
@@ -257,7 +258,10 @@ final class IrisObjectPlacementRunner {
}
}
} 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 (shouldBailForCarvingAnchor(placer, config, x, y, z)) {
bail = true;
@@ -302,12 +306,30 @@ final class IrisObjectPlacementRunner {
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
&& config.getCarvingSupport().supportsSurface()
&& config.getMode() != ObjectPlaceMode.FLOATING
&& !rawStructurePiece
&& IrisSurfaceSupport.intersectsHydrology(oplacer, x, z, config.getTranslate(),
config.getRotation(), spinx, spiny, spinz, self.getSurfaceSupportOffsets())) {
&& (paint ? IrisSurfaceSupport.intersectsHydrology(oplacer, x, z, null, null,
0, 0, 0, paintSupport)
: IrisSurfaceSupport.intersectsHydrology(oplacer, x, z, config.getTranslate(),
config.getRotation(), spinx, spiny, spinz, self.getSurfaceSupportOffsets()))) {
return -1;
}
@@ -325,9 +347,12 @@ final class IrisObjectPlacementRunner {
&& !config.isUnderwater()
&& !config.isOnwater()
&& 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.getSurfaceSupportBuffer(), config.getSurfaceSupportDepth())) {
config.getSurfaceSupportBuffer(), config.getSurfaceSupportDepth()))) {
return -1;
}
@@ -351,22 +376,27 @@ final class IrisObjectPlacementRunner {
}
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;
}
boolean paint = yv < 0 && config.getMode() == ObjectPlaceMode.PAINT;
WorldBounds worldBounds = null;
if (config.isBore() || (!config.isForcePlace() && !rawStructurePiece
&& (!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())) {
Engine engine = requireDataEngine(rdata, "collision settings");
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);
PlacedObject p = engine.resolveObjectPlacementMarker(i, k, placementMarker);
if (p == null) continue;
@@ -387,8 +417,14 @@ final class IrisObjectPlacementRunner {
if (config.isBore()) {
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());
}
}
@@ -546,8 +582,14 @@ final class IrisObjectPlacementRunner {
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)) {
yy = (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + placer.getHighest(xx, zz, self.getLoader(), config.isUnderwater());
if (paint) {
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)) {
@@ -864,6 +906,26 @@ final class IrisObjectPlacementRunner {
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,
boolean ceilingHang, int margin) {
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);
}
private WorldBounds resolveWorldBounds(IObjectPlacer placer, IrisObjectPlacement config, TransformedBounds bounds,
boolean paint, int x, int y, int z) {
private WorldBounds resolveWorldBounds(TransformedBounds bounds, IrisPaintSurfaceProjection projection,
int randomY, int x, int y, int z) {
int minX = x + bounds.minX();
int maxX = x + bounds.maxX();
int minZ = z + bounds.minZ();
int maxZ = z + bounds.maxZ();
if (!paint) {
if (projection == null) {
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;
for (int worldX = minX; worldX <= maxX; worldX++) {
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);
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,
maximumSurface + bounds.maxY() + paintOffset, minZ, maxZ);
}
@@ -960,7 +1025,8 @@ final class IrisObjectPlacementRunner {
*/
private boolean nativeStructureVetoes(IObjectPlacer placer, IrisObjectPlacement config, SpinKernel spin, TransformedBounds bounds,
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();
if (engine == null) {
return false;
@@ -976,8 +1042,8 @@ final class IrisObjectPlacementRunner {
}
int worldY = y + engine.getMinHeight();
int envelopeMinY = paint ? Integer.MIN_VALUE : worldY + bounds.minY() - warpMargin;
int envelopeMaxY = paint ? Integer.MAX_VALUE : worldY + bounds.maxY() + warpMargin;
int envelopeMinY = projection != null ? Integer.MIN_VALUE : worldY + bounds.minY() - warpMargin;
int envelopeMaxY = projection != null ? Integer.MAX_VALUE : worldY + bounds.maxY() + warpMargin;
boolean envelopeMeetsPiece = false;
for (NativeStructureVolume volume : volumes) {
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 zz = z + (int) Math.round(i.getZ());
int yy = paint
? (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2)
+ placer.getHighest(xx, zz, self.getLoader(), config.isUnderwater())
: y + (int) Math.round(i.getY());
int yy = y + (int) Math.round(i.getY());
if (projection != null) {
if (surfaceWarp != null) {
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();
for (NativeStructureVolume volume : volumes) {
if (volume.containsWithin(xx, worldBlockY, zz, warpMargin)) {
if (volume.containsWithin(xx, worldBlockY, zz, projection == null ? warpMargin : 0)) {
return true;
}
}
@@ -43,6 +43,8 @@ import java.util.Map;
@Desc("Scale objects")
@Data
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_MAX_ESTIMATED_BYTES = 64L * 1024L * 1024L;
private static final long CACHE_ENTRY_ESTIMATED_BYTES = 512L;
@@ -88,15 +90,32 @@ public class IrisObjectScale {
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) {
return (int) Math.ceil(getMaxScale() * indim);
}
public double getMaxScale() {
if (size != 1) {
return size;
ScaleRequest request = snapshotRequest();
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) {
@@ -109,7 +128,27 @@ public class IrisObjectScale {
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);
CacheLookup lookup = CACHE.lookup(key);
if (lookup.variant() != null) {
@@ -145,6 +184,9 @@ public class IrisObjectScale {
}
private ScaleRequest snapshotRequest() {
requireValidFactor(size, "size");
requireValidFactor(minimumScale, "minimumScale");
requireValidFactor(maximumScale, "maximumScale");
IrisObjectPlacementScaleInterpolator configuredInterpolation = interpolation == null
? IrisObjectPlacementScaleInterpolator.NONE
: interpolation;
@@ -88,130 +88,99 @@ final class IrisObjectTransforms {
}
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);
}
private static IrisObject scaled(IrisObject self, double scale, IrisObjectPlacementScaleInterpolator interpolation,
ScaleOrigin origin) {
IrisObjectScale.requireValidFactor(scale, "Object scale");
if (interpolation == null) {
interpolation = IrisObjectPlacementScaleInterpolator.NONE;
}
boolean savedOrigin = origin == ScaleOrigin.SAVED;
IrisVector sm1 = new IrisVector(scale - 1, scale - 1, scale - 1);
scale = Math.max(0.001, Math.min(50, scale));
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;
List<ScaledVoxel> voxels;
IrisObject output;
self.readLock.lock();
try {
center = new IrisVector(self.getCenter().getX(), self.getCenter().getY(), self.getCenter().getZ());
if (self.getH() == 2) {
center = center.setY(center.getBlockY() + 0.5);
}
if (self.getW() == 2) {
center = center.setX(center.getBlockX() + 0.5);
}
if (self.getD() == 2) {
center = center.setZ(center.getBlockZ() + 0.5);
}
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();
output = savedOrigin ? createOriginScaledObject(self, scale)
: createCenteredScaledObject(self, scale);
output.setLoadKey(self.getLoadKey());
output.setLoader(self.getLoader());
output.setLoadFile(self.getLoadFile());
voxels = new ArrayList<>(self.blocks.size());
Vector3i sourceCenter = self.getCenter();
IrisBlockVector targetCenter = new IrisBlockVector(output.getCenter().getX(),
output.getCenter().getY(), output.getCenter().getZ());
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : self.blocks) {
PlatformBlockState bd = entry.getValue();
IrisBlockVector sourcePosition = entry.getKey();
IrisBlockVector position = savedOrigin
? scaledMinimum(sourcePosition, scale)
: sourcePosition.clone().add(IrisObject.HALF).subtract(center)
.multiply(scale).add(sm1).toBlockVector();
placeBlock.put(position, bd);
if (placeMax != null) {
placeMax.put(position, scaledMaximum(sourcePosition, scale));
}
if (hasTiles) {
TileData tile = self.states.get(entry.getKey());
if (tile == null) {
placeTile.remove(position);
} else {
placeTile.put(position, tile);
}
IrisBlockVector position = savedOrigin ? sourcePosition : new IrisBlockVector(
sourcePosition.getBlockX() + sourceCenter.getX(),
sourcePosition.getBlockY() + sourceCenter.getY(),
sourcePosition.getBlockZ() + sourceCenter.getZ());
IrisBlockVector minimum = scaledMinimum(position, scale);
IrisBlockVector maximum = scaledMaximum(position, scale);
if (!savedOrigin) {
minimum.subtract(targetCenter);
maximum.subtract(targetCenter);
}
TileData tile = self.states.get(sourcePosition);
voxels.add(new ScaledVoxel(sourcePosition.clone(), minimum, maximum, entry.getValue(),
tile == null ? null : tile.clone()));
}
} finally {
self.readLock.unlock();
}
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : placeBlock) {
IrisBlockVector v = entry.getKey();
TileData tile = hasTiles ? placeTile.get(v) : null;
if (scale > 1) {
IrisVector minimum = savedOrigin ? v : v.clone().add(center);
IrisVector maximum = savedOrigin ? placeMax.get(v) : v.clone().add(center).add(sm1);
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());
}
}
}
if (scale != Math.rint(scale)) {
voxels.sort(Comparator.comparingInt((ScaledVoxel voxel) -> voxel.source().getBlockX())
.thenComparingInt(voxel -> voxel.source().getBlockY())
.thenComparingInt(voxel -> voxel.source().getBlockZ()));
}
for (ScaledVoxel voxel : voxels) {
writeScaledVoxel(output, voxel);
}
VectorMap<PlatformBlockState> scaledBlocks = oo.blocks;
VectorMap<PlatformBlockState> scaledBlocks = output.blocks;
if (scale > 1) {
switch (interpolation) {
case TRILINEAR -> trilinear(oo, (int) Math.round(scale));
case TRICUBIC -> tricubic(oo, (int) Math.round(scale));
case TRIHERMITE -> trihermite(oo, (int) Math.round(scale));
case TRILINEAR -> trilinear(output, (int) Math.round(scale));
case TRICUBIC -> tricubic(output, (int) Math.round(scale));
case TRIHERMITE -> trihermite(output, (int) Math.round(scale));
}
}
removeInapplicableTiles(output, scaledBlocks);
return output;
}
removeInapplicableTiles(oo, scaledBlocks);
return oo;
private static void writeScaledVoxel(IrisObject output, ScaledVoxel voxel) {
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) {
@@ -499,6 +468,10 @@ final class IrisObjectTransforms {
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 {
DEFAULT,
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();
@MinNumber(MINIMUM_SCALE)
@MaxNumber(MAXIMUM_SCALE)
@Desc("Fixed size multiplier. 1 preserves the saved size, 0.5 halves it and 2 doubles it.")
private double scale = 1D;
@Desc("Explicit fixed size multiplier. When omitted, the dimension's allObjectScaleFactor applies. An explicit 1 preserves the saved size.")
private Double scale;
@Desc("Interpolation used when enlarging the object. NONE preserves blocky shapes.")
private IrisObjectPlacementScaleInterpolator scaleInterpolation = IrisObjectPlacementScaleInterpolator.NONE;
@ArrayType(type = IrisObjectReplace.class)
@@ -74,9 +74,8 @@ public class IrisStaticObject {
throw new IllegalArgumentException("rotation must be an object.");
}
rotation.validate();
if (!Double.isFinite(scale) || scale < MINIMUM_SCALE || scale > MAXIMUM_SCALE) {
throw new IllegalArgumentException("scale must be finite and between "
+ MINIMUM_SCALE + " and " + MAXIMUM_SCALE + ".");
if (scale != null) {
IrisObjectScale.requireValidFactor(scale, "scale");
}
if (scaleInterpolation == null) {
throw new IllegalArgumentException("scaleInterpolation must be a supported interpolation mode.");
@@ -141,12 +140,23 @@ public class IrisStaticObject {
.setMode(ObjectPlaceMode.STRUCTURE_PIECE)
.setForcePlace(true)
.setRotation(rotation.toRotation())
.setScale(new IrisObjectScale().setSize(scale).setInterpolation(scaleInterpolation))
.setScale(scale == null ? null
: new IrisObjectScale().setSize(scale).setInterpolation(scaleInterpolation))
.setEdit(new KList<>(edit))
.setBore(bore)
.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) {
if (replacement.getFind() == null || replacement.getFind().isEmpty()
|| replacement.getFind().contains(null)) {
@@ -55,8 +55,9 @@ public final class IrisStaticObjectLayer {
if (object == null) {
throw new IllegalArgumentException("Object '" + entry.getObject() + "' could not be loaded");
}
if (entry.getScale() != 1D) {
object = object.scaledAroundOrigin(entry.getScale(), entry.getScaleInterpolation());
double scale = entry.resolveScale(dimension);
if (scale != 1D) {
object = object.scaledAroundOrigin(scale, entry.getScaleInterpolation());
} else if (entry.isSmartBore()) {
object = object.copy();
}
@@ -24,11 +24,20 @@ public final class IrisSurfaceSupport {
IrisObjectTranslate translate, IrisObjectRotation rotation,
int spinX, int spinY, int spinZ,
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();
if (!stencil.build(supportOffsets, translate, rotation, spinX, spinY, spinZ, Math.max(0, buffer))) {
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,
@@ -165,13 +174,14 @@ public final class IrisSurfaceSupport {
}
/** 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(
Math.max(centerX, width - 1 - centerX),
Math.max(centerZ, depth - 1 - centerZ));
for (int ring = 0; ring <= maxRing; ring++) {
if (ring == 0) {
if (isColumnUnsupported(placer, data, x, z, centerX, centerZ, minSolidDepth)) {
if (isColumnUnsupported(placer, data, x, z, centerX, centerZ, minSolidDepth, projection)) {
return true;
}
continue;
@@ -182,14 +192,14 @@ public final class IrisSurfaceSupport {
int lowZ = centerZ - ring;
int highZ = centerZ + ring;
for (int sx = lowX; sx <= highX; sx++) {
if (isColumnUnsupported(placer, data, x, z, sx, lowZ, minSolidDepth)
|| isColumnUnsupported(placer, data, x, z, sx, highZ, minSolidDepth)) {
if (isColumnUnsupported(placer, data, x, z, sx, lowZ, minSolidDepth, projection)
|| isColumnUnsupported(placer, data, x, z, sx, highZ, minSolidDepth, projection)) {
return true;
}
}
for (int sz = lowZ + 1; sz < highZ; sz++) {
if (isColumnUnsupported(placer, data, x, z, lowX, sz, minSolidDepth)
|| isColumnUnsupported(placer, data, x, z, highX, sz, minSolidDepth)) {
if (isColumnUnsupported(placer, data, x, z, lowX, sz, minSolidDepth, projection)
|| isColumnUnsupported(placer, data, x, z, highX, sz, minSolidDepth, projection)) {
return true;
}
}
@@ -214,7 +224,8 @@ public final class IrisSurfaceSupport {
}
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
|| !cells[(stencilX * depth) + stencilZ]) {
return false;
@@ -222,9 +233,14 @@ public final class IrisSurfaceSupport {
int columnX = x + minX + stencilX;
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++) {
if (placer.isCarved(columnX, surfaceY - below, columnZ)) {
if (placer.isCarved(columnX, surfaceY - below, columnZ)
|| projection != null && !placer.isSurfaceSolid(columnX, surfaceY - below, columnZ)) {
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
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(
IrisDimension.class,
@@ -7,6 +7,14 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
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
public void ordinaryMarkersAreSeparatedFromStructureMarkers() {
assertTrue(IrisStructureComponent.isOrdinaryObjectMarker("trees/oak@42"));
@@ -66,6 +66,7 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
.setFloatingObjects(new KList<>(placement));
IrisBiome biome = new IrisBiome().setFloatingChildBiomes(new KList<>(floatingChild));
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getAllObjectScaleFactor()).thenReturn(1D);
when(dimension.getReachableBiomes(org.mockito.ArgumentMatchers.any())).thenReturn(new KList<>(biome));
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
@@ -80,5 +81,7 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
when(engineMantle.getData()).thenReturn(data);
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))
.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
@@ -107,7 +107,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
.setMode(ObjectPlaceMode.VACUUM)
.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
@@ -126,6 +126,30 @@ public class MantleObjectComponentBoundaryRadiusTest {
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
public void sourceReplayUsesAscendingSignedCoordinates() {
List<String> replayedSources = new ArrayList<>();
@@ -185,6 +209,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
when(region.getObjects()).thenReturn(new KList<>());
when(region.getProceduralObjects()).thenReturn(proceduralObjects);
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getAllObjectScaleFactor()).thenReturn(1D);
when(dimension.getAllRegions(any())).thenReturn(new KList<>(region));
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>());
Engine engine = mock(Engine.class);
@@ -215,10 +240,12 @@ public class MantleObjectComponentBoundaryRadiusTest {
when(upperRegion.getObjects()).thenReturn(new KList<>());
when(upperRegion.getProceduralObjects()).thenReturn(proceduralObjects);
IrisDimension baseDimension = mock(IrisDimension.class);
when(baseDimension.getAllObjectScaleFactor()).thenReturn(1D);
when(baseDimension.isUpperDimensionObjects()).thenReturn(true);
when(baseDimension.getAllRegions(any())).thenReturn(new KList<>());
when(baseDimension.getReachableBiomes(any())).thenReturn(new KList<>());
IrisDimension upperDimension = mock(IrisDimension.class);
when(upperDimension.getAllObjectScaleFactor()).thenReturn(1D);
when(upperDimension.getAllRegions(any())).thenReturn(new KList<>(upperRegion));
when(upperDimension.getReachableBiomes(any())).thenReturn(new KList<>());
UpperDimensionContext upperContext = mock(UpperDimensionContext.class);
@@ -252,6 +279,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
.setTranslate(new IrisObjectTranslate().setX(32));
IrisBiome biome = new IrisBiome().setObjects(new KList<>(placement));
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getAllObjectScaleFactor()).thenReturn(1D);
when(dimension.isUseMantle()).thenReturn(true);
when(dimension.getAllRegions(any())).thenReturn(new KList<>());
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.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeStructureVolume;
import art.arcane.iris.engine.framework.PlacedObject;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
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.KMap;
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.anyDouble;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -279,6 +283,133 @@ public class IrisObjectPlacementRunnerRegressionTest {
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() {
IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
@@ -326,9 +457,11 @@ public class IrisObjectPlacementRunnerRegressionTest {
private final Map<String, PlatformBlockState> world = new HashMap<>();
private final Map<String, Object> data = new HashMap<>();
private final List<String> sampledColumns = new ArrayList<>();
private final Map<String, int[]> terrain = new HashMap<>();
private final Engine engine;
private int failAfterWrites = Integer.MAX_VALUE;
private boolean debugSmartBore;
private boolean highestFollowsWrites;
private RecordingPlacer(Engine engine) {
this.engine = engine;
@@ -342,6 +475,16 @@ public class IrisObjectPlacementRunnerRegressionTest {
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() {
return writes;
}
@@ -357,13 +500,13 @@ public class IrisObjectPlacementRunnerRegressionTest {
@Override
public int getHighest(int x, int z, IrisData data) {
sampledColumns.add(x + ":" + z);
return ANCHOR_Y;
return highest(x, z);
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
sampledColumns.add(x + ":" + z);
return ANCHOR_Y;
return highest(x, z);
}
@Override
@@ -390,6 +533,20 @@ public class IrisObjectPlacementRunnerRegressionTest {
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
public boolean isSolid(int x, int y, int 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());
}
@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
public void untypedTilesRemainAttachedToTheirScaledSourceMaterial() {
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);
}
@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
public void rotatedNegativeCoordinatesAndMinusOneYUseExactWorldOrigin() {
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.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -23,7 +24,7 @@ public class IrisStaticObjectTest {
assertEquals(ObjectPlaceMode.STRUCTURE_PIECE, placement.getMode());
assertTrue(placement.isForcePlace());
assertFalse(placement.getRotation().isEnabled());
assertFalse(placement.getScale().shouldScale());
assertNull(placement.getScale());
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, () -> validObject().setPosition(null).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(51).validate(-64, 320));
assertThrows(IllegalArgumentException.class, () -> validObject().setScale(0D).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(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));
}
}
}