diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/ImportedFeatureStage.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/ImportedFeatureStage.java index db370e592..06313f6fd 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/ImportedFeatureStage.java +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/ImportedFeatureStage.java @@ -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> 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) { diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java index 755b84571..972726209 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java @@ -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, diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NativeStructureWorldgenAccess.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NativeStructureWorldgenAccess.java index cc4ff499f..88c8dc94a 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NativeStructureWorldgenAccess.java +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NativeStructureWorldgenAccess.java @@ -64,6 +64,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel { private final ChunkPos generationCenter; private final IntBinaryOperator surfaceFirstFreeY; private final IntBinaryOperator floorFirstFreeY; + private final Predicate protectedPosition; private final Holder 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 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 Optional getBlockEntity( BlockPos position, BlockEntityType 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 protectedPosition) { } private static final class BoundedTickAccess implements LevelTickAccess { diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NativeStructureWorldgenAccessTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NativeStructureWorldgenAccessTest.java index 8a9cc68c4..b38ff5853 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NativeStructureWorldgenAccessTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NativeStructureWorldgenAccessTest.java @@ -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() { diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorEncaseTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorEncaseTest.java index 4b7c4ccc1..e459bea7c 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorEncaseTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorEncaseTest.java @@ -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); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedImportedFeatureStage.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedImportedFeatureStage.java index 9c4af5637..682bed88d 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedImportedFeatureStage.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedImportedFeatureStage.java @@ -25,6 +25,7 @@ import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiomeCustom; import art.arcane.iris.engine.object.IrisDecorationStep; import art.arcane.iris.engine.object.IrisImportedFeatureControl; +import art.arcane.iris.engine.object.IrisStaticObjectLayer; import art.arcane.iris.util.project.context.IrisContext; import it.unimi.dsi.fastutil.ints.IntArraySet; 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.LevelChunkSection; 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; @@ -321,13 +323,20 @@ final class ModdedImportedFeatureStage { WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ()); Set> 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 { 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) { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureStage.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureStage.java index e092de80d..58a870f4e 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureStage.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureStage.java @@ -25,6 +25,7 @@ import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord; import art.arcane.iris.engine.framework.NativeStructureStartPlan; 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.NativeStructureLocatePersistence; import art.arcane.iris.nativegen.NativeStructureLocateResults; @@ -365,9 +366,14 @@ final class ModdedNativeStructureStage { "heightmap priming", nativeStructureBatchContext(placementGroups), 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 { NativeStructureVegetationClearer.clearIntersectingVegetation( - world, chunk, area, vegetationTargets); + boundedWorld, chunk, area, vegetationTargets); } catch (Throwable error) { throw NativeStructureGenerationException.failure( "vegetation cleanup", nativeStructureBatchContext(placementGroups), @@ -376,7 +382,7 @@ final class ModdedNativeStructureStage { NativeStructureSurfaceFitter.VacuumFoundationPlan vacuumFoundationPlan; try { vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures( - world, area, terrainTargets, + boundedWorld, area, terrainTargets, (x, z) -> current.getHeight(x, z, true) + current.getMinHeight()); } catch (Throwable error) { throw NativeStructureGenerationException.failure( @@ -385,7 +391,7 @@ final class ModdedNativeStructureStage { } try { NativeStructurePostProcessor.prepareTerrain( - world, area, terrainTargets, this::resolvePaletteBlock); + boundedWorld, area, terrainTargets, this::resolvePaletteBlock); } catch (Throwable error) { throw NativeStructureGenerationException.failure( "terrain preparation", nativeStructureBatchContext(placementGroups), @@ -395,7 +401,7 @@ final class ModdedNativeStructureStage { random.setFeatureSeed(decorationSeed, 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) { @@ -405,7 +411,7 @@ final class ModdedNativeStructureStage { } try { NativeStructureSurfaceFitter.repairVacuumFoundations( - world, area, vacuumFoundationPlan); + boundedWorld, area, vacuumFoundationPlan); } catch (Throwable error) { throw NativeStructureGenerationException.failure( "foundation repair", nativeStructureBatchContext(placementGroups), @@ -436,11 +442,8 @@ final class ModdedNativeStructureStage { String structureId, StructureStart start, IrisNativeStructureDecision decision) { Engine current = generator.engine(); - int runtimeMinY = world.getMinY(); - WorldGenLevel boundedWorld = ModdedNativeStructureWorldgenAccess.create( - world, chunkPos, - worldgenSurfaceHeight(current, runtimeMinY), - worldgenFloorHeight(current, runtimeMinY)); + WorldGenLevel boundedWorld = world instanceof ModdedNativeStructureWorldgenAccess ? world : ModdedNativeStructureWorldgenAccess.create( + world, chunkPos, worldgenSurfaceHeight(current, world.getMinY()), worldgenFloorHeight(current, world.getMinY()), position -> false); world.setCurrentlyGenerating(() -> "Iris native structure " + structureId); try { NativeStructurePostProcessor.place( diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccess.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccess.java index 9aa074711..bb9c6540c 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccess.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccess.java @@ -63,6 +63,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel { private final ChunkPos generationCenter; private final IntBinaryOperator surfaceFirstFreeY; private final IntBinaryOperator floorFirstFreeY; + private final Predicate protectedPosition; private final Holder fallbackBiome; private final BiomeManager biomeManager; private final AABB generationBounds; @@ -79,6 +80,8 @@ final class ModdedNativeStructureWorldgenAccess 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); @@ -98,9 +101,10 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel { static ModdedNativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter, IntBinaryOperator surfaceFirstFreeY, - IntBinaryOperator floorFirstFreeY) { + IntBinaryOperator floorFirstFreeY, + Predicate protectedPosition) { return new ModdedNativeStructureWorldgenAccess(delegate, new Boundary( - generationCenter, surfaceFirstFreeY, floorFirstFreeY)); + generationCenter, surfaceFirstFreeY, floorFirstFreeY, protectedPosition)); } @Override @@ -350,13 +354,13 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel { @Override public BlockEntity getBlockEntity(BlockPos position) { - return isReadable(position) ? delegate.getBlockEntity(position) : null; + return isWritable(position) ? delegate.getBlockEntity(position) : null; } @Override public Optional getBlockEntity( BlockPos position, BlockEntityType type) { - return isReadable(position) ? delegate.getBlockEntity(position, type) : Optional.empty(); + return isWritable(position) ? delegate.getBlockEntity(position, type) : Optional.empty(); } @Override @@ -430,7 +434,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel { } private boolean isWritable(BlockPos position) { - return isReadable(position); + return isReadable(position) && !protectedPosition.test(position); } private boolean isWritableNeighbourhood(BlockPos position) { @@ -521,7 +525,8 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel { private record Boundary(ChunkPos generationCenter, IntBinaryOperator surfaceFirstFreeY, - IntBinaryOperator floorFirstFreeY) { + IntBinaryOperator floorFirstFreeY, + Predicate protectedPosition) { } private static final class BoundedTickAccess implements LevelTickAccess { diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccessTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccessTest.java index e231736e9..2d128db3e 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccessTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccessTest.java @@ -135,6 +135,34 @@ public class ModdedNativeStructureWorldgenAccessTest { 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 public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() { RecordingDelegate recording = new RecordingDelegate(); @@ -183,7 +211,7 @@ public class ModdedNativeStructureWorldgenAccessTest { return ModdedNativeStructureWorldgenAccess.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() { diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/NativeStructureFailureContractTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/NativeStructureFailureContractTest.java index 7ee5f5f67..8a04ec78e 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/NativeStructureFailureContractTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/NativeStructureFailureContractTest.java @@ -103,12 +103,12 @@ public class NativeStructureFailureContractTest { @Test public void nativeStructurePostProcessingUsesBoundedWorldgenAccess() throws IOException { String source = moddedSource("ModdedNativeStructureStage.java"); - int placementStart = source.indexOf("private void placeVanillaStructure"); + int placementStart = source.indexOf("void placeVanillaStructures"); int placementEnd = source.indexOf("private List> structuresByStep", placementStart); String placement = source.substring(placementStart, placementEnd); assertTrue(placement.contains("ModdedNativeStructureWorldgenAccess.create(")); - assertTrue(placement.contains("NativeStructurePostProcessor.place(\n boundedWorld")); + assertTrue(placement.contains("placeVanillaStructure(boundedWorld, structureManager")); assertFalse(placement.contains("NativeStructurePostProcessor.place(world,")); assertTrue(placement.contains("world.setCurrentlyGenerating(null);")); }