This commit is contained in:
Brian Neumann-Fopiano
2026-08-05 23:39:54 -06:00
parent ce8c4ff578
commit 967c082372
48 changed files with 2351 additions and 349 deletions
@@ -528,7 +528,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
int steps = GenerationStep.Decoration.values().length;
List<NativePlacementGroup> placementGroups = new ArrayList<>();
List<StructureStart> heightmapStarts = new ArrayList<>();
List<NativeStructureVegetationClearer.VegetationTarget> vegetationTargets = new ArrayList<>();
List<StructureStart> vegetationTargets = new ArrayList<>();
List<NativeStructureTerrainIntegrator.TerrainTarget> terrainTargets = new ArrayList<>();
for (int step = 0; step < steps; step++) {
int index = 0;
@@ -559,11 +559,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
structureId, start,
NativeStructureTerrainIntegrator.resolveNativeTerrain(
start, decision.terrain())));
boolean clearEntireFootprint = NativeStructureVegetationClearer
.shouldClearEntireVegetationFootprint(
structure.step(), decision.clearVegetation());
vegetationTargets.add(new NativeStructureVegetationClearer.VegetationTarget(
start, clearEntireFootprint));
vegetationTargets.add(start);
}
if (!resolvedPlacements.isEmpty()) {
placementGroups.add(new NativePlacementGroup(
@@ -592,8 +588,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
NativeStructureSurfaceFitter.VacuumFoundationPlan vacuumFoundationPlan;
try {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
world, area, terrainTargets,
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
} catch (Throwable error) {
@@ -621,6 +618,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
"placement", group.structureId(), chunkPos.x(), chunkPos.z(), error);
}
}
try {
NativeStructureSurfaceFitter.repairVacuumFoundations(
world, area, vacuumFoundationPlan);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"foundation repair", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
}
private static String nativeStructureBatchContext(List<NativePlacementGroup> placementGroups) {
@@ -45,6 +45,7 @@ public class IrisChunkGeneratorFailureContractTest {
assertTrue(placement.contains("\"resolution\""));
assertTrue(placement.contains("\"terrain integration\""));
assertTrue(placement.contains("\"terrain preparation\""));
assertTrue(placement.contains("\"foundation repair\""));
assertFalse(placement.contains("\"terrain carving\""));
assertTrue(placement.contains("\"vegetation cleanup\""));
assertTrue(placement.contains("\"placement\""));
@@ -55,6 +56,8 @@ public class IrisChunkGeneratorFailureContractTest {
< placement.indexOf("prepareSurfaceStructures"));
assertTrue(placement.indexOf("prepareSurfaceStructures")
< placement.indexOf("for (NativePlacementGroup group"));
assertTrue(placement.indexOf("repairVacuumFoundations")
> placement.indexOf("for (NativePlacementGroup group"));
assertFalse(placement.contains("IrisLogging.reportError"));
}
@@ -40,6 +40,7 @@ import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction;
import net.minecraft.world.level.levelgen.structure.pools.LegacySinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
@@ -54,6 +55,7 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProc
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
import org.junit.BeforeClass;
import org.junit.Test;
@@ -131,7 +133,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
}
@Test
public void explicitVacuumForcesThinSurfaceFittingWithoutAuthoredAdaptation() {
public void explicitVacuumUsesIndependentTerrainFittingWithoutAuthoredAdaptation() {
StructureStart none = desertStart(TerrainAdjustment.NONE);
StructureStart box = desertStart(TerrainAdjustment.BEARD_BOX);
NativeStructureTerrainIntegrator.TerrainTarget sourceNone =
@@ -150,11 +152,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(sourceNone));
assertTrue(NativeStructureSurfaceFitter.requiresSurfaceTerrain(vacuumNone));
assertTrue(NativeStructureSurfaceFitter.requiresSurfaceTerrain(vacuumBox));
assertEquals(TerrainAdjustment.BEARD_THIN,
NativeStructureSurfaceFitter.effectiveSurfaceAdjustment(vacuumNone));
assertEquals(TerrainAdjustment.BEARD_THIN,
NativeStructureSurfaceFitter.effectiveSurfaceAdjustment(vacuumBox));
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
none, vacuumNone.terrain()));
}
@@ -263,6 +261,476 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 11, 70, 1));
}
@Test
public void vacuumFitsOnlyProcessedSolidTemplateColumns() throws Exception {
StructureTemplate sparseTemplate = template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()),
block(15, 0, 0, Blocks.STONE.defaultBlockState())));
InlineSinglePoolElement element = new InlineSinglePoolElement(
sparseTemplate, List.of(new ReplaceBlockProcessor(
Blocks.STONE.defaultBlockState(), Blocks.AIR.defaultBlockState())));
PoolElementStructurePiece piece = rigidTemplatePiece(
element, new BoundingBox(0, 64, 0, 15, 70, 3), 1, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE);
BoundingBox area = new BoundingBox(0, 56, 0, 15, 72, 3);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 64, 0));
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 63, 0));
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 15, 60, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 15, 61, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 15, 64, 0));
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 15, 60, 3));
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 15, 64, 3));
}
@Test
public void vacuumSupportsGroundDeltaZeroFoundations() throws Exception {
StructureTemplate template = template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
PoolElementStructurePiece piece = rigidTemplatePiece(
new InlineSinglePoolElement(template),
new BoundingBox(0, 66, 0, 0, 70, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE);
BoundingBox area = new BoundingBox(0, 54, 0, 3, 72, 3);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 65, 0));
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, 64, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 66, 0));
}
@Test
public void vacuumFindsActualTerrainBelowNominalOpenAirSurface() throws Exception {
StructureTemplate template = template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
PoolElementStructurePiece piece = rigidTemplatePiece(
new InlineSinglePoolElement(template),
new BoundingBox(0, 75, 0, 0, 81, 0), 2, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE);
BoundingBox area = new BoundingBox(0, 54, 0, 3, 84, 3);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
(x, z) -> 80);
for (int y = 61; y <= 74; y++) {
assertFalse(state(blocks, 0, y, 0).isAir());
}
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 74, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 75, 0));
}
@Test
public void vacuumSupportsFoundationBelowHigherNaturalSurface() throws Exception {
StructureTemplate template = template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
PoolElementStructurePiece piece = rigidTemplatePiece(
new InlineLegacyPoolElement(template),
new BoundingBox(0, 75, 0, 0, 81, 0), 2, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE);
BoundingBox area = new BoundingBox(0, 54, 0, 15, 84, 3);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
put(blocks, 0, 78, 0, Blocks.DIRT.defaultBlockState());
put(blocks, 0, 79, 0, Blocks.GRASS_BLOCK.defaultBlockState());
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
(x, z) -> 79);
for (int y = 61; y <= 74; y++) {
assertFalse(state(blocks, 0, y, 0).isAir());
}
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 75, 0));
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, 78, 0));
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 79, 0));
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 15, 60, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 15, 61, 0));
}
@Test
public void vacuumRepairUsesOnlyProcessedSolidFoundationColumns() throws Exception {
StructureTemplate template = template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()),
block(1, 0, 0, Blocks.STONE.defaultBlockState()),
block(2, 0, 0, Blocks.AIR.defaultBlockState()),
block(2, 2, 0, Blocks.COBBLESTONE.defaultBlockState())));
InlineSinglePoolElement element = new InlineSinglePoolElement(
template, List.of(new ReplaceBlockProcessor(
Blocks.STONE.defaultBlockState(), Blocks.AIR.defaultBlockState())));
PoolElementStructurePiece piece = rigidTemplatePiece(
element, new BoundingBox(0, 65, 0, 2, 72, 0), 1, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE);
BoundingBox area = new BoundingBox(0, 54, 0, 3, 74, 3);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
NativeStructureSurfaceFitter.VacuumFoundationPlan plan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
(x, z) -> 60);
for (int y = 61; y <= 64; y++) {
put(blocks, 0, y, 0, Blocks.CAVE_AIR.defaultBlockState());
}
put(blocks, 0, 65, 0, Blocks.COBBLESTONE.defaultBlockState());
put(blocks, 1, 65, 0, Blocks.COBBLESTONE.defaultBlockState());
put(blocks, 2, 67, 0, Blocks.COBBLESTONE.defaultBlockState());
BlockState processorAirSupport = state(blocks, 1, 64, 0);
BlockState authoredAirSupport = state(blocks, 2, 66, 0);
NativeStructureSurfaceFitter.repairVacuumFoundations(
world(blocks), area, plan);
for (int y = 61; y <= 64; y++) {
assertFalse(state(blocks, 0, y, 0).isAir());
}
assertEquals(processorAirSupport, state(blocks, 1, 64, 0));
assertEquals(authoredAirSupport, state(blocks, 2, 66, 0));
}
@Test
public void vacuumRepairPreservesOverlappingAuthoredInterior() throws Exception {
PoolElementStructurePiece lower = rigidTemplatePiece(
new InlineLegacyPoolElement(template(List.of(
block(0, 0, 0, Blocks.AIR.defaultBlockState()),
block(0, 1, 0, Blocks.AIR.defaultBlockState())))),
new BoundingBox(0, 61, 0, 0, 62, 0), 0, Rotation.NONE);
PoolElementStructurePiece upper = rigidTemplatePiece(
new InlineLegacyPoolElement(template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
new BoundingBox(0, 65, 0, 0, 70, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(
List.of(lower, upper), TerrainAdjustment.NONE);
BoundingBox area = new BoundingBox(0, 54, 0, 3, 72, 3);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
NativeStructureSurfaceFitter.VacuumFoundationPlan plan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
(x, z) -> 60);
put(blocks, 0, 65, 0, Blocks.COBBLESTONE.defaultBlockState());
NativeStructureSurfaceFitter.repairVacuumFoundations(
world(blocks), area, plan);
for (int y = 61; y <= 64; y++) {
assertTrue(state(blocks, 0, y, 0).isAir());
}
}
@Test
public void vacuumRepairIsStableAcrossSplitChunkAreas() throws Exception {
StructureTemplate template = template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()),
block(1, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
PoolElementStructurePiece piece = rigidTemplatePiece(
new InlineLegacyPoolElement(template),
new BoundingBox(15, 65, 0, 16, 70, 0), 1, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE);
NativeStructureTerrainIntegrator.TerrainTarget target =
surfaceTarget(start, IrisStructureTerrainMode.VACUUM);
BoundingBox wideArea = new BoundingBox(0, 54, 0, 31, 72, 15);
BoundingBox westArea = new BoundingBox(0, 54, 0, 15, 72, 15);
BoundingBox eastArea = new BoundingBox(16, 54, 0, 31, 72, 15);
Map<BlockPos, BlockState> wideBlocks = flatTerrain(wideArea, 60);
Map<BlockPos, BlockState> splitBlocks = flatTerrain(wideArea, 60);
NativeStructureSurfaceFitter.VacuumFoundationPlan widePlan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(wideBlocks), wideArea, List.of(target), (x, z) -> 60);
NativeStructureSurfaceFitter.VacuumFoundationPlan westPlan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(splitBlocks), westArea, List.of(target), (x, z) -> 60);
NativeStructureSurfaceFitter.VacuumFoundationPlan eastPlan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(splitBlocks), eastArea, List.of(target), (x, z) -> 60);
for (int x = 15; x <= 16; x++) {
for (int y = 61; y <= 64; y++) {
put(wideBlocks, x, y, 0, Blocks.CAVE_AIR.defaultBlockState());
put(splitBlocks, x, y, 0, Blocks.CAVE_AIR.defaultBlockState());
}
put(wideBlocks, x, 65, 0, Blocks.COBBLESTONE.defaultBlockState());
put(splitBlocks, x, 65, 0, Blocks.COBBLESTONE.defaultBlockState());
}
NativeStructureSurfaceFitter.repairVacuumFoundations(
world(wideBlocks), wideArea, widePlan);
NativeStructureSurfaceFitter.repairVacuumFoundations(
world(splitBlocks), eastArea, eastPlan);
NativeStructureSurfaceFitter.repairVacuumFoundations(
world(splitBlocks), westArea, westPlan);
assertEquals(wideBlocks, splitBlocks);
}
@Test
public void vacuumNeverLowersExistingTerrainAroundSparsePieces() throws Exception {
StructureTemplate sparseTemplate = template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
PoolElementStructurePiece piece = rigidTemplatePiece(
new InlineSinglePoolElement(sparseTemplate),
new BoundingBox(0, 56, 0, 7, 62, 7), 1, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE);
BoundingBox area = new BoundingBox(0, 48, 0, 7, 70, 7);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
Map<BlockPos, BlockState> originalBlocks = new HashMap<>(blocks);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(originalBlocks, blocks);
}
@Test
public void vacuumDoesNotRaiseTerrainToRoofsAboveLegacyAir() throws Exception {
StructureTemplate template = template(List.of(
block(0, 0, 0, Blocks.AIR.defaultBlockState()),
block(0, 2, 0, Blocks.COBBLESTONE.defaultBlockState())));
PoolElementStructurePiece piece = rigidTemplatePiece(
new InlineLegacyPoolElement(template),
new BoundingBox(0, 66, 0, 7, 72, 7), 1, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE);
BoundingBox area = new BoundingBox(0, 54, 0, 15, 74, 15);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
Map<BlockPos, BlockState> originalBlocks = new HashMap<>(blocks);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(originalBlocks, blocks);
}
@Test
public void vacuumCapsNeighborTaperAtAuthoredAirGroundPlanes() throws Exception {
PoolElementStructurePiece roof = rigidTemplatePiece(
new InlineSinglePoolElement(template(List.of(
block(0, 0, 0, Blocks.AIR.defaultBlockState()),
block(0, 2, 0, Blocks.COBBLESTONE.defaultBlockState())))),
new BoundingBox(1, 66, 0, 1, 72, 0), 1, Rotation.NONE);
PoolElementStructurePiece highChild = rigidTemplatePiece(
new InlineSinglePoolElement(template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
new BoundingBox(0, 90, 0, 0, 94, 0), 1, Rotation.NONE);
StructureStart start = rigidSurfaceStart(
List.of(roof, highChild), TerrainAdjustment.NONE);
BoundingBox area = new BoundingBox(0, 54, 0, 15, 96, 15);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 1, 66, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 1, 67, 0));
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 89, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 90, 0));
}
@Test
public void vacuumDoesNotAnchorRoofsAboveProcessorCreatedAir() throws Exception {
StructureTemplate template = template(List.of(
block(0, 0, 0, Blocks.STONE.defaultBlockState()),
block(0, 2, 0, Blocks.COBBLESTONE.defaultBlockState())));
PoolElementStructurePiece piece = rigidTemplatePiece(
new InlineSinglePoolElement(template, List.of(new ReplaceBlockProcessor(
Blocks.STONE.defaultBlockState(), Blocks.AIR.defaultBlockState()))),
new BoundingBox(0, 66, 0, 7, 72, 7), 1, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE);
BoundingBox area = new BoundingBox(0, 54, 0, 15, 74, 15);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
Map<BlockPos, BlockState> originalBlocks = new HashMap<>(blocks);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(originalBlocks, blocks);
}
@Test
public void vacuumDoesNotTreatSparseRoofCellsAsFoundations() throws Exception {
StructureTemplate template = template(List.of(
block(0, 0, 0, Blocks.SANDSTONE.defaultBlockState()),
block(1, 20, 0, Blocks.SANDSTONE.defaultBlockState())));
PoolElementStructurePiece piece = rigidTemplatePiece(
new InlineSinglePoolElement(template),
new BoundingBox(0, 66, 0, 20, 100, 20), 1, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE);
BoundingBox area = new BoundingBox(0, 54, 0, 20, 102, 20);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 65, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 66, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 1, 67, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 1, 86, 0));
}
@Test
public void vacuumDoesNotFlattenTerrainMatchingPoolPieces() throws Exception {
StructureTemplate template = template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
PoolElementStructurePiece piece = rigidTemplatePiece(
new InlineSinglePoolElement(
template, List.of(),
StructureTemplatePool.Projection.TERRAIN_MATCHING),
new BoundingBox(0, 68, 0, 15, 72, 15), 1, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE);
BoundingBox area = new BoundingBox(0, 54, 0, 15, 74, 15);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
Map<BlockPos, BlockState> originalBlocks = new HashMap<>(blocks);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(originalBlocks, blocks);
}
@Test
public void vacuumRetainsJunctionSmoothingForRigidFeaturePieces() {
StructurePoolElement feature = StructurePoolElement.feature(
Holder.<PlacedFeature>direct(null))
.apply(StructureTemplatePool.Projection.RIGID);
PoolElementStructurePiece piece = rigidTemplatePiece(
feature, new BoundingBox(4, 66, 4, 4, 70, 4), 1, Rotation.NONE);
piece.addJunction(new JigsawJunction(
4, 66, 4, 0, StructureTemplatePool.Projection.RIGID));
StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE);
BoundingBox area = new BoundingBox(0, 54, 0, 15, 72, 15);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 4, 61, 4));
}
@Test
public void vacuumDoesNotInventNonPoolBoundingBoxFootprints() {
StructureStart start = desertStart(TerrainAdjustment.NONE);
BoundingBox bounds = start.getBoundingBox();
BoundingBox area = new BoundingBox(
bounds.minX() - 12, 40, bounds.minZ() - 12,
bounds.maxX() + 12, bounds.maxY() + 12, bounds.maxZ() + 12);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 50);
Map<BlockPos, BlockState> originalBlocks = new HashMap<>(blocks);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
(x, z) -> 50);
assertEquals(originalBlocks, blocks);
}
@Test
public void vacuumBudgetExhaustionSkipsFittingWithoutFailing() throws Exception {
PoolElementStructurePiece piece = rigidTemplatePiece(
new InlineSinglePoolElement(template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
new BoundingBox(0, 66, 0, 0, 70, 0), 1, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE);
BoundingBox area = new BoundingBox(0, 54, 0, 15, 72, 15);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
Map<BlockPos, BlockState> originalBlocks = new HashMap<>(blocks);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
(x, z) -> 60, 1);
assertEquals(originalBlocks, blocks);
}
@Test
public void vacuumContinuouslySupportsProcessedChildBases() throws Exception {
PoolElementStructurePiece center = rigidTemplatePiece(
new InlineSinglePoolElement(template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
new BoundingBox(0, 60, 0, 0, 64, 0), 1, Rotation.NONE);
StructureTemplate childTemplate = template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
PoolElementStructurePiece child = rigidTemplatePiece(
new InlineSinglePoolElement(childTemplate),
new BoundingBox(10, 66, 0, 10, 72, 0), 1, Rotation.NONE);
StructureStart start = rigidSurfaceStart(
List.of(center, child), TerrainAdjustment.NONE);
BoundingBox area = new BoundingBox(0, 54, 0, 15, 74, 3);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
(x, z) -> 60);
for (int y = 61; y <= 65; y++) {
assertFalse(state(blocks, 10, y, 0).isAir());
}
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 10, 65, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 10, 66, 0));
}
@Test
public void vacuumSurfaceFittingIsStableAcrossSplitChunkAreas() throws Exception {
StructureTemplate template = template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()),
block(1, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
PoolElementStructurePiece piece = rigidTemplatePiece(
new InlineSinglePoolElement(template),
new BoundingBox(15, 66, 0, 16, 72, 0), 1, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE);
NativeStructureTerrainIntegrator.TerrainTarget target =
surfaceTarget(start, IrisStructureTerrainMode.VACUUM);
BoundingBox wideArea = new BoundingBox(0, 54, 0, 31, 74, 15);
BoundingBox westArea = new BoundingBox(0, 54, 0, 15, 74, 15);
BoundingBox eastArea = new BoundingBox(16, 54, 0, 31, 74, 15);
Map<BlockPos, BlockState> wideBlocks = flatTerrain(wideArea, 60);
Map<BlockPos, BlockState> splitBlocks = flatTerrain(wideArea, 60);
Map<BlockPos, BlockState> reverseSplitBlocks = flatTerrain(wideArea, 60);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(wideBlocks), wideArea, List.of(target), (x, z) -> 60);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(splitBlocks), westArea, List.of(target), (x, z) -> 60);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(splitBlocks), eastArea, List.of(target), (x, z) -> 60);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(reverseSplitBlocks), eastArea, List.of(target), (x, z) -> 60);
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(reverseSplitBlocks), westArea, List.of(target), (x, z) -> 60);
assertEquals(wideBlocks, splitBlocks);
assertEquals(wideBlocks, reverseSplitBlocks);
}
@Test
public void surfaceAnchorIsFlushInsideAndUnchangedAtRadius() {
NativeStructureSurfaceFitter.SurfaceAnchor anchor = anchor(72, 2);
@@ -863,8 +1331,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
BoundingBox area = new BoundingBox(0, 63, 0, 0, 68, 0);
NativeStructureVegetationClearer.clearIntersectingVegetation(
world, chunk, area, List.of(
new NativeStructureVegetationClearer.VegetationTarget(desertStart(), false)));
world, chunk, area, List.of(desertStart()));
NativeStructureSurfaceFitter.applySurfaceColumn(
world, new BlockPos.MutableBlockPos(),
0, 0, 64, 68, -64, 319);
@@ -1642,9 +2109,15 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
private InlineSinglePoolElement(
StructureTemplate template, List<StructureProcessor> processors) {
this(template, processors, StructureTemplatePool.Projection.RIGID);
}
private InlineSinglePoolElement(
StructureTemplate template, List<StructureProcessor> processors,
StructureTemplatePool.Projection projection) {
super(Either.right(template),
Holder.direct(new StructureProcessorList(processors)),
StructureTemplatePool.Projection.RIGID,
projection,
Optional.<LiquidSettings>empty());
}
}
@@ -1,27 +1,55 @@
package art.arcane.iris.nativegen;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.IdMapper;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.Strategy;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorVegetationTest {
private static final long TEST_SEED = 1234L;
private static final int MIN_Y = -64;
private static final int MAX_Y = 319;
private static final int GROUND_Y = 64;
private static final BoundingBox AREA = new BoundingBox(0, MIN_Y, 0, 15, MAX_Y, 15);
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
@@ -29,52 +57,108 @@ public class NativeStructurePostProcessorVegetationTest {
}
@Test
public void surfaceStructuresClearTreeColumnsAutomatically() {
assertTrue(NativeStructureVegetationClearer.shouldClearVegetationColumn(100, 100, false));
assertTrue(NativeStructureVegetationClearer.shouldClearVegetationColumn(116, 100, false));
public void canopyAboveTheStructureRoofSurvivesVegetationClearing() {
ProtoChunk chunk = chunk();
StructureStart start = desertStart(0, 0, GROUND_Y + 15);
BoundingBox bounds = start.getBoundingBox();
for (int y = bounds.maxY() - 1; y <= GROUND_Y + 40; y++) {
fillLayer(chunk, y, leaves());
}
write(chunk, 4, bounds.minY() - 1, 4, log());
clearVegetation(chunk, start);
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
assertEquals(leaves(), chunk.getBlockState(new BlockPos(x, GROUND_Y + 40, z)));
assertEquals(leaves(), chunk.getBlockState(new BlockPos(x, bounds.maxY() + 1, z)));
assertEquals(air(), chunk.getBlockState(new BlockPos(x, bounds.maxY(), z)));
assertEquals(air(), chunk.getBlockState(new BlockPos(x, bounds.maxY() - 1, z)));
}
}
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 1, 4)));
}
@Test
public void buriedStructuresPreserveUnrelatedSurfaceForest() {
assertFalse(NativeStructureVegetationClearer.shouldClearVegetationColumn(99, 100, false));
assertFalse(NativeStructureVegetationClearer.shouldClearVegetationColumn(20, 100, false));
public void buriedStructuresPreserveTheForestAboveThem() {
ProtoChunk chunk = chunk();
StructureStart start = desertStart(0, 0, -20);
write(chunk, 8, GROUND_Y + 1, 8, log());
write(chunk, 8, GROUND_Y + 2, 8, log());
write(chunk, 8, GROUND_Y + 3, 8, log());
write(chunk, 8, GROUND_Y + 4, 8, leaves());
clearVegetation(chunk, start);
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 1, 8)));
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 2, 8)));
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 3, 8)));
assertEquals(leaves(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 4, 8)));
}
@Test
public void explicitVegetationOptionForcesUnusualPlacementCleanup() {
assertTrue(NativeStructureVegetationClearer.shouldClearVegetationColumn(20, 100, true));
public void columnsOutsideThePieceFootprintKeepEveryTreeBlock() {
ProtoChunk chunk = chunk();
StructureStart start = desertStart(8, 8, GROUND_Y + 15);
BoundingBox bounds = start.getBoundingBox();
int inside = bounds.minY() + 4;
write(chunk, 0, inside, 0, log());
write(chunk, 0, GROUND_Y + 40, 0, leaves());
write(chunk, 12, inside, 12, log());
write(chunk, 12, GROUND_Y + 40, 12, leaves());
clearVegetation(chunk, start);
assertEquals(log(), chunk.getBlockState(new BlockPos(0, inside, 0)));
assertEquals(leaves(), chunk.getBlockState(new BlockPos(0, GROUND_Y + 40, 0)));
assertEquals(air(), chunk.getBlockState(new BlockPos(12, inside, 12)));
assertEquals(leaves(), chunk.getBlockState(new BlockPos(12, GROUND_Y + 40, 12)));
}
@Test
public void nonForcedValidTargetsReachIntersectionProcessing() {
Structure structure = new DesertPyramidStructure(
new Structure.StructureSettings(HolderSet.empty()));
StructureStart start = new StructureStart(
structure, new ChunkPos(0, 0), 0,
new PiecesContainer(List.of(
new DesertPyramidPiece(RandomSource.create(1L), 0, 0))));
public void clearingStopsAtThePieceEnvelopeBoundaries() {
ProtoChunk chunk = chunk();
StructureStart start = desertStart(0, 0, GROUND_Y + 15);
BoundingBox bounds = start.getBoundingBox();
write(chunk, 4, bounds.maxY() + 1, 4, log());
write(chunk, 4, bounds.maxY(), 4, log());
write(chunk, 4, bounds.minY() - 1, 4, log());
write(chunk, 4, bounds.minY() - 2, 4, log());
assertTrue(NativeStructureVegetationClearer.shouldProcessTarget(
new NativeStructureVegetationClearer.VegetationTarget(start, false)));
assertFalse(NativeStructureVegetationClearer.shouldProcessTarget(
new NativeStructureVegetationClearer.VegetationTarget(
StructureStart.INVALID_START, true)));
clearVegetation(chunk, start);
assertEquals(log(), chunk.getBlockState(new BlockPos(4, bounds.maxY() + 1, 4)));
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.maxY(), 4)));
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 1, 4)));
assertEquals(log(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 2, 4)));
}
@Test
public void surfaceStructuresPreserveVegetationUnlessConfigured() {
assertFalse(NativeStructureVegetationClearer.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.SURFACE_STRUCTURES, false));
assertTrue(NativeStructureVegetationClearer.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.SURFACE_STRUCTURES, true));
public void overlappingPieceEnvelopesMergeIntoOneClearedRange() {
ProtoChunk chunk = chunk();
StructureStart low = desertStart(0, 0, GROUND_Y);
StructureStart high = desertStart(0, 0, GROUND_Y + 40);
int between = GROUND_Y + 20;
write(chunk, 4, between, 4, log());
write(chunk, 4, high.getBoundingBox().maxY() + 1, 4, log());
write(chunk, 4, low.getBoundingBox().minY() - 2, 4, log());
NativeStructureVegetationClearer.clearIntersectingVegetation(
world(chunk), chunk, AREA, List.of(low, high));
assertEquals(air(), chunk.getBlockState(new BlockPos(4, between, 4)));
assertEquals(log(), chunk.getBlockState(
new BlockPos(4, high.getBoundingBox().maxY() + 1, 4)));
assertEquals(log(), chunk.getBlockState(
new BlockPos(4, low.getBoundingBox().minY() - 2, 4)));
}
@Test
public void undergroundStructuresPreserveSurfaceVegetationUnlessConfigured() {
assertFalse(NativeStructureVegetationClearer.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES, false));
assertTrue(NativeStructureVegetationClearer.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES, true));
public void validStartsReachIntersectionProcessing() {
assertTrue(NativeStructureVegetationClearer.shouldProcessStart(
desertStart(0, 0, GROUND_Y + 15)));
assertFalse(NativeStructureVegetationClearer.shouldProcessStart(
StructureStart.INVALID_START));
}
@Test
@@ -103,4 +187,105 @@ public class NativeStructurePostProcessorVegetationTest {
assertEquals(-2, NativeStructureVerticalPlacer.resolveBuriedOffset(
bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100));
}
private static void clearVegetation(ProtoChunk chunk, StructureStart start) {
NativeStructureVegetationClearer.clearIntersectingVegetation(
world(chunk), chunk, AREA, List.of(start));
}
private static StructureStart desertStart(int originX, int originZ, int topY) {
Structure structure = new DesertPyramidStructure(
new Structure.StructureSettings(
HolderSet.empty(), Map.of(),
GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.NONE));
DesertPyramidPiece piece = new DesertPyramidPiece(RandomSource.create(7L), originX, originZ);
piece.move(0, topY - piece.getBoundingBox().maxY(), 0);
return new StructureStart(
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
}
private static ProtoChunk chunk() {
return new ProtoChunk(
new ChunkPos(0, 0), UpgradeData.EMPTY,
LevelHeightAccessor.create(MIN_Y, MAX_Y - MIN_Y + 1), containerFactory(), null);
}
private static void fillLayer(ProtoChunk chunk, int y, BlockState state) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
write(chunk, x, y, z, state);
}
}
}
private static void write(ProtoChunk chunk, int x, int y, int z, BlockState state) {
LevelChunkSection section = chunk.getSection(chunk.getSectionIndex(y));
section.setBlockState(x, y & 15, z, state, false);
}
private static BlockState log() {
return Blocks.OAK_LOG.defaultBlockState();
}
private static BlockState leaves() {
return Blocks.OAK_LEAVES.defaultBlockState();
}
private static BlockState air() {
return Blocks.AIR.defaultBlockState();
}
private static WorldGenLevel world(ChunkAccess chunk) {
InvocationHandler handler = (proxy, method, arguments) -> {
String methodName = method.getName();
if (methodName.equals("getBlockState")) {
return chunk.getBlockState((BlockPos) arguments[0]);
}
if (methodName.equals("setBlock")) {
return chunk.setBlockState(
((BlockPos) arguments[0]).immutable(),
(BlockState) arguments[1], (int) arguments[2]) != null;
}
if (methodName.equals("getSeed")) {
return TEST_SEED;
}
if (methodName.equals("getLevel")) {
return null;
}
if (methodName.equals("holderLookup")) {
return BuiltInRegistries.BLOCK;
}
if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (methodName.equals("equals")) {
return proxy == arguments[0];
}
if (methodName.equals("toString")) {
return "vegetation-test-world";
}
throw new UnsupportedOperationException(method.toString());
};
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
}
private static PalettedContainerFactory containerFactory() {
Strategy<BlockState> blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
BlockState air = Blocks.AIR.defaultBlockState();
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
Holder<Biome> defaultBiome = Holder.direct((Biome) null);
biomeIds.add(defaultBiome);
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(
name -> defaultBiome, holder -> "iris-test-biome");
return new PalettedContainerFactory(
blockStates,
air,
PalettedContainer.codecRW(BlockState.CODEC, blockStates, air),
biomes,
defaultBiome,
PalettedContainer.codecRO(biomeCodec, biomes, defaultBiome),
PalettedContainer.codecRW(biomeCodec, biomes, defaultBiome));
}
}
@@ -258,6 +258,6 @@ public class IrisStructureLocateCommandContractTest {
private static IrisNativeStructureDecision decision(NativeStructureGenerationStatus status) {
return new IrisNativeStructureDecision(
status, 0, null, false, false, null, null);
status, 0, null, false, null, null);
}
}
@@ -3,6 +3,7 @@ package art.arcane.iris.nativegen;
import art.arcane.iris.engine.object.IrisObjectVacuum;
import art.arcane.iris.engine.object.IrisStructureTerrain;
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
import art.arcane.iris.spi.IrisLogging;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
@@ -17,29 +18,94 @@ import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.IntBinaryOperator;
public final class NativeStructureSurfaceFitter {
private static final double SURFACE_TERRAIN_FALLOFF = 2.0;
private static final long SURFACE_TERRAIN_INFLUENCE_SCALE = 1_000_000L;
private static final int SURFACE_TERRAIN_RADIUS = 12;
private static final int MAX_VACUUM_TEMPLATE_CELLS = 4_194_304;
private static final Set<String> WARNED_VACUUM_BUDGET =
ConcurrentHashMap.newKeySet();
private NativeStructureSurfaceFitter() {
}
public static void prepareSurfaceStructures(WorldGenLevel world, BoundingBox area,
public static VacuumFoundationPlan prepareSurfaceStructures(
WorldGenLevel world, BoundingBox area,
List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
IntBinaryOperator surfaceHeight) {
return prepareSurfaceStructures(
world, area, targets, surfaceHeight, MAX_VACUUM_TEMPLATE_CELLS);
}
static VacuumFoundationPlan prepareSurfaceStructures(
WorldGenLevel world, BoundingBox area,
List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
IntBinaryOperator surfaceHeight, int maximumVacuumTemplateCells) {
if (targets == null || targets.isEmpty()) {
return;
return VacuumFoundationPlan.empty();
}
Objects.requireNonNull(surfaceHeight, "Surface structure terrain fitting requires an Iris height resolver");
List<SurfaceAnchor> anchors = collectSurfaceAnchors(targets);
List<SurfaceAnchor> anchors = collectSourceSurfaceAnchors(targets);
if (!anchors.isEmpty()) {
fitSurfaceTerrain(world, area, anchors, surfaceHeight);
}
VacuumFootprint vacuum = collectVacuumFootprint(
world, area, targets, maximumVacuumTemplateCells);
if (!vacuum.anchors().isEmpty()) {
fitVacuumTerrain(world, area, vacuum, surfaceHeight);
}
return VacuumFoundationPlan.create(
vacuum.foundationBases(), vacuum.occupiedCells(), area);
}
public static void repairVacuumFoundations(
WorldGenLevel world, BoundingBox area,
VacuumFoundationPlan plan) {
if (plan == null || plan.foundationBases.isEmpty()) {
return;
}
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (long packedBase : plan.foundationBases) {
BlockPos base = BlockPos.of(packedBase);
if (!area.isInside(base)) {
continue;
}
BlockState baseState = world.getBlockState(position.set(base));
if (!NativeStructureTemplateOccupancy.isSolidBase(baseState)) {
continue;
}
int supportY = findVacuumFoundationSupport(
world, area, position, base.getX(), base.getY() - 1, base.getZ());
if (supportY == Integer.MIN_VALUE || supportY == base.getY() - 1) {
continue;
}
if (intersectsVacuumOccupancy(
plan.occupiedCells, base.getX(), base.getZ(),
supportY + 1, base.getY() - 1)) {
continue;
}
BlockState fill = resolveSurfaceMaterials(
world, position, base.getX(), base.getZ(), supportY,
area.minY()).subsurface();
for (int y = supportY + 1; y < base.getY(); y++) {
BlockState existing = world.getBlockState(
position.set(base.getX(), y, base.getZ()));
if (existing.isAir() || !existing.getFluidState().isEmpty()) {
world.setBlock(position, fill, 2);
}
}
}
}
static boolean shouldPrepareSurfaceTerrain(TerrainAdjustment adjustment,
@@ -156,15 +222,15 @@ public final class NativeStructureSurfaceFitter {
return false;
}
private static List<SurfaceAnchor> collectSurfaceAnchors(
private static List<SurfaceAnchor> collectSourceSurfaceAnchors(
List<NativeStructureTerrainIntegrator.TerrainTarget> targets) {
List<SurfaceAnchor> anchors = new ArrayList<>();
for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) {
if (!requiresSurfaceTerrain(target)) {
if (!requiresSourceSurfaceTerrain(target)) {
continue;
}
StructureStart start = target.start();
TerrainAdjustment adjustment = effectiveSurfaceAdjustment(target);
TerrainAdjustment adjustment = start.getStructure().terrainAdaptation();
for (StructurePiece piece : start.getPieces()) {
if (piece instanceof PoolElementStructurePiece poolPiece) {
if (poolPiece.getElement().getProjection() == StructureTemplatePool.Projection.RIGID) {
@@ -189,6 +255,146 @@ public final class NativeStructureSurfaceFitter {
return List.copyOf(anchors);
}
private static VacuumFootprint collectVacuumFootprint(
WorldGenLevel world, BoundingBox area,
List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
int maximumTemplateCells) {
BoundingBox influenceArea = new BoundingBox(
area.minX() - SURFACE_TERRAIN_RADIUS, area.minY(),
area.minZ() - SURFACE_TERRAIN_RADIUS,
area.maxX() + SURFACE_TERRAIN_RADIUS, area.maxY(),
area.maxZ() + SURFACE_TERRAIN_RADIUS);
Map<Long, VacuumAnchor> anchors = new HashMap<>();
Map<Long, Integer> columnCaps = new HashMap<>();
Set<Long> foundationBases = new HashSet<>();
Set<Long> occupiedCells = new HashSet<>();
Set<StructureStart> seenStarts = Collections.newSetFromMap(
new IdentityHashMap<>());
for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) {
if (target == null || target.terrain() == null
|| target.terrain().resolvedMode() != IrisStructureTerrainMode.VACUUM
|| target.start() == null || !target.start().isValid()
|| !seenStarts.add(target.start())) {
continue;
}
StructureStart start = target.start();
BoundingBox firstBounds = start.getPieces().getFirst().getBoundingBox();
BlockPos firstCenter = firstBounds.getCenter();
BlockPos referencePosition = new BlockPos(
firstCenter.getX(), firstBounds.minY(), firstCenter.getZ());
Map<Long, VacuumAnchor> targetAnchors = new HashMap<>();
Map<Long, Integer> targetCaps = new HashMap<>();
Set<Long> targetFoundationBases = new HashSet<>();
Set<Long> targetOccupiedCells = new HashSet<>();
VacuumCellBudget budget = new VacuumCellBudget(maximumTemplateCells);
try {
for (StructurePiece piece : start.getPieces()) {
BoundingBox bounds = piece.getBoundingBox();
if (!intersectsHorizontally(bounds, influenceArea)
|| !(piece instanceof PoolElementStructurePiece poolPiece)
|| poolPiece.getElement().getProjection()
!= StructureTemplatePool.Projection.RIGID) {
continue;
}
addVacuumJunctionAnchors(
anchors, influenceArea, poolPiece);
NativeStructureTemplateOccupancy.OccupancyResult occupancy =
NativeStructureTemplateOccupancy.resolve(
world, poolPiece, referencePosition, influenceArea,
() -> world.getLevel().getStructureManager(),
influenceArea::isInside, budget::consume);
if (!occupancy.resolved()) {
continue;
}
targetOccupiedCells.addAll(occupancy.cells().keySet());
Map<Long, NativeStructureTemplateOccupancy.LowestCell> lowest =
NativeStructureTemplateOccupancy.lowestCells(
occupancy.cells());
int groundY = bounds.minY()
+ poolPiece.getGroundLevelDelta() - 1;
for (NativeStructureTemplateOccupancy.LowestCell cell : lowest.values()) {
budget.consume(1);
long column = NativeStructureTemplateOccupancy.columnKey(
cell.x(), cell.z());
targetCaps.merge(
column, Math.min(cell.y(), groundY), Math::min);
if (cell.occupancy().blocker()
|| cell.y() > groundY + 1
|| !NativeStructureTemplateOccupancy.isSolidBase(
cell.occupancy().state())) {
continue;
}
addVacuumAnchor(targetAnchors, cell.x(), cell.z(),
new VacuumAnchor(cell.y() - 1, 2));
targetFoundationBases.add(BlockPos.asLong(
cell.x(), cell.y(), cell.z()));
}
}
} catch (VacuumBudgetExceeded ignored) {
warnVacuumBudget(target);
continue;
}
targetAnchors.forEach((column, anchor) -> anchors.merge(
column, anchor, NativeStructureSurfaceFitter::preferredVacuumAnchor));
targetCaps.forEach((column, cap) -> columnCaps.merge(
column, cap, Math::min));
foundationBases.addAll(targetFoundationBases);
occupiedCells.addAll(targetOccupiedCells);
}
return new VacuumFootprint(
Map.copyOf(anchors), Map.copyOf(columnCaps),
Set.copyOf(foundationBases), Set.copyOf(occupiedCells));
}
private static void addVacuumJunctionAnchors(
Map<Long, VacuumAnchor> anchors, BoundingBox influenceArea,
PoolElementStructurePiece poolPiece) {
for (JigsawJunction junction : poolPiece.getJunctions()) {
if (junction.getSourceX() < influenceArea.minX()
|| junction.getSourceX() > influenceArea.maxX()
|| junction.getSourceZ() < influenceArea.minZ()
|| junction.getSourceZ() > influenceArea.maxZ()) {
continue;
}
addVacuumAnchor(anchors,
junction.getSourceX(), junction.getSourceZ(),
new VacuumAnchor(junction.getSourceGroundY() - 1, 1));
}
}
private static void warnVacuumBudget(
NativeStructureTerrainIntegrator.TerrainTarget target) {
String structureId = target.structureId() == null
? target.start().getStructure().getClass().getName()
: target.structureId();
if (WARNED_VACUUM_BUDGET.add(structureId)) {
IrisLogging.warn("Native structure VACUUM fitting for '"
+ structureId + "' exceeded its bounded template budget; "
+ "using jigsaw junction smoothing for oversized starts");
}
}
private static boolean intersectsHorizontally(
BoundingBox first, BoundingBox second) {
return first.maxX() >= second.minX() && first.minX() <= second.maxX()
&& first.maxZ() >= second.minZ() && first.minZ() <= second.maxZ();
}
private static void addVacuumAnchor(
Map<Long, VacuumAnchor> anchors, int x, int z,
VacuumAnchor candidate) {
anchors.merge(NativeStructureTemplateOccupancy.columnKey(x, z), candidate,
NativeStructureSurfaceFitter::preferredVacuumAnchor);
}
private static VacuumAnchor preferredVacuumAnchor(
VacuumAnchor first, VacuumAnchor second) {
if (first.strength() != second.strength()) {
return first.strength() > second.strength() ? first : second;
}
return first.surfaceY() <= second.surfaceY() ? first : second;
}
static SurfaceAnchor surfaceAnchor(BoundingBox bounds, int groundY, int strength,
TerrainAdjustment adjustment) {
int meetY = groundY - 1;
@@ -216,17 +422,17 @@ public final class NativeStructureSurfaceFitter {
}
IrisStructureTerrainMode mode = target.terrain().resolvedMode();
return mode == IrisStructureTerrainMode.VACUUM
|| mode == IrisStructureTerrainMode.SOURCE
&& shouldPrepareSurfaceTerrain(
start.getStructure().terrainAdaptation(), start.getStructure().step());
|| requiresSourceSurfaceTerrain(target);
}
static TerrainAdjustment effectiveSurfaceAdjustment(
private static boolean requiresSourceSurfaceTerrain(
NativeStructureTerrainIntegrator.TerrainTarget target) {
if (target.terrain().resolvedMode() == IrisStructureTerrainMode.VACUUM) {
return TerrainAdjustment.BEARD_THIN;
}
return target.start().getStructure().terrainAdaptation();
return target != null && target.terrain() != null
&& target.start() != null && target.start().isValid()
&& target.terrain().resolvedMode() == IrisStructureTerrainMode.SOURCE
&& shouldPrepareSurfaceTerrain(
target.start().getStructure().terrainAdaptation(),
target.start().getStructure().step());
}
private static void fitSurfaceTerrain(WorldGenLevel world, BoundingBox area,
@@ -260,6 +466,168 @@ public final class NativeStructureSurfaceFitter {
}
}
private static void fitVacuumTerrain(
WorldGenLevel world, BoundingBox area, VacuumFootprint footprint,
IntBinaryOperator surfaceHeight) {
int width = area.getXSpan();
int depth = area.getZSpan();
int[] originalHeights = new int[width * depth];
int[] targetHeights = new int[width * depth];
boolean[] rigidBaseSupport = new boolean[width * depth];
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (int z = area.minZ(); z <= area.maxZ(); z++) {
for (int x = area.minX(); x <= area.maxX(); x++) {
int column = (z - area.minZ()) * width + x - area.minX();
int nominalY = Math.max(area.minY(), Math.min(
area.maxY(), surfaceHeight.applyAsInt(x, z)));
long columnKey = NativeStructureTemplateOccupancy.columnKey(x, z);
VacuumAnchor local = footprint.anchors().get(columnKey);
Integer cap = footprint.columnCaps().get(columnKey);
boolean suppressed = local != null && local.strength() > 1
&& cap != null && cap < local.surfaceY();
int scanStartY = local != null && local.strength() > 1 && !suppressed
? Math.max(area.minY(), Math.min(area.maxY(), local.surfaceY()))
: nominalY;
int originalY = resolveVacuumSurfaceHeight(
world, position, x, z, scanStartY, area.minY());
SurfaceResolution resolution = resolveVacuumSurface(
footprint, x, z, originalY);
originalHeights[column] = originalY;
targetHeights[column] = Math.max(originalY, Math.min(
area.maxY(), resolution.targetY()));
rigidBaseSupport[column] = resolution.rigidBaseSupport()
&& targetHeights[column] > originalY;
}
}
for (int z = area.minZ(); z <= area.maxZ(); z++) {
for (int x = area.minX(); x <= area.maxX(); x++) {
int column = (z - area.minZ()) * width + x - area.minX();
applySurfaceColumn(world, position, x, z,
originalHeights[column], targetHeights[column],
area.minY(), area.maxY(), rigidBaseSupport[column]);
}
}
}
private static int resolveVacuumSurfaceHeight(
WorldGenLevel world, BlockPos.MutableBlockPos position,
int x, int z, int nominalY, int worldMinY) {
for (int y = nominalY; y >= worldMinY; y--) {
if (isTerrainBlock(world.getBlockState(position.set(x, y, z)))) {
return y;
}
}
return nominalY;
}
private static int findVacuumFoundationSupport(
WorldGenLevel world, BoundingBox area,
BlockPos.MutableBlockPos position, int x, int startY, int z) {
if (startY < area.minY()) {
return Integer.MIN_VALUE;
}
int maximumY = Math.min(area.maxY(), startY);
for (int y = maximumY; y >= area.minY(); y--) {
BlockState state = world.getBlockState(position.set(x, y, z));
if (isTerrainBlock(state)) {
return y;
}
if (!state.isAir() && state.getFluidState().isEmpty()) {
return Integer.MIN_VALUE;
}
}
return Integer.MIN_VALUE;
}
private static boolean intersectsVacuumOccupancy(
Set<Long> occupiedCells, int x, int z, int minimumY, int maximumY) {
for (int y = minimumY; y <= maximumY; y++) {
if (occupiedCells.contains(BlockPos.asLong(x, y, z))) {
return true;
}
}
return false;
}
private static SurfaceResolution resolveVacuumSurface(
VacuumFootprint footprint, int worldX, int worldZ, int originalY) {
long column = NativeStructureTemplateOccupancy.columnKey(worldX, worldZ);
Integer cap = footprint.columnCaps().get(column);
VacuumAnchor local = footprint.anchors().get(column);
if (local != null && local.strength() > 1) {
int localTargetY = cap == null
? local.surfaceY() : Math.min(local.surfaceY(), cap);
if (localTargetY <= originalY) {
return new SurfaceResolution(originalY, false);
}
return new SurfaceResolution(
localTargetY, localTargetY == local.surfaceY());
}
long totalInfluence = 0L;
long weightedSurfaceY = 0L;
long maximumInfluence = 0L;
int radiusSquared = SURFACE_TERRAIN_RADIUS * SURFACE_TERRAIN_RADIUS;
for (int offsetX = -SURFACE_TERRAIN_RADIUS;
offsetX <= SURFACE_TERRAIN_RADIUS; offsetX++) {
for (int offsetZ = -SURFACE_TERRAIN_RADIUS;
offsetZ <= SURFACE_TERRAIN_RADIUS; offsetZ++) {
int horizontalDistanceSquared = offsetX * offsetX + offsetZ * offsetZ;
if (horizontalDistanceSquared > radiusSquared) {
continue;
}
VacuumAnchor anchor = footprint.anchors().get(
NativeStructureTemplateOccupancy.columnKey(
worldX + offsetX, worldZ + offsetZ));
if (anchor == null || anchor.surfaceY() <= originalY) {
continue;
}
int verticalDistance = Math.abs(anchor.surfaceY() + 1 - originalY);
long distanceSquared = horizontalDistanceSquared
+ (long) verticalDistance * verticalDistance;
double factor = 0D;
if (distanceSquared <= radiusSquared) {
factor = surfaceFactor(Math.sqrt(distanceSquared));
}
if (anchor.strength() > 1
&& verticalDistance > SURFACE_TERRAIN_RADIUS) {
double rescueProgress = Math.min(1D,
(verticalDistance - SURFACE_TERRAIN_RADIUS)
/ (double) SURFACE_TERRAIN_RADIUS);
double rescueWeight = rescueProgress * rescueProgress
* (3D - 2D * rescueProgress);
factor = Math.max(factor,
surfaceFactor(Math.sqrt(horizontalDistanceSquared))
* rescueWeight);
}
long influence = Math.round(
factor * SURFACE_TERRAIN_INFLUENCE_SCALE);
if (influence <= 0L) {
continue;
}
long weightedInfluence = influence * Math.max(1, anchor.strength());
totalInfluence += weightedInfluence;
weightedSurfaceY += weightedInfluence * anchor.surfaceY();
maximumInfluence = Math.max(maximumInfluence, influence);
}
}
if (totalInfluence == 0L) {
return new SurfaceResolution(originalY, false);
}
double blendedSurfaceY = weightedSurfaceY / (double) totalInfluence;
double factor = maximumInfluence / (double) SURFACE_TERRAIN_INFLUENCE_SCALE;
int targetY = (int) Math.round(
originalY + ((blendedSurfaceY - originalY) * factor));
if (cap != null) {
targetY = Math.min(targetY, cap);
}
return new SurfaceResolution(Math.max(originalY, targetY), false);
}
private static double surfaceFactor(double distance) {
return Math.pow(1D - distance / SURFACE_TERRAIN_RADIUS,
SURFACE_TERRAIN_FALLOFF);
}
static void applySurfaceColumn(WorldGenLevel world, BlockPos.MutableBlockPos position,
int x, int z, int originalY, int targetY,
int worldMinY, int worldMaxY) {
@@ -405,4 +773,96 @@ public final class NativeStructureSurfaceFitter {
private record SurfaceResolution(int targetY, boolean rigidBaseSupport) {
}
private record VacuumFootprint(
Map<Long, VacuumAnchor> anchors, Map<Long, Integer> columnCaps,
Set<Long> foundationBases, Set<Long> occupiedCells) {
}
private record VacuumAnchor(int surfaceY, int strength) {
}
public static final class VacuumFoundationPlan {
private static final VacuumFoundationPlan EMPTY =
new VacuumFoundationPlan(List.of(), Set.of());
private final List<Long> foundationBases;
private final Set<Long> occupiedCells;
private VacuumFoundationPlan(
List<Long> foundationBases, Set<Long> occupiedCells) {
this.foundationBases = foundationBases;
this.occupiedCells = occupiedCells;
}
private static VacuumFoundationPlan empty() {
return EMPTY;
}
private static VacuumFoundationPlan create(
Set<Long> packedBases, Set<Long> packedOccupancy,
BoundingBox area) {
if (packedBases.isEmpty()) {
return EMPTY;
}
List<Long> bases = new ArrayList<>(packedBases.size());
Set<Long> columns = new HashSet<>();
for (long packedBase : packedBases) {
BlockPos base = BlockPos.of(packedBase);
if (!area.isInside(base)) {
continue;
}
bases.add(packedBase);
columns.add(NativeStructureTemplateOccupancy.columnKey(
base.getX(), base.getZ()));
}
if (bases.isEmpty()) {
return EMPTY;
}
bases.sort((first, second) -> {
int yOrder = Integer.compare(
BlockPos.of(first).getY(), BlockPos.of(second).getY());
return yOrder == 0 ? Long.compare(first, second) : yOrder;
});
Set<Long> occupancy = new HashSet<>();
for (long packedCell : packedOccupancy) {
BlockPos cell = BlockPos.of(packedCell);
if (columns.contains(NativeStructureTemplateOccupancy.columnKey(
cell.getX(), cell.getZ()))) {
occupancy.add(packedCell);
}
}
return new VacuumFoundationPlan(
List.copyOf(bases), Set.copyOf(occupancy));
}
}
private static final class VacuumCellBudget {
private final int maximum;
private int consumed;
private VacuumCellBudget(int maximum) {
if (maximum < 0) {
throw new IllegalArgumentException(
"Native structure vacuum cell budget cannot be negative");
}
this.maximum = maximum;
}
private void consume(int amount) {
if (amount < 0 || consumed > maximum - amount) {
throw VacuumBudgetExceeded.INSTANCE;
}
consumed += amount;
}
}
private static final class VacuumBudgetExceeded extends RuntimeException {
private static final VacuumBudgetExceeded INSTANCE =
new VacuumBudgetExceeded();
private VacuumBudgetExceeded() {
super(null, null, false, false);
}
}
}
@@ -2,19 +2,12 @@ package art.arcane.iris.nativegen;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.util.ArrayList;
@@ -31,7 +24,6 @@ import java.util.function.Supplier;
final class NativeStructureSurfaceSupportBuilder {
private static final int MAX_BATCH_CELLS = 524_288;
private static final int MAX_BRIDGE_SPAN = 2;
private static final OccupancyCell BLOCKER = new OccupancyCell(null, true);
private NativeStructureSurfaceSupportBuilder() {
}
@@ -100,15 +92,22 @@ final class NativeStructureSurfaceSupportBuilder {
if (lowerMeets.isEmpty()) {
continue;
}
Map<Long, OccupancyCell> occupancy = effectiveOccupancy(
world, area, upper.piece(), lowerMeets,
referencePos, templates, budget);
Map<Long, LowestCell> lowest = lowestCells(occupancy);
for (Map.Entry<Long, LowestCell> entry : lowest.entrySet()) {
LowestCell cell = entry.getValue();
NativeStructureTemplateOccupancy.OccupancyResult occupancy =
NativeStructureTemplateOccupancy.resolve(
world, upper.piece(), referencePos, area, templates,
position -> retain(position, area, lowerMeets), budget::consume);
if (!occupancy.resolved()) {
continue;
}
Map<Long, NativeStructureTemplateOccupancy.LowestCell> lowest =
NativeStructureTemplateOccupancy.lowestCells(occupancy.cells());
for (Map.Entry<Long, NativeStructureTemplateOccupancy.LowestCell> entry
: lowest.entrySet()) {
NativeStructureTemplateOccupancy.LowestCell cell = entry.getValue();
Integer lowerMeet = lowerMeets.get(entry.getKey());
if (lowerMeet == null || cell.occupancy().blocker()
|| !isSolidBase(cell.occupancy().state())
|| !NativeStructureTemplateOccupancy.isSolidBase(
cell.occupancy().state())
|| cell.y() - lowerMeet < 2
|| cell.y() - lowerMeet > MAX_BRIDGE_SPAN) {
continue;
@@ -168,75 +167,6 @@ final class NativeStructureSurfaceSupportBuilder {
return lowerMeets;
}
private static Map<Long, OccupancyCell> effectiveOccupancy(
WorldGenLevel world, BoundingBox area,
PoolElementStructurePiece piece, Map<Long, Integer> lowerMeets,
BlockPos referencePos, Supplier<StructureTemplateManager> templates,
BatchCellBudget budget) {
List<SinglePoolElement> leaves = new ArrayList<>();
if (!flattenSingles(piece.getElement(), leaves)) {
return Map.of();
}
List<LeafPlacement> placements = new ArrayList<>(leaves.size());
Map<Long, OccupancyCell> occupancy = new HashMap<>();
for (SinglePoolElement leaf : leaves) {
StructurePlaceSettings settings =
NativeStructureReflection.resolvePlacementSettings(leaf, piece, area);
StructureTemplate template = NativeStructureReflection.resolveTemplate(leaf, templates);
List<StructureTemplate.StructureBlockInfo> rawBlocks =
NativeStructureReflection.resolveTemplateBlocks(
template, settings, piece.getPosition());
budget.consume(rawBlocks.size());
placements.add(new LeafPlacement(settings, rawBlocks, template));
for (StructureTemplate.StructureBlockInfo raw : rawBlocks) {
BlockPos position = piece.getPosition().offset(
StructureTemplate.calculateRelativePosition(settings, raw.pos()));
if (retain(position, area, lowerMeets)) {
occupancy.put(position.asLong(), BLOCKER);
}
}
}
for (LeafPlacement placement : placements) {
List<StructureTemplate.StructureBlockInfo> processed =
NativeStructureReflection.processTemplateBlocks(
world, piece.getPosition(), referencePos,
placement.settings(), placement.rawBlocks(),
placement.template());
budget.consume(processed.size());
for (StructureTemplate.StructureBlockInfo block : processed) {
BlockPos position = block.pos();
if (!retain(position, area, lowerMeets)) {
continue;
}
BlockState state = block.state()
.mirror(placement.settings().getMirror())
.rotate(placement.settings().getRotation());
occupancy.put(position.asLong(), new OccupancyCell(state, false));
}
}
return occupancy;
}
private static boolean flattenSingles(
StructurePoolElement element, List<SinglePoolElement> leaves) {
if (element instanceof ListPoolElement listElement) {
for (StructurePoolElement child : listElement.getElements()) {
if (!flattenSingles(child, leaves)) {
return false;
}
}
return true;
}
if (element == EmptyPoolElement.INSTANCE) {
return true;
}
if (element instanceof SinglePoolElement singleElement) {
leaves.add(singleElement);
return true;
}
return false;
}
private static boolean retain(
BlockPos position, BoundingBox area, Map<Long, Integer> lowerMeets) {
if (!area.isInside(position)) {
@@ -246,22 +176,6 @@ final class NativeStructureSurfaceSupportBuilder {
return lowerMeet != null && position.getY() <= lowerMeet + MAX_BRIDGE_SPAN;
}
private static Map<Long, LowestCell> lowestCells(
Map<Long, OccupancyCell> occupancy) {
Map<Long, LowestCell> lowest = new HashMap<>();
for (Map.Entry<Long, OccupancyCell> entry : occupancy.entrySet()) {
BlockPos position = BlockPos.of(entry.getKey());
long column = columnKey(position.getX(), position.getZ());
LowestCell current = lowest.get(column);
if (current == null || position.getY() < current.y()) {
lowest.put(column, new LowestCell(
position.getX(), position.getY(), position.getZ(),
entry.getValue()));
}
}
return lowest;
}
private static Map<Long, BlockState> planWrites(
WorldGenLevel world, BoundingBox area,
List<SupportRequest> requests, BatchCellBudget budget) {
@@ -338,13 +252,6 @@ final class NativeStructureSurfaceSupportBuilder {
return world.getBlockState(position.set(x, lowerMeetY, z));
}
private static boolean isSolidBase(BlockState state) {
return state != null && state.isSolid()
&& !state.is(Blocks.STRUCTURE_VOID)
&& !state.is(Blocks.JIGSAW)
&& state.getFluidState().isEmpty();
}
private static boolean isTerrainSupport(BlockState state) {
return state.isSolid()
&& !NativeStructureVegetationClearer.isTreeBlock(state)
@@ -365,19 +272,6 @@ final class NativeStructureSurfaceSupportBuilder {
PoolElementStructurePiece piece, BoundingBox bounds, int meetY) {
}
private record LeafPlacement(
StructurePlaceSettings settings,
List<StructureTemplate.StructureBlockInfo> rawBlocks,
StructureTemplate template) {
}
private record OccupancyCell(BlockState state, boolean blocker) {
}
private record LowestCell(
int x, int y, int z, OccupancyCell occupancy) {
}
private record SupportRequest(int x, int z, int lowerMeetY, int baseY) {
}
@@ -0,0 +1,147 @@
package art.arcane.iris.nativegen;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
final class NativeStructureTemplateOccupancy {
private static final OccupancyCell BLOCKER = new OccupancyCell(null, true);
private NativeStructureTemplateOccupancy() {
}
static OccupancyResult resolve(
WorldGenLevel world, PoolElementStructurePiece piece,
BlockPos referencePosition, BoundingBox processingArea,
Supplier<StructureTemplateManager> templates,
PositionFilter filter, CellBudget budget) {
List<SinglePoolElement> leaves = new ArrayList<>();
if (!flattenSingles(piece.getElement(), leaves)) {
return new OccupancyResult(false, Map.of());
}
List<LeafPlacement> placements = new ArrayList<>(leaves.size());
Map<Long, OccupancyCell> occupancy = new HashMap<>();
for (SinglePoolElement leaf : leaves) {
StructurePlaceSettings settings = NativeStructureReflection.resolvePlacementSettings(
leaf, piece, processingArea);
StructureTemplate template = NativeStructureReflection.resolveTemplate(leaf, templates);
List<StructureTemplate.StructureBlockInfo> rawBlocks =
NativeStructureReflection.resolveTemplateBlocks(
template, settings, piece.getPosition());
budget.consume(rawBlocks.size());
placements.add(new LeafPlacement(settings, rawBlocks, template));
for (StructureTemplate.StructureBlockInfo raw : rawBlocks) {
BlockPos position = piece.getPosition().offset(
StructureTemplate.calculateRelativePosition(settings, raw.pos()));
if (filter.retain(position)) {
occupancy.put(position.asLong(), BLOCKER);
}
}
}
for (LeafPlacement placement : placements) {
List<StructureTemplate.StructureBlockInfo> processed =
NativeStructureReflection.processTemplateBlocks(
world, piece.getPosition(), referencePosition,
placement.settings(), placement.rawBlocks(), placement.template());
budget.consume(processed.size());
for (StructureTemplate.StructureBlockInfo block : processed) {
BlockPos position = block.pos();
if (!filter.retain(position)) {
continue;
}
BlockState state = block.state()
.mirror(placement.settings().getMirror())
.rotate(placement.settings().getRotation());
occupancy.put(position.asLong(), new OccupancyCell(state, false));
}
}
return new OccupancyResult(true, occupancy);
}
static Map<Long, LowestCell> lowestCells(Map<Long, OccupancyCell> occupancy) {
Map<Long, LowestCell> lowest = new HashMap<>();
for (Map.Entry<Long, OccupancyCell> entry : occupancy.entrySet()) {
OccupancyCell cell = entry.getValue();
BlockPos position = BlockPos.of(entry.getKey());
long column = columnKey(position.getX(), position.getZ());
LowestCell current = lowest.get(column);
if (current == null || position.getY() < current.y()) {
lowest.put(column, new LowestCell(
position.getX(), position.getY(), position.getZ(), cell));
}
}
return lowest;
}
static boolean isSolidBase(BlockState state) {
return state != null && state.isSolid()
&& !state.is(Blocks.STRUCTURE_VOID)
&& !state.is(Blocks.JIGSAW)
&& state.getFluidState().isEmpty();
}
static long columnKey(int x, int z) {
return (long) x << 32 ^ z & 0xffffffffL;
}
private static boolean flattenSingles(
StructurePoolElement element, List<SinglePoolElement> leaves) {
if (element instanceof ListPoolElement listElement) {
for (StructurePoolElement child : listElement.getElements()) {
if (!flattenSingles(child, leaves)) {
return false;
}
}
return true;
}
if (element == EmptyPoolElement.INSTANCE) {
return true;
}
if (element instanceof SinglePoolElement singleElement) {
leaves.add(singleElement);
return true;
}
return false;
}
@FunctionalInterface
interface PositionFilter {
boolean retain(BlockPos position);
}
@FunctionalInterface
interface CellBudget {
void consume(int amount);
}
record OccupancyResult(boolean resolved, Map<Long, OccupancyCell> cells) {
}
record OccupancyCell(BlockState state, boolean blocker) {
}
record LowestCell(int x, int y, int z, OccupancyCell occupancy) {
}
private record LeafPlacement(
StructurePlaceSettings settings,
List<StructureTemplate.StructureBlockInfo> rawBlocks,
StructureTemplate template) {
}
}
@@ -266,7 +266,6 @@ public final class NativeStructureTerrainIntegrator {
}
IrisStructureTerrainMode mode = terrain.resolvedMode();
return mode == IrisStructureTerrainMode.ENCASE
|| mode == IrisStructureTerrainMode.VACUUM
|| mode == IrisStructureTerrainMode.SOURCE
&& start.getStructure().terrainAdaptation() != TerrainAdjustment.NONE;
}
@@ -27,39 +27,36 @@ public final class NativeStructureVegetationClearer {
|| step == GenerationStep.Decoration.STRONGHOLDS;
}
public static boolean shouldClearEntireVegetationFootprint(GenerationStep.Decoration step,
boolean configured) {
return configured;
}
public static void clearIntersectingVegetation(WorldGenLevel world, ChunkAccess chunk, BoundingBox area,
List<VegetationTarget> targets) {
if (targets == null || targets.isEmpty()) {
List<StructureStart> starts) {
if (starts == null || starts.isEmpty()) {
return;
}
VegetationSnapshot snapshot = captureVegetation(chunk, area);
if (snapshot.treeBlockCount() == 0) {
return;
}
boolean[] clearColumns = new boolean[area.getXSpan() * area.getZSpan()];
for (VegetationTarget target : targets) {
if (shouldProcessTarget(target)) {
markVegetationColumns(area, snapshot, target, clearColumns);
int columns = area.getXSpan() * area.getZSpan();
int[] clearMinY = new int[columns];
int[] clearMaxY = new int[columns];
Arrays.fill(clearMinY, Integer.MAX_VALUE);
Arrays.fill(clearMaxY, Integer.MIN_VALUE);
for (StructureStart start : starts) {
if (shouldProcessStart(start)) {
markVegetationColumns(area, snapshot, start, clearMinY, clearMaxY);
}
}
clearVegetationColumns(world, area, snapshot, clearColumns);
clearVegetationColumns(world, area, snapshot, clearMinY, clearMaxY);
}
static boolean shouldProcessTarget(VegetationTarget target) {
return target != null && target.start() != null && target.start().isValid();
static boolean shouldProcessStart(StructureStart start) {
return start != null && start.isValid();
}
private static VegetationSnapshot captureVegetation(ChunkAccess chunk, BoundingBox area) {
int width = area.getXSpan();
int depth = area.getZSpan();
BitSet[] columns = new BitSet[width * depth];
int[] lowestY = new int[columns.length];
Arrays.fill(lowestY, Integer.MAX_VALUE);
int treeBlockCount = 0;
LevelChunkSection[] sections = chunk.getSections();
int chunkMinX = chunk.getPos().getMinBlockX();
@@ -89,21 +86,22 @@ public final class NativeStructureVegetationClearer {
columns[column] = treeBlocks;
}
treeBlocks.set(y - area.minY());
lowestY[column] = Math.min(lowestY[column], y);
treeBlockCount++;
}
}
}
}
return new VegetationSnapshot(columns, lowestY, treeBlockCount);
return new VegetationSnapshot(columns, treeBlockCount);
}
private static void markVegetationColumns(BoundingBox area, VegetationSnapshot snapshot,
VegetationTarget target, boolean[] clearColumns) {
StructureStart start, int[] clearMinY, int[] clearMaxY) {
int width = area.getXSpan();
int[] pieceTops = new int[clearColumns.length];
int[] pieceTops = new int[clearMinY.length];
int[] pieceBottoms = new int[clearMinY.length];
Arrays.fill(pieceTops, Integer.MIN_VALUE);
for (StructurePiece piece : target.start().getPieces()) {
Arrays.fill(pieceBottoms, Integer.MAX_VALUE);
for (StructurePiece piece : start.getPieces()) {
BoundingBox bounds = piece.getBoundingBox();
int minX = Math.max(area.minX(), bounds.minX());
int maxX = Math.min(area.maxX(), bounds.maxX());
@@ -116,6 +114,7 @@ public final class NativeStructureVegetationClearer {
for (int x = minX; x <= maxX; x++) {
int column = (z - area.minZ()) * width + x - area.minX();
pieceTops[column] = Math.max(pieceTops[column], bounds.maxY());
pieceBottoms[column] = Math.min(pieceBottoms[column], bounds.minY());
}
}
}
@@ -123,18 +122,13 @@ public final class NativeStructureVegetationClearer {
if (snapshot.columns()[column] == null || pieceTops[column] == Integer.MIN_VALUE) {
continue;
}
if (shouldClearVegetationColumn(pieceTops[column], snapshot.lowestY()[column], target.force())) {
clearColumns[column] = true;
clearMinY[column] = Math.min(clearMinY[column], pieceBottoms[column] - 1);
clearMaxY[column] = Math.max(clearMaxY[column], pieceTops[column]);
}
}
}
static boolean shouldClearVegetationColumn(int pieceTopY, int lowestTreeY, boolean force) {
return force || pieceTopY >= lowestTreeY;
}
private static void clearVegetationColumns(WorldGenLevel world, BoundingBox area,
VegetationSnapshot snapshot, boolean[] clearColumns) {
VegetationSnapshot snapshot, int[] clearMinY, int[] clearMaxY) {
int width = area.getXSpan();
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
BlockState air = Blocks.AIR.defaultBlockState();
@@ -142,10 +136,16 @@ public final class NativeStructureVegetationClearer {
for (int x = area.minX(); x <= area.maxX(); x++) {
int column = (z - area.minZ()) * width + x - area.minX();
BitSet treeBlocks = snapshot.columns()[column];
if (!clearColumns[column] || treeBlocks == null) {
if (treeBlocks == null || clearMaxY[column] < clearMinY[column]) {
continue;
}
for (int bit = treeBlocks.nextSetBit(0); bit >= 0; bit = treeBlocks.nextSetBit(bit + 1)) {
int firstBit = Math.max(0, clearMinY[column] - area.minY());
int lastBit = Math.min(area.getYSpan() - 1, clearMaxY[column] - area.minY());
if (firstBit > lastBit) {
continue;
}
for (int bit = treeBlocks.nextSetBit(firstBit); bit >= 0 && bit <= lastBit;
bit = treeBlocks.nextSetBit(bit + 1)) {
int y = area.minY() + bit;
position.set(x, y, z);
BlockState state = world.getBlockState(position);
@@ -167,9 +167,6 @@ public final class NativeStructureVegetationClearer {
|| path.endsWith("_leaves");
}
public record VegetationTarget(StructureStart start, boolean force) {
}
private record VegetationSnapshot(BitSet[] columns, int[] lowestY, int treeBlockCount) {
private record VegetationSnapshot(BitSet[] columns, int treeBlockCount) {
}
}
@@ -279,7 +279,7 @@ final class ModdedNativeStructureStage {
Engine current = generator.engine();
List<NativePlacementGroup> placementGroups = new ArrayList<>();
List<StructureStart> heightmapStarts = new ArrayList<>();
List<NativeStructureVegetationClearer.VegetationTarget> vegetationTargets = new ArrayList<>();
List<StructureStart> vegetationTargets = new ArrayList<>();
List<NativeStructureTerrainIntegrator.TerrainTarget> terrainTargets = new ArrayList<>();
for (int step = 0; step < steps; step++) {
int index = 0;
@@ -310,11 +310,7 @@ final class ModdedNativeStructureStage {
structureId, start,
NativeStructureTerrainIntegrator.resolveNativeTerrain(
start, decision.terrain())));
boolean clearEntireFootprint = NativeStructureVegetationClearer
.shouldClearEntireVegetationFootprint(
structure.step(), decision.clearVegetation());
vegetationTargets.add(new NativeStructureVegetationClearer.VegetationTarget(
start, clearEntireFootprint));
vegetationTargets.add(start);
}
if (!resolvedPlacements.isEmpty()) {
placementGroups.add(new NativePlacementGroup(
@@ -346,8 +342,9 @@ final class ModdedNativeStructureStage {
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
NativeStructureSurfaceFitter.VacuumFoundationPlan vacuumFoundationPlan;
try {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
world, area, terrainTargets,
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
} catch (Throwable error) {
@@ -375,6 +372,14 @@ final class ModdedNativeStructureStage {
"placement", group.structureId(), chunkPos.x(), chunkPos.z(), error);
}
}
try {
NativeStructureSurfaceFitter.repairVacuumFoundations(
world, area, vacuumFoundationPlan);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"foundation repair", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
}
private static String nativeStructureBatchContext(List<NativePlacementGroup> placementGroups) {
@@ -64,6 +64,7 @@ public class NativeStructureFailureContractTest {
assertTrue(placement.contains("\"terrain integration\""));
assertTrue(placement.contains("\"terrain preparation\""));
assertTrue(placement.contains("\"foundation repair\""));
assertFalse(placement.contains("\"terrain carving\""));
assertTrue(placement.contains("prepareSurfaceStructures"));
assertTrue(placement.contains("clearIntersectingVegetation"));
@@ -71,6 +72,8 @@ public class NativeStructureFailureContractTest {
< placement.indexOf("prepareSurfaceStructures"));
assertTrue(placement.indexOf("prepareSurfaceStructures")
< placement.indexOf("for (NativePlacementGroup group"));
assertTrue(placement.indexOf("repairVacuumFoundations")
> placement.indexOf("for (NativePlacementGroup group"));
}
@Test
@@ -332,6 +332,6 @@ public class IrisModdedStructureCommandTest {
}
private IrisNativeStructureDecision decision(NativeStructureGenerationStatus status) {
return new IrisNativeStructureDecision(status, 0, null, false, false, null, null);
return new IrisNativeStructureDecision(status, 0, null, false, null, null);
}
}
@@ -102,13 +102,13 @@ public class NativeStructureReferenceRepairTest {
public void deniedNaturalStartsCannotEnterCollisionArbitration() {
IrisNativeStructureDecision enabled = new IrisNativeStructureDecision(
NativeStructureGenerationStatus.GENERATE_NATIVE,
0, null, false, false, null, new IrisStructureTerrain());
0, null, false, null, new IrisStructureTerrain());
IrisNativeStructureDecision disabled = new IrisNativeStructureDecision(
NativeStructureGenerationStatus.DISABLED_BY_PACK,
0, null, false, false, null, new IrisStructureTerrain());
0, null, false, null, new IrisStructureTerrain());
IrisNativeStructureDecision replaced = new IrisNativeStructureDecision(
NativeStructureGenerationStatus.REPLACED_BY_IRIS,
0, null, false, false, null, new IrisStructureTerrain());
0, null, false, null, new IrisStructureTerrain());
assertTrue(NativeStructureReferenceRepair.naturalDecisionAllows(enabled));
assertFalse(NativeStructureReferenceRepair.naturalDecisionAllows(disabled));
@@ -20,7 +20,7 @@ import org.junit.Test;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructureVacuumParityTest {
@@ -31,7 +31,7 @@ public class NativeStructureVacuumParityTest {
}
@Test
public void explicitVacuumUsesSharedThinSurfaceFittingOnModdedLoaders() {
public void explicitVacuumUsesNonCarvingSurfaceFittingOnModdedLoaders() {
Structure structure = new DesertPyramidStructure(
new Structure.StructureSettings(
HolderSet.empty(), Map.of(),
@@ -45,7 +45,7 @@ public class NativeStructureVacuumParityTest {
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.VACUUM));
assertTrue(NativeStructureSurfaceFitter.requiresSurfaceTerrain(target));
assertEquals(TerrainAdjustment.BEARD_THIN,
NativeStructureSurfaceFitter.effectiveSurfaceAdjustment(target));
assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start, target.terrain()));
}
}
+11 -10
View File
@@ -73,11 +73,14 @@ String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${load
String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}")
apply plugin: ApiGenerator
// Where `buildAll` drops the per-platform jars for a local test server. Defaults to a repo-local
// directory so the task works on a fresh clone; point it at a real server tree with
// `-Plocation=/path/to/consumers`.
// Where `buildAll` drops the per-platform jars for a local test server. Use the approved sibling
// Multiplexor workspace when it exists, with a repo-local fallback for fresh clones.
File multiplexorConsumerLocation = new File(rootDir, '../../[Minecraft Server]/consumers').canonicalFile
String defaultConsumerLocation = multiplexorConsumerLocation.isDirectory()
? multiplexorConsumerLocation.absolutePath
: layout.buildDirectory.dir('consumers').get().asFile.absolutePath
String consumerLocation = providers.gradleProperty('location')
.getOrElse(layout.buildDirectory.dir('consumers').get().asFile.absolutePath)
.getOrElse(defaultConsumerLocation)
String bukkitConsumerPath = "${consumerLocation}/plugin-consumers/dropins/plugins"
String fabricConsumerPath = "${consumerLocation}/fabric-mod-consumers/dropins/mods"
String forgeConsumerPath = "${consumerLocation}/forge-mod-consumers/dropins/mods"
@@ -180,11 +183,9 @@ tasks.register('buildBukkit', Copy) {
}
}
// The three loader jars come out of nested standalone Gradle builds driven through Exec, so Gradle
// cannot see through to their real inputs and re-runs all three on every buildAll. The declarations
// below give each Exec a truthful input set. Anything that feeds a loader jar has to be listed here,
// including the VolmLib checkout when source substitution is active - an unlisted source tree means
// an edit there is silently ignored and a stale jar is treated as up to date.
// The three loader jars come out of nested standalone Gradle builds driven through Exec. Their own
// Gradle builds own the complete incremental model, so always launch them instead of allowing an
// incomplete outer input inventory to publish stale jars.
Closure<File> resolveLocalVolmLibDirectory = { ->
String configured = localVolmLibDirectory != null && !localVolmLibDirectory.isBlank()
? localVolmLibDirectory
@@ -242,7 +243,7 @@ Closure<Void> declareModdedJarIo = { Exec task, String adapter, String artifactN
task.inputs.property('volmLibCoordinate', volmLibCoordinate)
task.inputs.property('localVolmLibCheckout', localVolmLibCheckout == null ? '' : localVolmLibCheckout.absolutePath)
task.outputs.file(layout.projectDirectory.file("adapters/${adapter}/build/libs/${artifactName}"))
// The nested build owns its own caching; a remote cache entry here would be a lie.
task.outputs.upToDateWhen { false }
task.outputs.cacheIf { false }
return null
}
@@ -303,15 +303,19 @@ public class ServerConfigurator {
return "";
}
Path root = packsDir.toPath().toAbsolutePath().normalize();
if (Files.isSymbolicLink(root)) {
throw new IllegalArgumentException("Iris packs root is a symbolic link: " + root);
if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
return "";
}
if (!Files.isDirectory(root)) {
if (Files.isSymbolicLink(root)) {
throw new IllegalArgumentException("Iris packs root target is missing or unsafe: " + root);
}
if (!Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) {
return "";
}
try {
Path resolvedRoot = root.toRealPath();
MessageDigest digest = MessageDigest.getInstance("SHA-256");
List<FingerprintEntry> entries = collectFingerprintEntries(root);
List<FingerprintEntry> entries = collectFingerprintEntries(resolvedRoot);
entries.sort(Comparator.comparing(FingerprintEntry::relativePath));
byte[] buffer = new byte[8192];
for (FingerprintEntry entry : entries) {
@@ -91,6 +91,7 @@ import java.util.stream.Stream;
public final class DatapackIngestService {
private static final String USER_AGENT = "VolmitSoftware/Iris (datapack-ingest)";
private static final String FINDER_METADATA = ".DS_Store";
private static final String OVERRIDES_STRIPPED_MARKER = ".iris-overrides-stripped";
private static final String OWNERSHIP_MARKER = ".iris-managed.json";
private static final String TRANSACTION_DIRECTORY = ".iris-datapack-transactions";
@@ -803,6 +804,7 @@ public final class DatapackIngestService {
if (!ownershipSourceMatches(ownership, entry)) {
throw new IOException("Ownership marker at " + directory.getPath() + " belongs to '" + ownership.id + "'");
}
removeFinderMetadata(directory);
if (!Objects.equals(ownership.contentHash, directoryHash(directory))) {
throw new IOException("Refusing to remove modified or corrupt Iris-managed datapack " + directory.getPath());
}
@@ -1479,6 +1481,9 @@ public final class DatapackIngestService {
}
validateInstallTree(target, worldFolder, "Existing datapack install");
Ownership ownership = readOwnershipOrNull(target);
if (ownership != null) {
removeFinderMetadata(target);
}
String currentHash = directoryHash(target);
originalHash = currentHash;
originalMarkerHash = ownershipMarkerFingerprint(target);
@@ -1705,6 +1710,9 @@ public final class DatapackIngestService {
throw new IOException("Missing or unsafe " + purpose + " at " + directory.getPath());
}
validateInstallTree(directory, storeAnchor, purpose);
if (Files.exists(new File(directory, OWNERSHIP_MARKER).toPath(), LinkOption.NOFOLLOW_LINKS)) {
removeFinderMetadata(directory);
}
if (!expectedIdentity.isEmpty()
&& !Objects.equals(directoryIdentity(directory), expectedIdentity)) {
throw new IOException("Datapack directory identity changed in " + purpose + " at " + directory.getPath());
@@ -1991,6 +1999,7 @@ public final class DatapackIngestService {
if (!id.equals(ownership.id)) {
throw new IOException("Datapack ownership mismatch at " + directory.getPath());
}
removeFinderMetadata(directory);
}
private static void rejectSymbolicLinks(File root) throws IOException {
@@ -2019,6 +2028,13 @@ public final class DatapackIngestService {
if (path.equals(rootMarker)) {
continue;
}
if (isFinderMetadata(path)) {
if (Files.isSymbolicLink(path)
|| !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Suspicious Finder metadata in datapack: " + path);
}
continue;
}
pathCount++;
if (pathCount > MAX_MANAGED_PATHS) {
throw new IOException("Datapack contains more than " + MAX_MANAGED_PATHS + " paths");
@@ -2087,6 +2103,31 @@ public final class DatapackIngestService {
}
}
private static void removeFinderMetadata(File root) throws IOException {
List<Path> entries;
try (Stream<Path> paths = Files.walk(root.toPath())) {
entries = paths.limit(MAX_MANAGED_PATHS + 1L).toList();
}
if (entries.size() > MAX_MANAGED_PATHS) {
throw new IOException("Datapack contains more than " + MAX_MANAGED_PATHS + " paths");
}
for (Path path : entries) {
if (!isFinderMetadata(path)) {
continue;
}
if (Files.isSymbolicLink(path)
|| !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Suspicious Finder metadata in datapack: " + path);
}
Files.delete(path);
}
}
private static boolean isFinderMetadata(Path path) {
Path fileName = path.getFileName();
return fileName != null && FINDER_METADATA.equals(fileName.toString());
}
private static PackResources scanPackResources(File root) throws IOException {
TreeSet<String> structureKeys = new TreeSet<>();
TreeSet<String> templateKeys = new TreeSet<>();
@@ -2133,6 +2174,7 @@ public final class DatapackIngestService {
if (!id.equals(ownership.id)) {
throw new IOException("Ownership marker belongs to '" + ownership.id + "'");
}
removeFinderMetadata(directory);
if (!Objects.equals(ownership.contentHash, directoryHash(directory))) {
throw new IOException("Refusing to delete modified or corrupt Iris-managed datapack " + directory.getPath());
}
@@ -3440,7 +3482,7 @@ public final class DatapackIngestService {
}
private static boolean isHarmlessRecoveryArtifact(Path path) throws IOException {
if (!".DS_Store".equals(path.getFileName().toString())) {
if (!isFinderMetadata(path)) {
return false;
}
if (Files.isSymbolicLink(path) || !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
@@ -44,7 +44,7 @@ final class PackDimensionValidator {
continue;
}
validateImportedStructurePolicy(dimensionKey, dimJson, blockingErrors);
validateImportedStructurePolicy(dimensionKey, dimJson, blockingErrors, warnings);
JSONArray regionsArray = dimJson.optJSONArray("regions");
if (regionsArray == null || regionsArray.length() == 0) {
@@ -90,7 +90,7 @@ final class PackDimensionValidator {
}
static void validateImportedStructurePolicy(String dimensionKey, JSONObject dimension,
List<String> blockingErrors) {
List<String> blockingErrors, List<String> warnings) {
if (!dimension.has("importedStructures")) {
return;
}
@@ -127,6 +127,10 @@ final class PackDimensionValidator {
+ "' importedStructures.adjustments has a non-object entry at index " + index + ".");
continue;
}
if (adjustment.has("clearVegetation")) {
warnings.add("Dimension '" + dimensionKey + "' importedStructures.adjustments[" + index
+ "].clearVegetation was removed and is ignored. Vegetation is always cleared inside structure piece envelopes.");
}
validateStructureKeyList(dimensionKey, adjustment, "match", blockingErrors);
validateAdjustmentYBand(dimensionKey, adjustment, index, blockingErrors);
PackStructurePlacementValidator.validateNativeTerrain("Dimension '" + dimensionKey
@@ -47,7 +47,7 @@ public final class PackDirectoryResolver {
if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
return List.of();
}
if (Files.isSymbolicLink(root) || !Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) {
if (!Files.isDirectory(root)) {
throw new IOException("Pack workspace is missing or unsafe: " + root);
}
try (Stream<Path> stream = Files.list(root)) {
@@ -34,7 +34,7 @@ public record NativeStructureOwnershipRecord(
String contentFingerprint,
DecisionSnapshot decision
) {
public static final int CURRENT_SCHEMA = 1;
public static final int CURRENT_SCHEMA = 2;
public static final int MAX_REFERENCE_DISTANCE_CHUNKS = 8;
private static final int MAX_KEY_BYTES = 512;
private static final int MAX_FINGERPRINT_BYTES = 128;
@@ -251,7 +251,7 @@ public record NativeStructureOwnershipRecord(
}
}
public record DecisionSnapshot(boolean clearVegetation, String stiltJson, String terrainJson) {
public record DecisionSnapshot(String stiltJson, String terrainJson) {
private static final Gson GSON = new Gson();
private static final int MAX_JSON_BYTES = 65_536;
@@ -269,7 +269,6 @@ public record NativeStructureOwnershipRecord(
throw new IllegalArgumentException("Only generated native structure decisions can be persisted");
}
return new DecisionSnapshot(
resolved.clearVegetation(),
GSON.toJson(resolved.stilt()),
GSON.toJson(Objects.requireNonNullElseGet(
resolved.terrain(), IrisStructureTerrain::new))
@@ -288,21 +287,18 @@ public record NativeStructureOwnershipRecord(
0,
null,
false,
clearVegetation,
stilt,
terrain
);
}
void write(DataOutputStream output) throws IOException {
output.writeBoolean(clearVegetation);
writeString(output, stiltJson, MAX_JSON_BYTES, "stilt snapshot");
writeString(output, terrainJson, MAX_JSON_BYTES, "terrain snapshot");
}
static DecisionSnapshot read(DataInputStream input) throws IOException {
return new DecisionSnapshot(
input.readBoolean(),
readString(input, MAX_JSON_BYTES, "stilt snapshot"),
readString(input, MAX_JSON_BYTES, "terrain snapshot")
);
@@ -79,7 +79,6 @@ public final class NativeStructurePlacementPlanner {
0,
null,
false,
!placement.isUnderground(),
placement.getStilt(),
placement.resolvedTerrain()
);
@@ -0,0 +1,261 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.mantle.components;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice;
import java.util.Arrays;
public final class CarveOrphanSweep {
private static final int CHUNK_SIZE = 16;
private static final int CHUNK_AREA = CHUNK_SIZE * CHUNK_SIZE;
private static final int BAND_FLOOR_MARGIN = 4;
private static final int MAX_ORPHAN_CELLS = 16;
private static final MatterCavern ORPHAN_CAVERN = new MatterCavern(true, "", (byte) 0);
private static final ThreadLocal<SweepScratch> SCRATCH = ThreadLocal.withInitial(SweepScratch::new);
public interface CarveAccess {
boolean isCarved(int localX, int y, int localZ);
void markCarved(int localX, int y, int localZ);
}
private CarveOrphanSweep() {
}
static int sweepChunk(MantleChunk<Matter> chunk, int[] surfaceHeights, int maxSurfaceBreakDepth, int worldCeilingY) {
if (chunk == null) {
return 0;
}
return sweep(surfaceHeights, maxSurfaceBreakDepth, 0, worldCeilingY, new MantleCarveAccess(chunk));
}
public static int sweep(int[] surfaceHeights, int maxSurfaceBreakDepth, int worldFloorY, int worldCeilingY, CarveAccess access) {
if (surfaceHeights == null || surfaceHeights.length < CHUNK_AREA || access == null) {
return 0;
}
int minSurfaceY = Integer.MAX_VALUE;
int maxSurfaceY = Integer.MIN_VALUE;
for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) {
int surfaceY = surfaceHeights[columnIndex];
if (surfaceY < minSurfaceY) {
minSurfaceY = surfaceY;
}
if (surfaceY > maxSurfaceY) {
maxSurfaceY = surfaceY;
}
}
int bandTop = Math.min(worldCeilingY, maxSurfaceY);
int bandFloor = Math.max(worldFloorY + 1, minSurfaceY - Math.max(0, maxSurfaceBreakDepth) - BAND_FLOOR_MARGIN);
if (bandTop < bandFloor) {
return 0;
}
int bandHeight = bandTop - bandFloor + 1;
int cellCount = bandHeight * CHUNK_AREA;
SweepScratch scratch = SCRATCH.get();
scratch.prepare(cellCount);
long[] solid = scratch.solid;
long[] visited = scratch.visited;
int[] stack = scratch.stack;
int[] component = scratch.component;
boolean carvedPresent = false;
for (int y = bandFloor; y <= bandTop; y++) {
int layer = (y - bandFloor) * CHUNK_AREA;
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
if (y > surfaceHeights[columnIndex]) {
continue;
}
if (access.isCarved(localX, y, localZ)) {
carvedPresent = true;
continue;
}
setBit(solid, layer + columnIndex);
}
}
}
if (!carvedPresent) {
return 0;
}
int marked = 0;
for (int rootIndex = 0; rootIndex < cellCount; rootIndex++) {
if (!getBit(solid, rootIndex) || getBit(visited, rootIndex)) {
continue;
}
setBit(visited, rootIndex);
stack[0] = rootIndex;
int stackSize = 1;
int componentSize = 0;
boolean anchored = false;
while (stackSize > 0) {
int current = stack[--stackSize];
int columnIndex = current & (CHUNK_AREA - 1);
int localX = columnIndex >>> 4;
int localZ = columnIndex & (CHUNK_SIZE - 1);
int y = bandFloor + (current / CHUNK_AREA);
if (!anchored) {
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;
} else if (componentSize >= MAX_ORPHAN_CELLS) {
anchored = true;
} else {
component[componentSize++] = current;
}
}
if (y > bandFloor) {
stackSize = push(solid, visited, stack, stackSize, current - CHUNK_AREA);
}
if (y < bandTop) {
stackSize = push(solid, visited, stack, stackSize, current + CHUNK_AREA);
}
if (localX > 0) {
stackSize = push(solid, visited, stack, stackSize, current - CHUNK_SIZE);
}
if (localX < CHUNK_SIZE - 1) {
stackSize = push(solid, visited, stack, stackSize, current + CHUNK_SIZE);
}
if (localZ > 0) {
stackSize = push(solid, visited, stack, stackSize, current - 1);
}
if (localZ < CHUNK_SIZE - 1) {
stackSize = push(solid, visited, stack, stackSize, current + 1);
}
}
if (anchored) {
continue;
}
for (int index = 0; index < componentSize; index++) {
int cell = component[index];
int columnIndex = cell & (CHUNK_AREA - 1);
access.markCarved(columnIndex >>> 4, bandFloor + (cell / CHUNK_AREA), columnIndex & (CHUNK_SIZE - 1));
marked++;
}
}
return marked;
}
private static int push(long[] solid, long[] visited, int[] stack, int stackSize, int neighbor) {
if (!getBit(solid, neighbor) || getBit(visited, neighbor)) {
return stackSize;
}
setBit(visited, neighbor);
stack[stackSize] = neighbor;
return stackSize + 1;
}
private static boolean isSolidBelowBand(CarveAccess access, int[] surfaceHeights, int columnIndex, int localX, int localZ, int belowY, int worldFloorY) {
if (belowY < worldFloorY || belowY > surfaceHeights[columnIndex]) {
return false;
}
return !access.isCarved(localX, belowY, localZ);
}
private static boolean getBit(long[] bits, int index) {
return (bits[index >>> 6] & (1L << (index & 63))) != 0L;
}
private static void setBit(long[] bits, int index) {
bits[index >>> 6] |= 1L << (index & 63);
}
private static final class MantleCarveAccess implements CarveAccess {
private final MantleChunk<Matter> chunk;
private MatterSlice<MatterCavern> cachedSlice;
private int cachedSectionIndex = -1;
private MantleCarveAccess(MantleChunk<Matter> chunk) {
this.chunk = chunk;
}
@Override
public boolean isCarved(int localX, int y, int localZ) {
MatterSlice<MatterCavern> cavernSlice = resolveSlice(y >> 4);
if (cavernSlice == null) {
return false;
}
MatterCavern cavern = cavernSlice.get(localX, y & 15, localZ);
return cavern != null && cavern.isCavern();
}
@Override
public void markCarved(int localX, int y, int localZ) {
chunk.getOrCreate(y >> 4).slice(MatterCavern.class).set(localX, y & 15, localZ, ORPHAN_CAVERN);
cachedSectionIndex = -1;
cachedSlice = null;
}
private MatterSlice<MatterCavern> resolveSlice(int sectionIndex) {
if (sectionIndex == cachedSectionIndex) {
return cachedSlice;
}
Matter section = sectionIndex >= 0 && sectionIndex < chunk.sectionCount() ? chunk.get(sectionIndex) : null;
cachedSlice = section == null ? null : section.getSlice(MatterCavern.class);
cachedSectionIndex = sectionIndex;
return cachedSlice;
}
}
private static final class SweepScratch {
private final int[] component = new int[MAX_ORPHAN_CELLS];
private long[] solid = new long[0];
private long[] visited = new long[0];
private int[] stack = new int[0];
private void prepare(int cellCount) {
int words = (cellCount + 63) >>> 6;
if (solid.length < words) {
solid = new long[words];
visited = new long[words];
} else {
Arrays.fill(solid, 0, words, 0L);
Arrays.fill(visited, 0, words, 0L);
}
if (stack.length < cellCount) {
stack = new int[cellCount];
}
}
}
}
@@ -46,7 +46,6 @@ public class IrisCaveCarver3D {
private static final byte LIQUID_FORCED_AIR = 3;
private static final int ADAPTIVE_MIN_PLANE_COLUMNS = 16;
private static final int ADAPTIVE_DEEP_SAMPLE_STEP = 8;
private static final int ADAPTIVE_DEEP_SURFACE_MARGIN = 12;
private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.125D;
private static final double ADAPTIVE_DEEP_MARGIN_BOOST = 0.015D;
@@ -586,6 +585,12 @@ public class IrisCaveCarver3D {
boolean[] planeCarve = scratch.planeCarve;
int minSection = PowerOfTwoCoordinates.floorDivPow2(minY, 4);
int maxSection = PowerOfTwoCoordinates.floorDivPow2(maxY, 4);
int effectiveAdaptiveSampleStep = Math.max(adaptiveSampleStep, ADAPTIVE_DEEP_SAMPLE_STEP);
double effectiveAdaptiveThresholdMargin = resolveAdaptivePlaneThresholdMargin(
adaptiveThresholdMargin,
adaptiveSampleStep,
effectiveAdaptiveSampleStep
);
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
@@ -614,12 +619,6 @@ public class IrisCaveCarver3D {
continue;
}
int effectiveAdaptiveSampleStep = resolveAdaptivePlaneSampleStep(y, adaptiveSampleStep);
double effectiveAdaptiveThresholdMargin = resolveAdaptivePlaneThresholdMargin(
adaptiveThresholdMargin,
adaptiveSampleStep,
effectiveAdaptiveSampleStep
);
classifyDensityPlaneAdaptive(
scratch,
x0,
@@ -678,16 +677,6 @@ public class IrisCaveCarver3D {
return carved;
}
private int resolveAdaptivePlaneSampleStep(int y, int adaptiveSampleStep) {
if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) {
return adaptiveSampleStep;
}
int profileMaxY = (int) Math.ceil(profile.getVerticalRange().getMax());
int fineBandFloorY = profileMaxY - profile.getSurfaceBreakDepth() - ADAPTIVE_DEEP_SURFACE_MARGIN;
return y >= fineBandFloorY ? adaptiveSampleStep : ADAPTIVE_DEEP_SAMPLE_STEP;
}
private double resolveAdaptivePlaneThresholdMargin(
double adaptiveThresholdMargin,
int adaptiveSampleStep,
@@ -101,6 +101,23 @@ public class MantleCarvingComponent extends IrisMantleComponent {
carveUpperTerrain(upperCtx, weightedProfiles, writer, x, z, chunkSurfaceHeights, waterSupportPlan);
}
waterSupportPlan.resolve(writer.acquireChunk(x, z));
if (!weightedProfiles.isEmpty()) {
CarveOrphanSweep.sweepChunk(
writer.acquireChunk(x, z),
chunkSurfaceHeights,
maxSurfaceBreakDepth(weightedProfiles),
writer.getMantle().getWorldHeight() - 1
);
}
}
private static int maxSurfaceBreakDepth(List<WeightedProfile> weightedProfiles) {
int maxDepth = 0;
for (WeightedProfile weightedProfile : weightedProfiles) {
maxDepth = Math.max(maxDepth, Math.max(0, weightedProfile.profile.getSurfaceBreakDepth()));
}
return maxDepth;
}
@ChunkCoordinates
@@ -100,10 +100,8 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
sGenMatter,
sTerrain
));
registerStage(burst(
sCave,
sPost
));
registerStage(sCave);
registerStage(sPost);
registerStage(sFloatingTerrainSolid);
registerStage(burst(
sDeposit,
@@ -81,16 +81,6 @@ public class IrisCaveProfile {
@Desc("Additional adaptive ambiguity margin used before the cave predictor falls back to exact sampling.")
private double adaptiveThresholdMargin = 0.04;
@MinNumber(0)
@MaxNumber(4096)
@Desc("Minimum carved cells expected from this profile before recovery boost applies.")
private int minCarveCells = 0;
@MinNumber(0)
@MaxNumber(1)
@Desc("Additional threshold boost used when profile carve output is too sparse.")
private double recoveryThresholdBoost = 0.08;
@MinNumber(0)
@MaxNumber(64)
@Desc("Minimum solid clearance below terrain surface where carving may occur.")
@@ -52,7 +52,7 @@ public class IrisImportedStructureControl {
private boolean datapackOverrides = true;
@ArrayType(type = IrisVanillaStructureAdjustment.class, min = 1)
@Desc("Per-structure adjustments applied to vanilla, mod, and datapack structures that still generate natively. Vertical shifts from every matching entry stack. A matching vegetation option explicitly clears logs and leaves inside structure piece bounds. A matching preserveSourceY option disables Iris burial repositioning for that structure. The last matching entry with stilt settings controls foundation columns, and likewise for terrain and yBand settings. A structure suppressed by an Iris placement is unaffected.")
@Desc("Per-structure adjustments applied to vanilla, mod, and datapack structures that still generate natively. Vertical shifts from every matching entry stack. A matching preserveSourceY option disables Iris burial repositioning for that structure. The last matching entry with stilt settings controls foundation columns, and likewise for terrain and yBand settings. A structure suppressed by an Iris placement is unaffected.")
private KList<IrisVanillaStructureAdjustment> adjustments = new KList<>();
public boolean shouldGenerate(String key) {
@@ -64,7 +64,6 @@ public class IrisImportedStructureControl {
adjustments, "importedStructures.adjustments must not be null");
int y = undergroundStep ? undergroundYShift : 0;
boolean preserveSourceY = false;
boolean clearVegetation = false;
IrisStructureStiltSettings stilt = null;
IrisStructureTerrain terrain = null;
IrisStructureYBand yBand = null;
@@ -72,7 +71,6 @@ public class IrisImportedStructureControl {
if (adjustment != null && adjustment.matches(key)) {
y += adjustment.getYShift();
preserveSourceY |= adjustment.isPreserveSourceY();
clearVegetation |= adjustment.isClearVegetation();
if (adjustment.getStilt() != null) {
stilt = adjustment.getStilt();
}
@@ -85,7 +83,7 @@ public class IrisImportedStructureControl {
}
}
return new IrisNativeStructureDecision(
generationStatus(key), y, yBand, preserveSourceY, clearVegetation, stilt, terrain);
generationStatus(key), y, yBand, preserveSourceY, stilt, terrain);
}
private NativeStructureGenerationStatus generationStatus(String key) {
@@ -23,7 +23,6 @@ public record IrisNativeStructureDecision(
int yShift,
IrisStructureYBand yBand,
boolean preserveSourceY,
boolean clearVegetation,
IrisStructureStiltSettings stilt,
IrisStructureTerrain terrain
) {
@@ -33,6 +32,6 @@ public record IrisNativeStructureDecision(
public IrisNativeStructureDecision withStatus(NativeStructureGenerationStatus replacement) {
return new IrisNativeStructureDecision(
replacement, yShift, yBand, preserveSourceY, clearVegetation, stilt, terrain);
replacement, yShift, yBand, preserveSourceY, stilt, terrain);
}
}
@@ -22,7 +22,7 @@ public class IrisStructureTerrain {
private static final double MAX_EROSION_FREQUENCY = 1D;
private static final double MAX_LOBE_FREQUENCY = 1D;
@Desc("Terrain operation. SOURCE applies the registered native structure's authored terrain adaptation and is a no-op for editable Iris structures. PRESERVE disables terrain integration. VACUUM forces a 12-block surface bend to every rigid native piece base. BORE and FORCE_CARVE clear the requested envelope, while ENCASE fills it before placement so native shells are not lost to pre-carved air.")
@Desc("Terrain operation. SOURCE applies the registered native structure's authored terrain adaptation and is a no-op for editable Iris structures. PRESERVE disables terrain integration. VACUUM raises terrain from processed rigid-template foundations at or below each authored ground plane with a 12-block falloff without lowering existing ground. BORE and FORCE_CARVE clear the requested envelope, while ENCASE fills it before placement so native shells are not lost to pre-carved air.")
private IrisStructureTerrainMode mode = IrisStructureTerrainMode.SOURCE;
@MinNumber(0)
@@ -9,7 +9,7 @@ public enum IrisStructureTerrainMode {
BORE,
FORCE_CARVE,
@Desc("Bends surface terrain to every rigid piece base with a 12-block falloff, even when the registered structure has no authored terrain adaptation. Terrain is raised or lowered rather than carved away.")
@Desc("Raises surface terrain from processed solid rigid-template foundations at or below each authored ground plane with a 12-block falloff, even when the registered structure has no terrain adaptation. Existing higher terrain and authored air remain untouched.")
VACUUM,
@Desc("Fills the padded piece volume with solid blocks before any piece is placed so shells, walls, and floors land in solid ground instead of pre-carved air. Only air and liquid cells are filled; existing terrain and structures are never overwritten. Native pieces then carve their own interiors.")
@@ -32,7 +32,7 @@ import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
@Desc("A per-structure adjustment applied to vanilla, mod, and datapack structures that still generate natively (those NOT suppressed by an Iris 'structures' placement). Vertical shifts move the structure start, pieces, bounds, and jigsaw metadata together before references and placement. Surface structures clear intersecting trees automatically; optional postprocessing can force vegetation clearing or build palette-driven foundation columns.")
@Desc("A per-structure adjustment applied to vanilla, mod, and datapack structures that still generate natively (those NOT suppressed by an Iris 'structures' placement). Vertical shifts move the structure start, pieces, bounds, and jigsaw metadata together before references and placement. Intersecting trees are cleared automatically inside structure piece envelopes; optional postprocessing can build palette-driven foundation columns.")
@Data
public class IrisVanillaStructureAdjustment {
@ArrayType(type = String.class, min = 1)
@@ -51,13 +51,10 @@ public class IrisVanillaStructureAdjustment {
@Desc("When true, skip Iris burial repositioning so the structure keeps the Y its own vanilla placement chose. Underground structures are otherwise pushed below the lowest solid column across their whole footprint, which hides terrain-aware structures such as mineshafts that intentionally breach cliffs and surfaces. Vertical shifts still apply on top of the preserved Y: this dimension's undergroundYShift plus every matching adjustment's yShift.")
private boolean preserveSourceY = false;
@Desc("When true, force logs and leaves out of the structure footprint even when the structure does not reach the detected tree base. Normal surface-intersecting structures are protected automatically.")
private boolean clearVegetation = false;
@Desc("Optional foundation columns placed beneath the native structure piece bases after placement.")
private IrisStructureStiltSettings stilt = null;
@Desc("Optional terrain integration override. VACUUM forces surface terrain to bend to every rigid native piece base even when the structure did not author terrain adaptation. BORE and FORCE_CARVE clear every intersecting chunk before native pieces are placed, while ENCASE fills it with solid blocks instead. Left unset, SOURCE replays the registered structure's authored terrain adaptation, including surface fitting, burial, and encapsulation.")
@Desc("Optional terrain integration override. VACUUM raises surface terrain from processed solid rigid-template foundations at or below each authored ground plane with a 12-block falloff without lowering existing ground. BORE and FORCE_CARVE clear every intersecting chunk before native pieces are placed, while ENCASE fills it with solid blocks instead. Left unset, SOURCE replays the registered structure's authored terrain adaptation, including surface fitting, burial, and encapsulation.")
private IrisStructureTerrain terrain = null;
public boolean matches(String key) {
@@ -8,6 +8,7 @@ import org.junit.Assume;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -81,6 +82,23 @@ public class IrisDatapackCompilerTest {
assertTrue(Files.isRegularFile(datapackRoot.resolve("pack.mcmeta")));
}
@Test
public void collectsInstalledPacksThroughSymbolicLinkWorkspace() throws Exception {
Path dataDirectory = temporaryFolder.newFolder("linked-data").toPath();
Path serverRoot = temporaryFolder.newFolder("linked-server").toPath();
Path sharedPacks = temporaryFolder.newFolder("linked-shared-packs").toPath();
Path overworld = sharedPacks.resolve("overworld");
createPack(overworld, "overworld", "linked_custom");
try {
Files.createSymbolicLink(dataDirectory.resolve("packs"), sharedPacks);
} catch (IOException | UnsupportedOperationException | SecurityException exception) {
Assume.assumeNoException(exception);
}
assertEquals(List.of(dataDirectory.resolve("packs/overworld").toFile()),
IrisDatapackCompiler.collectPackRoots(dataDirectory, serverRoot));
}
@Test
public void compilingNoPacksPublishesCleanEmptyDatapack() throws Exception {
Path datapackRoot = temporaryFolder.newFolder("empty-datapack").toPath();
@@ -148,6 +148,46 @@ public class ServerConfiguratorDatapackFingerprintTest {
assertNotEquals(before, ServerConfigurator.computePackFingerprint(packsDir));
}
@Test
public void computePackFingerprintReadsSafeSymbolicWorkspaceRoots() throws Exception {
Path workspace = tmp.newFolder("pack-workspace").toPath();
Path externalPack = tmp.newFolder("workspace-linked-pack").toPath();
Path dimension = externalPack.resolve("dimensions/overworld.json");
Files.createDirectories(dimension.getParent());
Files.writeString(dimension, "first", StandardCharsets.UTF_8);
Path packLink = workspace.resolve("overworld");
Path workspaceLink = tmp.getRoot().toPath().resolve("packs-link");
try {
Files.createSymbolicLink(packLink, externalPack);
Files.createSymbolicLink(workspaceLink, workspace);
} catch (IOException | UnsupportedOperationException | SecurityException exception) {
Assume.assumeNoException(exception);
}
String before = ServerConfigurator.computePackFingerprint(workspaceLink.toFile());
assertEquals(ServerConfigurator.computePackFingerprint(workspace.toFile()), before);
Files.writeString(dimension, "other", StandardCharsets.UTF_8);
assertNotEquals(before, ServerConfigurator.computePackFingerprint(workspaceLink.toFile()));
}
@Test
public void computePackFingerprintRejectsDanglingSymbolicWorkspaceRoots() throws Exception {
Path workspaceLink = tmp.getRoot().toPath().resolve("dangling-packs-link");
try {
Files.createSymbolicLink(workspaceLink, tmp.getRoot().toPath().resolve("missing-workspace"));
} catch (IOException | UnsupportedOperationException | SecurityException exception) {
Assume.assumeNoException(exception);
}
try {
ServerConfigurator.computePackFingerprint(workspaceLink.toFile());
fail("Dangling symbolic workspace roots must be rejected");
} catch (IllegalArgumentException expected) {
assertTrue(expected.getMessage().contains("missing or unsafe"));
}
}
@Test
public void incompleteExternalDatapackRecoveryBlocksCompilation() throws Exception {
String source = Files.readString(Path.of(
@@ -449,6 +449,39 @@ public class DatapackIngestServiceTest {
assertEquals(List.of("original:template"), entry.templateKeys);
}
@Test
public void finderMetadataDoesNotInvalidateManagedStaging() throws Exception {
File staging = datapackDirectory("finder-metadata-staging");
File nested = new File(staging, "data/example");
assertTrue(nested.mkdirs());
DatapackIngestService.Entry entry = entry("finder-metadata-staging", "v1", "1", "sha");
DatapackIngestService.writeOwnership(staging, entry);
File rootMetadata = new File(staging, ".DS_Store");
File nestedMetadata = new File(nested, ".DS_Store");
Files.writeString(rootMetadata.toPath(), "finder", StandardCharsets.UTF_8);
Files.writeString(nestedMetadata.toPath(), "finder", StandardCharsets.UTF_8);
assertTrue(DatapackIngestService.isUsableStaging(staging, entry));
assertFalse(rootMetadata.exists());
assertFalse(nestedMetadata.exists());
}
@Test
public void finderMetadataDirectoryCannotBypassManagedHashing() throws Exception {
File staging = datapackDirectory("finder-metadata-directory");
assertTrue(new File(staging, ".DS_Store").mkdir());
DatapackIngestService.Entry entry = entry("finder-metadata-directory", "v1", "1", "sha");
try {
DatapackIngestService.writeOwnership(staging, entry);
fail("Expected suspicious Finder metadata to be rejected");
} catch (IOException expected) {
assertTrue(expected.getMessage().contains("Suspicious Finder metadata"));
}
assertFalse(new File(staging, ".iris-managed.json").exists());
}
@Test
public void nestedOwnershipNamedResourceRemainsInsideTheManagedHash() throws Exception {
File staging = datapackDirectory("nested-ownership-resource");
@@ -743,7 +776,7 @@ public class DatapackIngestServiceTest {
DatapackIngestService.InstallPlan plan = prepareLegacyStagingPlan(fixture);
DatapackIngestService.publishInstallPlan(plan);
assertTrue(plan.contentChanged());
assertFalse(plan.contentChanged());
assertTrue(DatapackIngestService.freshInstallRequiresRestart(plan.contentChanged(), true));
assertEquals("same", Files.readString(
new File(fixture.target(), "value.txt").toPath(), StandardCharsets.UTF_8));
@@ -1566,6 +1599,80 @@ public class DatapackIngestServiceTest {
assertFalse(transaction.exists());
}
@Test
public void publishingInstallCrashRemovesFinderMetadataBeforeRollback() throws Exception {
File root = temporaryFolder.newFolder("finder-install-crash-rollback-root");
File world = temporaryFolder.newFolder("finder-install-crash-rollback-world");
DatapackIngestService.Entry entry = entry("managed", "v1", "1", "sha");
writeManifest(root, entry);
File target = new File(world, entry.id);
writeManagedDatapack(target, entry, "old");
String originalHash = ownershipHash(target);
File scratch = new File(world.getParentFile(), ".iris-datapack-install");
assertTrue(scratch.mkdirs());
File pending = new File(scratch, "managed-pending-finder");
File backup = new File(scratch, "managed-backup-finder");
writeManagedDatapack(pending, entry, "new");
assertTrue(new File(pending, "data/nova_structures").mkdirs());
DatapackIngestService.writeOwnership(pending, entry);
String desiredHash = ownershipHash(pending);
Files.move(target.toPath(), backup.toPath());
Files.move(pending.toPath(), target.toPath());
Map<String, Object> directory = installDirectory(
target, pending, backup, true, originalHash, desiredHash);
File transaction = writeCoordinator(root, "INSTALL", "PUBLISHING", entry, true,
List.of(directory), List.of());
Files.writeString(new File(target, ".DS_Store").toPath(), "finder", StandardCharsets.UTF_8);
Files.writeString(new File(target, "data/.DS_Store").toPath(), "finder", StandardCharsets.UTF_8);
Files.writeString(new File(target, "data/nova_structures/.DS_Store").toPath(),
"finder", StandardCharsets.UTF_8);
DatapackIngestService.recoverTransactions(root, List.of(world));
assertEquals("old", Files.readString(new File(target, "value.txt").toPath(), StandardCharsets.UTF_8));
assertFalse(new File(target, ".DS_Store").exists());
assertFalse(backup.exists());
assertFalse(transaction.exists());
}
@Test
public void publishingInstallCrashStillRejectsAuthoredContentMutation() throws Exception {
File root = temporaryFolder.newFolder("changed-install-crash-rollback-root");
File world = temporaryFolder.newFolder("changed-install-crash-rollback-world");
DatapackIngestService.Entry entry = entry("managed", "v1", "1", "sha");
writeManifest(root, entry);
File target = new File(world, entry.id);
writeManagedDatapack(target, entry, "old");
String originalHash = ownershipHash(target);
File scratch = new File(world.getParentFile(), ".iris-datapack-install");
assertTrue(scratch.mkdirs());
File pending = new File(scratch, "managed-pending-changed");
File backup = new File(scratch, "managed-backup-changed");
writeManagedDatapack(pending, entry, "new");
String desiredHash = ownershipHash(pending);
Files.move(target.toPath(), backup.toPath());
Files.move(pending.toPath(), target.toPath());
Map<String, Object> directory = installDirectory(
target, pending, backup, true, originalHash, desiredHash);
File transaction = writeCoordinator(root, "INSTALL", "PUBLISHING", entry, true,
List.of(directory), List.of());
Files.writeString(new File(target, "value.txt").toPath(), "changed", StandardCharsets.UTF_8);
try {
DatapackIngestService.recoverTransactions(root, List.of(world));
fail("Expected authored datapack mutation to block recovery");
} catch (IOException expected) {
assertTrue(expected.getMessage().contains("content changed"));
}
assertEquals("changed", Files.readString(
new File(target, "value.txt").toPath(), StandardCharsets.UTF_8));
assertTrue(backup.exists());
assertTrue(transaction.exists());
}
@Test
public void publishingInstallCrashRestoresManagedStagingWithEveryWorld() throws Exception {
File root = temporaryFolder.newFolder("staging-install-crash-rollback-root");
@@ -2,6 +2,7 @@ package art.arcane.iris.core.pack;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import org.junit.Assume;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
@@ -111,6 +112,42 @@ public class DefaultPackBootstrapProvisionerTest {
}
}
@Test
public void coldInstallPreservesSymbolicLinkPackWorkspace() throws Exception {
byte[] archive = packArchive("overworld", "bootstrap_biome");
AtomicInteger requests = new AtomicInteger();
HttpServer server = server(archive, requests);
Path root = Files.createTempDirectory("iris-bootstrap-linked-workspace");
try {
Path dataDirectory = root.resolve("plugins/Iris");
Path sharedPacks = root.resolve("shared-plugin-data/iris/packs");
Files.createDirectories(dataDirectory);
Files.createDirectories(sharedPacks);
try {
Files.createSymbolicLink(dataDirectory.resolve("packs"), sharedPacks);
} catch (IOException | UnsupportedOperationException | SecurityException exception) {
Assume.assumeNoException(exception);
}
DefaultPackBootstrapProvisioner.ProvisionOptions options = options(server, root, Duration.ofHours(1));
DefaultPackBootstrapProvisioner.ProvisionResult installed = DefaultPackBootstrapProvisioner.provision(
dataDirectory,
ignored -> {
},
options
);
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.INSTALLED, installed.status());
assertEquals(1, requests.get());
assertTrue(Files.isSymbolicLink(dataDirectory.resolve("packs")));
assertTrue(Files.isRegularFile(sharedPacks.resolve("overworld/dimensions/overworld.json")));
assertTrue(Files.isRegularFile(installed.datapackRoot().resolve("pack.mcmeta")));
} finally {
server.stop(0);
delete(root);
}
}
@Test
public void corruptCacheRedownloadsAndRepairsArchive() throws Exception {
byte[] archive = packArchive("overworld", "bootstrap_biome");
@@ -13,6 +13,7 @@ import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -78,6 +79,42 @@ public class PackDirectoryResolverTest {
assertNull(PackDirectoryResolver.resolveExisting(packs, ".custom-stage"));
}
@Test
public void listsPacksThroughSymbolicLinkWorkspace() throws Exception {
File sharedPacks = temporaryFolder.newFolder("shared-packs");
File overworld = new File(sharedPacks, "overworld");
Files.createDirectory(overworld.toPath());
Path workspace = temporaryFolder.getRoot().toPath().resolve("packs");
try {
Files.createSymbolicLink(workspace, sharedPacks.toPath());
} catch (IOException | UnsupportedOperationException | SecurityException exception) {
Assume.assumeNoException(exception);
}
assertEquals(List.of(workspace.resolve("overworld").toFile()),
PackDirectoryResolver.listVisiblePackDirectoriesOrThrow(workspace.toFile()));
}
@Test
public void missingWorkspaceListsNoPacks() throws Exception {
File missing = new File(temporaryFolder.getRoot(), "missing-packs");
assertEquals(List.of(), PackDirectoryResolver.listVisiblePackDirectoriesOrThrow(missing));
}
@Test
public void rejectsDanglingSymbolicLinkWorkspace() throws Exception {
Path workspace = temporaryFolder.getRoot().toPath().resolve("dangling-packs");
try {
Files.createSymbolicLink(workspace, temporaryFolder.getRoot().toPath().resolve("missing-target"));
} catch (IOException | UnsupportedOperationException | SecurityException exception) {
Assume.assumeNoException(exception);
}
assertThrows(IOException.class,
() -> PackDirectoryResolver.listVisiblePackDirectoriesOrThrow(workspace.toFile()));
}
@Test
public void rejectsSymbolicLinksInsidePackTrees() throws Exception {
File packs = temporaryFolder.newFolder("nested-link-root");
@@ -110,19 +110,38 @@ public class PackValidatorImportedStructurePolicyTest {
@Test
public void explicitNullPolicyIsRejectedWhileOmissionUsesDefaults() {
List<String> missingErrors = new ArrayList<>();
PackDimensionValidator.validateImportedStructurePolicy("overworld", new JSONObject(), missingErrors);
PackDimensionValidator.validateImportedStructurePolicy("overworld", new JSONObject(),
missingErrors, new ArrayList<>());
assertTrue(missingErrors.isEmpty());
List<String> nullErrors = new ArrayList<>();
PackDimensionValidator.validateImportedStructurePolicy("overworld",
new JSONObject().put("importedStructures", JSONObject.NULL), nullErrors);
new JSONObject().put("importedStructures", JSONObject.NULL), nullErrors, new ArrayList<>());
assertEquals(List.of("Dimension 'overworld' importedStructures must be an object."), nullErrors);
}
@Test
public void removedClearVegetationAdjustmentWarnsWithoutBlockingThePack() {
JSONObject policy = new JSONObject()
.put("adjustments", new JSONArray().put(new JSONObject()
.put("match", new JSONArray().put("minecraft:village"))
.put("clearVegetation", true)));
List<String> errors = new ArrayList<>();
List<String> warnings = new ArrayList<>();
PackDimensionValidator.validateImportedStructurePolicy("overworld",
new JSONObject().put("importedStructures", policy), errors, warnings);
assertTrue(errors.toString(), errors.isEmpty());
assertEquals(1, warnings.size());
assertTrue(warnings.get(0), warnings.get(0)
.contains("adjustments[0].clearVegetation was removed and is ignored"));
}
private List<String> validate(JSONObject policy) {
List<String> errors = new ArrayList<>();
PackDimensionValidator.validateImportedStructurePolicy("overworld",
new JSONObject().put("importedStructures", policy), errors);
new JSONObject().put("importedStructures", policy), errors, new ArrayList<>());
return errors;
}
}
@@ -87,7 +87,8 @@ public class PackValidatorStructureTerrainBackendTest {
List<String> errors = new ArrayList<>();
PackDimensionValidator.validateImportedStructurePolicy(
"overworld", new JSONObject().put("importedStructures", policy), errors);
"overworld", new JSONObject().put("importedStructures", policy),
errors, new ArrayList<>());
assertTrue(mode + ": " + errors, errors.isEmpty());
}
@@ -13,6 +13,7 @@ import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -25,6 +26,7 @@ import static org.junit.Assert.assertTrue;
public class NativeStructureOwnershipRecordTest {
private static final String FINGERPRINT = "12".repeat(32);
private static final int SCHEMA_OFFSET = 8;
@Test
public void binaryRoundTripPreservesMultipleVersionedOwnershipRecords() throws Exception {
@@ -44,7 +46,7 @@ public class NativeStructureOwnershipRecordTest {
}
@Test
public void decisionSnapshotFreezesTerrainVegetationAndStiltSettings() {
public void decisionSnapshotFreezesTerrainAndStiltSettings() {
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setHorizontalPadding(9)
@@ -64,7 +66,6 @@ public class NativeStructureOwnershipRecordTest {
0,
null,
false,
true,
stilt,
terrain
);
@@ -76,7 +77,6 @@ public class NativeStructureOwnershipRecordTest {
IrisNativeStructureDecision restored = snapshot.restore();
assertTrue(restored.generate());
assertTrue(restored.clearVegetation());
assertEquals(IrisStructureTerrainMode.FORCE_CARVE, restored.terrain().resolvedMode());
assertEquals(IrisStructureCarveShape.ERODED, restored.terrain().resolvedShape());
assertEquals(9, restored.terrain().getHorizontalPadding());
@@ -186,7 +186,6 @@ public class NativeStructureOwnershipRecordTest {
new LinkedHashMap<>();
NativeStructureOwnershipRecord.DecisionSnapshot largeDecision =
new NativeStructureOwnershipRecord.DecisionSnapshot(
false,
"null",
"{\"unused\":\"" + "x".repeat(65_000) + "\"}"
);
@@ -302,6 +301,21 @@ public class NativeStructureOwnershipRecordTest {
assertNull(record.restoredDecision().stilt());
}
@Test
public void bundlesWrittenUnderAnOlderSchemaRevisionFailClosed() throws Exception {
NativeStructureOwnershipRecord record = record("nova_structures:tavern_oak", 4, -7, 71L);
NativeStructureOwnershipBundle bundle = NativeStructureOwnershipBundle.empty().with(record);
ByteArrayOutputStream encoded = new ByteArrayOutputStream();
bundle.write(new DataOutputStream(encoded));
byte[] payload = encoded.toByteArray();
ByteBuffer.wrap(payload).putInt(SCHEMA_OFFSET, NativeStructureOwnershipRecord.CURRENT_SCHEMA - 1);
IOException failure = assertThrows(IOException.class, () -> NativeStructureOwnershipBundle.read(
new DataInputStream(new ByteArrayInputStream(payload))));
assertTrue(failure.getMessage(), failure.getMessage().contains("schema"));
}
@Test
public void malformedButSizedBinaryRecordsFailAsIoErrors() throws Exception {
ByteArrayOutputStream encoded = new ByteArrayOutputStream();
@@ -362,7 +376,6 @@ public class NativeStructureOwnershipRecordTest {
0,
null,
false,
true,
null,
new IrisStructureTerrain()
));
@@ -31,7 +31,7 @@ public class NativeStructureOwnershipStoreTest {
TestStorage storage = new TestStorage();
NativeStructureOwnershipStore.State state =
new NativeStructureOwnershipStore.State(engine, storage);
NativeStructureOwnershipRecord record = record("test:origin_only", 4, -7, 91L, false);
NativeStructureOwnershipRecord record = record("test:origin_only", 4, -7, 91L);
state.record(record);
@@ -48,8 +48,10 @@ public class NativeStructureOwnershipStoreTest {
public void originAuthorityIgnoresAStaleTargetReplicaWhenOnlyPolicyChanged() {
Engine engine = engine();
TestStorage storage = new TestStorage();
NativeStructureOwnershipRecord stale = record("test:replacement", 2, 3, 11L, false);
NativeStructureOwnershipRecord current = record("test:replacement", 2, 3, 11L, true);
NativeStructureOwnershipRecord stale = record("test:replacement", 2, 3, 11L,
new IrisStructureTerrain().setHorizontalPadding(2), 8);
NativeStructureOwnershipRecord current = record("test:replacement", 2, 3, 11L,
new IrisStructureTerrain().setHorizontalPadding(9), 8);
long origin = NativeStructureOwnershipStore.pack(2, 3);
long target = NativeStructureOwnershipStore.pack(3, 4);
storage.write(origin, current);
@@ -61,7 +63,7 @@ public class NativeStructureOwnershipStoreTest {
3, 4, current.structureKey(), 2, 3);
assertEquals(current, resolved);
assertTrue(resolved.restoredDecision().clearVegetation());
assertEquals(9, resolved.restoredDecision().terrain().getHorizontalPadding());
assertEquals(stale.contentFingerprint(), resolved.contentFingerprint());
assertEquals(stale, storage.find(target, stale));
}
@@ -73,7 +75,7 @@ public class NativeStructureOwnershipStoreTest {
NativeStructureOwnershipStore.State state =
new NativeStructureOwnershipStore.State(engine, storage);
NativeStructureOwnershipRecord record = record(
"test:narrow_authority", 8, -3, 19L, false, 1);
"test:narrow_authority", 8, -3, 19L, 1);
state.record(record);
assertNull(state.find(
@@ -86,7 +88,7 @@ public class NativeStructureOwnershipStoreTest {
public void staleTargetReplicaCannotReplaceAMissingOriginAuthority() {
Engine engine = engine();
TestStorage storage = new TestStorage();
NativeStructureOwnershipRecord stale = record("test:deleted", -2, 5, 17L, false);
NativeStructureOwnershipRecord stale = record("test:deleted", -2, 5, 17L);
storage.write(NativeStructureOwnershipStore.pack(-1, 5), stale);
NativeStructureOwnershipStore.State state =
new NativeStructureOwnershipStore.State(engine, storage);
@@ -105,7 +107,7 @@ public class NativeStructureOwnershipStoreTest {
for (int chunkZ = -8; chunkZ <= 8; chunkZ++) {
NativeStructureOwnershipRecord record = record(
"test:dense_" + chunkX + "_" + chunkZ,
chunkX, chunkZ, records, false);
chunkX, chunkZ, records);
state.record(record);
assertEquals(record, state.find(
0, 0, record.structureKey(), chunkX, chunkZ));
@@ -126,7 +128,7 @@ public class NativeStructureOwnershipStoreTest {
BlockingStorage storage = new BlockingStorage();
NativeStructureOwnershipStore.State state =
new NativeStructureOwnershipStore.State(engine, storage);
NativeStructureOwnershipRecord record = record("test:flush_race", -4, 8, 42L, false);
NativeStructureOwnershipRecord record = record("test:flush_race", -4, 8, 42L);
storage.blockedTarget = NativeStructureOwnershipStore.pack(-4, 8);
ExecutorService callers = Executors.newFixedThreadPool(2);
try {
@@ -152,7 +154,7 @@ public class NativeStructureOwnershipStoreTest {
BlockingStorage storage = new BlockingStorage();
NativeStructureOwnershipStore.State state =
new NativeStructureOwnershipStore.State(engine, storage);
NativeStructureOwnershipRecord record = record("test:close_race", 6, -9, 73L, false);
NativeStructureOwnershipRecord record = record("test:close_race", 6, -9, 73L);
storage.blockedTarget = NativeStructureOwnershipStore.pack(6, -9);
ExecutorService callers = Executors.newFixedThreadPool(2);
try {
@@ -178,7 +180,7 @@ public class NativeStructureOwnershipStoreTest {
NativeStructureOwnershipStore.State state =
new NativeStructureOwnershipStore.State(engine, storage);
NativeStructureOwnershipRecord record = record(
"test:closing_session", -7, 12, 74L, false);
"test:closing_session", -7, 12, 74L);
state.record(record);
@@ -207,8 +209,8 @@ public class NativeStructureOwnershipStoreTest {
PostWriteBlockingStorage storage = new PostWriteBlockingStorage();
NativeStructureOwnershipStore.State state =
new NativeStructureOwnershipStore.State(engine, storage);
NativeStructureOwnershipRecord first = record("test:same_origin", 3, -6, 1L, false);
NativeStructureOwnershipRecord second = record("test:same_origin", 3, -6, 2L, true);
NativeStructureOwnershipRecord first = record("test:same_origin", 3, -6, 1L);
NativeStructureOwnershipRecord second = record("test:same_origin", 3, -6, 2L);
ExecutorService callers = Executors.newFixedThreadPool(2);
try {
Future<?> firstWrite = callers.submit(() -> state.record(first));
@@ -238,7 +240,7 @@ public class NativeStructureOwnershipStoreTest {
NativeStructureOwnershipStore.State state =
new NativeStructureOwnershipStore.State(engine, storage);
NativeStructureOwnershipRecord record = record(
"test:autosave_durable", -11, 14, 101L, true);
"test:autosave_durable", -11, 14, 101L);
state.record(record);
state.flush();
@@ -258,7 +260,7 @@ public class NativeStructureOwnershipStoreTest {
NativeStructureOwnershipStore.State state =
new NativeStructureOwnershipStore.State(engine, storage);
NativeStructureOwnershipRecord record = record(
"test:flush_retry", 12, -15, 102L, false);
"test:flush_retry", 12, -15, 102L);
state.record(record);
assertThrows(IllegalStateException.class, state::flush);
@@ -279,7 +281,7 @@ public class NativeStructureOwnershipStoreTest {
NativeStructureOwnershipStore.State state =
new NativeStructureOwnershipStore.State(engine, storage);
NativeStructureOwnershipRecord record = record(
"test:clean_flush", 16, 17, 103L, false);
"test:clean_flush", 16, 17, 103L);
state.flush();
state.record(record);
@@ -294,7 +296,7 @@ public class NativeStructureOwnershipStoreTest {
Engine engine = engine();
CrashableStorage storage = new CrashableStorage();
NativeStructureOwnershipRecord record = record(
"test:discard_durable", -18, 19, 104L, false);
"test:discard_durable", -18, 19, 104L);
storage.write(NativeStructureOwnershipStore.pack(-18, 19), record);
storage.flush();
NativeStructureOwnershipStore.State state =
@@ -318,7 +320,7 @@ public class NativeStructureOwnershipStoreTest {
NativeStructureOwnershipStore.State state =
new NativeStructureOwnershipStore.State(engine, storage);
NativeStructureOwnershipRecord record = record(
"test:close_durable", 20, -21, 105L, true);
"test:close_durable", 20, -21, 105L);
state.record(record);
state.close();
@@ -338,7 +340,7 @@ public class NativeStructureOwnershipStoreTest {
NativeStructureOwnershipStore.State state =
new NativeStructureOwnershipStore.State(engine, storage);
NativeStructureOwnershipRecord record = record(
"test:close_retry", -22, 23, 106L, false);
"test:close_retry", -22, 23, 106L);
state.record(record);
assertThrows(IllegalStateException.class, state::close);
@@ -362,14 +364,19 @@ public class NativeStructureOwnershipStoreTest {
}
private static NativeStructureOwnershipRecord record(String key, int originX, int originZ,
long placementIdentity,
boolean clearVegetation) {
return record(key, originX, originZ, placementIdentity, clearVegetation, 8);
long placementIdentity) {
return record(key, originX, originZ, placementIdentity, new IrisStructureTerrain(), 8);
}
private static NativeStructureOwnershipRecord record(String key, int originX, int originZ,
long placementIdentity,
boolean clearVegetation,
int referenceRadius) {
return record(key, originX, originZ, placementIdentity, new IrisStructureTerrain(), referenceRadius);
}
private static NativeStructureOwnershipRecord record(String key, int originX, int originZ,
long placementIdentity,
IrisStructureTerrain terrain,
int referenceRadius) {
return new NativeStructureOwnershipRecord(
NativeStructureOwnershipRecord.CURRENT_SCHEMA,
@@ -396,9 +403,8 @@ public class NativeStructureOwnershipStoreTest {
0,
null,
false,
clearVegetation,
null,
new IrisStructureTerrain()
terrain
))
);
}
@@ -82,7 +82,6 @@ public class NativeStructurePlacementPlannerTest {
IrisNativeStructureDecision decision = NativeStructurePlacementPlanner.decisionFor(plan);
assertEquals(NativeStructureGenerationStatus.GENERATE_NATIVE, decision.status());
assertEquals(false, decision.clearVegetation());
assertSame(terrain, decision.terrain());
}
@@ -0,0 +1,219 @@
package art.arcane.iris.engine.mantle.components;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class CarveOrphanSweepTest {
private static final int WORLD_HEIGHT = 64;
private static final int WORLD_FLOOR_Y = 0;
private static final int WORLD_CEILING_Y = WORLD_HEIGHT - 1;
private static final int SURFACE_Y = 40;
private static final int SURFACE_BREAK_DEPTH = 18;
private static final int BAND_FLOOR_Y = SURFACE_Y - SURFACE_BREAK_DEPTH - 4;
@Test
public void interiorClumpsOfOneTwoAndFiveCellsAreMarkedCarved() {
Fixture fixture = new Fixture();
fixture.carveBox(1, 14, 25, 35, 1, 14);
fixture.uncarve(5, 30, 5);
fixture.uncarve(8, 30, 8);
fixture.uncarve(9, 30, 8);
fixture.uncarve(5, 33, 10);
fixture.uncarve(4, 33, 10);
fixture.uncarve(6, 33, 10);
fixture.uncarve(5, 33, 9);
fixture.uncarve(5, 33, 11);
int marked = fixture.sweep();
assertEquals(8, marked);
assertTrue(fixture.wasMarked(5, 30, 5));
assertTrue(fixture.wasMarked(8, 30, 8));
assertTrue(fixture.wasMarked(9, 30, 8));
assertTrue(fixture.wasMarked(5, 33, 10));
assertTrue(fixture.wasMarked(4, 33, 10));
assertTrue(fixture.wasMarked(6, 33, 10));
assertTrue(fixture.wasMarked(5, 33, 9));
assertTrue(fixture.wasMarked(5, 33, 11));
}
@Test
public void stalactiteHangingFromSurfaceCrustIsKept() {
Fixture fixture = new Fixture();
fixture.carveBox(1, 14, 25, 35, 1, 14);
fixture.uncarve(7, 35, 7);
fixture.uncarve(7, 34, 7);
fixture.uncarve(7, 33, 7);
int marked = fixture.sweep();
assertEquals(0, marked);
assertFalse(fixture.wasMarked(7, 35, 7));
assertFalse(fixture.wasMarked(7, 34, 7));
assertFalse(fixture.wasMarked(7, 33, 7));
}
@Test
public void componentTouchingSolidBelowBandFloorIsKept() {
Fixture fixture = new Fixture();
fixture.carveBox(0, 15, BAND_FLOOR_Y, 35, 0, 15);
fixture.uncarve(7, BAND_FLOOR_Y, 7);
int marked = fixture.sweep();
assertEquals(0, marked);
assertFalse(fixture.wasMarked(7, BAND_FLOOR_Y, 7));
}
@Test
public void componentContainingSeamCellIsKept() {
Fixture fixture = new Fixture();
fixture.carveBox(0, 15, 25, 35, 0, 15);
fixture.uncarve(0, 30, 7);
fixture.uncarve(15, 31, 9);
fixture.uncarve(6, 32, 0);
fixture.uncarve(6, 33, 15);
int marked = fixture.sweep();
assertEquals(0, marked);
assertFalse(fixture.wasMarked(0, 30, 7));
assertFalse(fixture.wasMarked(15, 31, 9));
assertFalse(fixture.wasMarked(6, 32, 0));
assertFalse(fixture.wasMarked(6, 33, 15));
}
@Test
public void componentLargerThanSixteenCellsIsKept() {
Fixture fixture = new Fixture();
fixture.carveBox(1, 14, 25, 35, 1, 14);
for (int localX = 3; localX <= 5; localX++) {
for (int localZ = 3; localZ <= 5; localZ++) {
fixture.uncarve(localX, 27, localZ);
fixture.uncarve(localX, 28, localZ);
}
}
int marked = fixture.sweep();
assertEquals(0, marked);
assertFalse(fixture.wasMarked(4, 27, 4));
assertFalse(fixture.wasMarked(4, 28, 4));
}
@Test
public void componentOfExactlySixteenCellsIsMarkedCarved() {
Fixture fixture = new Fixture();
fixture.carveBox(1, 14, 25, 35, 1, 14);
for (int localX = 3; localX <= 6; localX++) {
for (int localZ = 3; localZ <= 6; localZ++) {
fixture.uncarve(localX, 30, localZ);
}
}
int marked = fixture.sweep();
assertEquals(16, marked);
assertTrue(fixture.wasMarked(3, 30, 3));
assertTrue(fixture.wasMarked(6, 30, 6));
}
@Test
public void sweepIsDeterministicAndIdempotent() {
Fixture first = new Fixture();
first.carveBox(1, 14, 25, 35, 1, 14);
first.uncarve(5, 30, 5);
first.uncarve(8, 30, 8);
first.uncarve(9, 30, 8);
Fixture second = new Fixture();
second.carveBox(1, 14, 25, 35, 1, 14);
second.uncarve(5, 30, 5);
second.uncarve(8, 30, 8);
second.uncarve(9, 30, 8);
int firstMarked = first.sweep();
int secondMarked = second.sweep();
assertEquals(firstMarked, secondMarked);
assertEquals(first.marks(), second.marks());
first.clearMarks();
int rerun = first.sweep();
assertEquals(0, rerun);
assertTrue(first.marks().isEmpty());
}
@Test
public void chunkWithoutCarveMarkersInBandDoesNothing() {
Fixture fixture = new Fixture();
int marked = fixture.sweep();
assertEquals(0, marked);
assertTrue(fixture.marks().isEmpty());
}
private static final class Fixture implements CarveOrphanSweep.CarveAccess {
private final boolean[] carved = new boolean[16 * WORLD_HEIGHT * 16];
private final int[] surfaceHeights = new int[256];
private final List<Integer> marks = new ArrayList<>();
private Fixture() {
Arrays.fill(surfaceHeights, SURFACE_Y);
}
private int sweep() {
return CarveOrphanSweep.sweep(surfaceHeights, SURFACE_BREAK_DEPTH, WORLD_FLOOR_Y, WORLD_CEILING_Y, this);
}
private void carveBox(int minX, int maxX, int minY, int maxY, int minZ, int maxZ) {
for (int localX = minX; localX <= maxX; localX++) {
for (int y = minY; y <= maxY; y++) {
for (int localZ = minZ; localZ <= maxZ; localZ++) {
carved[index(localX, y, localZ)] = true;
}
}
}
}
private void uncarve(int localX, int y, int localZ) {
carved[index(localX, y, localZ)] = false;
}
private boolean wasMarked(int localX, int y, int localZ) {
return marks.contains(index(localX, y, localZ));
}
private List<Integer> marks() {
return marks;
}
private void clearMarks() {
marks.clear();
}
private static int index(int localX, int y, int localZ) {
return (y * 256) + (localX * 16) + localZ;
}
@Override
public boolean isCarved(int localX, int y, int localZ) {
return carved[index(localX, y, localZ)];
}
@Override
public void markCarved(int localX, int y, int localZ) {
carved[index(localX, y, localZ)] = true;
marks.add(index(localX, y, localZ));
}
}
}
@@ -673,8 +673,6 @@ public class IrisCaveCarver3DNearParityTest {
profile.setDensityThreshold(new IrisStyledRange(1D, 1D, new IrisGeneratorStyle(NoiseStyle.FLAT)));
profile.setThresholdBias(0D);
profile.setSampleStep(1);
profile.setMinCarveCells(0);
profile.setRecoveryThresholdBoost(0D);
profile.setSurfaceClearance(5);
profile.setAllowSurfaceBreak(true);
profile.setSurfaceBreakNoiseThreshold(0.16D);
@@ -159,9 +159,7 @@ public class IrisImportedStructureControlTest {
@Test
public void postprocessingDefaultsAreDisabled() {
IrisImportedStructureControl control = new IrisImportedStructureControl();
assertFalse(control.resolve("minecraft:woodland_mansion", false).clearVegetation());
assertNull(control.resolve("minecraft:village_plains", false).stilt());
assertFalse(control.resolve(null, false).clearVegetation());
assertNull(control.resolve(null, false).stilt());
assertFalse(control.resolve("minecraft:mineshaft_mesa", true).preserveSourceY());
}
@@ -253,24 +251,21 @@ public class IrisImportedStructureControlTest {
IrisStructureStiltSettings stilt = new IrisStructureStiltSettings();
IrisVanillaStructureAdjustment adjustment = new IrisVanillaStructureAdjustment()
.setMatch(keys("minecraft:village"))
.setClearVegetation(true)
.setStilt(stilt);
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setAdjustments(new KList<IrisVanillaStructureAdjustment>().qadd(adjustment));
assertTrue(control.resolve("minecraft:village_plains", false).clearVegetation());
assertSame(stilt, control.resolve("minecraft:village_taiga", false).stilt());
assertFalse(control.resolve("minecraft:woodland_mansion", false).clearVegetation());
assertNull(control.resolve("minecraft:woodland_mansion", false).stilt());
assertNull(control.resolve("minecraft:stronghold", true).stilt());
}
@Test
public void multipleMatchesMergeVegetationAndUseLastConfiguredStilt() {
public void multipleMatchesUseTheLastConfiguredStilt() {
IrisStructureStiltSettings broadStilt = new IrisStructureStiltSettings().setMaxDepth(32);
IrisStructureStiltSettings specificStilt = new IrisStructureStiltSettings().setMaxDepth(96);
IrisVanillaStructureAdjustment broad = new IrisVanillaStructureAdjustment()
.setMatch(keys("minecraft:village"))
.setClearVegetation(true)
.setStilt(broadStilt);
IrisVanillaStructureAdjustment exactWithoutStilt = new IrisVanillaStructureAdjustment()
.setMatch(keys("minecraft:village_plains"));
@@ -284,7 +279,6 @@ public class IrisImportedStructureControlTest {
IrisImportedStructureControl control = new IrisImportedStructureControl().setAdjustments(adjustments);
IrisNativeStructureDecision plains = control.resolve("minecraft:village_plains", false);
assertTrue(plains.clearVegetation());
assertSame(specificStilt, plains.stilt());
assertEquals(96, plains.stilt().getMaxDepth());
assertSame(broadStilt, control.resolve("minecraft:village_desert", false).stilt());
@@ -0,0 +1,47 @@
package art.arcane.iris.spi;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
public class IrisLoggingTest {
@Before
public void resetBinding() {
IrisPlatforms.unbind();
}
@After
public void clearBinding() {
IrisPlatforms.unbind();
}
@Test
public void contextualReportPrintsFullStacktraceWithBoundPlatform() {
IrisPlatform platform = mock(IrisPlatform.class);
IrisPlatforms.bind(platform);
IllegalStateException failure = new IllegalStateException("outer", new IllegalArgumentException("inner"));
ByteArrayOutputStream output = new ByteArrayOutputStream();
PrintStream originalErr = System.err;
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
try {
IrisLogging.reportError("Runtime world creation failed.", failure);
} finally {
System.setErr(originalErr);
}
verify(platform).log(LogLevel.ERROR, "Runtime world creation failed.");
verify(platform).reportError(failure);
String text = output.toString(StandardCharsets.UTF_8);
assertTrue(text.contains("IllegalStateException"));
assertTrue(text.contains("IllegalArgumentException"));
assertTrue(text.contains("inner"));
}
}
@@ -250,7 +250,7 @@ public class StructureHandlerTest {
}
private static IrisNativeStructureDecision decision(NativeStructureGenerationStatus status) {
return new IrisNativeStructureDecision(status, 0, null, false, false, null, null);
return new IrisNativeStructureDecision(status, 0, null, false, null, null);
}
private static IrisStructurePlacement nativePlacement(String key, StructureDistribution distribution,
@@ -98,6 +98,7 @@ public final class IrisLogging {
}
reportError(cause);
cause.printStackTrace(System.err);
}
/**