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