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.IrisDecorationStep;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisImportedFeatureControl; import art.arcane.iris.engine.object.IrisImportedFeatureControl;
import art.arcane.iris.engine.object.IrisStaticObjectLayer;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import it.unimi.dsi.fastutil.ints.IntArraySet; import it.unimi.dsi.fastutil.ints.IntArraySet;
import it.unimi.dsi.fastutil.ints.IntSet; 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.LevelChunkSection;
import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.RandomSupport; 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.WorldgenRandom;
import net.minecraft.world.level.levelgen.XoroshiroRandomSource; import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.placement.PlacedFeature;
@@ -287,13 +289,20 @@ final class ImportedFeatureStage {
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ()); long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table); 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 { try {
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) { for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
if (!table.control().shouldGenerateStep(IrisDecorationStep.byOrdinal(stepIndex))) { if (!table.control().shouldGenerateStep(IrisDecorationStep.byOrdinal(stepIndex))) {
continue; continue;
} }
placeStep(level, table, steps.get(stepIndex), featureRegistry, chunkBiomes, owner, placeStep(placementLevel, table, steps.get(stepIndex), featureRegistry, chunkBiomes, owner,
random, decorationSeed, origin, stepIndex); random, decorationSeed, origin, stepIndex);
} }
} catch (Throwable error) { } 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.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisMaterialPalette; import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisNativeStructureDecision; 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.NativeStructureGenerationException;
import art.arcane.iris.nativegen.NativeStructureStartInjector; import art.arcane.iris.nativegen.NativeStructureStartInjector;
import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope; import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope;
@@ -873,9 +874,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
"heightmap priming", nativeStructureBatchContext(placementGroups), "heightmap priming", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error); 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 { try {
NativeStructureVegetationClearer.clearIntersectingVegetation( NativeStructureVegetationClearer.clearIntersectingVegetation(
world, chunk, area, vegetationTargets); boundedWorld, chunk, area, vegetationTargets);
} catch (Throwable error) { } catch (Throwable error) {
throw NativeStructureGenerationException.failure( throw NativeStructureGenerationException.failure(
"vegetation cleanup", nativeStructureBatchContext(placementGroups), "vegetation cleanup", nativeStructureBatchContext(placementGroups),
@@ -884,7 +890,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
NativeStructureSurfaceFitter.VacuumFoundationPlan vacuumFoundationPlan; NativeStructureSurfaceFitter.VacuumFoundationPlan vacuumFoundationPlan;
try { try {
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures( vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
world, area, terrainTargets, boundedWorld, 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) {
throw NativeStructureGenerationException.failure( throw NativeStructureGenerationException.failure(
@@ -893,7 +899,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
} }
try { try {
NativeStructurePostProcessor.prepareTerrain( NativeStructurePostProcessor.prepareTerrain(
world, area, terrainTargets, this::resolvePaletteBlock); boundedWorld, area, terrainTargets, this::resolvePaletteBlock);
} catch (Throwable error) { } catch (Throwable error) {
throw NativeStructureGenerationException.failure( throw NativeStructureGenerationException.failure(
"terrain preparation", nativeStructureBatchContext(placementGroups), "terrain preparation", nativeStructureBatchContext(placementGroups),
@@ -903,7 +909,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
random.setFeatureSeed(decoSeed, group.featureIndex(), group.step()); random.setFeatureSeed(decoSeed, group.featureIndex(), group.step());
try { try {
for (NativePlacement placement : group.placements()) { for (NativePlacement placement : group.placements()) {
placeVanillaStructure(world, structureManager, random, area, chunkPos, placeVanillaStructure(boundedWorld, structureManager, random, area, chunkPos,
group.structureId(), placement.start(), placement.decision()); group.structureId(), placement.start(), placement.decision());
} }
} catch (Throwable error) { } catch (Throwable error) {
@@ -913,7 +919,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
} }
try { try {
NativeStructureSurfaceFitter.repairVacuumFoundations( NativeStructureSurfaceFitter.repairVacuumFoundations(
world, area, vacuumFoundationPlan); boundedWorld, area, vacuumFoundationPlan);
} catch (Throwable error) { } catch (Throwable error) {
throw NativeStructureGenerationException.failure( throw NativeStructureGenerationException.failure(
"foundation repair", nativeStructureBatchContext(placementGroups), "foundation repair", nativeStructureBatchContext(placementGroups),
@@ -938,8 +944,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager, WorldgenRandom random, private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager, WorldgenRandom random,
BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start, BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
IrisNativeStructureDecision decision) { IrisNativeStructureDecision decision) {
WorldGenLevel boundedWorld = NativeStructureWorldgenAccess.create( WorldGenLevel boundedWorld = world instanceof NativeStructureWorldgenAccess ? world : NativeStructureWorldgenAccess.create(
world, chunkPos, worldgenSurfaceHeight(), worldgenFloorHeight()); world, chunkPos, worldgenSurfaceHeight(), worldgenFloorHeight(), position -> false);
world.setCurrentlyGenerating(() -> "Iris native structure " + structureId); world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
try { try {
NativeStructurePostProcessor.place(boundedWorld, structureManager, this, random, area, chunkPos, NativeStructurePostProcessor.place(boundedWorld, structureManager, this, random, area, chunkPos,
@@ -64,6 +64,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
private final ChunkPos generationCenter; private final ChunkPos generationCenter;
private final IntBinaryOperator surfaceFirstFreeY; private final IntBinaryOperator surfaceFirstFreeY;
private final IntBinaryOperator floorFirstFreeY; private final IntBinaryOperator floorFirstFreeY;
private final Predicate<BlockPos> protectedPosition;
private final Holder<Biome> fallbackBiome; private final Holder<Biome> fallbackBiome;
private final BiomeManager biomeManager; private final BiomeManager biomeManager;
private final AABB generationBounds; private final AABB generationBounds;
@@ -80,6 +81,8 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver"); boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
this.floorFirstFreeY = Objects.requireNonNull( this.floorFirstFreeY = Objects.requireNonNull(
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver"); 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()); BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
this.fallbackBiome = delegate.getBiome(biomeSample); this.fallbackBiome = delegate.getBiome(biomeSample);
this.biomeManager = delegate.getBiomeManager().withDifferentSource(this); this.biomeManager = delegate.getBiomeManager().withDifferentSource(this);
@@ -99,9 +102,10 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter, static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY, IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) { IntBinaryOperator floorFirstFreeY,
Predicate<BlockPos> protectedPosition) {
return new NativeStructureWorldgenAccess(delegate, new Boundary( return new NativeStructureWorldgenAccess(delegate, new Boundary(
generationCenter, surfaceFirstFreeY, floorFirstFreeY)); generationCenter, surfaceFirstFreeY, floorFirstFreeY, protectedPosition));
} }
@Override @Override
@@ -359,13 +363,13 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
@Override @Override
public BlockEntity getBlockEntity(BlockPos position) { public BlockEntity getBlockEntity(BlockPos position) {
return isReadable(position) ? delegate.getBlockEntity(position) : null; return isWritable(position) ? delegate.getBlockEntity(position) : null;
} }
@Override @Override
public <T extends BlockEntity> Optional<T> getBlockEntity( public <T extends BlockEntity> Optional<T> getBlockEntity(
BlockPos position, BlockEntityType<T> type) { BlockPos position, BlockEntityType<T> type) {
return isReadable(position) ? delegate.getBlockEntity(position, type) : Optional.empty(); return isWritable(position) ? delegate.getBlockEntity(position, type) : Optional.empty();
} }
@Override @Override
@@ -453,7 +457,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
} }
private boolean isWritable(BlockPos position) { private boolean isWritable(BlockPos position) {
return isReadable(position); return isReadable(position) && !protectedPosition.test(position);
} }
private boolean isWritableNeighbourhood(BlockPos position) { private boolean isWritableNeighbourhood(BlockPos position) {
@@ -544,7 +548,8 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
private record Boundary(ChunkPos generationCenter, private record Boundary(ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY, IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) { IntBinaryOperator floorFirstFreeY,
Predicate<BlockPos> protectedPosition) {
} }
private static final class BoundedTickAccess<T> implements LevelTickAccess<T> { private static final class BoundedTickAccess<T> implements LevelTickAccess<T> {
@@ -135,6 +135,34 @@ public class NativeStructureWorldgenAccessTest {
assertEquals(1, recording.blockTicks.count()); 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 @Test
public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() { public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() {
RecordingDelegate recording = new RecordingDelegate(); RecordingDelegate recording = new RecordingDelegate();
@@ -183,7 +211,7 @@ public class NativeStructureWorldgenAccessTest {
return NativeStructureWorldgenAccess.create( return NativeStructureWorldgenAccess.create(
recording.world(), generationCenter(), recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y, (x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y); (x, z) -> FLOOR_FIRST_FREE_Y, position -> false);
} }
private static ChunkPos generationCenter() { 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"); String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int prepareTerrain = source.indexOf("NativeStructurePostProcessor.prepareTerrain("); int prepareTerrain = source.indexOf("NativeStructurePostProcessor.prepareTerrain(");
int placementLoop = source.indexOf("for (NativePlacementGroup group : placementGroups)", 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(prepareTerrain >= 0);
assertTrue(placementLoop > prepareTerrain); assertTrue(placementLoop > prepareTerrain);
@@ -25,6 +25,7 @@ import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom; import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDecorationStep; import art.arcane.iris.engine.object.IrisDecorationStep;
import art.arcane.iris.engine.object.IrisImportedFeatureControl; import art.arcane.iris.engine.object.IrisImportedFeatureControl;
import art.arcane.iris.engine.object.IrisStaticObjectLayer;
import art.arcane.iris.util.project.context.IrisContext; import art.arcane.iris.util.project.context.IrisContext;
import it.unimi.dsi.fastutil.ints.IntArraySet; import it.unimi.dsi.fastutil.ints.IntArraySet;
import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.ints.IntSet;
@@ -43,6 +44,7 @@ import net.minecraft.world.level.biome.FeatureSorter;
import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.levelgen.RandomSupport; 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.WorldgenRandom;
import net.minecraft.world.level.levelgen.XoroshiroRandomSource; import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.placement.PlacedFeature;
@@ -321,13 +323,20 @@ final class ModdedImportedFeatureStage {
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ()); long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table); Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table);
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
int staticMinY = engine.getMinHeight();
WorldGenLevel placementLevel = staticObjects.isEmpty() ? level : ModdedNativeStructureWorldgenAccess.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 { try {
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) { for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
if (!table.control().shouldGenerateStep(IrisDecorationStep.byOrdinal(stepIndex))) { if (!table.control().shouldGenerateStep(IrisDecorationStep.byOrdinal(stepIndex))) {
continue; continue;
} }
placeStep(level, table, steps.get(stepIndex), featureRegistry, chunkBiomes, owner, placeStep(placementLevel, table, steps.get(stepIndex), featureRegistry, chunkBiomes, owner,
random, decorationSeed, origin, stepIndex); random, decorationSeed, origin, stepIndex);
} }
} catch (Throwable error) { } catch (Throwable error) {
@@ -25,6 +25,7 @@ import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
import art.arcane.iris.engine.framework.NativeStructureStartPlan; import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.object.IrisMaterialPalette; import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisNativeStructureDecision; 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.NativeStructureGenerationException;
import art.arcane.iris.nativegen.NativeStructureLocatePersistence; import art.arcane.iris.nativegen.NativeStructureLocatePersistence;
import art.arcane.iris.nativegen.NativeStructureLocateResults; import art.arcane.iris.nativegen.NativeStructureLocateResults;
@@ -365,9 +366,14 @@ final class ModdedNativeStructureStage {
"heightmap priming", nativeStructureBatchContext(placementGroups), "heightmap priming", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error); chunkPos.x(), chunkPos.z(), error);
} }
IrisStaticObjectLayer staticObjects = current.getDimension().getStaticObjectLayer(current.getData());
int staticMinY = current.getMinHeight();
WorldGenLevel boundedWorld = staticObjects.isEmpty() ? world : ModdedNativeStructureWorldgenAccess.create(
world, chunkPos, worldgenSurfaceHeight(current, world.getMinY()), worldgenFloorHeight(current, world.getMinY()),
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ()));
try { try {
NativeStructureVegetationClearer.clearIntersectingVegetation( NativeStructureVegetationClearer.clearIntersectingVegetation(
world, chunk, area, vegetationTargets); boundedWorld, chunk, area, vegetationTargets);
} catch (Throwable error) { } catch (Throwable error) {
throw NativeStructureGenerationException.failure( throw NativeStructureGenerationException.failure(
"vegetation cleanup", nativeStructureBatchContext(placementGroups), "vegetation cleanup", nativeStructureBatchContext(placementGroups),
@@ -376,7 +382,7 @@ final class ModdedNativeStructureStage {
NativeStructureSurfaceFitter.VacuumFoundationPlan vacuumFoundationPlan; NativeStructureSurfaceFitter.VacuumFoundationPlan vacuumFoundationPlan;
try { try {
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures( vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
world, area, terrainTargets, boundedWorld, 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) {
throw NativeStructureGenerationException.failure( throw NativeStructureGenerationException.failure(
@@ -385,7 +391,7 @@ final class ModdedNativeStructureStage {
} }
try { try {
NativeStructurePostProcessor.prepareTerrain( NativeStructurePostProcessor.prepareTerrain(
world, area, terrainTargets, this::resolvePaletteBlock); boundedWorld, area, terrainTargets, this::resolvePaletteBlock);
} catch (Throwable error) { } catch (Throwable error) {
throw NativeStructureGenerationException.failure( throw NativeStructureGenerationException.failure(
"terrain preparation", nativeStructureBatchContext(placementGroups), "terrain preparation", nativeStructureBatchContext(placementGroups),
@@ -395,7 +401,7 @@ final class ModdedNativeStructureStage {
random.setFeatureSeed(decorationSeed, group.featureIndex(), group.step()); random.setFeatureSeed(decorationSeed, group.featureIndex(), group.step());
try { try {
for (NativePlacement placement : group.placements()) { for (NativePlacement placement : group.placements()) {
placeVanillaStructure(world, structureManager, random, area, chunkPos, placeVanillaStructure(boundedWorld, structureManager, random, area, chunkPos,
group.structureId(), placement.start(), placement.decision()); group.structureId(), placement.start(), placement.decision());
} }
} catch (Throwable error) { } catch (Throwable error) {
@@ -405,7 +411,7 @@ final class ModdedNativeStructureStage {
} }
try { try {
NativeStructureSurfaceFitter.repairVacuumFoundations( NativeStructureSurfaceFitter.repairVacuumFoundations(
world, area, vacuumFoundationPlan); boundedWorld, area, vacuumFoundationPlan);
} catch (Throwable error) { } catch (Throwable error) {
throw NativeStructureGenerationException.failure( throw NativeStructureGenerationException.failure(
"foundation repair", nativeStructureBatchContext(placementGroups), "foundation repair", nativeStructureBatchContext(placementGroups),
@@ -436,11 +442,8 @@ final class ModdedNativeStructureStage {
String structureId, StructureStart start, String structureId, StructureStart start,
IrisNativeStructureDecision decision) { IrisNativeStructureDecision decision) {
Engine current = generator.engine(); Engine current = generator.engine();
int runtimeMinY = world.getMinY(); WorldGenLevel boundedWorld = world instanceof ModdedNativeStructureWorldgenAccess ? world : ModdedNativeStructureWorldgenAccess.create(
WorldGenLevel boundedWorld = ModdedNativeStructureWorldgenAccess.create( world, chunkPos, worldgenSurfaceHeight(current, world.getMinY()), worldgenFloorHeight(current, world.getMinY()), position -> false);
world, chunkPos,
worldgenSurfaceHeight(current, runtimeMinY),
worldgenFloorHeight(current, runtimeMinY));
world.setCurrentlyGenerating(() -> "Iris native structure " + structureId); world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
try { try {
NativeStructurePostProcessor.place( NativeStructurePostProcessor.place(
@@ -63,6 +63,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
private final ChunkPos generationCenter; private final ChunkPos generationCenter;
private final IntBinaryOperator surfaceFirstFreeY; private final IntBinaryOperator surfaceFirstFreeY;
private final IntBinaryOperator floorFirstFreeY; private final IntBinaryOperator floorFirstFreeY;
private final Predicate<BlockPos> protectedPosition;
private final Holder<Biome> fallbackBiome; private final Holder<Biome> fallbackBiome;
private final BiomeManager biomeManager; private final BiomeManager biomeManager;
private final AABB generationBounds; private final AABB generationBounds;
@@ -79,6 +80,8 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver"); boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
this.floorFirstFreeY = Objects.requireNonNull( this.floorFirstFreeY = Objects.requireNonNull(
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver"); 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()); BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
this.fallbackBiome = delegate.getBiome(biomeSample); this.fallbackBiome = delegate.getBiome(biomeSample);
this.biomeManager = delegate.getBiomeManager().withDifferentSource(this); this.biomeManager = delegate.getBiomeManager().withDifferentSource(this);
@@ -98,9 +101,10 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
static ModdedNativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter, static ModdedNativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY, IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) { IntBinaryOperator floorFirstFreeY,
Predicate<BlockPos> protectedPosition) {
return new ModdedNativeStructureWorldgenAccess(delegate, new Boundary( return new ModdedNativeStructureWorldgenAccess(delegate, new Boundary(
generationCenter, surfaceFirstFreeY, floorFirstFreeY)); generationCenter, surfaceFirstFreeY, floorFirstFreeY, protectedPosition));
} }
@Override @Override
@@ -350,13 +354,13 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
@Override @Override
public BlockEntity getBlockEntity(BlockPos position) { public BlockEntity getBlockEntity(BlockPos position) {
return isReadable(position) ? delegate.getBlockEntity(position) : null; return isWritable(position) ? delegate.getBlockEntity(position) : null;
} }
@Override @Override
public <T extends BlockEntity> Optional<T> getBlockEntity( public <T extends BlockEntity> Optional<T> getBlockEntity(
BlockPos position, BlockEntityType<T> type) { BlockPos position, BlockEntityType<T> type) {
return isReadable(position) ? delegate.getBlockEntity(position, type) : Optional.empty(); return isWritable(position) ? delegate.getBlockEntity(position, type) : Optional.empty();
} }
@Override @Override
@@ -430,7 +434,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
} }
private boolean isWritable(BlockPos position) { private boolean isWritable(BlockPos position) {
return isReadable(position); return isReadable(position) && !protectedPosition.test(position);
} }
private boolean isWritableNeighbourhood(BlockPos position) { private boolean isWritableNeighbourhood(BlockPos position) {
@@ -521,7 +525,8 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
private record Boundary(ChunkPos generationCenter, private record Boundary(ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY, IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) { IntBinaryOperator floorFirstFreeY,
Predicate<BlockPos> protectedPosition) {
} }
private static final class BoundedTickAccess<T> implements LevelTickAccess<T> { private static final class BoundedTickAccess<T> implements LevelTickAccess<T> {
@@ -135,6 +135,34 @@ public class ModdedNativeStructureWorldgenAccessTest {
assertEquals(1, recording.blockTicks.count()); assertEquals(1, recording.blockTicks.count());
} }
@Test
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
RecordingDelegate recording = new RecordingDelegate();
BlockPos position = generationCenter().getMiddleBlockPosition(70);
ModdedNativeStructureWorldgenAccess access = ModdedNativeStructureWorldgenAccess.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 @Test
public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() { public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() {
RecordingDelegate recording = new RecordingDelegate(); RecordingDelegate recording = new RecordingDelegate();
@@ -183,7 +211,7 @@ public class ModdedNativeStructureWorldgenAccessTest {
return ModdedNativeStructureWorldgenAccess.create( return ModdedNativeStructureWorldgenAccess.create(
recording.world(), generationCenter(), recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y, (x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y); (x, z) -> FLOOR_FIRST_FREE_Y, position -> false);
} }
private static ChunkPos generationCenter() { private static ChunkPos generationCenter() {
@@ -103,12 +103,12 @@ public class NativeStructureFailureContractTest {
@Test @Test
public void nativeStructurePostProcessingUsesBoundedWorldgenAccess() throws IOException { public void nativeStructurePostProcessingUsesBoundedWorldgenAccess() throws IOException {
String source = moddedSource("ModdedNativeStructureStage.java"); String source = moddedSource("ModdedNativeStructureStage.java");
int placementStart = source.indexOf("private void placeVanillaStructure"); int placementStart = source.indexOf("void placeVanillaStructures");
int placementEnd = source.indexOf("private List<List<Structure>> structuresByStep", placementStart); int placementEnd = source.indexOf("private List<List<Structure>> structuresByStep", placementStart);
String placement = source.substring(placementStart, placementEnd); String placement = source.substring(placementStart, placementEnd);
assertTrue(placement.contains("ModdedNativeStructureWorldgenAccess.create(")); assertTrue(placement.contains("ModdedNativeStructureWorldgenAccess.create("));
assertTrue(placement.contains("NativeStructurePostProcessor.place(\n boundedWorld")); assertTrue(placement.contains("placeVanillaStructure(boundedWorld, structureManager"));
assertFalse(placement.contains("NativeStructurePostProcessor.place(world,")); assertFalse(placement.contains("NativeStructurePostProcessor.place(world,"));
assertTrue(placement.contains("world.setCurrentlyGenerating(null);")); assertTrue(placement.contains("world.setCurrentlyGenerating(null);"));
} }