diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/CarveOrphanSweep.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/CarveOrphanSweep.java index 065fb7c60..a9e964f2e 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/CarveOrphanSweep.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/CarveOrphanSweep.java @@ -39,6 +39,10 @@ public final class CarveOrphanSweep { void markCarved(int localX, int y, int localZ); + default boolean isProtected(int localX, int y, int localZ) { + return false; + } + /** * Cheap pre-check: may any block in [minY, maxY] be carved at all? Default true keeps * every implementor correct; the mantle-backed access answers from section slice @@ -52,12 +56,20 @@ public final class CarveOrphanSweep { private CarveOrphanSweep() { } - static int sweepChunk(MantleChunk chunk, int[] surfaceHeights, int maxSurfaceBreakDepth, int worldCeilingY) { + static int sweepChunk( + MantleChunk chunk, + int[] surfaceHeights, + int maxSurfaceBreakDepth, + int worldCeilingY, + int[] surfaceFluidBoundaryStartY, + int fluidHeight + ) { if (chunk == null) { return 0; } - return sweep(surfaceHeights, maxSurfaceBreakDepth, 0, worldCeilingY, new MantleCarveAccess(chunk)); + return sweep(surfaceHeights, maxSurfaceBreakDepth, 0, worldCeilingY, + new MantleCarveAccess(chunk, surfaceFluidBoundaryStartY, fluidHeight)); } public static int sweep(int[] surfaceHeights, int maxSurfaceBreakDepth, int worldFloorY, int worldCeilingY, CarveAccess access) { @@ -141,7 +153,9 @@ public final class CarveOrphanSweep { int y = bandFloor + (current / CHUNK_AREA); if (!anchored) { - if (localX == 0 || localX == CHUNK_SIZE - 1 || localZ == 0 || localZ == CHUNK_SIZE - 1) { + if (access.isProtected(localX, y, localZ)) { + anchored = true; + } else if (localX == 0 || localX == CHUNK_SIZE - 1 || localZ == 0 || localZ == CHUNK_SIZE - 1) { anchored = true; } else if (y == bandFloor && isSolidBelowBand(access, surfaceHeights, columnIndex, localX, localZ, bandFloor - 1, worldFloorY)) { anchored = true; @@ -215,11 +229,15 @@ public final class CarveOrphanSweep { private static final class MantleCarveAccess implements CarveAccess { private final MantleChunk chunk; + private final int[] surfaceFluidBoundaryStartY; + private final int fluidHeight; private MatterSlice cachedSlice; private int cachedSectionIndex = -1; - private MantleCarveAccess(MantleChunk chunk) { + private MantleCarveAccess(MantleChunk chunk, int[] surfaceFluidBoundaryStartY, int fluidHeight) { this.chunk = chunk; + this.surfaceFluidBoundaryStartY = surfaceFluidBoundaryStartY; + this.fluidHeight = fluidHeight; } @Override @@ -240,6 +258,12 @@ public final class CarveOrphanSweep { cachedSlice = null; } + @Override + public boolean isProtected(int localX, int y, int localZ) { + int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); + return SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight); + } + @Override public boolean mayContainCarvedCells(int minY, int maxY) { int minSection = Math.max(0, minY >> 4); diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java index 619f70050..a42b92f45 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java @@ -183,7 +183,7 @@ public class IrisCaveCarver3D { ) { CaveFluidSupportPlan fluidSupportPlan = new CaveFluidSupportPlan(); int carved = carve(writer, chunkX, chunkZ, columnWeights, minWeight, thresholdPenalty, - worldYRange, precomputedSurfaceHeights, overrideVerticalRange, fluidSupportPlan); + worldYRange, precomputedSurfaceHeights, null, overrideVerticalRange, fluidSupportPlan); fluidSupportPlan.resolve(writer.acquireChunk(chunkX, chunkZ)); return carved; } @@ -197,6 +197,7 @@ public class IrisCaveCarver3D { double thresholdPenalty, IrisRange worldYRange, int[] precomputedSurfaceHeights, + int[] surfaceFluidBoundaryStartY, IrisRange overrideVerticalRange, CaveFluidSupportPlan fluidSupportPlan ) { @@ -317,6 +318,7 @@ public class IrisCaveCarver3D { surfaceBreakThresholdBoost, columnMaxY, fluidMaxY, + surfaceFluidBoundaryStartY, surfaceBreakFloorY, surfaceBreakColumn, columnThreshold, @@ -339,6 +341,7 @@ public class IrisCaveCarver3D { surfaceBreakThresholdBoost, columnMaxY, fluidMaxY, + surfaceFluidBoundaryStartY, surfaceBreakFloorY, surfaceBreakColumn, columnThreshold, @@ -364,6 +367,7 @@ public class IrisCaveCarver3D { surfaceBreakThresholdBoost, columnMaxY, fluidMaxY, + surfaceFluidBoundaryStartY, surfaceBreakFloorY, surfaceBreakColumn, columnThreshold, @@ -387,6 +391,7 @@ public class IrisCaveCarver3D { surfaceBreakThresholdBoost, columnMaxY, fluidMaxY, + surfaceFluidBoundaryStartY, surfaceBreakFloorY, surfaceBreakColumn, columnThreshold, @@ -417,6 +422,7 @@ public class IrisCaveCarver3D { double surfaceBreakThresholdBoost, int[] columnMaxY, int[] fluidMaxY, + int[] surfaceFluidBoundaryStartY, int[] surfaceBreakFloorY, boolean[] surfaceBreakColumn, double[] columnThreshold, @@ -472,6 +478,9 @@ public class IrisCaveCarver3D { } int columnIndex = activeColumnIndices[activeIndex]; + if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight)) { + continue; + } planeColumnIndices[planeCount] = columnIndex; double localThreshold = passThreshold[columnIndex]; if (surfaceBreakColumn[columnIndex] && y >= surfaceBreakFloorY[columnIndex]) { @@ -544,6 +553,7 @@ public class IrisCaveCarver3D { double surfaceBreakThresholdBoost, int[] columnMaxY, int[] fluidMaxY, + int[] surfaceFluidBoundaryStartY, int[] surfaceBreakFloorY, boolean[] surfaceBreakColumn, double[] columnThreshold, @@ -605,6 +615,9 @@ public class IrisCaveCarver3D { } int columnIndex = activeColumnIndices[activeIndex]; + if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight)) { + continue; + } planeColumnIndices[planeCount] = columnIndex; double localThreshold = passThreshold[columnIndex]; if (surfaceBreakColumn[columnIndex] && y >= surfaceBreakFloorY[columnIndex]) { @@ -699,6 +712,7 @@ public class IrisCaveCarver3D { double surfaceBreakThresholdBoost, int[] columnMaxY, int[] fluidMaxY, + int[] surfaceFluidBoundaryStartY, int[] surfaceBreakFloorY, boolean[] surfaceBreakColumn, double[] columnThreshold, @@ -800,6 +814,9 @@ public class IrisCaveCarver3D { } int index = tileIndices[columnIndex]; + if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, index, yy, fluidHeight)) { + continue; + } double localThreshold = passThreshold[index]; if (surfaceBreakColumn[index] && yy >= surfaceBreakFloorY[index]) { localThreshold += surfaceBreakThresholdBoost; @@ -844,6 +861,7 @@ public class IrisCaveCarver3D { double surfaceBreakThresholdBoost, int[] columnMaxY, int[] fluidMaxY, + int[] surfaceFluidBoundaryStartY, int[] surfaceBreakFloorY, boolean[] surfaceBreakColumn, double[] columnThreshold, @@ -891,6 +909,9 @@ public class IrisCaveCarver3D { int carveMaxY = Math.min(columnTopY, y + sampleStep - 1); for (int yy = y; yy <= carveMaxY; yy++) { + if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, index, yy, fluidHeight)) { + continue; + } MatterCavern verticalMatter = matterByY[yy - minY]; MatterCavern matter = resolveMatter(verticalMatter, x, yy, z, index, fluidMaxY, localThreshold); diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java index ba8ad110a..3afd69d85 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java @@ -30,6 +30,8 @@ import art.arcane.iris.engine.object.IrisDimensionCarvingEntry; import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.object.IrisRange; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.iris.util.project.stream.utility.ChunkFillableDoubleStream2D; @@ -102,9 +104,20 @@ public class MantleCarvingComponent extends IrisMantleComponent { PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); List weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState); getEngineMantle().getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds()); + int fluidHeight = getDimension().getFluidHeight(); + int[] surfaceFluidBoundaryStartY = blendScratch.surfaceFluidBoundaryStartY; + SurfaceFluidBoundaryPlan.fill( + chunkSurfaceHeights, + blendScratch.fieldSurfaceHeights, + blendScratch.fieldHasFluid, + FIELD_SIZE, + BLEND_RADIUS, + fluidHeight, + surfaceFluidBoundaryStartY + ); CaveFluidSupportPlan fluidSupportPlan = new CaveFluidSupportPlan(); for (WeightedProfile weightedProfile : weightedProfiles) { - carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights, fluidSupportPlan); + carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights, surfaceFluidBoundaryStartY, fluidSupportPlan); } UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext(); @@ -118,7 +131,9 @@ public class MantleCarvingComponent extends IrisMantleComponent { writer.acquireChunk(x, z), chunkSurfaceHeights, maxSurfaceBreakDepth(weightedProfiles), - writer.getMantle().getWorldHeight() - 1 + writer.getMantle().getWorldHeight() - 1, + surfaceFluidBoundaryStartY, + fluidHeight ); } } @@ -133,10 +148,11 @@ public class MantleCarvingComponent extends IrisMantleComponent { @ChunkCoordinates private void carveProfile(WeightedProfile weightedProfile, MantleWriter writer, int cx, int cz, - int[] chunkSurfaceHeights, CaveFluidSupportPlan fluidSupportPlan) { + int[] chunkSurfaceHeights, int[] surfaceFluidBoundaryStartY, + CaveFluidSupportPlan fluidSupportPlan) { IrisCaveCarver3D carver = getCarver(weightedProfile.profile); carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, - weightedProfile.worldYRange, chunkSurfaceHeights, null, fluidSupportPlan); + weightedProfile.worldYRange, chunkSurfaceHeights, surfaceFluidBoundaryStartY, null, fluidSupportPlan); } private void carveUpperTerrain(UpperDimensionContext upperCtx, List normalProfiles, @@ -189,7 +205,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { } IrisCaveCarver3D carver = getCarver(weightedProfile.profile); carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, - constrainedRange, ceilingSurfaceHeights, fullVerticalRange, fluidSupportPlan); + constrainedRange, ceilingSurfaceHeights, null, fullVerticalRange, fluidSupportPlan); } } @@ -459,6 +475,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) { fillFieldHeights(complex.getHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights); + fillFieldFluidPresence(complex.getFluidStream(), startX, startZ, blendScratch.fieldHasFluid); fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions); fillFieldObjects(complex.getTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes); fillFieldObjects(complex.getCaveBiomeStream(), startX, startZ, blendScratch.fieldCaveBiomes); @@ -482,6 +499,15 @@ public class MantleCarvingComponent extends IrisMantleComponent { } } + private void fillFieldFluidPresence(ProceduralStream stream, int startX, int startZ, boolean[] target) { + for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) { + int worldX = startX + fieldX; + for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { + target[(fieldX * FIELD_SIZE) + fieldZ] = B.isFluid(stream.get(worldX, startZ + fieldZ)); + } + } + } + private IrisCaveCarver3D getCarver(IrisCaveProfile profile) { IrisCaveCarver3D carver = profileCarvers.get(profile); if (carver != null) { @@ -671,10 +697,12 @@ public class MantleCarvingComponent extends IrisMantleComponent { private final IdentityHashMap activeProfiles = new IdentityHashMap<>(); private final List profileOrder = new ArrayList<>(); private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE]; + private final boolean[] fieldHasFluid = new boolean[FIELD_SIZE * FIELD_SIZE]; private final IrisRegion[] fieldRegions = new IrisRegion[FIELD_SIZE * FIELD_SIZE]; private final IrisBiome[] fieldSurfaceBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE]; private final IrisBiome[] fieldCaveBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE]; private final int[] chunkSurfaceHeights = new int[CHUNK_AREA]; + private final int[] surfaceFluidBoundaryStartY = new int[CHUNK_AREA]; private final double[] chunkSurfaceHeightSamples = new double[CHUNK_AREA]; } } 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 949fea43b..c20a08ac7 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 @@ -57,6 +57,7 @@ import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.mantle.flag.ReservedFlag; import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.MatterStructurePOI; import art.arcane.iris.util.project.noise.CNG; import art.arcane.iris.util.project.noise.NoiseType; @@ -77,6 +78,7 @@ import java.util.concurrent.atomic.AtomicLong; public class MantleObjectComponent extends IrisMantleComponent { private static final long CAVE_REJECT_LOG_THROTTLE_MS = 5000L; private static final int BEDROCK_CLEARANCE = 6; + private static final byte LIQUID_FORCED_AIR = 3; private static final Map CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>(); private static final Set MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet(); @@ -384,11 +386,11 @@ public class MantleObjectComponent extends IrisMantleComponent { private void placeProceduralObjects(MantleWriter writer, RNG rng, int x, int z, IrisBiome surfaceBiome, IrisBiome caveBiome, IrisRegion region) { IrisCaveProfile surfaceCaveProfile = resolveCaveProfile(surfaceBiome.getCaveProfile(), region.getCaveProfile()); IrisCaveProfile regionCaveProfile = resolveCaveProfile(region.getCaveProfile(), caveBiome == null ? null : caveBiome.getCaveProfile()); - placeProceduralFrom(writer, rng, x, z, surfaceBiome.getProceduralObjects(), surfaceBiome.getName(), surfaceCaveProfile); - placeProceduralFrom(writer, rng, x, z, region.getProceduralObjects(), region.getName(), regionCaveProfile); + placeProceduralFrom(writer, rng, x, z, surfaceBiome.getProceduralObjects(), surfaceBiome.getName(), surfaceCaveProfile, surfaceBiome.getLoadKey()); + placeProceduralFrom(writer, rng, x, z, region.getProceduralObjects(), region.getName(), regionCaveProfile, null); if (caveBiome != null && caveBiome != surfaceBiome) { IrisCaveProfile caveProfile = resolveCaveProfile(caveBiome.getCaveProfile(), region.getCaveProfile()); - placeProceduralFrom(writer, rng, x, z, caveBiome.getProceduralObjects(), caveBiome.getName(), caveProfile); + placeProceduralFrom(writer, rng, x, z, caveBiome.getProceduralObjects(), caveBiome.getName(), caveProfile, caveBiome.getLoadKey()); } } @@ -400,7 +402,8 @@ public class MantleObjectComponent extends IrisMantleComponent { int z, IrisProceduralObjects proceduralObjects, String scope, - IrisCaveProfile caveProfile + IrisCaveProfile caveProfile, + String expectedCaveBiomeKey ) { if (proceduralObjects == null || proceduralObjects.isEmpty()) { return; @@ -457,7 +460,8 @@ public class MantleObjectComponent extends IrisMantleComponent { anchorScanStep, minDepthBelowSurface, anchorSearchAttempts, - null, + expectedCaveBiomeKey, + placement.isUnderwater(), caveAnchorCache ); } @@ -561,6 +565,7 @@ public class MantleObjectComponent extends IrisMantleComponent { int minDepthBelowSurface, int searchAttempts, String expectedCaveBiomeKey, + boolean underwater, CaveAnchorCache anchorCache ) { for (int search = 0; search < searchAttempts; search++) { @@ -576,7 +581,13 @@ public class MantleObjectComponent extends IrisMantleComponent { minDepthBelowSurface, anchorCache ); - if (candidateY < 0 || caveAnchorBiomeConflicts(candidateX, candidateY, candidateZ, expectedCaveBiomeKey)) { + if (candidateY < 0 + || caveAnchorBiomeConflicts(candidateX, candidateY, candidateZ, expectedCaveBiomeKey) + || !acceptsCaveAnchorFluid( + underwater, + writer.getDataIfPresent(candidateX, candidateY, candidateZ, MatterCavern.class), + candidateY, + getDimension().getCaveLavaHeight())) { continue; } return new CavePlacementAnchor(candidateX, candidateY, candidateZ); @@ -584,6 +595,16 @@ public class MantleObjectComponent extends IrisMantleComponent { return null; } + static boolean acceptsCaveAnchorFluid(boolean underwater, MatterCavern cavern, int y, int lavaHeight) { + if (cavern == null || !cavern.isCavern()) { + return false; + } + if (underwater || cavern.getLiquid() == LIQUID_FORCED_AIR) { + return true; + } + return cavern.isAir() && y > lavaHeight; + } + private ContainedPlacementResult placeContainedCaveObject( IObjectPlacer placer, IrisObject object, @@ -732,14 +753,18 @@ public class MantleObjectComponent extends IrisMantleComponent { } Engine engine = getEngineMantle().getEngine(); IrisBiome at = engine.getCaveBiome(x, y, z); - if (at == null) { + IrisBiome surface = engine.getSurfaceBiome(x, z); + return caveAnchorBiomeConflicts(at, surface, expectedCaveBiomeKey); + } + + static boolean caveAnchorBiomeConflicts(IrisBiome at, IrisBiome surface, String expectedCaveBiomeKey) { + if (expectedCaveBiomeKey == null || at == null) { return false; } String atKey = at.getLoadKey(); if (atKey == null || atKey.equals(expectedCaveBiomeKey)) { return false; } - IrisBiome surface = engine.getSurfaceBiome(x, z); if (surface != null && atKey.equals(surface.getLoadKey())) { return false; } @@ -818,6 +843,7 @@ public class MantleObjectComponent extends IrisMantleComponent { objectMinDepthBelowSurface, anchorSearchAttempts, expectedCaveBiomeKey, + objectPlacement.isUnderwater(), anchorCache ); diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlan.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlan.java new file mode 100644 index 000000000..ec0cf02e9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlan.java @@ -0,0 +1,94 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; + +final class SurfaceFluidBoundaryPlan { + static final int NO_BOUNDARY = Integer.MAX_VALUE; + private static final int CHUNK_SIZE = 16; + private static final int CHUNK_AREA = CHUNK_SIZE * CHUNK_SIZE; + + private SurfaceFluidBoundaryPlan() { + } + + static void fill( + int[] chunkSurfaceHeights, + double[] fieldSurfaceHeights, + boolean[] fieldHasFluid, + int fieldSize, + int padding, + int fluidHeight, + int[] boundaryStartY + ) { + if (chunkSurfaceHeights == null || chunkSurfaceHeights.length < CHUNK_AREA + || boundaryStartY == null || boundaryStartY.length < CHUNK_AREA + || padding < 1 || fieldSize < CHUNK_SIZE + (padding * 2) + || fieldSurfaceHeights == null || fieldSurfaceHeights.length < fieldSize * fieldSize + || fieldHasFluid == null || fieldHasFluid.length < fieldSize * fieldSize) { + throw new IllegalArgumentException("Surface fluid boundary fields do not cover a padded chunk"); + } + + for (int localX = 0; localX < CHUNK_SIZE; localX++) { + int fieldX = localX + padding; + for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) { + int fieldZ = localZ + padding; + int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); + int boundaryY = NO_BOUNDARY; + int surfaceY = chunkSurfaceHeights[columnIndex]; + int fieldIndex = (fieldX * fieldSize) + fieldZ; + if (fieldHasFluid[fieldIndex] && surfaceY < fluidHeight) { + boundaryY = surfaceY; + } + + boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid, + ((fieldX - 1) * fieldSize) + fieldZ, fluidHeight); + boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid, + ((fieldX + 1) * fieldSize) + fieldZ, fluidHeight); + boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid, + (fieldX * fieldSize) + fieldZ - 1, fluidHeight); + boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid, + (fieldX * fieldSize) + fieldZ + 1, fluidHeight); + boundaryStartY[columnIndex] = boundaryY; + } + } + } + + static boolean protects(int[] boundaryStartY, int columnIndex, int y, int fluidHeight) { + return boundaryStartY != null + && columnIndex >= 0 + && columnIndex < boundaryStartY.length + && y >= boundaryStartY[columnIndex] + && y <= fluidHeight; + } + + private static int lowerBoundary( + int currentBoundaryY, + double[] fieldSurfaceHeights, + boolean[] fieldHasFluid, + int fieldIndex, + int fluidHeight + ) { + int neighborSurfaceY = (int) Math.round(fieldSurfaceHeights[fieldIndex]); + if (!fieldHasFluid[fieldIndex] || neighborSurfaceY >= fluidHeight) { + return currentBoundaryY; + } + return Math.min(currentBoundaryY, neighborSurfaceY + 1); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java index a641a0d3a..4a8e82851 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java @@ -35,7 +35,6 @@ import art.arcane.iris.spi.PlatformBlockState; public class IrisPostModifier extends EngineAssignedModifier { private static final class States { private static final PlatformBlockState AIR = B.getState("AIR"); - private static final PlatformBlockState WATER = B.getState("WATER"); } private final RNG rng; @@ -245,10 +244,6 @@ public class IrisPostModifier extends EngineAssignedModifier if (w != "true".equals(IrisProceduralBlocks.propertyValue(b, "waterlogged"))) { setPostBlock(x, h, z, b.withProperty("waterlogged", String.valueOf(w)), originX, originZ, currentData); } - } else if (IrisProceduralBlocks.materialKey(b).equals("minecraft:air") && h <= fluidHeight) { - if ((isWaterOrWaterlogged(x + 1, h, z, originX, originZ, currentData) || isWaterOrWaterlogged(x - 1, h, z, originX, originZ, currentData) || isWaterOrWaterlogged(x, h, z + 1, originX, originZ, currentData) || isWaterOrWaterlogged(x, h, z - 1, originX, originZ, currentData))) { - setPostBlock(x, h, z, States.WATER, originX, originZ, currentData); - } } // Foliage diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositBiomeScope.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositBiomeScope.java index 441fa833b..e68daae49 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositBiomeScope.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositBiomeScope.java @@ -1,6 +1,11 @@ package art.arcane.iris.engine.object; +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Selects which biome identity is checked by a deposit's include and exclude filters.") public enum IrisDepositBiomeScope { + @Desc("Checks the surface biome selected for the deposit column.") SURFACE, + @Desc("Checks the cave biome selected at the deposit origin.") CAVE } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositHeightDistribution.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositHeightDistribution.java index 75bfd3f4b..7e3400ae4 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositHeightDistribution.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositHeightDistribution.java @@ -1,7 +1,13 @@ package art.arcane.iris.engine.object; +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls how a deposit chooses its origin height within the configured vertical band.") public enum IrisDepositHeightDistribution { + @Desc("Samples uniformly after clipping the configured band to the terrain and build-height bounds.") CLIPPED_UNIFORM, + @Desc("Samples uniformly from the configured band, then rejects origins outside the terrain or build-height bounds.") UNIFORM, + @Desc("Samples toward the middle of the configured band with a triangular distribution, then rejects invalid origins.") TRIANGLE } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositPlacementScope.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositPlacementScope.java index 756ef0b9c..e14c49ba6 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositPlacementScope.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositPlacementScope.java @@ -1,7 +1,13 @@ package art.arcane.iris.engine.object; +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls which vertical part of the world may contain a deposit origin.") public enum IrisDepositPlacementScope { + @Desc("Places in solid terrain below the generated surface while preserving the configured surface clearance.") TERRAIN, + @Desc("Places only in existing solid hosts above the generated terrain surface.") ABOVE_TERRAIN, + @Desc("Places in existing solid hosts anywhere within the dimension build height.") FULL_HEIGHT } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositShape.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositShape.java index e6f6e82b0..4073eb5fc 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositShape.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositShape.java @@ -1,7 +1,13 @@ package art.arcane.iris.engine.object; +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Selects the geometry used to build each deposit clump.") public enum IrisDepositShape { + @Desc("Builds the traditional Iris fixed-block-count cube clump.") IRIS, + @Desc("Builds a chain of overlapping ellipsoids matching Minecraft's ordinary ore-vein geometry.") VANILLA_ELLIPSOID, + @Desc("Builds sparse candidate offsets matching Minecraft's scattered-ore geometry.") VANILLA_SCATTERED } diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/CarveOrphanSweepTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/CarveOrphanSweepTest.java index d1e385ea1..d90d1e8e3 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/CarveOrphanSweepTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/CarveOrphanSweepTest.java @@ -125,6 +125,19 @@ public class CarveOrphanSweepTest { assertTrue(fixture.wasMarked(6, 30, 6)); } + @Test + public void protectedSurfaceFluidSupportIsNotMarkedCarved() { + Fixture fixture = new Fixture(); + fixture.carveBox(1, 14, 25, 35, 1, 14); + fixture.uncarve(7, 30, 7); + fixture.protect(7, 30, 7); + + int marked = fixture.sweep(); + + assertEquals(0, marked); + assertFalse(fixture.wasMarked(7, 30, 7)); + } + @Test public void sweepIsDeterministicAndIdempotent() { Fixture first = new Fixture(); @@ -166,6 +179,7 @@ public class CarveOrphanSweepTest { private final boolean[] carved = new boolean[16 * WORLD_HEIGHT * 16]; private final int[] surfaceHeights = new int[256]; private final List marks = new ArrayList<>(); + private int protectedCell = -1; private Fixture() { Arrays.fill(surfaceHeights, SURFACE_Y); @@ -193,6 +207,10 @@ public class CarveOrphanSweepTest { return marks.contains(index(localX, y, localZ)); } + private void protect(int localX, int y, int localZ) { + protectedCell = index(localX, y, localZ); + } + private List marks() { return marks; } @@ -215,5 +233,10 @@ public class CarveOrphanSweepTest { carved[index(localX, y, localZ)] = true; marks.add(index(localX, y, localZ)); } + + @Override + public boolean isProtected(int localX, int y, int localZ) { + return protectedCell == index(localX, y, localZ); + } } } diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java index 514a6917b..f862da0c5 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java @@ -286,6 +286,13 @@ public class IrisCaveCarver3DNearParityTest { assertTrue(containsLiquidInRange(capture.carvedLiquids, 65, 70, (byte) 0)); } + @Test + public void surfaceFluidBoundaryProtectsReservoirWithoutFloodingDeeperCaves() { + assertSurfaceFluidBoundaryForProfile(createFluidProfile().setAdaptiveSampling(false).setSampleStep(1)); + assertSurfaceFluidBoundaryForProfile(createFluidProfile().setAdaptiveSampling(true).setSampleStep(1)); + assertSurfaceFluidBoundaryForProfile(createFluidProfile().setAdaptiveSampling(false).setSampleStep(4)); + } + @Test public void floorRequiredFluidResolvesAfterTheCompleteCarveMask() { Engine engine = createEngine(80, 70); @@ -301,7 +308,7 @@ public class IrisCaveCarver3DNearParityTest { WriterCapture firstCapture = createWriterCapture(80); CaveFluidSupportPlan supportPlan = new CaveFluidSupportPlan(); new IrisCaveCarver3D(engine, supportedProfile).carve( - firstCapture.writer, chunkX, chunkZ, fullWeights(), 0D, 0D, null, surfaceHeights, null, supportPlan); + firstCapture.writer, chunkX, chunkZ, fullWeights(), 0D, 0D, null, surfaceHeights, null, null, supportPlan); int candidateCount = countLiquid(firstCapture, (byte) 1); supportPlan.resolve(firstCapture.writer.acquireChunk(chunkX, chunkZ)); @@ -362,13 +369,13 @@ public class IrisCaveCarver3DNearParityTest { new IrisCaveCarver3D(engine, fluidProfile).carve( capture.writer, 0, 0, fullWeights(), 0D, 0D, null, surfaceHeights, - new IrisRange(20D, 64D), fluidSupportPlan); + null, new IrisRange(20D, 64D), fluidSupportPlan); String fluidCell = firstCellWithLiquid(capture, (byte) 1); assertTrue(fluidCell != null); int fluidY = coordinate(fluidCell, 1); new IrisCaveCarver3D(engine, airProfile).carve( capture.writer, 0, 0, fullWeights(), 0D, 0D, null, surfaceHeights, - new IrisRange(fluidY - 1D, fluidY - 1D), fluidSupportPlan); + null, new IrisRange(fluidY - 1D, fluidY - 1D), fluidSupportPlan); assertEquals(Byte.valueOf((byte) 1), capture.carvedLiquids.get(fluidCell)); fluidSupportPlan.resolve(capture.writer.acquireChunk(0, 0)); @@ -732,6 +739,43 @@ public class IrisCaveCarver3DNearParityTest { .setAllowLava(true); } + private void assertSurfaceFluidBoundaryForProfile(IrisCaveProfile profile) { + Engine engine = createEngine(80, 70); + int[] surfaceHeights = filledHeights(70); + surfaceHeights[0] = 60; + int[] boundaryStartY = new int[256]; + Arrays.fill(boundaryStartY, SurfaceFluidBoundaryPlan.NO_BOUNDARY); + boundaryStartY[0] = 60; + boundaryStartY[16] = 61; + WriterCapture capture = createWriterCapture(80); + CaveFluidSupportPlan supportPlan = new CaveFluidSupportPlan(); + + new IrisCaveCarver3D(engine, profile.setFluidMinDepthBelowSurface(0).setFluidRequiresFloor(false)).carve( + capture.writer, + 0, + 0, + fullWeights(), + 0D, + 0D, + null, + surfaceHeights, + boundaryStartY, + null, + supportPlan + ); + + assertTrue(capture.carvedCells.contains(cellKey(0, 56, 0))); + assertFalse(capture.carvedCells.contains(cellKey(0, 60, 0))); + assertTrue(capture.carvedCells.contains(cellKey(1, 60, 0))); + for (int y = 61; y <= 64; y++) { + assertFalse(capture.carvedCells.contains(cellKey(1, y, 0))); + } + assertTrue(capture.carvedCells.contains(cellKey(1, 65, 0))); + assertTrue(capture.carvedCells.contains(cellKey(2, 64, 0))); + assertTrue(countLiquid(capture, (byte) 1) > 0); + assertTrue(countLiquid(capture, (byte) 0) > 0); + } + private WriterCapture createWriterCapture(int worldHeight) { MantleWriter writer = mock(MantleWriter.class); @SuppressWarnings("unchecked") diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentCaveAnchorTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentCaveAnchorTest.java new file mode 100644 index 000000000..3d8205de4 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentCaveAnchorTest.java @@ -0,0 +1,54 @@ +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.volmlib.util.matter.MatterCavern; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class MantleObjectComponentCaveAnchorTest { + @Test + public void biomeOwnedPlacementsRejectForeignCaveBands() { + IrisBiome frozen = biome("carving/ice"); + IrisBiome deepDark = biome("carving/standard-deepdark"); + IrisBiome surface = biome("frozen/ice-spikes"); + + assertFalse(MantleObjectComponent.caveAnchorBiomeConflicts(frozen, surface, "carving/ice")); + assertTrue(MantleObjectComponent.caveAnchorBiomeConflicts(deepDark, surface, "carving/ice")); + assertFalse(MantleObjectComponent.caveAnchorBiomeConflicts(deepDark, surface, null)); + } + + @Test + public void dryPlacementsRejectFluidAndDefaultLavaCells() { + MatterCavern air = new MatterCavern(true, "", (byte) 0); + MatterCavern water = new MatterCavern(true, "", (byte) 1); + MatterCavern lava = new MatterCavern(true, "", (byte) 2); + + assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid(false, water, 20, 8)); + assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid(false, lava, 20, 8)); + assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid(false, air, 8, 8)); + assertTrue(MantleObjectComponent.acceptsCaveAnchorFluid(false, air, 9, 8)); + } + + @Test + public void explicitWetAndForcedAirPlacementsRemainEligible() { + MatterCavern water = new MatterCavern(true, "", (byte) 1); + MatterCavern forcedAir = new MatterCavern(true, "", (byte) 3); + + assertTrue(MantleObjectComponent.acceptsCaveAnchorFluid(true, water, 20, 8)); + assertTrue(MantleObjectComponent.acceptsCaveAnchorFluid(false, forcedAir, 0, 8)); + assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid( + true, + new MatterCavern(false, "", (byte) 0), + 20, + 8 + )); + } + + private static IrisBiome biome(String loadKey) { + IrisBiome biome = new IrisBiome(); + biome.setLoadKey(loadKey); + return biome; + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlanTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlanTest.java new file mode 100644 index 000000000..ef1550777 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlanTest.java @@ -0,0 +1,160 @@ +package art.arcane.iris.engine.mantle.components; + +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; +import org.junit.Test; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class SurfaceFluidBoundaryPlanTest { + private static final int CHUNK_SIZE = 16; + private static final int FIELD_SIZE = 22; + private static final int PADDING = 3; + private static final int FLUID_HEIGHT = 64; + + @Test + public void submergedColumnProtectsItsSeabedWithoutBlockingTheCaveBelow() { + Fixture fixture = new Fixture(); + fixture.setChunkSurface(8, 8, 60); + + fixture.resolve(); + + int boundaryY = fixture.boundary(8, 8); + assertEquals(60, boundaryY); + assertFalse(SurfaceFluidBoundaryPlan.protects(fixture.boundaryStartY, fixture.index(8, 8), 59, FLUID_HEIGHT)); + assertTrue(SurfaceFluidBoundaryPlan.protects(fixture.boundaryStartY, fixture.index(8, 8), 60, FLUID_HEIGHT)); + } + + @Test + public void dryCoastProtectsOnlyTheAdjacentFluidBand() { + Fixture fixture = new Fixture(); + fixture.setFieldSurface(7, 8, 60D); + + fixture.resolve(); + + int columnIndex = fixture.index(8, 8); + assertEquals(61, fixture.boundary(8, 8)); + assertFalse(SurfaceFluidBoundaryPlan.protects(fixture.boundaryStartY, columnIndex, 60, FLUID_HEIGHT)); + assertTrue(SurfaceFluidBoundaryPlan.protects(fixture.boundaryStartY, columnIndex, 61, FLUID_HEIGHT)); + assertTrue(SurfaceFluidBoundaryPlan.protects(fixture.boundaryStartY, columnIndex, 64, FLUID_HEIGHT)); + assertFalse(SurfaceFluidBoundaryPlan.protects(fixture.boundaryStartY, columnIndex, 65, FLUID_HEIGHT)); + } + + @Test + public void lowestCardinalReservoirBoundaryWins() { + Fixture fixture = new Fixture(); + fixture.setFieldSurface(7, 8, 62D); + fixture.setFieldSurface(9, 8, 58D); + + fixture.resolve(); + + assertEquals(59, fixture.boundary(8, 8)); + } + + @Test + public void roundedWaterlineAndDiagonalReservoirDoNotOverprotect() { + Fixture wetRoundedDown = new Fixture(); + wetRoundedDown.setFieldSurface(7, 8, 63.49D); + wetRoundedDown.resolve(); + assertEquals(64, wetRoundedDown.boundary(8, 8)); + + Fixture dryRoundedUp = new Fixture(); + dryRoundedUp.setFieldSurface(7, 8, 63.5D); + dryRoundedUp.setFieldSurface(7, 7, 40D); + dryRoundedUp.resolve(); + assertEquals(SurfaceFluidBoundaryPlan.NO_BOUNDARY, dryRoundedUp.boundary(8, 8)); + } + + @Test + public void nonFluidPaletteDoesNotCreateAReservoirBoundary() { + Fixture fixture = new Fixture(); + fixture.setFieldSurface(7, 8, 40D); + fixture.setFieldHasFluid(7, 8, false); + + fixture.resolve(); + + assertEquals(SurfaceFluidBoundaryPlan.NO_BOUNDARY, fixture.boundary(8, 8)); + } + + @Test + public void paddedHaloProtectsEveryChunkEdge() { + assertEdgeBoundary(0, 8, -1, 8); + assertEdgeBoundary(15, 8, 16, 8); + assertEdgeBoundary(8, 0, 8, -1); + assertEdgeBoundary(8, 15, 8, 16); + } + + @Test + public void invalidFieldShapeIsRejected() { + assertThrows(IllegalArgumentException.class, () -> SurfaceFluidBoundaryPlan.fill( + new int[CHUNK_SIZE * CHUNK_SIZE], + new double[FIELD_SIZE * FIELD_SIZE], + new boolean[FIELD_SIZE * FIELD_SIZE], + FIELD_SIZE, + 0, + FLUID_HEIGHT, + new int[CHUNK_SIZE * CHUNK_SIZE] + )); + } + + private void assertEdgeBoundary(int localX, int localZ, int neighborLocalX, int neighborLocalZ) { + Fixture fixture = new Fixture(); + fixture.setFieldSurface(neighborLocalX, neighborLocalZ, 60D); + fixture.resolve(); + assertEquals(61, fixture.boundary(localX, localZ)); + } + + private static final class Fixture { + private final int[] chunkSurfaceHeights = new int[CHUNK_SIZE * CHUNK_SIZE]; + private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE]; + private final boolean[] fieldHasFluid = new boolean[FIELD_SIZE * FIELD_SIZE]; + private final int[] boundaryStartY = new int[CHUNK_SIZE * CHUNK_SIZE]; + + private Fixture() { + Arrays.fill(chunkSurfaceHeights, 70); + Arrays.fill(fieldSurfaceHeights, 70D); + Arrays.fill(fieldHasFluid, true); + } + + private void setChunkSurface(int localX, int localZ, int surfaceY) { + chunkSurfaceHeights[index(localX, localZ)] = surfaceY; + setFieldSurface(localX, localZ, surfaceY); + } + + private void setFieldSurface(int localX, int localZ, double surfaceY) { + int fieldX = localX + PADDING; + int fieldZ = localZ + PADDING; + fieldSurfaceHeights[(fieldX * FIELD_SIZE) + fieldZ] = surfaceY; + } + + private void setFieldHasFluid(int localX, int localZ, boolean hasFluid) { + int fieldX = localX + PADDING; + int fieldZ = localZ + PADDING; + fieldHasFluid[(fieldX * FIELD_SIZE) + fieldZ] = hasFluid; + } + + private void resolve() { + SurfaceFluidBoundaryPlan.fill( + chunkSurfaceHeights, + fieldSurfaceHeights, + fieldHasFluid, + FIELD_SIZE, + PADDING, + FLUID_HEIGHT, + boundaryStartY + ); + } + + private int boundary(int localX, int localZ) { + return boundaryStartY[index(localX, localZ)]; + } + + private int index(int localX, int localZ) { + return PowerOfTwoCoordinates.packLocal16(localX, localZ); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisDepositTuningTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisDepositTuningTest.java index 6686f4ea5..0ec8d93c1 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisDepositTuningTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisDepositTuningTest.java @@ -1,18 +1,44 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.volmlib.util.math.RNG; import org.junit.Test; +import java.lang.reflect.Field; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class IrisDepositTuningTest { + @Test + public void everyDepositEnumAndValueHasSchemaDescription() throws ReflectiveOperationException { + for (Field modelField : IrisDepositGenerator.class.getDeclaredFields()) { + Class enumType = modelField.getType(); + if (!enumType.isEnum()) { + continue; + } + + Desc typeDescription = enumType.getAnnotation(Desc.class); + assertNotNull(enumType.getSimpleName(), typeDescription); + assertFalse(typeDescription.value().isBlank()); + + Object[] constants = enumType.getEnumConstants(); + for (Object constant : constants) { + String constantName = ((Enum) constant).name(); + Desc constantDescription = enumType.getField(constantName).getAnnotation(Desc.class); + assertNotNull(enumType.getSimpleName() + "." + constantName, constantDescription); + assertFalse(constantDescription.value().isBlank()); + } + } + } + @Test public void depositSizesScaleAndRemainWithinSchemaLimit() { assertEquals(8, IrisDepositGenerator.scaledDepositSize(4, 2D)); diff --git a/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java b/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java index 5468ca7c1..9620b9572 100644 --- a/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java +++ b/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java @@ -113,12 +113,12 @@ public final class StubPlatform implements IrisPlatform { @Override public boolean isAir() { - return key.endsWith("air"); + return blockKey().endsWith("air"); } @Override public boolean isSolid() { - return !isAir(); + return !isAir() && !isFluid(); } @Override @@ -133,12 +133,16 @@ public final class StubPlatform implements IrisPlatform { @Override public boolean isFluid() { - return false; + String blockKey = blockKey(); + return blockKey.equals("minecraft:water") + || blockKey.equals("minecraft:lava") + || blockKey.equals("minecraft:bubble_column"); } @Override public boolean isWater() { - return false; + String blockKey = blockKey(); + return blockKey.equals("minecraft:water") || blockKey.equals("minecraft:bubble_column"); } @Override @@ -163,11 +167,7 @@ public final class StubPlatform implements IrisPlatform { @Override public boolean isTreeBlock() { - String blockKey = key; - int properties = blockKey.indexOf('['); - if (properties >= 0) { - blockKey = blockKey.substring(0, properties); - } + String blockKey = blockKey(); return blockKey.endsWith("_log") || blockKey.endsWith("_wood") || blockKey.endsWith("_stem") @@ -236,6 +236,11 @@ public final class StubPlatform implements IrisPlatform { public Object nativeHandle() { return key; } + + private String blockKey() { + int properties = key.indexOf('['); + return properties >= 0 ? key.substring(0, properties) : key; + } } private static PlatformBlockState rotateState(IrisObjectRotation rotation, PlatformBlockState state, diff --git a/probe/src/test/java/art/arcane/iris/probe/StubPlatformStateTest.java b/probe/src/test/java/art/arcane/iris/probe/StubPlatformStateTest.java index 1f68ea5e3..3a118d693 100644 --- a/probe/src/test/java/art/arcane/iris/probe/StubPlatformStateTest.java +++ b/probe/src/test/java/art/arcane/iris/probe/StubPlatformStateTest.java @@ -7,6 +7,8 @@ import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; public final class StubPlatformStateTest { @BeforeClass @@ -41,6 +43,22 @@ public final class StubPlatformStateTest { assertEquals("minecraft:oak_leaves[distance=7,persistent=true]", merged.key()); } + @Test + public void classifiesVanillaFluidStates() { + PlatformBlockState water = state("minecraft:water[level=0]"); + PlatformBlockState lava = state("minecraft:lava[level=0]"); + PlatformBlockState stone = state("minecraft:stone"); + + assertTrue(water.isFluid()); + assertTrue(water.isWater()); + assertFalse(water.isSolid()); + assertTrue(lava.isFluid()); + assertFalse(lava.isWater()); + assertFalse(lava.isSolid()); + assertFalse(stone.isFluid()); + assertTrue(stone.isSolid()); + } + private PlatformBlockState state(String key) { return IrisPlatforms.get().registries().block(key); }