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,
List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
IntBinaryOperator surfaceHeight) {
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()));
}
}