From 0782bbf0b468605c75b15a05c857b082d318581c Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Tue, 8 Sep 2026 16:15:19 -0400 Subject: [PATCH] Whoops Core --- .../iris/core/commands/CommandObject.java | 22 +- .../modded/command/ModdedObjectCommands.java | 3 +- .../iris/core/loader/ResourceLoader.java | 2 +- .../core/pack/PackDimensionValidator.java | 7 + .../arcane/iris/core/pack/PackValidator.java | 3 +- .../art/arcane/iris/core/service/TreeSVC.java | 3 +- .../components/IrisStructureComponent.java | 10 +- .../MantleFloatingObjectComponent.java | 8 +- .../components/MantleObjectComponent.java | 32 +-- .../iris/engine/object/IrisDimension.java | 14 ++ .../engine/object/IrisObjectPlacement.java | 16 +- .../object/IrisObjectPlacementRunner.java | 130 +++++++++--- .../iris/engine/object/IrisObjectScale.java | 50 ++++- .../engine/object/IrisObjectTransforms.java | 171 +++++++-------- .../object/IrisPaintSurfaceProjection.java | 138 +++++++++++++ .../iris/engine/object/IrisStaticObject.java | 22 +- .../engine/object/IrisStaticObjectLayer.java | 5 +- .../engine/object/IrisSurfaceSupport.java | 36 +++- .../specialhandlers/ObjectScaleHandler.java | 30 +++ .../core/pack/PackObjectScaleFactorTest.java | 35 ++++ .../project/SchemaBuilderIdentityTest.java | 2 +- .../IrisStructureComponentOverboreTest.java | 8 + ...tingObjectComponentBoundaryRadiusTest.java | 3 + ...ntleObjectComponentBoundaryRadiusTest.java | 32 ++- .../object/IrisObjectGlobalScaleTest.java | 195 ++++++++++++++++++ ...isObjectPlacementRunnerRegressionTest.java | 161 ++++++++++++++- .../object/IrisObjectTransformsTileTest.java | 50 +++++ .../IrisPaintSurfaceProjectionTest.java | 109 ++++++++++ .../object/IrisStaticObjectLayerTest.java | 35 ++++ .../engine/object/IrisStaticObjectTest.java | 7 +- .../ObjectScaleHandlerTest.java | 29 +++ 31 files changed, 1178 insertions(+), 190 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisPaintSurfaceProjection.java create mode 100644 core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ObjectScaleHandler.java create mode 100644 core/src/test/java/art/arcane/iris/core/pack/PackObjectScaleFactorTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/object/IrisObjectGlobalScaleTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/object/IrisPaintSurfaceProjectionTest.java create mode 100644 core/src/test/java/art/arcane/iris/util/common/director/specialhandlers/ObjectScaleHandlerTest.java diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java index ae8222c90..3e7ef65e4 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java @@ -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 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) { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectCommands.java index 79e9860f2..5f8f9eac3 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectCommands.java @@ -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()); diff --git a/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java index 67375aacd..c05bbf181 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java +++ b/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java @@ -418,7 +418,7 @@ public class ResourceLoader 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); diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackDimensionValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackDimensionValidator.java index 04150d9f0..bb7d3d675 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackDimensionValidator.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackDimensionValidator.java @@ -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 errors) { + validateFiniteNumber(dimension, "allObjectScaleFactor", IrisObjectScale.MINIMUM_FACTOR, + IrisObjectScale.MAXIMUM_FACTOR, "Dimension '" + dimensionKey + "'", errors); + } + static void validateDimensionStack(File packFolder, String dimensionKey, JSONObject dimension, Set dimensionKeys, List blockingErrors) { if (!dimension.has("dimensionStack")) { diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java index 9e1bc82bf..e4174d44f 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java @@ -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 errors = new ArrayList<>(); PackDimensionValidator.validateStaticObjects(packFolder, dimensionKey, dimension, errors); + PackDimensionValidator.validateObjectScaleFactor(dimensionKey, dimension, errors); if (!errors.isEmpty()) { throw new IllegalArgumentException(String.join("; ", errors)); } diff --git a/core/src/main/java/art/arcane/iris/core/service/TreeSVC.java b/core/src/main/java/art/arcane/iris/core/service/TreeSVC.java index bd0c3bf8d..6bdaf5b9c 100644 --- a/core/src/main/java/art/arcane/iris/core/service/TreeSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/TreeSVC.java @@ -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; diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java index 3a4c409ae..aa345c50c 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java @@ -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 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, diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponent.java index 90106eccb..8546d235c 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponent.java @@ -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!"); } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java index a5037414c..d84ab6c50 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java @@ -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 guessPlacedKeys(RNG rng, int x, int z, IrisObjectPlacement objectPlacement) { Set 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 placements, + IrisDimension dimension, DataProvider dataProvider, KMap sizeCache, KSet 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) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java index 0da44881b..8b1e27629 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java @@ -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 diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java index cd60c2c41..b86e89696 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java @@ -162,8 +162,8 @@ public class IrisObjectPlacement { private KList 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 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)) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java index 47d4b6f4f..936ce4042 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java @@ -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 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 transformedPaintSupport(SpinKernel spin, IrisBlockVector translateOffset, + CNG warp, double half, int x, int y, int z) { + KList 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; } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectScale.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectScale.java index 5e0bc41a6..21fc17275 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectScale.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectScale.java @@ -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; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTransforms.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTransforms.java index 1e606d057..fb605c8d1 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTransforms.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTransforms.java @@ -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 placeBlock = new VectorMap<>(); - VectorMap placeTile = new VectorMap<>(); - VectorMap placeMax = savedOrigin && scale > 1 ? new VectorMap<>() : null; - - IrisVector center; - IrisObject oo; - boolean hasTiles; + List 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 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 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 scaledBlocks = oo.blocks; + VectorMap 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 diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisPaintSurfaceProjection.java b/core/src/main/java/art/arcane/iris/engine/object/IrisPaintSurfaceProjection.java new file mode 100644 index 000000000..84d99b607 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisPaintSurfaceProjection.java @@ -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) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStaticObject.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStaticObject.java index 4be4cbda6..e3b422061 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisStaticObject.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStaticObject.java @@ -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)) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStaticObjectLayer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStaticObjectLayer.java index 2c3cb7f41..442afbb29 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisStaticObjectLayer.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStaticObjectLayer.java @@ -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(); } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceSupport.java b/core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceSupport.java index 2364505a9..2c12e7296 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceSupport.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceSupport.java @@ -24,11 +24,20 @@ public final class IrisSurfaceSupport { IrisObjectTranslate translate, IrisObjectRotation rotation, int spinX, int spinY, int spinZ, List 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 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; } } diff --git a/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ObjectScaleHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ObjectScaleHandler.java new file mode 100644 index 000000000..789c590dc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ObjectScaleHandler.java @@ -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"; + } +} diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackObjectScaleFactorTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackObjectScaleFactorTest.java new file mode 100644 index 000000000..63c14125e --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/pack/PackObjectScaleFactorTest.java @@ -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 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 errors = new ArrayList<>(); + PackDimensionValidator.validateObjectScaleFactor("main", json, errors); + assertEquals(value, 1, errors.size()); + assertThrows(IllegalArgumentException.class, + () -> PackValidator.requireValidObjectSettings(null, "main", json)); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/core/project/SchemaBuilderIdentityTest.java b/core/src/test/java/art/arcane/iris/core/project/SchemaBuilderIdentityTest.java index 1186cf01f..d9244869e 100644 --- a/core/src/test/java/art/arcane/iris/core/project/SchemaBuilderIdentityTest.java +++ b/core/src/test/java/art/arcane/iris/core/project/SchemaBuilderIdentityTest.java @@ -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> SCHEMA_ROOTS = List.of( IrisDimension.class, diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisStructureComponentOverboreTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisStructureComponentOverboreTest.java index ff75234d2..78b5b6d61 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisStructureComponentOverboreTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisStructureComponentOverboreTest.java @@ -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")); diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponentBoundaryRadiusTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponentBoundaryRadiusTest.java index 3e4ac3522..b6fb22631 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponentBoundaryRadiusTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponentBoundaryRadiusTest.java @@ -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 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()); } } diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentBoundaryRadiusTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentBoundaryRadiusTest.java index be7f71765..5b63a6a5f 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentBoundaryRadiusTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentBoundaryRadiusTest.java @@ -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 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)); diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisObjectGlobalScaleTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisObjectGlobalScaleTest.java new file mode 100644 index 000000000..98acbc6e3 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisObjectGlobalScaleTest.java @@ -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()); + } + +} diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisObjectPlacementRunnerRegressionTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisObjectPlacementRunnerRegressionTest.java index 1dc1ba59c..c4b43a32a 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisObjectPlacementRunnerRegressionTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisObjectPlacementRunnerRegressionTest.java @@ -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 world = new HashMap<>(); private final Map data = new HashMap<>(); private final List sampledColumns = new ArrayList<>(); + private final Map 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 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); diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisObjectTransformsTileTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisObjectTransformsTileTest.java index 00e543bf8..140ae7a6f 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisObjectTransformsTileTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisObjectTransformsTileTest.java @@ -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 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); diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisPaintSurfaceProjectionTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisPaintSurfaceProjectionTest.java new file mode 100644 index 000000000..113a8f6a1 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisPaintSurfaceProjectionTest.java @@ -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); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisStaticObjectLayerTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisStaticObjectLayerTest.java index 89e2e6497..e090b381c 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisStaticObjectLayerTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisStaticObjectLayerTest.java @@ -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); diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisStaticObjectTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisStaticObjectTest.java index 57e696daf..0a8d4f932 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisStaticObjectTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisStaticObjectTest.java @@ -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)); diff --git a/core/src/test/java/art/arcane/iris/util/common/director/specialhandlers/ObjectScaleHandlerTest.java b/core/src/test/java/art/arcane/iris/util/common/director/specialhandlers/ObjectScaleHandlerTest.java new file mode 100644 index 000000000..19314955e --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/common/director/specialhandlers/ObjectScaleHandlerTest.java @@ -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)); + } + } +}