Keep native structures out of static object cells

Vanilla structure and feature placement now runs through the bounded
worldgen access with a protected-position predicate, so a jigsaw piece
or an imported feature can read a static object's blocks but never
overwrite them, and never schedules a tick or block entity there.
Dimensions without static objects keep the plain level as before.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-03 05:02:53 -04:00
1 parent 0a791bb8bf
commit e92c0efe09
10 files changed
+129 -36

No files matched your search

@@ -7,6 +7,7 @@ import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDecorationStep;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisImportedFeatureControl;
import art.arcane.iris.engine.object.IrisStaticObjectLayer;
import art.arcane.iris.spi.IrisLogging;
import it.unimi.dsi.fastutil.ints.IntArraySet;
import it.unimi.dsi.fastutil.ints.IntSet;
@@ -26,6 +27,7 @@ import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
@@ -287,13 +289,20 @@ final class ImportedFeatureStage {
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table);
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
int staticMinY = engine.getMinHeight();
WorldGenLevel placementLevel = staticObjects.isEmpty() ? level : NativeStructureWorldgenAccess.create(
level, centerPos,
(x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z),
(x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z),
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ()));
try {
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
if (!table.control().shouldGenerateStep(IrisDecorationStep.byOrdinal(stepIndex))) {
continue;
}
placeStep(level, table, steps.get(stepIndex), featureRegistry, chunkBiomes, owner,
placeStep(placementLevel, table, steps.get(stepIndex), featureRegistry, chunkBiomes, owner,
random, decorationSeed, origin, stepIndex);
}
} catch (Throwable error) {
@@ -13,6 +13,7 @@ import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisStaticObjectLayer;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
import art.arcane.iris.nativegen.NativeStructureStartInjector;
import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope;
@@ -873,9 +874,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
"heightmap priming", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
int staticMinY = engine.getMinHeight();
WorldGenLevel boundedWorld = staticObjects.isEmpty() ? world : NativeStructureWorldgenAccess.create(
world, chunkPos, worldgenSurfaceHeight(), worldgenFloorHeight(),
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ()));
try {
NativeStructureVegetationClearer.clearIntersectingVegetation(
world, chunk, area, vegetationTargets);
boundedWorld, chunk, area, vegetationTargets);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
@@ -884,7 +890,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
NativeStructureSurfaceFitter.VacuumFoundationPlan vacuumFoundationPlan;
try {
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
world, area, terrainTargets,
boundedWorld, area, terrainTargets,
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
@@ -893,7 +899,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
try {
NativeStructurePostProcessor.prepareTerrain(
world, area, terrainTargets, this::resolvePaletteBlock);
boundedWorld, area, terrainTargets, this::resolvePaletteBlock);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain preparation", nativeStructureBatchContext(placementGroups),
@@ -903,7 +909,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
random.setFeatureSeed(decoSeed, group.featureIndex(), group.step());
try {
for (NativePlacement placement : group.placements()) {
placeVanillaStructure(world, structureManager, random, area, chunkPos,
placeVanillaStructure(boundedWorld, structureManager, random, area, chunkPos,
group.structureId(), placement.start(), placement.decision());
}
} catch (Throwable error) {
@@ -913,7 +919,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
try {
NativeStructureSurfaceFitter.repairVacuumFoundations(
world, area, vacuumFoundationPlan);
boundedWorld, area, vacuumFoundationPlan);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"foundation repair", nativeStructureBatchContext(placementGroups),
@@ -938,8 +944,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager, WorldgenRandom random,
BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
IrisNativeStructureDecision decision) {
WorldGenLevel boundedWorld = NativeStructureWorldgenAccess.create(
world, chunkPos, worldgenSurfaceHeight(), worldgenFloorHeight());
WorldGenLevel boundedWorld = world instanceof NativeStructureWorldgenAccess ? world : NativeStructureWorldgenAccess.create(
world, chunkPos, worldgenSurfaceHeight(), worldgenFloorHeight(), position -> false);
world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
try {
NativeStructurePostProcessor.place(boundedWorld, structureManager, this, random, area, chunkPos,
@@ -64,6 +64,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
private final ChunkPos generationCenter;
private final IntBinaryOperator surfaceFirstFreeY;
private final IntBinaryOperator floorFirstFreeY;
private final Predicate<BlockPos> protectedPosition;
private final Holder<Biome> fallbackBiome;
private final BiomeManager biomeManager;
private final AABB generationBounds;
@@ -80,6 +81,8 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
this.floorFirstFreeY = Objects.requireNonNull(
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
this.protectedPosition = Objects.requireNonNull(
boundary.protectedPosition(), "World access requires a protected-position predicate");
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
this.fallbackBiome = delegate.getBiome(biomeSample);
this.biomeManager = delegate.getBiomeManager().withDifferentSource(this);
@@ -99,9 +102,10 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) {
IntBinaryOperator floorFirstFreeY,
Predicate<BlockPos> protectedPosition) {
return new NativeStructureWorldgenAccess(delegate, new Boundary(
generationCenter, surfaceFirstFreeY, floorFirstFreeY));
generationCenter, surfaceFirstFreeY, floorFirstFreeY, protectedPosition));
}
@Override
@@ -359,13 +363,13 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
@Override
public BlockEntity getBlockEntity(BlockPos position) {
return isReadable(position) ? delegate.getBlockEntity(position) : null;
return isWritable(position) ? delegate.getBlockEntity(position) : null;
}
@Override
public <T extends BlockEntity> Optional<T> getBlockEntity(
BlockPos position, BlockEntityType<T> type) {
return isReadable(position) ? delegate.getBlockEntity(position, type) : Optional.empty();
return isWritable(position) ? delegate.getBlockEntity(position, type) : Optional.empty();
}
@Override
@@ -453,7 +457,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
}
private boolean isWritable(BlockPos position) {
return isReadable(position);
return isReadable(position) && !protectedPosition.test(position);
}
private boolean isWritableNeighbourhood(BlockPos position) {
@@ -544,7 +548,8 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
private record Boundary(ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) {
IntBinaryOperator floorFirstFreeY,
Predicate<BlockPos> protectedPosition) {
}
private static final class BoundedTickAccess<T> implements LevelTickAccess<T> {
@@ -135,6 +135,34 @@ public class NativeStructureWorldgenAccessTest {
assertEquals(1, recording.blockTicks.count());
}
@Test
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
RecordingDelegate recording = new RecordingDelegate();
BlockPos position = generationCenter().getMiddleBlockPosition(70);
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y, position::equals);
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
assertFalse(access.ensureCanWrite(position));
assertFalse(access.setBlock(position, Blocks.STONE.defaultBlockState(), 2));
assertFalse(access.removeBlock(position, false));
assertFalse(access.destroyBlock(position, false));
assertNull(access.getBlockEntity(position));
assertTrue(access.getBlockEntity(position, null).isEmpty());
access.getBlockTicks().schedule(new ScheduledTick<>(Blocks.DIRT, position, 1L, 0L));
access.updateNeighborsAt(position.east(), Blocks.DIRT);
access.neighborShapeChanged(
Direction.EAST, position, position.east(), Blocks.DIRT.defaultBlockState(), 2, 512);
assertEquals(1, recording.terrainReads);
assertEquals(0, recording.mutations);
assertEquals(0, recording.blockTicks.count());
assertTrue(access.setBlock(position.above(), Blocks.STONE.defaultBlockState(), 2));
assertEquals(1, recording.mutations);
}
@Test
public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() {
RecordingDelegate recording = new RecordingDelegate();
@@ -183,7 +211,7 @@ public class NativeStructureWorldgenAccessTest {
return NativeStructureWorldgenAccess.create(
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y);
(x, z) -> FLOOR_FIRST_FREE_Y, position -> false);
}
private static ChunkPos generationCenter() {
@@ -226,7 +226,7 @@ public class NativeStructurePostProcessorEncaseTest {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int prepareTerrain = source.indexOf("NativeStructurePostProcessor.prepareTerrain(");
int placementLoop = source.indexOf("for (NativePlacementGroup group : placementGroups)", prepareTerrain);
int placement = source.indexOf("placeVanillaStructure(world, structureManager, random", placementLoop);
int placement = source.indexOf("placeVanillaStructure(boundedWorld, structureManager, random", placementLoop);
assertTrue(prepareTerrain >= 0);
assertTrue(placementLoop > prepareTerrain);