mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
d
This commit is contained in:
+12
-7
@@ -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) {
|
||||
|
||||
+3
@@ -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"));
|
||||
}
|
||||
|
||||
|
||||
+482
-9
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+215
-30
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+476
-16
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-120
@@ -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) {
|
||||
}
|
||||
|
||||
|
||||
+147
@@ -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) {
|
||||
}
|
||||
}
|
||||
-1
@@ -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;
|
||||
}
|
||||
|
||||
+32
-35
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
+12
-7
@@ -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) {
|
||||
|
||||
+3
@@ -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
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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));
|
||||
|
||||
+4
-4
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user