From f0e7fea13bdcfe70d9921d7e4d82e0b5d9fc2690 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sat, 19 Sep 2026 23:48:44 -0400 Subject: [PATCH] Test- 26.3 --- .../bukkit/nms/v26_2_R1/NMSBinding.java | 2 +- .../ForcedStructureChunkGenerator.java | 83 + .../v26_3_R1/NativeBuildFutures.java | 31 + .../v26_3_R1/NativeGenerationWriteGuard.java | 78 + .../v26_3_R1/NativeStructureFactory.java | 286 ++ .../NativeStructureFoundationBuilder.java | 212 ++ .../NativeStructureGenerationException.java | 35 + .../NativeStructureGenerationKeys.java | 121 + .../NativeStructureLocatePersistence.java | 351 ++ .../NativeStructureLocateResults.java | 58 + .../NativeStructureOwnershipFingerprint.java | 239 ++ .../NativeStructureOwnershipRecovery.java | 195 ++ .../NativeStructurePostProcessor.java | 66 + .../NativeStructureReferenceEnvelope.java | 239 ++ .../NativeStructureReferenceRepair.java | 246 ++ .../v26_3_R1/NativeStructureReflection.java | 386 +++ .../NativeStructureStartInjector.java | 147 + .../NativeStructureSurfaceFitter.java | 1221 +++++++ .../NativeStructureSurfaceSupportBuilder.java | 305 ++ .../NativeStructureTemplateOccupancy.java | 165 + .../NativeStructureTemplatePoolBounds.java | 228 ++ .../NativeStructureTerrainIntegrator.java | 1096 +++++++ .../NativeStructureVanillaLocator.java | 224 ++ .../NativeStructureVegetationClearer.java | 174 + .../NativeStructureVerticalPlacer.java | 384 +++ .../v26_3_R1/NativeStructureVolumeIndex.java | 643 ++++ .../v26_3_R1/NativeTerrainHeightCache.java | 42 + .../v26_3_R1/NativeTransitionColumn.java | 63 + .../v26_3_R1/WorldgenTerrainHeightmaps.java | 113 + .../nms/v26_3_R1/CustomBiomeSource.java | 1287 ++++++++ .../DatapackStructureStateFilter.java | 513 +++ .../nms/v26_3_R1/ImportedFeatureStage.java | 441 +++ .../nms/v26_3_R1/InitialMobSpawner.java | 132 + .../nms/v26_3_R1/IrisChunkGenerator.java | 1621 +++++++++ .../bukkit/nms/v26_3_R1/NMSBinding.java | 1645 ++++++++++ .../v26_3_R1/NativeStructurePoiUpdates.java | 142 + .../NativeStructureWorldgenAccess.java | 606 ++++ .../nms/v26_3_R1/NmsGenerationHooks.java | 61 + .../nms/v26_3_R1/NmsGenerationRegistry.java | 182 ++ .../nms/v26_3_R1/NmsSavedTerrainCapture.java | 266 ++ .../nms/v26_3_R1/NmsWorldLifecycle.java | 403 +++ .../nms/v26_3_R1/VanillaStructureBiomes.java | 80 + .../IrisChunkGeneratorHeightCacheTest.java | 131 + .../NativeGenerationWriteGuardTest.java | 125 + .../nativegen/NativeStructureFactoryTest.java | 462 +++ ...veStructureGenerationKeysContractTest.java | 44 + .../NativeStructureLocatePersistenceTest.java | 197 ++ ...tiveStructureOwnershipFingerprintTest.java | 346 ++ .../NativeStructureOwnershipRecoveryTest.java | 314 ++ ...ativeStructurePostProcessorEncaseTest.java | 532 +++ ...iveStructurePostProcessorMonumentTest.java | 202 ++ ...turePostProcessorScatteredFeatureTest.java | 198 ++ ...ucturePostProcessorSurfaceTerrainTest.java | 2900 +++++++++++++++++ ...eStructurePostProcessorVegetationTest.java | 354 ++ .../NativeStructureTemplateOccupancyTest.java | 236 ++ .../NativeStructureVolumeIndexTest.java | 561 ++++ .../NativeTerrainHeightCacheTest.java | 129 + .../nativegen/NativeTransitionColumnTest.java | 106 + .../nativegen/v26_3_R1/NativeBlockTags.java | 28 + .../nms/v26_3_R1/CustomBiomeCodecTest.java | 57 + ...ustomBiomeSourceStructureContractTest.java | 64 + ...IrisChunkGeneratorFailureContractTest.java | 97 + ...nkGeneratorMonumentLocateContractTest.java | 58 + .../IrisChunkGeneratorSpawnAdmissionTest.java | 148 + .../NMSBindingDatapackStructureScopeTest.java | 437 +++ .../NativeStructurePoiUpdatesTest.java | 250 ++ .../NativeStructureWorldgenAccessTest.java | 464 +++ .../nms/v26_3_R1/NmsChunkIOFlushTest.java | 100 + .../nms/v26_3_R1/NmsChunkTaskPollingTest.java | 95 + .../NmsEntitySpawnDifficultyTest.java | 103 + .../NmsSavedTerrainCheckpointTest.java | 114 + .../v26_3_R1/NmsSavedTerrainSnapshotTest.java | 223 ++ .../NmsSavedTerrainWriteBarrierTest.java | 135 + .../WorldgenTerrainHeightmapsTest.java | 345 ++ ...terListPoolElementRuntimeContractTest.java | 294 ++ .../java/art/arcane/iris/IrisBootstrap.java | 10 +- .../iris/modded/ModdedForcedDatapack.java | 10 +- .../modded/WorldCheckDimensionContract.java | 2 +- .../command/ModdedDatapackCommands.java | 4 +- .../modded/ModdedDimensionTypeParityTest.java | 2 +- .../iris/modded/ModdedForcedDatapackTest.java | 16 + build.gradle | 37 +- .../buildtools/GenerationRevisionScope.java | 2 + .../pack/DefaultPackBootstrapProvisioner.java | 39 +- .../iris/platform/bukkit/nms/INMSBinding.java | 2 +- .../bukkit/nms/NmsBindingSelector.java | 5 +- .../bukkit/nms/datapack/DataVersion.java | 28 +- .../nms/datapack/v263/DataFixerV263.java | 54 + .../platform/bukkit/nms/v1X/NMSBinding1X.java | 2 +- .../structure/export/VanillaBlockState.java | 7 +- .../export/VanillaJigsawExportCompiler.java | 14 +- .../export/VanillaJigsawExportRequest.java | 2 +- .../VanillaStructureTemplateEncoder.java | 18 +- .../GenerationRegistryContractFactory.java | 12 +- .../iris/world/history/SavedTerrainChunk.java | 2 +- .../history/SavedTerrainChunkReader.java | 40 +- .../iris/world/storage/region/NBTWorld.java | 14 +- .../DefaultPackBootstrapProvisionerTest.java | 38 +- .../datapack/IrisDatapackCompilerTest.java | 14 +- .../bukkit/nms/NmsBindingSelectorTest.java | 10 +- .../bukkit/nms/datapack/DataVersionTest.java | 13 +- ...VillageImporterConnectorRoundTripTest.java | 17 + .../export/VanillaBlockStateTest.java | 15 +- .../VanillaJigsawDatapackExporterTest.java | 25 + .../world/history/SavedTerrainChunkTest.java | 78 + gradle/generation-build-revision.gradle | 2 +- settings.gradle | 1 + 107 files changed, 25110 insertions(+), 86 deletions(-) create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/ForcedStructureChunkGenerator.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeBuildFutures.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeGenerationWriteGuard.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureFactory.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureFoundationBuilder.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureGenerationException.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureGenerationKeys.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureLocatePersistence.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureLocateResults.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureOwnershipFingerprint.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureOwnershipRecovery.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructurePostProcessor.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureReferenceEnvelope.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureReferenceRepair.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureReflection.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureStartInjector.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureSurfaceFitter.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureSurfaceSupportBuilder.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureTemplateOccupancy.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureTemplatePoolBounds.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureTerrainIntegrator.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureVanillaLocator.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureVegetationClearer.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureVerticalPlacer.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureVolumeIndex.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeTerrainHeightCache.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeTransitionColumn.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/WorldgenTerrainHeightmaps.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/CustomBiomeSource.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/DatapackStructureStateFilter.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/ImportedFeatureStage.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/InitialMobSpawner.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/IrisChunkGenerator.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NMSBinding.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructurePoiUpdates.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructureWorldgenAccess.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsGenerationHooks.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsGenerationRegistry.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsSavedTerrainCapture.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsWorldLifecycle.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/VanillaStructureBiomes.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/IrisChunkGeneratorHeightCacheTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeGenerationWriteGuardTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureFactoryTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureGenerationKeysContractTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureLocatePersistenceTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureOwnershipFingerprintTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureOwnershipRecoveryTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorEncaseTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorMonumentTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorScatteredFeatureTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancyTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureVolumeIndexTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeTerrainHeightCacheTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeTransitionColumnTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/v26_3_R1/NativeBlockTags.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/CustomBiomeCodecTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/CustomBiomeSourceStructureContractTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/IrisChunkGeneratorFailureContractTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/IrisChunkGeneratorMonumentLocateContractTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/IrisChunkGeneratorSpawnAdmissionTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NMSBindingDatapackStructureScopeTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructurePoiUpdatesTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructureWorldgenAccessTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsChunkIOFlushTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsChunkTaskPollingTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsEntitySpawnDifficultyTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsSavedTerrainCheckpointTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsSavedTerrainSnapshotTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsSavedTerrainWriteBarrierTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/WorldgenTerrainHeightmapsTest.java create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/structure/VillageImporterListPoolElementRuntimeContractTest.java create mode 100644 core/src/main/java/art/arcane/iris/platform/bukkit/nms/datapack/v263/DataFixerV263.java diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_2_R1/NMSBinding.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_2_R1/NMSBinding.java index d1d24de79..eb2e6fcfd 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_2_R1/NMSBinding.java +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_2_R1/NMSBinding.java @@ -958,7 +958,7 @@ public class NMSBinding implements INMSBinding { if (global == null) { throw new IllegalStateException("Iris cannot build the global block state palette on this server version"); } - java.util.Map innerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64); + java.util.Map, BlockState> innerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64); java.util.Map outerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64); MCAPalettedContainer container = new MCAPalettedContainer<>(global, registry, i -> innerDecodeCache.computeIfAbsent(i, t -> ((CraftBlockData) NBTWorld.getBlockData(t)).getState()), diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/ForcedStructureChunkGenerator.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/ForcedStructureChunkGenerator.java new file mode 100644 index 000000000..409b3eb40 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/ForcedStructureChunkGenerator.java @@ -0,0 +1,83 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import net.minecraft.world.level.levelgen.densityfunction.SamplerContext; +import java.util.Set; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.NoiseColumn; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.biome.FixedBiomeSource; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.blending.Blender; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +final class ForcedStructureChunkGenerator extends ChunkGenerator { + private final ChunkGenerator delegate; + + ForcedStructureChunkGenerator(ChunkGenerator delegate, Holder sourceBiome) { + super(new FixedBiomeSource(sourceBiome)); + this.delegate = delegate; + } + + @Override + protected MapCodec codec() { + return MapCodec.unit(this); + } + + @Override + public void spawnOriginalMobs(WorldGenRegion worldGenRegion) { + delegate.spawnOriginalMobs(worldGenRegion); + } + + @Override + public int getGenDepth() { + return delegate.getGenDepth(); + } + + @Override + public CompletableFuture buildTerrain(ChunkAccess centerChunk, Blender blender, RandomState randomState, + StructureManager structureManager, BiomeManager biomeManager, + WorldGenRegion carverBiomeRegion, Set> possibleBiomes) { + return delegate.buildTerrain( + centerChunk, blender, randomState, structureManager, biomeManager, carverBiomeRegion, possibleBiomes); + } + + @Override + public int getSeaLevel() { + return delegate.getSeaLevel(); + } + + @Override + public int getMinY() { + return delegate.getMinY(); + } + + @Override + public int getBaseHeight(int x, int z, Heightmap.Types type, + LevelHeightAccessor heightAccessor, RandomState randomState) { + return delegate.getBaseHeight(x, z, type, heightAccessor, randomState); + } + + @Override + public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, + RandomState randomState) { + return delegate.getBaseColumn(x, z, heightAccessor, randomState); + } + + @Override + public void addDebugScreenInfo(List result, RandomState randomState, + BlockPos feetPos, SamplerContext samplerContext) { + delegate.addDebugScreenInfo(result, randomState, feetPos, samplerContext); + } + +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeBuildFutures.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeBuildFutures.java new file mode 100644 index 000000000..704ec9d53 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeBuildFutures.java @@ -0,0 +1,31 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * Shared waiter for the racy build caches in this package: the loser of a putIfAbsent race + * must surface the same exception the builder threw, not a CompletionException wrapper. + */ +final class NativeBuildFutures { + private NativeBuildFutures() { + } + + static T awaitBuild(CompletableFuture future, String what) { + try { + return future.join(); + } catch (CompletionException error) { + Throwable cause = error.getCause(); + if (cause instanceof RuntimeException runtime) { + throw runtime; + } + if (cause instanceof Error fatal) { + throw fatal; + } + if (cause == null) { + throw error; + } + throw new IllegalStateException(what + " failed", cause); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeGenerationWriteGuard.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeGenerationWriteGuard.java new file mode 100644 index 000000000..44e47bcd2 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeGenerationWriteGuard.java @@ -0,0 +1,78 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.generation.runtime.DimensionStackContext; +import art.arcane.iris.generation.runtime.DimensionStackLayout; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.IrisEngine; +import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter; +import art.arcane.iris.structure.object.IrisStaticObjectLayer; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkPyramid; +import net.minecraft.world.level.chunk.status.ChunkStatus; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; + +public final class NativeGenerationWriteGuard { + private NativeGenerationWriteGuard() { + } + + public static boolean allowsDecoration(Engine engine, WorldGenLevel region, ChunkPos center) { + int radius = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FEATURES).blockStateWriteRadius(); + for (int offsetX = -radius; offsetX <= radius; offsetX++) { + for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) { + int chunkX = Math.addExact(center.x(), offsetX); + int chunkZ = Math.addExact(center.z(), offsetZ); + ChunkAccess target = region.getChunk(chunkX, chunkZ); + if (target.getPersistedStatus().isOrAfter(ChunkStatus.FULL) + && !engine.getComplex().allowsMantleChunkWrite(chunkX, chunkZ)) { + return false; + } + } + } + return true; + } + + public static boolean isHistoricalStructure(Engine engine, long activation) { + if (activation <= 0 || !(engine instanceof IrisEngine irisEngine)) { + return false; + } + GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null); + return router != null && activation < router.history().activeActivation().activationId(); + } + + public static boolean allowsPendingStage(Engine engine, ChunkAccess chunk, ChunkStatus stage) { + return stage.isOrAfter(ChunkStatus.FEATURES) + && chunk.getPersistedStatus().isOrAfter(ChunkStatus.TERRAIN) + && !chunk.getPersistedStatus().isOrAfter(stage) + && !engine.getComplex().allowsMantleChunkWrite(chunk.getPos().x(), chunk.getPos().z()); + } + + public static Predicate protectedPositions( + IrisStaticObjectLayer staticObjects, + DimensionStackContext stackContext, + int minimumY + ) { + Map layouts = new ConcurrentHashMap<>(); + return position -> { + if (!staticObjects.isEmpty() && staticObjects.contains( + position.getX(), position.getY() - minimumY, position.getZ())) { + return true; + } + if (stackContext == null) { + return false; + } + long columnKey = ((long) position.getX() << 32) + ^ (position.getZ() & 0xFFFFFFFFL); + DimensionStackLayout layout = layouts.computeIfAbsent( + columnKey, + ignored -> stackContext.sample(position.getX(), position.getZ()) + ); + return layout.isHostFeatureProtectedY(position.getY() - minimumY); + }; + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureFactory.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureFactory.java new file mode 100644 index 000000000..562ee510a --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureFactory.java @@ -0,0 +1,286 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import net.minecraft.world.level.levelgen.densityfunction.SamplerContext; +import art.arcane.iris.structure.nativegen.NativeStructureStartPlan; +import art.arcane.iris.structure.jigsaw.IrisJigsawConfiguration; +import art.arcane.iris.structure.jigsaw.IrisJigsawHeightmap; +import art.arcane.iris.structure.jigsaw.IrisJigsawLiquidSettings; +import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.RegistryOps; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.Locale; +import java.util.Objects; +import java.util.function.IntBinaryOperator; +import java.util.function.Predicate; + +public final class NativeStructureFactory { + private NativeStructureFactory() { + } + + public static StructureStart generate(GenerationContext context, Holder sourceHolder, + NativeStructureStartPlan plan, int references) { + Structure source = sourceHolder.value(); + Structure configured = configure( + context.registryAccess(), source, plan.source().getJigsaw(), + plan.placement().isUnderground(), plan.baseY()); + Holder sourceBiome = source.biomes().stream().findFirst() + .or(() -> context.biomeSource().possibleBiomes().stream().findFirst()) + .orElseThrow(() -> new IllegalStateException("Configured native structure '" + + plan.source().getStructure() + "' has no usable generation biome")); + ChunkGenerator forcedGenerator = new ForcedStructureChunkGenerator( + context.generator(), sourceBiome); + StructureStart generated = configured.generate( + sourceHolder, + context.levelKey(), + context.registryAccess(), + forcedGenerator, + forcedGenerator.getBiomeSource(), + context.randomState().createClimateSampler(SamplerContext.EMPTY_UNCACHED), + context.randomState(), + context.templateManager(), + context.levelSeed(), + new ChunkPos(plan.chunkX(), plan.chunkZ()), + references, + context.heightAccessor(), + context.biomePredicate() + ); + if (generated == null || !generated.isValid()) { + return StructureStart.INVALID_START; + } + StructureStart positioned; + if (plan.placement().isUnderground()) { + positioned = NativeStructureVerticalPlacer.relocateToMinY( + generated, source, plan.baseY(), context.heightAccessor()); + } else { + NativeStructureVerticalPlacer.applyVerticalPlacement( + generated, + plan.source().getStructure(), + 0, + context.seaLevel(), + context.heightAccessor().getMinY(), + Math.addExact(context.heightAccessor().getMinY(), context.heightAccessor().getHeight()), + false, + false, + null, + context.surfaceHeight()); + positioned = generated; + } + if (!positioned.isValid()) { + return StructureStart.INVALID_START; + } + return NativeStructureReferenceEnvelope.wrapForPublication( + positioned, + source, + references, + plan.placement().resolvedTerrain(), + plan.source().getStructure() + ); + } + + public static JigsawSourceMetadata sourceMetadata(RegistryAccess registryAccess, + StructureTemplateManager templateManager, + JigsawStructure source) { + CompoundTag sourceTag = encodeJigsaw(registryAccess, source); + return new JigsawSourceMetadata( + sourceDistance(sourceTag, "horizontal"), + horizontalReferenceExpansion(source), + NativeStructureTemplatePoolBounds.sourceHorizontalSpan( + registryAccess, templateManager, source)); + } + + public static int templatePoolHorizontalSpan(RegistryAccess registryAccess, + StructureTemplateManager templateManager, + String templatePoolKey) { + return NativeStructureTemplatePoolBounds.horizontalSpan( + registryAccess, templateManager, templatePoolKey); + } + + public static int jigsawStartPoolHorizontalSpan(RegistryAccess registryAccess, + StructureTemplateManager templateManager, + JigsawStructure source, + String templatePoolKey) { + return NativeStructureTemplatePoolBounds.horizontalSpan( + registryAccess, templateManager, source, templatePoolKey); + } + + static Structure configure(RegistryAccess registryAccess, Structure source, + IrisJigsawConfiguration configuration, + boolean underground, int baseY) { + Objects.requireNonNull(source, "Native structure source must not be null"); + if (!(source instanceof JigsawStructure sourceJigsaw)) { + if (configuration != null) { + throw new IllegalStateException("Jigsaw options require a registered jigsaw structure, found " + + source.getClass().getName()); + } + return source; + } + RegistryOps registryOps = RegistryOps.create(NbtOps.INSTANCE, registryAccess); + CompoundTag structureTag = encodeJigsaw(registryOps, sourceJigsaw); + CompoundTag configuredTag = structureTag.copy(); + if (underground) { + CompoundTag height = new CompoundTag(); + height.putInt("absolute", baseY); + configuredTag.put("start_height", height); + configuredTag.remove("project_start_to_heightmap"); + } + applyConfiguration(configuredTag, configuration); + Structure decoded = Structure.DIRECT_CODEC.parse(registryOps, configuredTag).getOrThrow(); + if (!(decoded instanceof JigsawStructure configured)) { + throw new IllegalStateException("Configured native jigsaw codec produced " + + decoded.getClass().getName()); + } + return configured; + } + + private static CompoundTag encodeJigsaw(RegistryAccess registryAccess, + JigsawStructure source) { + return encodeJigsaw(RegistryOps.create(NbtOps.INSTANCE, registryAccess), source); + } + + private static CompoundTag encodeJigsaw(RegistryOps registryOps, + JigsawStructure source) { + Tag encoded = Structure.DIRECT_CODEC.encodeStart(registryOps, source).getOrThrow(); + if (!(encoded instanceof CompoundTag structureTag)) { + throw new IllegalStateException("Native jigsaw codec did not produce a compound"); + } + return structureTag; + } + + private static void applyConfiguration(CompoundTag tag, IrisJigsawConfiguration configuration) { + if (configuration == null) { + return; + } + if (configuration.getStartPool() != null && !configuration.getStartPool().isBlank()) { + tag.putString("start_pool", configuration.getStartPool().trim()); + } + if (configuration.getStartJigsawName() != null + && !configuration.getStartJigsawName().isBlank()) { + if ("NONE".equalsIgnoreCase(configuration.getStartJigsawName().trim())) { + tag.remove("start_jigsaw_name"); + } else { + tag.putString("start_jigsaw_name", configuration.getStartJigsawName().trim()); + } + } + if (configuration.getMaxDepth() != null) { + tag.putInt("size", configuration.getMaxDepth()); + } + applyDistance(tag, configuration); + if (configuration.getUseExpansionHack() != null) { + tag.putBoolean("use_expansion_hack", configuration.getUseExpansionHack()); + } + applyHeightmap(tag, configuration.getProjectStartToHeightmap()); + applyDimensionPadding(tag, configuration); + applyLiquidSettings(tag, configuration.getLiquidSettings()); + } + + private static void applyDistance(CompoundTag tag, IrisJigsawConfiguration configuration) { + Integer horizontal = configuration.getMaxDistanceHorizontal(); + Integer vertical = configuration.getMaxDistanceVertical(); + if (horizontal == null && vertical == null) { + return; + } + int resolvedHorizontal = horizontal == null ? sourceDistance(tag, "horizontal") : horizontal; + int resolvedVertical = vertical == null ? sourceDistance(tag, "vertical") : vertical; + if (resolvedHorizontal == resolvedVertical) { + tag.putInt("max_distance_from_center", resolvedHorizontal); + return; + } + CompoundTag distance = new CompoundTag(); + distance.putInt("horizontal", resolvedHorizontal); + distance.putInt("vertical", resolvedVertical); + tag.put("max_distance_from_center", distance); + } + + static int sourceDistance(CompoundTag tag, String axis) { + Tag raw = tag.get("max_distance_from_center"); + if (raw instanceof CompoundTag compound) { + return compound.getIntOr(axis, 128); + } + return tag.getIntOr("max_distance_from_center", 128); + } + + static int horizontalReferenceExpansion(Structure structure) { + BoundingBox content = new BoundingBox(0, 0, 0, 0, 0, 0); + BoundingBox adjusted = structure.adjustBoundingBox(content); + int expansion = Math.max(content.minX() - adjusted.minX(), adjusted.maxX() - content.maxX()); + expansion = Math.max(expansion, content.minZ() - adjusted.minZ()); + return Math.max(0, Math.max(expansion, adjusted.maxZ() - content.maxZ())); + } + + private static void applyHeightmap(CompoundTag tag, IrisJigsawHeightmap heightmap) { + if (heightmap == null || heightmap == IrisJigsawHeightmap.SOURCE) { + return; + } + if (heightmap == IrisJigsawHeightmap.NONE) { + tag.remove("project_start_to_heightmap"); + return; + } + tag.putString("project_start_to_heightmap", heightmap.name()); + } + + private static void applyDimensionPadding(CompoundTag tag, IrisJigsawConfiguration configuration) { + Integer bottom = configuration.getDimensionPaddingBottom(); + Integer top = configuration.getDimensionPaddingTop(); + if (bottom == null && top == null) { + return; + } + int resolvedBottom = bottom == null ? sourcePadding(tag, "bottom") : bottom; + int resolvedTop = top == null ? sourcePadding(tag, "top") : top; + if (resolvedBottom == resolvedTop) { + tag.putInt("dimension_padding", resolvedBottom); + return; + } + CompoundTag padding = new CompoundTag(); + padding.putInt("bottom", resolvedBottom); + padding.putInt("top", resolvedTop); + tag.put("dimension_padding", padding); + } + + private static int sourcePadding(CompoundTag tag, String side) { + Tag raw = tag.get("dimension_padding"); + if (raw instanceof CompoundTag compound) { + return compound.getIntOr(side, 0); + } + return tag.getIntOr("dimension_padding", 0); + } + + private static void applyLiquidSettings(CompoundTag tag, IrisJigsawLiquidSettings settings) { + if (settings == null || settings == IrisJigsawLiquidSettings.SOURCE) { + return; + } + tag.putString("liquid_settings", settings.name().toLowerCase(Locale.ROOT)); + } + + public record GenerationContext( + RegistryAccess registryAccess, + ChunkGenerator generator, + BiomeSource biomeSource, + RandomState randomState, + StructureTemplateManager templateManager, + long levelSeed, + ResourceKey levelKey, + LevelHeightAccessor heightAccessor, + Predicate> biomePredicate, + int seaLevel, + IntBinaryOperator surfaceHeight + ) { + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureFoundationBuilder.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureFoundationBuilder.java new file mode 100644 index 000000000..3ca6bbd73 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureFoundationBuilder.java @@ -0,0 +1,212 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.structure.placement.IrisStructureStiltSettings; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SupportType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.Objects; +import java.util.function.IntBinaryOperator; + +public final class NativeStructureFoundationBuilder { + private static final int FOUNDATION_VERTICAL_TOLERANCE = 1; + + private NativeStructureFoundationBuilder() { + } + + private static List foundationEnvelope(BoundingBox area, StructureStart start) { + BoundingBox structure = NativeStructureReferenceEnvelope.contentBounds(start); + int minX = Math.max(area.minX(), structure.minX()); + int minZ = Math.max(area.minZ(), structure.minZ()); + int maxX = Math.min(area.maxX(), structure.maxX()); + int maxZ = Math.min(area.maxZ(), structure.maxZ()); + if (minX > maxX || minZ > maxZ) { + return List.of(); + } + List pieces = start.getPieces(); + List columns = new ArrayList<>((maxX - minX + 1) * (maxZ - minZ + 1)); + BitSet envelope = new BitSet(area.getYSpan()); + for (int z = minZ; z <= maxZ; z++) { + for (int x = minX; x <= maxX; x++) { + envelope.clear(); + markFoundationEnvelope(envelope, pieces, area, x, z); + int cellCount = envelope.cardinality(); + if (cellCount == 0) { + continue; + } + int[] ys = new int[cellCount]; + int cell = 0; + for (int bit = envelope.nextSetBit(0); bit >= 0; bit = envelope.nextSetBit(bit + 1)) { + ys[cell++] = area.minY() + bit; + } + columns.add(new FoundationColumn(x, z, ys)); + } + } + return List.copyOf(columns); + } + + private static void markFoundationEnvelope(BitSet envelope, List pieces, BoundingBox area, + int x, int z) { + for (StructurePiece piece : pieces) { + BoundingBox bounds = piece.getBoundingBox(); + if (x < bounds.minX() || x > bounds.maxX() || z < bounds.minZ() || z > bounds.maxZ()) { + continue; + } + int groundY = bounds.minY(); + if (piece instanceof PoolElementStructurePiece poolPiece) { + groundY += poolPiece.getGroundLevelDelta(); + if (groundY < bounds.minY()) { + continue; + } + } + int minY = Math.max(area.minY(), bounds.minY()); + int maxY = Math.min(area.maxY(), Math.min(bounds.maxY(), groundY + FOUNDATION_VERTICAL_TOLERANCE)); + if (minY <= maxY) { + envelope.set(minY - area.minY(), maxY - area.minY() + 1); + } + } + } + + static void placeStilts(WorldGenLevel world, BoundingBox area, String structureId, + StructureStart start, IrisStructureStiltSettings settings, + NativeStructurePostProcessor.PaletteBlockResolver paletteBlockResolver, + IntBinaryOperator surfaceHeight, + boolean surfaceStructure) { + Objects.requireNonNull(surfaceHeight, "Structure stilts require an Iris terrain height resolver"); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + int structureHash = structureId == null ? 0 : structureId.hashCode(); + RNG rng = new RNG(world.getSeed() ^ structureHash); + for (FoundationColumn column : foundationEnvelope(area, start)) { + if (!isStiltColumn(column.x(), column.z(), settings.getSpacing())) { + continue; + } + int foundationY = findFoundationY(world, column, position); + if (foundationY == Integer.MIN_VALUE) { + continue; + } + int terrainY = surfaceStructure + ? Math.max(area.minY(), Math.min( + area.maxY(), surfaceHeight.applyAsInt(column.x(), column.z()))) + : area.minY() - 1; + int anchorY = findStiltAnchorY( + world, column.x(), column.z(), foundationY, + Math.max(1, settings.getMaxDepth()), terrainY, area.minY(), position); + if (anchorY == Integer.MIN_VALUE) { + continue; + } + for (int y = foundationY - 1; y > anchorY; y--) { + position.set(column.x(), y, column.z()); + BlockState stilt = settings.getPalette() == null + ? Blocks.COBBLESTONE.defaultBlockState() + : Objects.requireNonNull( + paletteBlockResolver.resolve( + settings.getPalette(), rng, column.x(), y, column.z()), + "Stilt palette returned no block for " + structureId + " at " + + column.x() + "," + y + "," + column.z()); + world.setBlock(position, stilt, 2); + } + } + } + + static boolean isStiltColumn(int x, int z, int spacing) { + int resolvedSpacing = Math.max(1, spacing); + return resolvedSpacing == 1 + || Math.floorMod(x, resolvedSpacing) == 0 + && Math.floorMod(z, resolvedSpacing) == 0; + } + + static int findStiltAnchorY( + WorldGenLevel world, int x, int z, int foundationY, int maxDepth, + int terrainY, int areaMinY, BlockPos.MutableBlockPos position) { + int minimumAnchorY = Math.max( + areaMinY, Math.max(terrainY, foundationY - maxDepth - 1)); + for (int y = foundationY - 1; y >= minimumAnchorY; y--) { + BlockState state = world.getBlockState(position.set(x, y, z)); + boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES); + if (!vegetation && state.isFaceSturdy( + world, position, Direction.UP, SupportType.FULL)) { + return y; + } + } + return Integer.MIN_VALUE; + } + + public static StiltSupportAudit auditStiltSupport(WorldGenLevel world, BoundingBox area, + StructureStart start, BlockState expectedStilt, + IntBinaryOperator surfaceHeight) { + Objects.requireNonNull(expectedStilt, "Expected stilt state must not be null"); + Objects.requireNonNull(surfaceHeight, "Structure stilt audit requires an Iris terrain height resolver"); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + int baseColumns = 0; + int stiltBlocks = 0; + int stiltColumns = 0; + int unsupportedColumns = 0; + for (FoundationColumn column : foundationEnvelope(area, start)) { + int foundationY = findFoundationY(world, column, position); + if (foundationY == Integer.MIN_VALUE) { + continue; + } + baseColumns++; + int terrainY = Math.max(area.minY(), Math.min( + area.maxY(), surfaceHeight.applyAsInt(column.x(), column.z()))); + boolean grounded = foundationY <= terrainY + 1; + boolean stiltColumn = false; + for (int y = foundationY - 1; y >= area.minY(); y--) { + if (y <= terrainY) { + grounded = true; + break; + } + BlockState state = world.getBlockState(position.set(column.x(), y, column.z())); + if (state.is(expectedStilt.getBlock())) { + stiltBlocks++; + stiltColumn = true; + if (y == area.minY()) { + grounded = true; + } + continue; + } + boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES); + grounded = state.isSolid() && !vegetation; + break; + } + if (stiltColumn) { + stiltColumns++; + } + if (!grounded) { + unsupportedColumns++; + } + } + return new StiltSupportAudit(baseColumns, stiltBlocks, stiltColumns, unsupportedColumns); + } + + private static int findFoundationY(WorldGenLevel world, FoundationColumn column, + BlockPos.MutableBlockPos position) { + for (int cell = 0; cell < column.ys().length; cell++) { + int y = column.ys()[cell]; + BlockState state = world.getBlockState(position.set(column.x(), y, column.z())); + if (state.isSolid()) { + return y; + } + } + return Integer.MIN_VALUE; + } + + private record FoundationColumn(int x, int z, int[] ys) { + } + + public record StiltSupportAudit(int baseColumns, int stiltBlocks, int stiltColumns, + int unsupportedColumns) { + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureGenerationException.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureGenerationException.java new file mode 100644 index 000000000..db470e80e --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureGenerationException.java @@ -0,0 +1,35 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +public final class NativeStructureGenerationException extends IllegalStateException { + private NativeStructureGenerationException(FailureContext context, Throwable cause) { + super(message(context), cause); + } + + private NativeStructureGenerationException(FailureContext context) { + super(message(context)); + } + + public static NativeStructureGenerationException failure(String operation, String structureContext, + int chunkX, int chunkZ, Throwable cause) { + return new NativeStructureGenerationException( + new FailureContext(operation, structureContext, chunkX, chunkZ), cause); + } + + public static NativeStructureGenerationException failure(String operation, String structureContext, + int chunkX, int chunkZ) { + return new NativeStructureGenerationException( + new FailureContext(operation, structureContext, chunkX, chunkZ)); + } + + private static String message(FailureContext context) { + String resolvedContext = context.structureContext() == null || context.structureContext().isBlank() + ? "" + : context.structureContext(); + return "Iris native structure " + context.operation() + " failed for " + resolvedContext + + " in chunk " + context.chunkX() + "," + context.chunkZ() + + "; chunk generation was aborted to prevent partial structure state"; + } + + private record FailureContext(String operation, String structureContext, int chunkX, int chunkZ) { + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureGenerationKeys.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureGenerationKeys.java new file mode 100644 index 000000000..3afcbfe63 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureGenerationKeys.java @@ -0,0 +1,121 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 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 . + */ + +package art.arcane.iris.nativegen.v26_3_R1; + +import net.minecraft.core.Holder; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +public final class NativeStructureGenerationKeys { + private static final Method FREQUENCY_METHOD = resolveFrequencyMethod(); + + private NativeStructureGenerationKeys() { + } + + public static Set reachable(ServerLevel level, Set possibleBiomeKeys) { + ServerLevel activeLevel = Objects.requireNonNull(level, "Native structure reachability requires a level"); + Set possibleBiomes = Objects.requireNonNull( + possibleBiomeKeys, "Native structure reachability requires possible biome keys"); + if (!activeLevel.getServer().getWorldGenSettings().options().generateStructures()) { + return Collections.emptySet(); + } + + Set keys = new LinkedHashSet<>(); + List> structureSets = + activeLevel.getChunkSource().getGeneratorState().possibleStructureSets(); + for (Holder structureSetHolder : structureSets) { + StructureSet structureSet = structureSetHolder.value(); + if (!isEnabledPlacement(structureSet.placement())) { + continue; + } + for (StructureSet.StructureSelectionEntry entry : structureSet.structures()) { + if (entry.weight() <= 0 || !hasPossibleBiome(entry.structure().value(), possibleBiomes)) { + continue; + } + Optional> key = entry.structure().unwrapKey(); + key.ifPresent(resourceKey -> keys.add(resourceKey.identifier().toString())); + } + } + return Collections.unmodifiableSet(keys); + } + + static boolean hasPossibleBiome(Structure structure, Set possibleBiomeKeys) { + for (Holder holder : structure.biomes()) { + Optional> key = holder.unwrapKey(); + if (key.isPresent() && possibleBiomeKeys.contains(key.get().identifier().toString())) { + return true; + } + } + return false; + } + + static boolean isEnabledPlacement(StructurePlacement placement) { + return placementFrequency(placement) > 0.0F; + } + + private static float placementFrequency(StructurePlacement placement) { + StructurePlacement activePlacement = Objects.requireNonNull( + placement, "Native structure placement frequency requires a placement"); + if (!(activePlacement instanceof AbstractSpreadingStructurePlacement)) { + return 1F; + } + try { + return ((Float) FREQUENCY_METHOD.invoke(activePlacement)).floatValue(); + } catch (ReflectiveOperationException error) { + throw new IllegalStateException( + "Unable to read native structure placement frequency from " + + activePlacement.getClass().getName(), error); + } + } + + private static Method resolveFrequencyMethod() { + Method frequencyMethod = null; + for (Method method : AbstractSpreadingStructurePlacement.class.getDeclaredMethods()) { + if (Modifier.isStatic(method.getModifiers()) + || method.getParameterCount() != 0 + || method.getReturnType() != float.class) { + continue; + } + if (frequencyMethod != null) { + throw new IllegalStateException( + "Native structure placement exposes multiple zero-argument float accessors"); + } + frequencyMethod = method; + } + if (frequencyMethod == null || !frequencyMethod.trySetAccessible()) { + throw new IllegalStateException( + "Native structure placement frequency accessor is unavailable"); + } + return frequencyMethod; + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureLocatePersistence.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureLocatePersistence.java new file mode 100644 index 000000000..fd22b553a --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureLocatePersistence.java @@ -0,0 +1,351 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.structure.placement.IrisStructureLocator; +import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord; +import art.arcane.iris.spi.IrisLogging; +import net.minecraft.SharedConstants; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.StreamTagVisitor; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.visitors.CollectFields; +import net.minecraft.nbt.visitors.FieldSelector; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.storage.SimpleRegionStorage; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public final class NativeStructureLocatePersistence { + public static final int MAX_SELECTED_CANDIDATE_RETRIES = 64; + private static final int MAX_STORAGE_PROBES = 512; + private static final Method PAPER_LEVEL_TYPE_KEY = paperLevelTypeKey(); + + private NativeStructureLocatePersistence() { + } + + public static Probe probe(ServerLevel level, Structure structure, boolean requireUnreferenced) { + return probe(level, structure, requireUnreferenced, new ProbeBudget(MAX_STORAGE_PROBES)); + } + + public static ProbeBudget probeBudget() { + return new ProbeBudget(MAX_STORAGE_PROBES); + } + + public static Probe probe(ServerLevel level, Structure structure, boolean requireUnreferenced, + ProbeBudget budget) { + return new Probe(level, structure, requireUnreferenced, budget); + } + + public static Search search(Engine engine, String structureKey, + int blockX, int blockZ, int radius, Probe probe) { + return new Search(engine, structureKey, blockX, blockZ, radius, probe); + } + + public static final class Probe { + private final ServerLevel level; + private final Structure structure; + private final String structureKey; + private final boolean requireUnreferenced; + private final ProbeBudget budget; + private final Map storedDecisions = new HashMap<>(); + + Probe(ServerLevel level, Structure structure, boolean requireUnreferenced, + ProbeBudget budget) { + this.level = level; + this.structure = structure; + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + Identifier identifier = registry.getKey(structure); + if (identifier == null) { + throw new IllegalStateException("Cannot probe an unregistered native structure"); + } + this.structureKey = identifier.toString(); + this.requireUnreferenced = requireUnreferenced; + this.budget = budget; + } + + public boolean accepts(int chunkX, int chunkZ) { + ChunkAccess loaded = level.getChunkSource().getChunk( + chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false); + if (loaded != null) { + StructureStart start = loaded.getStartForStructure(structure); + return start != null && start.isValid() + && (!requireUnreferenced || start.canBeReferenced()); + } + + ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); + Boolean storedDecision = storedDecisions.get(chunkPos.pack()); + if (storedDecision != null) { + return storedDecision; + } + boolean accepted = budget.acceptsStored( + level, chunkPos, structureKey, requireUnreferenced); + storedDecisions.put(chunkPos.pack(), accepted); + return accepted; + } + + public StructureStart verifySelected(int chunkX, int chunkZ) { + ChunkAccess chunk = level.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS); + StructureStart start = chunk.getStartForStructure(structure); + if (start == null || !start.isValid()) { + return null; + } + if (requireUnreferenced) { + if (!start.canBeReferenced()) { + return null; + } + } + return start; + } + + private void reference(StructureStart start) { + if (requireUnreferenced) { + level.structureManager().addReference(start); + } + } + } + + public static final class ProbeBudget { + private final int maximum; + private final Map storedChunks = new HashMap<>(); + private final ChunkStorageScanner scanner; + private final StoredChunkDatafixer datafixer; + private int used; + private boolean scanFailureReported; + + private ProbeBudget(int maximum) { + this(maximum, null, ProbeBudget::datafix); + } + + ProbeBudget(int maximum, ChunkStorageScanner scanner) { + this(maximum, scanner, ProbeBudget::datafix); + } + + ProbeBudget(int maximum, ChunkStorageScanner scanner, + StoredChunkDatafixer datafixer) { + this.maximum = maximum; + this.scanner = scanner; + this.datafixer = Objects.requireNonNull( + datafixer, "Stored chunk datafixer must not be null"); + } + + private void claim() { + if (used >= maximum) { + throw new IrisStructureLocator.CandidateSearchLimitException(); + } + used++; + } + + private StoredChunkState storedState(ServerLevel level, ChunkPos chunkPos) { + StoredChunkState cached = storedChunks.get(chunkPos.pack()); + if (cached != null) { + return cached; + } + claim(); + CollectFields fields = new CollectFields( + new FieldSelector(IntTag.TYPE, "DataVersion"), + new FieldSelector("Level", "Structures", CompoundTag.TYPE, "Starts"), + new FieldSelector("structures", CompoundTag.TYPE, "starts")); + ChunkStorageScanner activeScanner = scanner == null + ? level.getChunkSource().chunkMap.chunkScanner()::scanChunk : scanner; + try { + activeScanner.scan(chunkPos, fields).join(); + } catch (RuntimeException error) { + if (!scanFailureReported) { + scanFailureReported = true; + IrisLogging.reportError( + "Native structure locate could not scan stored chunk state; candidates will be verified by loading their structure starts.", + error); + } + StoredChunkState unresolved = StoredChunkState.missing(); + storedChunks.put(chunkPos.pack(), unresolved); + return unresolved; + } + Tag result = fields.getResult(); + StoredChunkState resolved; + try { + resolved = result instanceof CompoundTag storedChunk + ? StoredChunkState.parse(datafixer.datafix(level, storedChunk)) + : StoredChunkState.missing(); + } catch (RuntimeException error) { + if (!scanFailureReported) { + scanFailureReported = true; + IrisLogging.reportError( + "Native structure locate could not datafix stored chunk state; candidates will be verified by loading their structure starts.", + error); + } + resolved = StoredChunkState.missing(); + } + storedChunks.put(chunkPos.pack(), resolved); + return resolved; + } + + private static CompoundTag datafix(ServerLevel level, CompoundTag storedChunk) { + int dataVersion = NbtUtils.getDataVersion(storedChunk); + int currentDataVersion = SharedConstants.getCurrentVersion().dataVersion().version(); + if (dataVersion >= currentDataVersion) { + return storedChunk; + } + if (level == null) { + throw new IllegalStateException("Stored chunk datafix requires an active level"); + } + CompoundTag context = chunkDataFixContext(level); + SimpleRegionStorage.injectDatafixingContext(storedChunk, context); + return DataFixTypes.CHUNK.updateToCurrentVersion( + level.getServer().getFixerUpper(), storedChunk, dataVersion); + } + + private static CompoundTag chunkDataFixContext(ServerLevel level) { + CompoundTag context = new CompoundTag(); + String levelIdentifier = level.dimension().identifier().toString(); + if (PAPER_LEVEL_TYPE_KEY == null) { + context.putString("dimension", levelIdentifier); + } else { + context.putString("dimension", paperLevelTypeKey(level).identifier().toString()); + context.putString("level_identifier", levelIdentifier); + } + level.getChunkSource().getGenerator().getTypeNameForDataFixer() + .ifPresent(identifier -> context.putString("generator", identifier.toString())); + return context; + } + + boolean acceptsStored(ServerLevel level, ChunkPos chunkPos, + String structureKey, boolean requireUnreferenced) { + return storedState(level, chunkPos).accepts(structureKey, requireUnreferenced); + } + + int used() { + return used; + } + } + + public static final class Search { + private final Engine engine; + private final String structureKey; + private final int blockX; + private final int blockZ; + private final int radius; + private final Probe probe; + private final Set rejectedChunks = new HashSet<>(); + + private Search(Engine engine, String structureKey, int blockX, int blockZ, + int radius, Probe probe) { + this.engine = engine; + this.structureKey = structureKey; + this.blockX = blockX; + this.blockZ = blockZ; + this.radius = radius; + this.probe = probe; + } + + public IrisStructureLocator.LocateResult predict() { + if (rejectedChunks.size() >= MAX_SELECTED_CANDIDATE_RETRIES) { + return new IrisStructureLocator.LocateResult( + IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED, 0, 0, 0); + } + return IrisStructureLocator.locateInPlacementRings( + engine, structureKey, blockX, blockZ, radius, + (chunkX, chunkZ) -> !rejectedChunks.contains(ChunkPos.pack(chunkX, chunkZ)) + && probe.accepts(chunkX, chunkZ)); + } + + public VerifiedStart verify(IrisStructureLocator.LocateResult result) { + int chunkX = result.originX() >> 4; + int chunkZ = result.originZ() >> 4; + StructureStart start = probe.verifySelected(chunkX, chunkZ); + if (start == null) { + return null; + } + NativeStructureOwnershipRecord ownership = NativeStructureOwnershipRecovery.resolve( + engine, probe.level, structureKey, probe.structure, start); + if (ownership == null) { + return null; + } + return new VerifiedStart(start, ownership); + } + + public void reject(IrisStructureLocator.LocateResult result) { + rejectedChunks.add(ChunkPos.pack(result.originX() >> 4, result.originZ() >> 4)); + } + + public void reference(VerifiedStart verified) { + probe.reference(verified.start()); + } + } + + private static Method paperLevelTypeKey() { + try { + return ServerLevel.class.getMethod("getTypeKey"); + } catch (NoSuchMethodException ignored) { + return null; + } + } + + private static ResourceKey paperLevelTypeKey(ServerLevel level) { + try { + Object value = PAPER_LEVEL_TYPE_KEY.invoke(level); + if (value instanceof ResourceKey key) { + return key; + } + throw new IllegalStateException("Paper level type key has an unexpected value"); + } catch (IllegalAccessException | InvocationTargetException error) { + throw new IllegalStateException("Cannot read Paper level type key", error); + } + } + + public record VerifiedStart(StructureStart start, + NativeStructureOwnershipRecord ownership) { + } + + @FunctionalInterface + interface ChunkStorageScanner { + CompletableFuture scan(ChunkPos chunkPos, StreamTagVisitor visitor); + } + + @FunctionalInterface + interface StoredChunkDatafixer { + CompoundTag datafix(ServerLevel level, CompoundTag storedChunk); + } + + private record StoredChunkState(boolean stored, boolean startsPresent, CompoundTag starts) { + private static StoredChunkState missing() { + return new StoredChunkState(false, false, new CompoundTag()); + } + + private static StoredChunkState parse(CompoundTag chunk) { + CompoundTag structures = chunk.getCompoundOrEmpty("structures"); + return structures.getCompound("starts") + .map(starts -> new StoredChunkState(true, true, starts)) + .orElseGet(() -> new StoredChunkState(true, false, new CompoundTag())); + } + + private boolean accepts(String structureKey, boolean requireUnreferenced) { + if (!stored || !startsPresent) { + return true; + } + return starts.getCompound(structureKey).map(start -> { + String id = start.getStringOr("id", ""); + return !StructureStart.INVALID_START_ID.equals(id) + && (!requireUnreferenced || start.getIntOr("references", 0) == 0); + }).orElse(false); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureLocateResults.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureLocateResults.java new file mode 100644 index 000000000..7ee4adb22 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureLocateResults.java @@ -0,0 +1,58 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 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 . + */ + +package art.arcane.iris.nativegen.v26_3_R1; + +import com.mojang.datafixers.util.Pair; +import net.minecraft.core.BlockPos; + +public final class NativeStructureLocateResults { + private NativeStructureLocateResults() { + } + + public static Pair nearest(BlockPos origin, Pair replacement, + Pair nativeResult) { + if (replacement == null) { + return nativeResult; + } + if (nativeResult == null) { + return replacement; + } + return horizontalDistanceSquared(origin, nativeResult.getFirst()) + <= horizontalDistanceSquared(origin, replacement.getFirst()) ? nativeResult : replacement; + } + + public static Pair selectAndReference( + BlockPos origin, + Pair replacement, Runnable replacementReference, + Pair nativeResult, Runnable nativeReference) { + Pair selected = nearest(origin, replacement, nativeResult); + if (selected == replacement && replacement != null) { + replacementReference.run(); + } else if (selected == nativeResult && nativeResult != null) { + nativeReference.run(); + } + return selected; + } + + private static long horizontalDistanceSquared(BlockPos origin, BlockPos target) { + long dx = (long) target.getX() - origin.getX(); + long dz = (long) target.getZ() - origin.getZ(); + return dx * dx + dz * dz; + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureOwnershipFingerprint.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureOwnershipFingerprint.java new file mode 100644 index 000000000..cbba9630b --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureOwnershipFingerprint.java @@ -0,0 +1,239 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord; +import art.arcane.iris.structure.nativegen.NativeStructurePlacementPlanner; +import art.arcane.iris.structure.nativegen.NativeStructureStartPlan; +import art.arcane.iris.structure.placement.StructurePlacementGrid; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure; +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 java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HexFormat; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +public final class NativeStructureOwnershipFingerprint { + private static final Comparator PIECE_ORDER = Comparator + .comparing(PieceIdentity::type) + .thenComparingInt(PieceIdentity::minX) + .thenComparingInt(PieceIdentity::minY) + .thenComparingInt(PieceIdentity::minZ) + .thenComparingInt(PieceIdentity::maxX) + .thenComparingInt(PieceIdentity::maxY) + .thenComparingInt(PieceIdentity::maxZ) + .thenComparingInt(PieceIdentity::orientation) + .thenComparingInt(PieceIdentity::generationDepth) + .thenComparing(PieceIdentity::detail); + + private NativeStructureOwnershipFingerprint() { + } + + public static NativeStructureOwnershipRecord capture(String structureKey, + StructureStart start, + NativeStructureStartPlan plan, + BoundingBox referenceEnvelope) { + StructureStart resolvedStart = requireValid(start); + NativeStructureStartPlan resolvedPlan = Objects.requireNonNull( + plan, "Native structure start plan must not be null"); + BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(resolvedStart); + BoundingBox references = Objects.requireNonNull( + referenceEnvelope, "Native structure reference envelope must not be null"); + ChunkPos origin = resolvedStart.getChunkPos(); + return NativeStructureOwnershipRecord.create( + structureKey, + origin.x(), + origin.z(), + StructurePlacementGrid.placementIdentity(resolvedPlan.placement()), + resolvedPlan.baseY(), + content.minX(), + content.minY(), + content.minZ(), + content.maxX(), + content.maxY(), + content.maxZ(), + locatorY(resolvedStart), + references.minX() >> 4, + references.maxX() >> 4, + references.minZ() >> 4, + references.maxZ() >> 4, + fingerprint(structureKey, resolvedStart), + NativeStructurePlacementPlanner.decisionFor(resolvedPlan) + ); + } + + public static boolean matches(NativeStructureOwnershipRecord record, StructureStart start) { + if (record == null || start == null || !start.isValid()) { + return false; + } + ChunkPos origin = start.getChunkPos(); + if (origin.x() != record.originChunkX() + || origin.z() != record.originChunkZ() + || !fingerprint(record.structureKey(), start).equals(record.contentFingerprint())) { + return false; + } + BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(start); + if (content.getXSpan() != (record.contentMaxX() - record.contentMinX()) + 1 + || content.getYSpan() != (record.contentMaxY() - record.contentMinY()) + 1 + || content.getZSpan() != (record.contentMaxZ() - record.contentMinZ()) + 1) { + return false; + } + return start.getStructure() instanceof OceanMonumentStructure + || content.minX() == record.contentMinX() + && content.minY() == record.contentMinY() + && content.minZ() == record.contentMinZ() + && content.maxX() == record.contentMaxX() + && content.maxY() == record.contentMaxY() + && content.maxZ() == record.contentMaxZ(); + } + + public static String fingerprint(String structureKey, StructureStart start) { + StructureStart resolvedStart = requireValid(start); + MessageDigest digest = sha256(); + updateString(digest, Objects.requireNonNull(structureKey, + "Native structure key must not be null").trim().toLowerCase(Locale.ROOT)); + updateInt(digest, resolvedStart.getChunkPos().x()); + updateInt(digest, resolvedStart.getChunkPos().z()); + BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(resolvedStart); + List pieces = contentPieces(resolvedStart, content.minY()); + updateInt(digest, pieces.size()); + for (PieceIdentity piece : pieces) { + updateString(digest, piece.type()); + updateInt(digest, piece.minX()); + updateInt(digest, piece.minY()); + updateInt(digest, piece.minZ()); + updateInt(digest, piece.maxX()); + updateInt(digest, piece.maxY()); + updateInt(digest, piece.maxZ()); + updateInt(digest, piece.orientation()); + updateInt(digest, piece.generationDepth()); + updateString(digest, piece.detail()); + } + return HexFormat.of().formatHex(digest.digest()); + } + + public static int locatorY(StructureStart start) { + StructureStart resolvedStart = requireValid(start); + if (!resolvedStart.getPieces().isEmpty()) { + return resolvedStart.getPieces().getFirst().getLocatorPosition().getY(); + } + throw new IllegalStateException("Native structure contains no locatable content pieces"); + } + + private static List contentPieces(StructureStart start, int contentMinY) { + List pieces = new ArrayList<>(); + for (StructurePiece piece : start.getPieces()) { + BoundingBox bounds = piece.getBoundingBox(); + Identifier type = BuiltInRegistries.STRUCTURE_PIECE.getKey(piece.getType()); + Direction orientation = piece.getOrientation(); + String detail = pieceDetail(piece, contentMinY); + pieces.add(new PieceIdentity( + type == null ? "" : type.toString(), + bounds.minX(), bounds.minY() - contentMinY, bounds.minZ(), + bounds.maxX(), bounds.maxY() - contentMinY, bounds.maxZ(), + orientation == null ? -1 : orientation.get2DDataValue(), + piece.getGenDepth(), + detail + )); + } + if (pieces.isEmpty()) { + throw new IllegalStateException("Native structure contains no content pieces"); + } + pieces.sort(PIECE_ORDER); + return List.copyOf(pieces); + } + + private static String pieceDetail(StructurePiece piece, int contentMinY) { + if (!(piece instanceof PoolElementStructurePiece poolPiece)) { + return ""; + } + return poolElementIdentity(poolPiece.getElement()) + "|" + + poolPiece.getPosition().getX() + "," + + (poolPiece.getPosition().getY() - contentMinY) + "," + + poolPiece.getPosition().getZ() + "|" + + poolPiece.getGroundLevelDelta() + "|" + + poolPiece.getRotation().getSerializedName(); + } + + private static String poolElementIdentity(StructurePoolElement element) { + Identifier type = BuiltInRegistries.STRUCTURE_POOL_ELEMENT.getKey(element.getType()); + StringBuilder identity = new StringBuilder(element.getClass().getName()) + .append('|') + .append(type == null ? "" : type) + .append('|') + .append(element.getProjection().getSerializedName()) + .append('|') + .append(element.getGroundLevelDelta()); + if (element == EmptyPoolElement.INSTANCE) { + return identity.append("|empty").toString(); + } + if (element instanceof SinglePoolElement single) { + try { + return identity.append("|template=") + .append(single.getTemplateLocation()).toString(); + } catch (RuntimeException inlineTemplate) { + return identity.append("|runtime-template").toString(); + } + } + if (element instanceof ListPoolElement list) { + identity.append("|list["); + for (StructurePoolElement child : list.getElements()) { + String childIdentity = poolElementIdentity(child); + identity.append(childIdentity.length()).append(':').append(childIdentity); + } + return identity.append(']').toString(); + } + return identity.append("|bounded-custom").toString(); + } + + private static StructureStart requireValid(StructureStart start) { + StructureStart resolved = Objects.requireNonNull( + start, "Native structure start must not be null"); + if (!resolved.isValid()) { + throw new IllegalArgumentException("Native structure start must be valid"); + } + return resolved; + } + + private static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException error) { + throw new IllegalStateException("SHA-256 is unavailable", error); + } + } + + private static void updateString(MessageDigest digest, String value) { + byte[] encoded = value.getBytes(StandardCharsets.UTF_8); + updateInt(digest, encoded.length); + digest.update(encoded); + } + + private static void updateInt(MessageDigest digest, int value) { + digest.update((byte) (value >>> 24)); + digest.update((byte) (value >>> 16)); + digest.update((byte) (value >>> 8)); + digest.update((byte) value); + } + + private record PieceIdentity(String type, + int minX, int minY, int minZ, + int maxX, int maxY, int maxZ, + int orientation, int generationDepth, String detail) { + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureOwnershipRecovery.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureOwnershipRecovery.java new file mode 100644 index 000000000..c237a7e1f --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureOwnershipRecovery.java @@ -0,0 +1,195 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy; +import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord; +import art.arcane.iris.structure.nativegen.NativeStructureOwnershipStore; +import art.arcane.iris.structure.nativegen.NativeStructurePlacementPlanner; +import art.arcane.iris.structure.nativegen.NativeStructureStartPlan; +import art.arcane.iris.structure.placement.StructurePlacementGrid; +import art.arcane.iris.structure.placement.IrisStructureTerrain; +import art.arcane.iris.structure.placement.IrisStructureTerrainMode; +import art.arcane.iris.structure.nativegen.NativeStructureGenerationStatus; +import art.arcane.iris.structure.nativegen.NativeStructureSuppression; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; + +import java.util.Locale; +import java.util.Objects; + +public final class NativeStructureOwnershipRecovery { + private NativeStructureOwnershipRecovery() { + } + + public static NativeStructureOwnershipRecord resolve( + Engine engine, ServerLevel level, String structureKey, + Structure structure, StructureStart start) { + Objects.requireNonNull(engine, "Native structure ownership recovery requires an engine"); + ServerLevel activeLevel = Objects.requireNonNull( + level, "Native structure ownership recovery requires a level"); + Structure activeStructure = Objects.requireNonNull( + structure, "Native structure ownership recovery requires a structure"); + if (start == null || !start.isValid() || start.getStructure() != activeStructure) { + return null; + } + ChunkPos origin = start.getChunkPos(); + NativeStructureOwnershipRecord persisted = NativeStructureOwnershipStore.findPersisted( + engine, structureKey, origin.x(), origin.z()); + if (persisted != null) { + NativeStructureOwnershipRecord refreshed = refreshReferenceEnvelope( + structureKey, activeStructure, start, persisted); + if (refreshed != null) { + if (refreshed != persisted) { + NativeStructureOwnershipStore.record(engine, refreshed); + } + return refreshed; + } + NativeStructureOwnershipStore.discard( + engine, structureKey, origin.x(), origin.z()); + } + NativeStructureStartPlan plan = NativeStructurePlacementPlanner.matchingPlan( + engine, structureKey, origin.x(), origin.z()); + if (!matchesPlan(structureKey, origin, plan)) { + return null; + } + Registry registry = activeLevel.registryAccess().lookupOrThrow(Registries.STRUCTURE); + Holder holder = registry.wrapAsHolder(activeStructure); + ChunkGeneratorStructureState state = activeLevel.getChunkSource().getGeneratorState(); + if (naturalStartIsAmbiguous(engine, structureKey, activeStructure, holder, state, plan)) { + return null; + } + StructureStart expected = generateExpected( + engine, activeLevel, holder, plan, start.getReferences()); + NativeStructureOwnershipRecord recovered = proveCandidate( + structureKey, activeStructure, start, plan, expected, false); + if (recovered == null) { + return null; + } + NativeStructureOwnershipStore.record(engine, recovered); + return recovered; + } + + static NativeStructureOwnershipRecord refreshReferenceEnvelope( + String structureKey, Structure structure, StructureStart start, + NativeStructureOwnershipRecord ownership) { + if (structure == null || start == null || !start.isValid() + || start.getStructure() != structure || ownership == null + || !ownership.structureKey().equals(normalize(structureKey)) + || !NativeStructureOwnershipFingerprint.matches(ownership, start)) { + return null; + } + IrisStructureTerrain terrain = NativeStructureTerrainIntegrator.resolveNativeTerrain( + start, ownership.restoredDecision().terrain()); + if (terrain.resolvedMode() != IrisStructureTerrainMode.VACUUM) { + return ownership; + } + BoundingBox expected = NativeStructureReferenceEnvelope.referenceBounds( + start, structure, terrain, structureKey); + int referenceMinChunkX = expected.minX() >> 4; + int referenceMaxChunkX = expected.maxX() >> 4; + int referenceMinChunkZ = expected.minZ() >> 4; + int referenceMaxChunkZ = expected.maxZ() >> 4; + if (ownership.referenceMinChunkX() == referenceMinChunkX + && ownership.referenceMaxChunkX() == referenceMaxChunkX + && ownership.referenceMinChunkZ() == referenceMinChunkZ + && ownership.referenceMaxChunkZ() == referenceMaxChunkZ) { + return ownership; + } + return ownership.withReferenceEnvelope( + referenceMinChunkX, referenceMaxChunkX, + referenceMinChunkZ, referenceMaxChunkZ); + } + + static NativeStructureOwnershipRecord proveCandidate( + String structureKey, Structure structure, StructureStart persisted, + NativeStructureStartPlan plan, StructureStart expected, + boolean naturalStartAmbiguous) { + if (naturalStartAmbiguous || structure == null + || persisted == null || !persisted.isValid() + || expected == null || !expected.isValid() + || persisted.getStructure() != structure + || expected.getStructure() != structure + || !matchesPlan(structureKey, persisted.getChunkPos(), plan) + || !persisted.getChunkPos().equals(expected.getChunkPos())) { + return null; + } + BoundingBox referenceEnvelope = NativeStructureReferenceEnvelope.referenceBounds( + expected, structure, plan.placement().resolvedTerrain(), structureKey); + NativeStructureOwnershipRecord candidate = NativeStructureOwnershipFingerprint.capture( + structureKey, expected, plan, referenceEnvelope); + if (candidate.placementIdentity() + != StructurePlacementGrid.placementIdentity(plan.placement())) { + return null; + } + return NativeStructureOwnershipFingerprint.matches(candidate, persisted) + ? candidate : null; + } + + private static boolean matchesPlan(String structureKey, ChunkPos origin, + NativeStructureStartPlan plan) { + if (structureKey == null || structureKey.isBlank() || plan == null) { + return false; + } + return plan.chunkX() == origin.x() + && plan.chunkZ() == origin.z() + && normalize(structureKey).equals(normalize(plan.source().getStructure())); + } + + private static StructureStart generateExpected( + Engine engine, ServerLevel level, Holder holder, + NativeStructureStartPlan plan, int references) { + ChunkGeneratorStructureState state = level.getChunkSource().getGeneratorState(); + ChunkGenerator generator = level.getChunkSource().getGenerator(); + BiomeSource biomeSource = generator.getBiomeSource(); + NativeStructureFactory.GenerationContext generationContext = + new NativeStructureFactory.GenerationContext( + level.registryAccess(), + generator, + biomeSource, + state.randomState(), + level.getStructureTemplateManager(), + state.getLevelSeed(), + level.dimension(), + level, + biome -> true, + generator.getSeaLevel(), + (x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight() + ); + return NativeStructureFactory.generate(generationContext, holder, plan, references); + } + + private static boolean naturalStartIsAmbiguous( + Engine engine, String structureKey, Structure structure, + Holder holder, ChunkGeneratorStructureState state, + NativeStructureStartPlan plan) { + if (plan.placement().getNativeSuppression() == NativeStructureSuppression.REPLACE_SOURCE) { + return false; + } + NativeStructureGenerationStatus sourceStatus = NativeStructureGenerationPolicy.resolve( + engine, structureKey, + NativeStructureVegetationClearer.isUndergroundStep(structure.step())).status(); + if (sourceStatus == NativeStructureGenerationStatus.REPLACED_BY_IRIS) { + return false; + } + for (StructurePlacement placement : state.getPlacementsForStructure(holder)) { + if (placement.isStructureChunk(state, plan.chunkX(), plan.chunkZ())) { + return true; + } + } + return false; + } + + private static String normalize(String structureKey) { + return structureKey == null ? "" : structureKey.trim().toLowerCase(Locale.ROOT); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructurePostProcessor.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructurePostProcessor.java new file mode 100644 index 000000000..329592aef --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructurePostProcessor.java @@ -0,0 +1,66 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.generation.terrain.IrisMaterialPalette; +import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision; +import art.arcane.iris.structure.placement.IrisStructureStiltSettings; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.StructureStart; + +import java.util.List; +import java.util.function.IntBinaryOperator; + +public final class NativeStructurePostProcessor { + private NativeStructurePostProcessor() { + } + + public static void place(WorldGenLevel world, StructureManager structureManager, ChunkGenerator generator, + WorldgenRandom random, BoundingBox area, ChunkPos chunkPos, String structureId, + StructureStart start, IrisNativeStructureDecision decision, + PaletteBlockResolver paletteBlockResolver, + IntBinaryOperator surfaceHeight) { + IrisStructureStiltSettings stilt = decision.stilt(); + NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment(start, structureId, decision.yShift(), + generator.getSeaLevel(), area.minY(), area.maxY() + 1); + start.placeInChunk(world, structureManager, generator, random, area, chunkPos); + if (stilt != null) { + NativeStructureFoundationBuilder.placeStilts(world, area, structureId, start, stilt, + paletteBlockResolver, surfaceHeight, + !NativeStructureVegetationClearer.isUndergroundStep(start.getStructure().step())); + } + } + + public static void prepareTerrain(WorldGenLevel world, BoundingBox area, + List targets, + PaletteBlockResolver paletteBlockResolver) { + if (targets == null || targets.isEmpty()) { + return; + } + NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain = + NativeStructureTerrainIntegrator.captureSourceTerrain(world, area, targets); + for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) { + NativeStructureTerrainIntegrator.integrateTerrain(world, area, target.structureId(), target.start(), + target.terrain(), paletteBlockResolver, sourceTerrain); + } + for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) { + if (NativeStructureTerrainIntegrator.clearsLegacyTemplateAir( + target.start(), target.terrain())) { + NativeStructureTerrainIntegrator.clearLegacyTemplateAir( + world, area, target.start(), () -> world.getLevel().getStructureTemplateManager()); + } + } + NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world, area, targets, () -> world.getLevel().getStructureTemplateManager()); + } + + @FunctionalInterface + public interface PaletteBlockResolver { + BlockState resolve(IrisMaterialPalette palette, RNG rng, int x, int y, int z); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureReferenceEnvelope.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureReferenceEnvelope.java new file mode 100644 index 000000000..4f59cb9b8 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureReferenceEnvelope.java @@ -0,0 +1,239 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.structure.placement.IrisStructureTerrain; +import art.arcane.iris.structure.placement.IrisStructureTerrainMode; +import art.arcane.iris.spi.IrisLogging; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; + +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class NativeStructureReferenceEnvelope { + private static final int MAX_REFERENCE_DISTANCE_CHUNKS = 8; + private static final Set WARNED_CLIPPED_TERRAIN = ConcurrentHashMap.newKeySet(); + private static final Set WARNED_SKIPPED_CONTENT = ConcurrentHashMap.newKeySet(); + + private NativeStructureReferenceEnvelope() { + } + + public static StructureStart wrap(StructureStart generated, Structure source, int references, + IrisStructureTerrain terrain) { + return wrap(generated, source, references, terrain, null); + } + + public static StructureStart wrap(StructureStart generated, Structure source, int references, + IrisStructureTerrain terrain, String structureKey) { + referenceBounds(generated, source, terrain, structureKey); + return new StructureStart( + source, + generated.getChunkPos(), + references, + new PiecesContainer(List.copyOf(generated.getPieces())) + ); + } + + public static StructureStart wrapForPublication( + StructureStart generated, Structure source, int references, + IrisStructureTerrain terrain, String structureKey) { + try { + return wrap(generated, source, references, terrain, structureKey); + } catch (UnrepresentableContentException exception) { + warnSkippedContent(generated, source, structureKey); + return StructureStart.INVALID_START; + } + } + + public static BoundingBox referenceBounds(StructureStart start, Structure source, + IrisStructureTerrain terrain) { + return referenceBounds(start, source, terrain, null); + } + + public static BoundingBox referenceBounds(StructureStart start, Structure source, + IrisStructureTerrain terrain, + String structureKey) { + IrisStructureTerrainMode mode = terrain == null + ? IrisStructureTerrainMode.PRESERVE : terrain.resolvedMode(); + boolean usesEnvelope = mode == IrisStructureTerrainMode.BORE + || mode == IrisStructureTerrainMode.FORCE_CARVE + || mode == IrisStructureTerrainMode.VACUUM + || mode == IrisStructureTerrainMode.FLATTEN + || mode == IrisStructureTerrainMode.ENCASE; + int horizontalPadding = mode == IrisStructureTerrainMode.VACUUM + ? NativeStructureSurfaceFitter.surfaceTerrainRadius() + : usesEnvelope ? Math.max(0, terrain.getHorizontalPadding()) : 0; + BoundingBox content = contentBounds(start); + if (!fitsReferenceRange(start.getChunkPos(), content)) { + throw new UnrepresentableContentException("Native structure content at " + + start.getChunkPos().x() + "," + start.getChunkPos().z() + + " exceeds Minecraft's " + MAX_REFERENCE_DISTANCE_CHUNKS + + "-chunk structure reference range"); + } + BoundingBox adjustedEnvelope; + boolean arithmeticClipped = false; + try { + BoundingBox envelope = horizontalPadding == 0 + ? content + : new BoundingBox( + Math.subtractExact(content.minX(), horizontalPadding), + content.minY(), + Math.subtractExact(content.minZ(), horizontalPadding), + Math.addExact(content.maxX(), horizontalPadding), + content.maxY(), + Math.addExact(content.maxZ(), horizontalPadding) + ); + adjustedEnvelope = mode == IrisStructureTerrainMode.SOURCE + ? source.adjustBoundingBox(content) : envelope; + adjustedEnvelope = encapsulating(adjustedEnvelope, content); + } catch (ArithmeticException exception) { + adjustedEnvelope = maximumReferenceBounds(start.getChunkPos(), content); + arithmeticClipped = true; + } + BoundingBox referencedEnvelope = clampReferenceRange(start.getChunkPos(), adjustedEnvelope); + if (arithmeticClipped || !sameHorizontalBounds(adjustedEnvelope, referencedEnvelope)) { + warnClippedTerrain(start, source, structureKey); + return referencedEnvelope; + } + return adjustedEnvelope; + } + + public static boolean contentFitsReferenceRange(StructureStart start) { + try { + return fitsReferenceRange(start.getChunkPos(), contentBounds(start)); + } catch (UnrepresentableContentException exception) { + return false; + } + } + + public static BoundingBox contentBounds(StructureStart start) { + return contentBounds(start.getPieces()); + } + + private static BoundingBox contentBounds(List pieces) { + BoundingBox bounds = null; + for (StructurePiece piece : pieces) { + bounds = bounds == null + ? copy(piece.getBoundingBox()) + : bounds.encapsulate(piece.getBoundingBox()); + } + if (bounds == null) { + throw new IllegalStateException("Native jigsaw generated no content pieces"); + } + return bounds; + } + + private static BoundingBox clampReferenceRange(ChunkPos startChunk, BoundingBox envelope) { + int minX = minimumReferenceBlock(startChunk.x()); + int maxX = maximumReferenceBlock(startChunk.x()); + int minZ = minimumReferenceBlock(startChunk.z()); + int maxZ = maximumReferenceBlock(startChunk.z()); + return new BoundingBox( + Math.max(envelope.minX(), minX), + envelope.minY(), + Math.max(envelope.minZ(), minZ), + Math.min(envelope.maxX(), maxX), + envelope.maxY(), + Math.min(envelope.maxZ(), maxZ) + ); + } + + private static BoundingBox maximumReferenceBounds(ChunkPos startChunk, BoundingBox content) { + return new BoundingBox( + minimumReferenceBlock(startChunk.x()), content.minY(), + minimumReferenceBlock(startChunk.z()), + maximumReferenceBlock(startChunk.x()), content.maxY(), + maximumReferenceBlock(startChunk.z()) + ); + } + + private static boolean fitsReferenceRange(ChunkPos startChunk, BoundingBox content) { + return content.minX() >= minimumReferenceBlock(startChunk.x()) + && content.maxX() <= maximumReferenceBlock(startChunk.x()) + && content.minZ() >= minimumReferenceBlock(startChunk.z()) + && content.maxZ() <= maximumReferenceBlock(startChunk.z()); + } + + private static int minimumReferenceBlock(int originChunk) { + return checkedReferenceBlock( + ((long) originChunk - MAX_REFERENCE_DISTANCE_CHUNKS) << 4, originChunk); + } + + private static int maximumReferenceBlock(int originChunk) { + return checkedReferenceBlock( + (((long) originChunk + MAX_REFERENCE_DISTANCE_CHUNKS) << 4) + 15L, originChunk); + } + + private static int checkedReferenceBlock(long block, int originChunk) { + if (block < Integer.MIN_VALUE || block > Integer.MAX_VALUE) { + throw new UnrepresentableContentException("Native structure origin chunk " + originChunk + + " exceeds Minecraft's block coordinate range"); + } + return (int) block; + } + + private static boolean sameHorizontalBounds(BoundingBox left, BoundingBox right) { + return left.minX() == right.minX() + && left.minZ() == right.minZ() + && left.maxX() == right.maxX() + && left.maxZ() == right.maxZ(); + } + + private static BoundingBox copy(BoundingBox bounds) { + return new BoundingBox( + bounds.minX(), bounds.minY(), bounds.minZ(), + bounds.maxX(), bounds.maxY(), bounds.maxZ()); + } + + private static BoundingBox encapsulating(BoundingBox left, BoundingBox right) { + return new BoundingBox( + Math.min(left.minX(), right.minX()), + Math.min(left.minY(), right.minY()), + Math.min(left.minZ(), right.minZ()), + Math.max(left.maxX(), right.maxX()), + Math.max(left.maxY(), right.maxY()), + Math.max(left.maxZ(), right.maxZ()) + ); + } + + private static void warnClippedTerrain( + StructureStart start, Structure source, String structureKey) { + String key = warningKey(source, structureKey); + if (WARNED_CLIPPED_TERRAIN.add(key)) { + IrisLogging.warn("Clipping optional terrain envelope for native structure '" + + key + "' at " + start.getChunkPos().x() + "," + start.getChunkPos().z() + + " to Minecraft's " + MAX_REFERENCE_DISTANCE_CHUNKS + + "-chunk reference range; all generated content remains exactly referenced"); + } + } + + private static void warnSkippedContent( + StructureStart start, Structure source, String structureKey) { + String key = warningKey(source, structureKey); + if (WARNED_SKIPPED_CONTENT.add(key)) { + IrisLogging.warn("Skipping native structure '" + key + "' at " + + start.getChunkPos().x() + "," + start.getChunkPos().z() + + " because its generated content exceeds Minecraft's " + + MAX_REFERENCE_DISTANCE_CHUNKS + + "-chunk reference range; no structure start, ownership, references, or blocks were published"); + } + } + + private static String warningKey(Structure source, String structureKey) { + if (structureKey != null && !structureKey.isBlank()) { + return structureKey.trim().toLowerCase(Locale.ROOT); + } + return source.getClass().getName(); + } + + public static final class UnrepresentableContentException extends IllegalStateException { + private UnrepresentableContentException(String message) { + super(message); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureReferenceRepair.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureReferenceRepair.java new file mode 100644 index 000000000..ba8c6cf1b --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureReferenceRepair.java @@ -0,0 +1,246 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.generation.runtime.IrisEngine; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy; +import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord; +import art.arcane.iris.structure.nativegen.NativeStructureOwnershipStore; +import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter; +import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision; +import art.arcane.iris.spi.IrisLogging; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import net.minecraft.core.Registry; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; + +import java.util.ArrayList; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class NativeStructureReferenceRepair { + private static final int REFERENCE_DISTANCE_CHUNKS = + NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS; + private static final Set WARNED_POLICY_INVALIDATIONS = ConcurrentHashMap.newKeySet(); + + private NativeStructureReferenceRepair() { + } + + public static void createReferences(Engine engine, WorldGenLevel level, + StructureManager structureManager, ChunkAccess targetChunk) { + ChunkPos target = targetChunk.getPos(); + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + ServerLevel serverLevel = level.getLevel(); + List starts = new ArrayList<>(); + for (int originChunkX = target.x() - REFERENCE_DISTANCE_CHUNKS; + originChunkX <= target.x() + REFERENCE_DISTANCE_CHUNKS; originChunkX++) { + for (int originChunkZ = target.z() - REFERENCE_DISTANCE_CHUNKS; + originChunkZ <= target.z() + REFERENCE_DISTANCE_CHUNKS; originChunkZ++) { + try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = + openOriginRuntimeScope(engine, originChunkX, originChunkZ)) { + ChunkAccess originChunk = level.getChunk( + originChunkX, originChunkZ, ChunkStatus.STRUCTURE_STARTS); + for (Map.Entry entry : originChunk.getAllStarts().entrySet()) { + ScannedStart start = scanStart( + engine, serverLevel, structureManager, targetChunk, registry, + originChunk, entry.getKey(), entry.getValue()); + if (start != null && isTargetRelevant(engine, targetChunk, start)) { + starts.add(start); + } + } + } + } + } + for (ScannedStart start : starts) { + boolean valid; + synchronized (start.originChunk()) { + StructureStart current = start.originChunk().getStartForStructure(start.structure()); + valid = current == start.start() && current.isValid(); + } + if (!valid) { + continue; + } + structureManager.addReferenceForStructure( + start.structure(), + start.origin().pack(), targetChunk); + } + } + + private static GenerationHistoryRuntimeRouter.CoordinateScope openOriginRuntimeScope( + Engine engine, + int chunkX, + int chunkZ + ) { + if (!(engine instanceof IrisEngine irisEngine)) { + return null; + } + try { + return irisEngine.openGenerationHistoryCoordinateScope(chunkX << 4, chunkZ << 4); + } catch (IOException failure) { + throw new IllegalStateException("Unable to resolve native structure references for origin " + + chunkX + "," + chunkZ + " through generation history.", failure); + } + } + + private static boolean isTargetRelevant( + Engine engine, ChunkAccess targetChunk, ScannedStart scanned) { + StructureStart start = scanned.start(); + Structure structure = scanned.structure(); + ChunkPos target = targetChunk.getPos(); + if (scanned.ownership() != null) { + return requiresReference(target, scanned.structureKey(), start, scanned.ownership()); + } + if (scanned.registered()) { + return requiresNaturalReference( + engine, target, scanned.structureKey(), structure, start); + } + return NativeStructureReferenceEnvelope.contentBounds(start).intersects( + target.getMinBlockX(), target.getMinBlockZ(), + target.getMaxBlockX(), target.getMaxBlockZ()); + } + + private static ScannedStart scanStart( + Engine engine, ServerLevel level, StructureManager structureManager, + ChunkAccess targetChunk, Registry registry, + ChunkAccess originChunk, Structure structure, StructureStart start) { + Identifier identifier = registry.getKey(structure); + String structureKey = identifier == null + ? structure.getClass().getName() : identifier.toString(); + if (start == null || !start.isValid()) { + return null; + } + if (!NativeStructureReferenceEnvelope.contentFitsReferenceRange(start)) { + if (identifier != null) { + NativeStructureOwnershipStore.discard( + engine, structureKey, start.getChunkPos().x(), start.getChunkPos().z()); + } + invalidateStart(structureManager, originChunk, structure, start, targetChunk); + return null; + } + ChunkPos startOrigin = start.getChunkPos(); + NativeStructureOwnershipRecord ownership = identifier == null ? null + : NativeStructureOwnershipRecovery.resolve( + engine, level, structureKey, structure, start); + if (ownership != null) { + return new ScannedStart( + originChunk, structure, start, structureKey, ownership, true); + } + if (identifier != null && !naturalPolicyAllows(engine, structureKey, structure)) { + invalidateStart(structureManager, originChunk, structure, start, targetChunk); + if (WARNED_POLICY_INVALIDATIONS.add(structureKey)) { + IrisLogging.warn("Invalidating persisted natural structure start '" + + structureKey + "' because the current Iris dimension policy disables it"); + } + return null; + } + return new ScannedStart( + originChunk, structure, start, structureKey, null, identifier != null); + } + + private static boolean naturalPolicyAllows( + Engine engine, String structureKey, Structure structure) { + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve( + engine, structureKey, + NativeStructureVegetationClearer.isUndergroundStep(structure.step())); + return naturalDecisionAllows(decision); + } + + static boolean naturalDecisionAllows(IrisNativeStructureDecision decision) { + return Objects.requireNonNull( + decision, "Native structure decision must not be null").generate(); + } + + private static void invalidateStart( + StructureManager structureManager, ChunkAccess originChunk, + Structure structure, StructureStart start, ChunkAccess targetChunk) { + synchronized (originChunk) { + StructureStart current = originChunk.getStartForStructure(structure); + if (current == start && current.isValid()) { + structureManager.setStartForStructure( + structure, + StructureStart.INVALID_START, originChunk); + } + } + if (targetChunk != null) { + removeTargetReference(targetChunk, structure, start.getChunkPos().pack()); + } + } + + private static void removeTargetReference( + ChunkAccess targetChunk, Structure structure, long origin) { + LongSet current = targetChunk.getAllReferences().get(structure); + if (current == null || !current.contains(origin)) { + return; + } + Map updated = new HashMap<>(targetChunk.getAllReferences()); + LongSet retained = new LongOpenHashSet(current); + retained.remove(origin); + if (retained.isEmpty()) { + updated.remove(structure); + } else { + updated.put(structure, retained); + } + targetChunk.setAllReferences(updated); + } + + static boolean requiresNaturalReference(Engine engine, ChunkPos target, String structureKey, + Structure structure, StructureStart start) { + if (target == null || start == null || !start.isValid()) { + return false; + } + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve( + engine, structureKey, + NativeStructureVegetationClearer.isUndergroundStep(structure.step())); + if (!decision.generate()) { + return false; + } + BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds( + start, structure, + NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()), + structureKey); + return referenceBounds.intersects( + target.getMinBlockX(), target.getMinBlockZ(), + target.getMaxBlockX(), target.getMaxBlockZ()); + } + + static boolean requiresReference(ChunkPos target, String structureKey, + StructureStart start, + NativeStructureOwnershipRecord ownership) { + if (target == null || start == null || !start.isValid() + || ownership == null || !ownership.structureKey().equals(structureKey) + || !ownership.covers(target.x(), target.z())) { + return false; + } + return NativeStructureOwnershipFingerprint.matches(ownership, start); + } + + private record ScannedStart( + ChunkAccess originChunk, + Structure structure, + StructureStart start, + String structureKey, + NativeStructureOwnershipRecord ownership, + boolean registered + ) { + private ChunkPos origin() { + return ownership == null + ? start.getChunkPos() + : new ChunkPos(ownership.originChunkX(), ownership.originChunkZ()); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureReflection.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureReflection.java new file mode 100644 index 000000000..51ab6596d --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureReflection.java @@ -0,0 +1,386 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import com.mojang.datafixers.util.Either; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; +import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces; +import net.minecraft.world.level.levelgen.structure.structures.RuinedPortalPiece; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +final class NativeStructureReflection { + private NativeStructureReflection() { + } + + static Field resolveScatteredHeightPositionField() { + Field resolved = null; + for (Field field : ScatteredFeaturePiece.class.getDeclaredFields()) { + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) || field.getType() != int.class + || !Modifier.isProtected(modifiers) || Modifier.isFinal(modifiers)) { + continue; + } + if (resolved != null) { + throw new IllegalStateException("ScatteredFeaturePiece has multiple mutable protected int fields"); + } + resolved = field; + } + if (resolved == null) { + throw new IllegalStateException("ScatteredFeaturePiece height-position field is missing"); + } + if (!resolved.trySetAccessible()) { + throw new IllegalStateException("ScatteredFeaturePiece height-position field is inaccessible"); + } + return resolved; + } + + static RuinedPortalPiece.VerticalPlacement ruinedPortalVerticalPlacement(RuinedPortalPiece piece) { + try { + return (RuinedPortalPiece.VerticalPlacement) RuinedPortalPlacementAccess.FIELD.get(piece); + } catch (IllegalAccessException error) { + throw new IllegalStateException("Cannot read native ruined portal vertical placement", error); + } + } + + private static Field resolveRuinedPortalPlacementField() { + Field resolved = null; + for (Field field : RuinedPortalPiece.class.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) + || field.getType() != RuinedPortalPiece.VerticalPlacement.class) { + continue; + } + if (resolved != null) { + throw new IllegalStateException("RuinedPortalPiece has multiple vertical-placement fields"); + } + resolved = field; + } + if (resolved == null || !resolved.trySetAccessible()) { + throw new IllegalStateException("RuinedPortalPiece vertical-placement field is inaccessible"); + } + return resolved; + } + + private static Field resolveMonumentChildPiecesField() { + Field resolved = null; + for (Field field : OceanMonumentPieces.MonumentBuilding.class.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || field.getType() != List.class) { + continue; + } + if (resolved != null) { + throw new IllegalStateException("Ocean Monument has multiple instance List fields"); + } + resolved = field; + } + if (resolved == null) { + throw new IllegalStateException("Ocean Monument child-pieces List field is missing"); + } + if (!Modifier.isPrivate(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) { + throw new IllegalStateException("Ocean Monument child-pieces field has an unexpected access contract"); + } + if (!resolved.trySetAccessible()) { + throw new IllegalStateException("Ocean Monument child-pieces field is inaccessible"); + } + return resolved; + } + + static Field resolveSinglePoolTemplateField() { + Field resolved = null; + for (Field field : SinglePoolElement.class.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || field.getType() != Either.class) { + continue; + } + if (resolved != null) { + throw new IllegalStateException("SinglePoolElement has multiple instance Either fields"); + } + resolved = field; + } + if (resolved == null) { + throw new IllegalStateException("SinglePoolElement template Either field is missing"); + } + if (!Modifier.isProtected(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) { + throw new IllegalStateException("SinglePoolElement template field has an unexpected access contract"); + } + if (!resolved.trySetAccessible()) { + throw new IllegalStateException("SinglePoolElement template field is inaccessible"); + } + return resolved; + } + + static StructureTemplate resolveTemplate(SinglePoolElement element, + Supplier templates) { + Object value; + try { + value = SinglePoolTemplateAccess.FIELD.get(element); + } catch (IllegalAccessException error) { + throw new IllegalStateException("Cannot read native structure pool template", error); + } + if (!(value instanceof Either reference)) { + throw new IllegalStateException("Native structure pool template field is not an Either"); + } + return resolveTemplateReference(reference, templates); + } + + @SuppressWarnings("unchecked") + static List resolveTemplateBlocks( + StructureTemplate template, StructurePlaceSettings settings, + BlockPos origin) { + Object value; + try { + value = StructureTemplatePalettesAccess.FIELD.get(template); + } catch (IllegalAccessException error) { + throw new IllegalStateException("Cannot read native structure template palettes", error); + } + if (!(value instanceof List paletteValues)) { + throw new IllegalStateException("Native structure template palettes field is not a List"); + } + List palettes = + (List) paletteValues; + if (palettes.isEmpty()) { + return List.of(); + } + return settings.getRandomPalette(palettes, origin).blocks(); + } + + static StructurePlaceSettings resolvePlacementSettings( + SinglePoolElement element, PoolElementStructurePiece piece, + BoundingBox area) { + LiquidSettings liquidSettings; + try { + liquidSettings = (LiquidSettings) PoolPieceLiquidSettingsAccess.FIELD.get(piece); + } catch (IllegalAccessException error) { + throw new IllegalStateException("Cannot read native structure piece liquid settings", error); + } + try { + return (StructurePlaceSettings) SinglePoolSettingsAccess.METHOD.invoke( + element, piece.getRotation(), area, liquidSettings, false); + } catch (IllegalAccessException | InvocationTargetException error) { + throw new IllegalStateException("Cannot create native structure placement settings", error); + } + } + + @SuppressWarnings("unchecked") + static List processTemplateBlocks( + ServerLevelAccessor world, BlockPos origin, BlockPos referencePos, + StructurePlaceSettings settings, + List blocks, + StructureTemplate template) { + Method forgeMethod = StructureTemplateProcessingAccess.FORGE_METHOD; + if (forgeMethod == null) { + return StructureTemplate.processBlockInfos( + world, origin, referencePos, settings, blocks); + } + try { + return (List) forgeMethod.invoke( + null, world, origin, referencePos, settings, blocks, template); + } catch (IllegalAccessException | InvocationTargetException error) { + throw new IllegalStateException("Cannot process native structure template blocks", error); + } + } + + static StructureTemplate resolveTemplateReference(Either reference, + Supplier templates) { + return reference.map( + location -> resolveNamedTemplate(location, templates), + NativeStructureReflection::requireRuntimeTemplate); + } + + private static StructureTemplate resolveNamedTemplate(Object value, + Supplier templates) { + if (!(value instanceof Identifier identifier)) { + throw new IllegalStateException("Native structure pool template identifier is " + + (value == null ? "null" : value.getClass().getName())); + } + return Objects.requireNonNull(templates == null ? null : templates.get(), + "Native structure template manager is unavailable").getOrCreate(identifier); + } + + private static StructureTemplate requireRuntimeTemplate(Object value) { + if (!(value instanceof StructureTemplate template)) { + throw new IllegalStateException("Native runtime structure pool template is " + + (value == null ? "null" : value.getClass().getName())); + } + return template; + } + + private static Field resolveStructureTemplatePalettesField() { + Field resolved = null; + for (Field field : StructureTemplate.class.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || field.getType() != List.class + || !isPaletteList(field.getGenericType())) { + continue; + } + if (resolved != null) { + throw new IllegalStateException("StructureTemplate has multiple palette List fields"); + } + resolved = field; + } + if (resolved == null) { + throw new IllegalStateException("StructureTemplate palette List field is missing"); + } + if (!Modifier.isFinal(resolved.getModifiers())) { + throw new IllegalStateException("StructureTemplate palette field has an unexpected access contract"); + } + if (!resolved.trySetAccessible()) { + throw new IllegalStateException("StructureTemplate palette field is inaccessible"); + } + return resolved; + } + + private static boolean isPaletteList(Type type) { + if (!(type instanceof ParameterizedType parameterized)) { + return false; + } + Type[] arguments = parameterized.getActualTypeArguments(); + return arguments.length == 1 && arguments[0] == StructureTemplate.Palette.class; + } + + private static Method resolveSinglePoolSettingsMethod() { + Method resolved = null; + Class[] parameterTypes = { + Rotation.class, BoundingBox.class, LiquidSettings.class, boolean.class + }; + for (Method method : SinglePoolElement.class.getDeclaredMethods()) { + if (Modifier.isStatic(method.getModifiers()) + || method.getReturnType() != StructurePlaceSettings.class + || !Arrays.equals(method.getParameterTypes(), parameterTypes)) { + continue; + } + if (resolved != null) { + throw new IllegalStateException("SinglePoolElement has multiple placement-settings methods"); + } + resolved = method; + } + if (resolved == null) { + throw new IllegalStateException("SinglePoolElement placement-settings method is missing"); + } + if (!Modifier.isProtected(resolved.getModifiers())) { + throw new IllegalStateException("SinglePoolElement placement-settings method has an unexpected access contract"); + } + if (!resolved.trySetAccessible()) { + throw new IllegalStateException("SinglePoolElement placement-settings method is inaccessible"); + } + return resolved; + } + + private static Field resolvePoolPieceLiquidSettingsField() { + Field resolved = null; + for (Field field : PoolElementStructurePiece.class.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) + || field.getType() != LiquidSettings.class) { + continue; + } + if (resolved != null) { + throw new IllegalStateException("PoolElementStructurePiece has multiple liquid-settings fields"); + } + resolved = field; + } + if (resolved == null) { + throw new IllegalStateException("PoolElementStructurePiece liquid-settings field is missing"); + } + if (!Modifier.isPrivate(resolved.getModifiers()) + || !Modifier.isFinal(resolved.getModifiers())) { + throw new IllegalStateException("PoolElementStructurePiece liquid-settings field has an unexpected access contract"); + } + if (!resolved.trySetAccessible()) { + throw new IllegalStateException("PoolElementStructurePiece liquid-settings field is inaccessible"); + } + return resolved; + } + + private static Method resolveForgeTemplateProcessingMethod() { + Class[] parameterTypes = { + ServerLevelAccessor.class, BlockPos.class, BlockPos.class, + StructurePlaceSettings.class, List.class, StructureTemplate.class + }; + Method resolved = null; + for (Method method : StructureTemplate.class.getDeclaredMethods()) { + if (!Modifier.isPublic(method.getModifiers()) + || !Modifier.isStatic(method.getModifiers()) + || method.getReturnType() != List.class + || !Arrays.equals(method.getParameterTypes(), parameterTypes)) { + continue; + } + if (resolved != null) { + throw new IllegalStateException( + "StructureTemplate has multiple Forge block-processing methods"); + } + resolved = method; + } + return resolved; + } + + static final class MonumentChildPiecesAccess { + static final Field FIELD = resolveMonumentChildPiecesField(); + + private MonumentChildPiecesAccess() { + } + } + + private static final class RuinedPortalPlacementAccess { + private static final Field FIELD = resolveRuinedPortalPlacementField(); + + private RuinedPortalPlacementAccess() { + } + } + + static final class ScatteredHeightPositionAccess { + static final Field FIELD = resolveScatteredHeightPositionField(); + + private ScatteredHeightPositionAccess() { + } + } + + private static final class SinglePoolTemplateAccess { + private static final Field FIELD = resolveSinglePoolTemplateField(); + + private SinglePoolTemplateAccess() { + } + } + + private static final class StructureTemplatePalettesAccess { + private static final Field FIELD = resolveStructureTemplatePalettesField(); + + private StructureTemplatePalettesAccess() { + } + } + + private static final class SinglePoolSettingsAccess { + private static final Method METHOD = resolveSinglePoolSettingsMethod(); + + private SinglePoolSettingsAccess() { + } + } + + private static final class PoolPieceLiquidSettingsAccess { + private static final Field FIELD = resolvePoolPieceLiquidSettingsField(); + + private PoolPieceLiquidSettingsAccess() { + } + } + + private static final class StructureTemplateProcessingAccess { + private static final Method FORGE_METHOD = resolveForgeTemplateProcessingMethod(); + + private StructureTemplateProcessingAccess() { + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureStartInjector.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureStartInjector.java new file mode 100644 index 000000000..e78026086 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureStartInjector.java @@ -0,0 +1,147 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy; +import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord; +import art.arcane.iris.structure.nativegen.NativeStructureOwnershipStore; +import art.arcane.iris.structure.nativegen.NativeStructurePlacementPlanner; +import art.arcane.iris.structure.nativegen.NativeStructureStartPlan; +import art.arcane.iris.structure.nativegen.NativeStructureGenerationStatus; +import art.arcane.iris.structure.nativegen.NativeStructureSuppression; +import art.arcane.iris.spi.IrisLogging; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class NativeStructureStartInjector { + private static final Set WARNED_DUPLICATE_STRUCTURES = ConcurrentHashMap.newKeySet(); + + private NativeStructureStartInjector() { + } + + public static Map inject(InjectionContext context) { + Objects.requireNonNull(context, "Native structure injection context must not be null"); + ChunkAccess chunk = context.chunk(); + Registry registry = context.registryAccess().lookupOrThrow(Registries.STRUCTURE); + SectionPos section = SectionPos.bottomOf(chunk); + Map configuredStarts = new LinkedHashMap<>(); + for (NativeStructureStartPlan plan : NativeStructurePlacementPlanner.plansAt( + context.engine(), chunk.getPos().x(), chunk.getPos().z())) { + Identifier identifier = Identifier.tryParse(plan.source().getStructure()); + if (identifier == null) { + throw new IllegalStateException("Configured native structure key is invalid: " + + plan.source().getStructure()); + } + Structure structure = registry.getValue(identifier); + if (structure == null) { + throw new IllegalStateException("Configured native structure is not registered: " + identifier); + } + Holder holder = registry.wrapAsHolder(structure); + if (configuredStarts.containsKey(structure)) { + if (WARNED_DUPLICATE_STRUCTURES.add(identifier.toString())) { + IrisLogging.warn("Ignoring duplicate native structure placements for '" + + identifier + "'; the first deterministic candidate owns each chunk start"); + } + continue; + } + StructureStart existing = context.structureManager().getStartForStructure( + structure, chunk); + boolean replacement = plan.placement().getNativeSuppression() + == NativeStructureSuppression.REPLACE_SOURCE; + if (!replacement && existing != null && existing.isValid()) { + NativeStructureGenerationStatus sourceStatus = NativeStructureGenerationPolicy.resolve( + context.engine(), identifier.toString(), + NativeStructureVegetationClearer.isUndergroundStep(structure.step())).status(); + replacement = sourceStatus == NativeStructureGenerationStatus.REPLACED_BY_IRIS; + } + if (!replacement && existing != null && existing.isValid()) { + continue; + } + int references = existing != null && existing.isValid() ? existing.getReferences() : 0; + NativeStructureFactory.GenerationContext generationContext = + new NativeStructureFactory.GenerationContext( + context.registryAccess(), + context.generator(), + context.biomeSource(), + context.structureState().randomState(), + context.templateManager(), + context.structureState().getLevelSeed(), + context.levelKey(), + chunk, + biome -> true, + context.generator().getSeaLevel(), + (x, z) -> Engine.hostHeight(context.engine(), x, z, true) + + context.engine().getMinHeight() + ); + StructureStart generated = NativeStructureFactory.generate( + generationContext, holder, plan, references); + if (!isUsableGeneratedStart(generated)) { + if (replacement) { + context.structureManager().setStartForStructure( + structure, StructureStart.INVALID_START, chunk); + } + NativeStructureOwnershipStore.discard( + context.engine(), identifier.toString(), + chunk.getPos().x(), chunk.getPos().z()); + continue; + } + BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds( + generated, structure, plan.placement().resolvedTerrain(), identifier.toString()); + NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture( + identifier.toString(), generated, plan, referenceBounds); + NativeStructureOwnershipStore.record(context.engine(), ownership); + try { + context.structureManager().setStartForStructure( + structure, generated, chunk); + } catch (RuntimeException | Error publicationError) { + try { + NativeStructureOwnershipStore.discard( + context.engine(), identifier.toString(), + chunk.getPos().x(), chunk.getPos().z()); + } catch (RuntimeException | Error cleanupError) { + publicationError.addSuppressed(cleanupError); + } + throw publicationError; + } + configuredStarts.put(structure, plan); + } + return Map.copyOf(configuredStarts); + } + + static boolean isUsableGeneratedStart(StructureStart start) { + return start != null && start.isValid(); + } + + public record InjectionContext( + Engine engine, + RegistryAccess registryAccess, + ChunkGeneratorStructureState structureState, + StructureManager structureManager, + ChunkAccess chunk, + StructureTemplateManager templateManager, + ResourceKey levelKey, + ChunkGenerator generator, + BiomeSource biomeSource + ) { + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureSurfaceFitter.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureSurfaceFitter.java new file mode 100644 index 000000000..6beb66dad --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureSurfaceFitter.java @@ -0,0 +1,1221 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.structure.object.IrisObjectVacuum; +import art.arcane.iris.structure.placement.IrisStructureTerrain; +import art.arcane.iris.structure.placement.IrisStructureTerrainMode; +import art.arcane.iris.spi.IrisLogging; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +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.structures.IglooPieces; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure; +import net.minecraft.world.level.levelgen.structure.structures.OceanRuinStructure; +import net.minecraft.world.level.levelgen.structure.structures.RuinedPortalPiece; +import net.minecraft.world.level.levelgen.structure.structures.ShipwreckStructure; +import net.minecraft.world.level.levelgen.structure.structures.StrongholdStructure; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece; +import net.minecraft.world.level.levelgen.structure.structures.WoodlandMansionPieces; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.IntBinaryOperator; + +public final class NativeStructureSurfaceFitter { + private static final double SURFACE_TERRAIN_FALLOFF = 2.0; + private static final long SURFACE_TERRAIN_INFLUENCE_SCALE = 1_000_000L; + private static final int SURFACE_TERRAIN_RADIUS = 12; + private static final int MAX_SOURCE_SURFACE_DELTA = 6; + private static final int MAX_SURFACE_TEMPLATE_CELLS = 4_194_304; + private static final Set WARNED_SOURCE_BUDGET = + ConcurrentHashMap.newKeySet(); + private static final Set WARNED_VACUUM_BUDGET = + ConcurrentHashMap.newKeySet(); + private static final Set WARNED_FLATTEN_BUDGET = + ConcurrentHashMap.newKeySet(); + + private NativeStructureSurfaceFitter() { + } + + public static SurfaceTerrainPlan prepareSurfaceStructures( + WorldGenLevel world, BoundingBox area, + List targets, + IntBinaryOperator surfaceHeight) { + return prepareSurfaceStructures( + world, area, targets, surfaceHeight, MAX_SURFACE_TEMPLATE_CELLS); + } + + static SurfaceTerrainPlan prepareSurfaceStructures( + WorldGenLevel world, BoundingBox area, + List targets, + IntBinaryOperator surfaceHeight, int maximumTemplateCells) { + if (targets == null || targets.isEmpty()) { + return SurfaceTerrainPlan.empty(); + } + Objects.requireNonNull(surfaceHeight, "Surface structure terrain fitting requires an Iris height resolver"); + List anchors = collectSourceSurfaceAnchors( + world, area, targets, maximumTemplateCells); + if (!anchors.isEmpty()) { + fitSurfaceTerrain(world, area, anchors, surfaceHeight); + } + FlattenFootprint flatten = collectFlattenFootprint( + world, area, targets, surfaceHeight, maximumTemplateCells); + Map flattenedHeights = flatten.anchors().isEmpty() && flatten.projectedSupport().isEmpty() ? Map.of() + : fitFlattenTerrain(world, area, flatten, surfaceHeight); + VacuumFootprint vacuum = collectVacuumFootprint( + world, area, targets, maximumTemplateCells); + if (!vacuum.anchors().isEmpty()) { + fitVacuumTerrain(world, area, vacuum, surfaceHeight); + } + return SurfaceTerrainPlan.create( + vacuum, area, flattenedHeights); + } + + public static void repairVacuumFoundations( + WorldGenLevel world, BoundingBox area, + SurfaceTerrainPlan 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, + GenerationStep.Decoration step) { + return step == GenerationStep.Decoration.SURFACE_STRUCTURES + && (adjustment == TerrainAdjustment.BEARD_THIN + || adjustment == TerrainAdjustment.BEARD_BOX); + } + + static int surfaceTerrainRadius() { + return SURFACE_TERRAIN_RADIUS; + } + + static int resolveSurfaceTarget(List anchors, int worldX, int worldZ, + int originalY) { + return resolveSurface(anchors, worldX, worldZ, originalY).targetY(); + } + + private static SurfaceResolution resolveSurface(List anchors, + int worldX, int worldZ, int originalY) { + int localTargetY = originalY; + SurfaceAnchor selectedLocal = null; + long totalInfluence = 0L; + long weightedTargetY = 0L; + long maximumInfluence = 0L; + for (SurfaceAnchor anchor : anchors) { + int outX = IrisObjectVacuum.outset(worldX, anchor.minX(), anchor.maxX()); + int outZ = IrisObjectVacuum.outset(worldZ, anchor.minZ(), anchor.maxZ()); + boolean containsColumn = outX == 0 && outZ == 0; + long distanceSquared = (long) outX * outX + (long) outZ * outZ; + long radiusSquared = (long) SURFACE_TERRAIN_RADIUS * SURFACE_TERRAIN_RADIUS; + if (distanceSquared > radiusSquared) { + continue; + } + double distance = Math.sqrt(distanceSquared); + double factor = Math.pow( + 1D - distance / SURFACE_TERRAIN_RADIUS, SURFACE_TERRAIN_FALLOFF); + if (factor <= 0D) { + continue; + } + int boundedTargetY = boundedSurfaceTarget(originalY, anchor.meetY()); + if (containsColumn) { + if (precedes(anchor, selectedLocal)) { + localTargetY = boundedTargetY; + selectedLocal = anchor; + } + continue; + } + long influence = Math.round(factor * SURFACE_TERRAIN_INFLUENCE_SCALE); + if (influence <= 0L) { + continue; + } + long weightedInfluence = influence * Math.max(1, anchor.strength()); + totalInfluence += weightedInfluence; + weightedTargetY += weightedInfluence * boundedTargetY; + maximumInfluence = Math.max(maximumInfluence, influence); + } + if (selectedLocal != null) { + return new SurfaceResolution( + localTargetY, + selectedLocal.strength() > 1 + && localTargetY == selectedLocal.meetY()); + } + if (totalInfluence == 0L) { + return new SurfaceResolution(originalY, false); + } + double blendedTargetY = weightedTargetY / (double) totalInfluence; + double factor = maximumInfluence / (double) SURFACE_TERRAIN_INFLUENCE_SCALE; + return new SurfaceResolution( + blendSurfaceTarget(originalY, blendedTargetY, factor), false); + } + + private static int boundedSurfaceTarget(int originalY, int meetY) { + int delta = Math.max( + -MAX_SOURCE_SURFACE_DELTA, + Math.min(MAX_SOURCE_SURFACE_DELTA, meetY - originalY)); + return originalY + delta; + } + + private static int blendSurfaceTarget(int originalY, double targetY, double factor) { + double delta = (targetY - originalY) * factor; + int magnitude = (int) Math.round(Math.abs(delta)); + return originalY + (delta < 0D ? -magnitude : magnitude); + } + + private static boolean precedes(SurfaceAnchor candidate, SurfaceAnchor selected) { + if (selected == null) { + return true; + } + if (candidate.strength() != selected.strength()) { + return candidate.strength() > selected.strength(); + } + if (candidate.meetY() != selected.meetY()) { + return candidate.meetY() < selected.meetY(); + } + if (candidate.minX() != selected.minX()) { + return candidate.minX() < selected.minX(); + } + if (candidate.minZ() != selected.minZ()) { + return candidate.minZ() < selected.minZ(); + } + if (candidate.maxX() != selected.maxX()) { + return candidate.maxX() < selected.maxX(); + } + if (candidate.maxZ() != selected.maxZ()) { + return candidate.maxZ() < selected.maxZ(); + } + return false; + } + + private static List collectSourceSurfaceAnchors( + WorldGenLevel world, BoundingBox area, + List 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); + List anchors = new ArrayList<>(); + Set seenStarts = Collections.newSetFromMap( + new IdentityHashMap<>()); + for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) { + if (!requiresSourceSurfaceTerrain(target)) { + continue; + } + StructureStart start = target.start(); + if (!seenStarts.add(start)) { + continue; + } + BoundingBox firstBounds = start.getPieces().getFirst().getBoundingBox(); + BlockPos firstCenter = firstBounds.getCenter(); + BlockPos referencePosition = new BlockPos( + firstCenter.getX(), firstBounds.minY(), firstCenter.getZ()); + TemplateCellBudget budget = new TemplateCellBudget(maximumTemplateCells); + boolean budgetExceeded = false; + for (StructurePiece piece : start.getPieces()) { + if (piece instanceof PoolElementStructurePiece poolPiece) { + StructureTemplatePool.Projection projection = + poolPiece.getElement().getProjection(); + if (!budgetExceeded + && (projection == StructureTemplatePool.Projection.RIGID + || projection == StructureTemplatePool.Projection.TERRAIN_MATCHING) + && intersectsHorizontally(poolPiece.getBoundingBox(), influenceArea)) { + try { + List pieceAnchors = new ArrayList<>(); + addProcessedSurfaceAnchors( + world, influenceArea, poolPiece, referencePosition, + projection, budget, pieceAnchors); + anchors.addAll(pieceAnchors); + } catch (TemplateBudgetExceeded ignored) { + budgetExceeded = true; + warnSourceBudget(target); + } + } + for (JigsawJunction junction : poolPiece.getJunctions()) { + anchors.add(new SurfaceAnchor( + junction.getSourceX(), junction.getSourceX(), + junction.getSourceZ(), junction.getSourceZ(), + junction.getSourceGroundY() - 1, 1)); + } + continue; + } + BoundingBox bounds = piece.getBoundingBox(); + anchors.add(surfaceAnchor(bounds, bounds.minY(), 2)); + } + } + return List.copyOf(anchors); + } + + private static void addProcessedSurfaceAnchors( + WorldGenLevel world, BoundingBox influenceArea, + PoolElementStructurePiece piece, BlockPos referencePosition, + StructureTemplatePool.Projection projection, + TemplateCellBudget budget, + List anchors) { + NativeStructureTemplateOccupancy.OccupancyResult occupancy = + NativeStructureTemplateOccupancy.resolve( + world, piece, referencePosition, influenceArea, + () -> world.getLevel().getStructureTemplateManager(), + influenceArea::isInside, budget::consume); + if (!occupancy.resolved()) { + return; + } + int maximumFoundationY = piece.getBoundingBox().minY() + + piece.getGroundLevelDelta(); + Map lowest = + NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells()); + for (NativeStructureTemplateOccupancy.LowestCell cell : lowest.values()) { + budget.consume(1); + if (projection == StructureTemplatePool.Projection.RIGID + && cell.y() > maximumFoundationY) { + continue; + } + int groundY = projection == StructureTemplatePool.Projection.RIGID + ? maximumFoundationY : cell.y(); + BoundingBox column = new BoundingBox( + cell.x(), groundY, cell.z(), cell.x(), groundY, cell.z()); + anchors.add(surfaceAnchor(column, groundY, 2)); + } + } + + private static void warnSourceBudget( + NativeStructureTerrainIntegrator.TerrainTarget target) { + String structureId = target.structureId() == null + ? target.start().getStructure().getClass().getName() + : target.structureId(); + if (WARNED_SOURCE_BUDGET.add(structureId)) { + IrisLogging.warn("Native structure SOURCE fitting for '" + + structureId + "' exceeded its bounded template budget; " + + "skipping remaining processed footprints for this start"); + } + } + + private static VacuumFootprint collectVacuumFootprint( + WorldGenLevel world, BoundingBox area, + List 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 anchors = new HashMap<>(); + Map columnCaps = new HashMap<>(); + Set foundationBases = new HashSet<>(); + Set occupiedCells = new HashSet<>(); + Set 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 targetAnchors = new HashMap<>(); + Map targetCaps = new HashMap<>(); + Set targetFoundationBases = new HashSet<>(); + Set targetOccupiedCells = new HashSet<>(); + TemplateCellBudget budget = new TemplateCellBudget(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().getStructureTemplateManager(), + influenceArea::isInside, budget::consume); + if (!occupancy.resolved()) { + continue; + } + targetOccupiedCells.addAll(occupancy.cells().keySet()); + Map 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 (TemplateBudgetExceeded 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 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 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) { + return new SurfaceAnchor(bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(), + groundY - 1, strength); + } + + static boolean requiresSurfaceTerrain(StructureStart start) { + return requiresSurfaceTerrain(new NativeStructureTerrainIntegrator.TerrainTarget( + null, start, new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE))); + } + + static boolean requiresSurfaceTerrain(NativeStructureTerrainIntegrator.TerrainTarget target) { + if (target == null || target.terrain() == null) { + return false; + } + StructureStart start = target.start(); + if (start == null || !start.isValid()) { + return false; + } + IrisStructureTerrainMode mode = target.terrain().resolvedMode(); + return mode == IrisStructureTerrainMode.VACUUM + || requiresFlattenTerrain(target) + || requiresSourceSurfaceTerrain(target); + } + + private static boolean requiresSourceSurfaceTerrain( + NativeStructureTerrainIntegrator.TerrainTarget target) { + return target != null && target.terrain() != null + && target.start() != null && target.start().isValid() + && target.terrain().resolvedMode() == IrisStructureTerrainMode.SOURCE + && shouldPrepareSurfaceTerrain( + target.start().getStructure().terrainAdaptation(), + target.start().getStructure().step()); + } + + private static void fitSurfaceTerrain(WorldGenLevel world, BoundingBox area, + List anchors, + 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]; + 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 originalY = Math.max(area.minY(), Math.min( + area.maxY(), surfaceHeight.applyAsInt(x, z))); + SurfaceResolution resolution = resolveSurface(anchors, x, z, originalY); + originalHeights[column] = originalY; + targetHeights[column] = Math.max(area.minY(), Math.min( + area.maxY(), resolution.targetY())); + rigidBaseSupport[column] = resolution.rigidBaseSupport(); + } + } + 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(); + applySurfaceColumn(world, position, x, z, + originalHeights[column], targetHeights[column], area.minY(), area.maxY(), + rigidBaseSupport[column]); + } + } + } + + static boolean requiresFlattenTerrain(NativeStructureTerrainIntegrator.TerrainTarget target) { + if (target == null || target.terrain() == null + || target.terrain().resolvedMode() != IrisStructureTerrainMode.FLATTEN + || target.terrain().resolvedFlattenRange() == 0 + || target.start() == null || !target.start().isValid()) { + return false; + } + StructureStart start = target.start(); + return start.getStructure().step() == GenerationStep.Decoration.SURFACE_STRUCTURES + && start.getStructure().terrainAdaptation() != TerrainAdjustment.BURY + && start.getStructure().terrainAdaptation() != TerrainAdjustment.ENCAPSULATE + && !(start.getStructure() instanceof StrongholdStructure) + && !(start.getStructure() instanceof OceanMonumentStructure) + && !(start.getStructure() instanceof OceanRuinStructure) + && (!(start.getStructure() instanceof ShipwreckStructure) + || "minecraft:shipwreck_beached".equals(target.structureId())); + } + + private static FlattenFootprint collectFlattenFootprint( + WorldGenLevel world, BoundingBox area, + List targets, + IntBinaryOperator surfaceHeight, int maximumTemplateCells) { + List anchors = new ArrayList<>(); + Map projectedSupport = new HashMap<>(); + Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); + for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) { + if (!requiresFlattenTerrain(target) || !seen.add(target.start())) { + continue; + } + int radius = Math.max(0, Math.min(128, target.terrain().getHorizontalPadding())); + int range = target.terrain().resolvedFlattenRange(); + BoundingBox influenceArea = new BoundingBox( + area.minX() - radius, area.minY(), area.minZ() - radius, + area.maxX() + radius, area.maxY(), area.maxZ() + radius); + StructureStart start = target.start(); + if (!hasExposedFlattenPiece(start, surfaceHeight, range)) { + continue; + } + BoundingBox first = start.getPieces().getFirst().getBoundingBox(); + BlockPos reference = new BlockPos(first.getCenter().getX(), first.minY(), first.getCenter().getZ()); + TemplateCellBudget budget = new TemplateCellBudget(maximumTemplateCells); + List sources = new ArrayList<>(); + Set projectedColumns = new HashSet<>(); + try { + for (StructurePiece piece : start.getPieces()) { + BoundingBox bounds = piece.getBoundingBox(); + if (!intersectsHorizontally(bounds, influenceArea) || !flattenPieceAllowed(start, piece)) { + continue; + } + if (piece instanceof PoolElementStructurePiece poolPiece) { + if (poolPiece.getElement().getProjection() == StructureTemplatePool.Projection.RIGID) { + addProcessedSurfaceAnchors(world, influenceArea, poolPiece, reference, + StructureTemplatePool.Projection.RIGID, budget, sources); + } else { + addProjectedFlattenSupport(world, area, poolPiece, reference, budget, projectedColumns); + } + for (JigsawJunction junction : poolPiece.getJunctions()) { + sources.add(new SurfaceAnchor(junction.getSourceX(), junction.getSourceX(), + junction.getSourceZ(), junction.getSourceZ(), junction.getSourceGroundY() - 1, 1)); + } + } else { + sources.add(new SurfaceAnchor(bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(), + flattenGroundY(start, piece, surfaceHeight), 2)); + } + } + } catch (TemplateBudgetExceeded ignored) { + String structureId = target.structureId() == null + ? start.getStructure().getClass().getName() : target.structureId(); + if (WARNED_FLATTEN_BUDGET.add(structureId)) { + IrisLogging.warn("Native structure FLATTEN fitting for '" + structureId + + "' exceeded its bounded template budget; skipping this start"); + } + continue; + } + for (SurfaceAnchor source : sources) { + anchors.add(new FlattenAnchor(source, radius, range)); + } + for (long column : projectedColumns) { + projectedSupport.merge(column, range, Math::max); + } + } + return new FlattenFootprint(List.copyOf(anchors), Map.copyOf(projectedSupport)); + } + + private static void addProjectedFlattenSupport( + WorldGenLevel world, BoundingBox area, PoolElementStructurePiece piece, + BlockPos reference, TemplateCellBudget budget, Set columns) { + NativeStructureTemplateOccupancy.OccupancyResult occupancy = NativeStructureTemplateOccupancy.resolve( + world, piece, reference, area, () -> world.getLevel().getStructureTemplateManager(), + area::isInside, budget::consume); + for (Map.Entry entry : occupancy.cells().entrySet()) { + NativeStructureTemplateOccupancy.OccupancyCell cell = entry.getValue(); + if (cell.blocker() || !NativeStructureTemplateOccupancy.isSolidBase(cell.state())) { + continue; + } + BlockPos position = BlockPos.of(entry.getKey()); + columns.add(NativeStructureTemplateOccupancy.columnKey(position.getX(), position.getZ())); + } + } + + private static boolean flattenPieceAllowed(StructureStart start, StructurePiece piece) { + if (piece instanceof RuinedPortalPiece portal) { + RuinedPortalPiece.VerticalPlacement placement = NativeStructureReflection.ruinedPortalVerticalPlacement(portal); + return placement == RuinedPortalPiece.VerticalPlacement.ON_LAND_SURFACE + || placement == RuinedPortalPiece.VerticalPlacement.IN_NETHER; + } + if (piece instanceof IglooPieces.IglooPiece) { + int highest = Integer.MIN_VALUE; + for (StructurePiece candidate : start.getPieces()) { + if (candidate instanceof IglooPieces.IglooPiece) { + highest = Math.max(highest, candidate.getBoundingBox().minY()); + } + } + return piece.getBoundingBox().minY() == highest; + } + return true; + } + + static boolean hasExposedFlattenPiece( + StructureStart start, IntBinaryOperator surfaceHeight, int range) { + for (StructurePiece piece : start.getPieces()) { + if (!flattenPieceAllowed(start, piece)) { + continue; + } + BoundingBox bounds = piece.getBoundingBox(); + int groundY = flattenGroundY(start, piece, surfaceHeight); + int roofY = piece instanceof IglooPieces.IglooPiece || piece instanceof SwampHutPiece + ? groundY + bounds.getYSpan() : bounds.maxY(); + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + int surfaceY = surfaceHeight.applyAsInt(x, z); + if (surfaceY <= roofY && (long) groundY - surfaceY <= range) { + return true; + } + } + } + } + return false; + } + + private static int flattenGroundY( + StructureStart start, StructurePiece piece, IntBinaryOperator surfaceHeight) { + BoundingBox bounds = piece.getBoundingBox(); + if (piece instanceof PoolElementStructurePiece poolPiece) { + return bounds.minY() + poolPiece.getGroundLevelDelta() - 1; + } + if (piece instanceof IglooPieces.IglooPiece igloo) { + BlockPos entrance = igloo.templatePosition().offset( + StructureTemplate.calculateRelativePosition(igloo.placeSettings(), new BlockPos(3, 0, 0))); + return surfaceHeight.applyAsInt(entrance.getX(), entrance.getZ()); + } + if (piece instanceof SwampHutPiece || start.getStructure() instanceof ShipwreckStructure) { + long sum = 0; + int minimum = Integer.MAX_VALUE; + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + int surfaceY = surfaceHeight.applyAsInt(x, z); + sum += surfaceY; + minimum = Math.min(minimum, surfaceY); + } + } + return piece instanceof SwampHutPiece + ? (int) Math.floorDiv(sum, (long) bounds.getXSpan() * bounds.getZSpan()) : minimum; + } + if (piece instanceof WoodlandMansionPieces.WoodlandMansionPiece) { + return start.getBoundingBox().minY() - 1; + } + return bounds.minY() - 1; + } + + static FlattenResolution resolveFlattenSurface( + List anchors, int x, int z, int originalY) { + FlattenAnchor selected = null; + long weightedY = 0; + long totalWeight = 0; + long maximumInfluence = 0; + int range = 0; + for (FlattenAnchor anchor : anchors) { + SurfaceAnchor source = anchor.source(); + int outX = IrisObjectVacuum.outset(x, source.minX(), source.maxX()); + int outZ = IrisObjectVacuum.outset(z, source.minZ(), source.maxZ()); + boolean containsColumn = outX == 0 && outZ == 0; + if (containsColumn && source.strength() > 1) { + if (selected == null || precedes(source, selected.source())) { + selected = anchor; + } + continue; + } + double distance = Math.sqrt((double) outX * outX + (double) outZ * outZ); + if (!containsColumn && (anchor.radius() == 0 || distance >= anchor.radius())) { + continue; + } + double progress = containsColumn ? 0D : distance / anchor.radius(); + double factor = 1D - progress * progress * (3D - 2D * progress); + long influence = Math.round(factor * SURFACE_TERRAIN_INFLUENCE_SCALE); + long weight = influence * source.strength(); + int targetY = originalY + Math.max(-anchor.range(), Math.min(anchor.range(), source.meetY() - originalY)); + weightedY += weight * targetY; + totalWeight += weight; + maximumInfluence = Math.max(maximumInfluence, influence); + range = Math.max(range, anchor.range()); + } + if (selected != null) { + int targetY = originalY + Math.max(-selected.range(), + Math.min(selected.range(), selected.source().meetY() - originalY)); + return new FlattenResolution(targetY, selected.range()); + } + if (totalWeight == 0) { + return new FlattenResolution(originalY, 0); + } + int targetY = blendSurfaceTarget(originalY, weightedY / (double) totalWeight, + maximumInfluence / (double) SURFACE_TERRAIN_INFLUENCE_SCALE); + return new FlattenResolution(targetY, range); + } + + private static Map fitFlattenTerrain( + WorldGenLevel world, BoundingBox area, FlattenFootprint footprint, + IntBinaryOperator surfaceHeight) { + Map heights = new HashMap<>(); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (int x = area.minX(); x <= area.maxX(); x++) { + for (int z = area.minZ(); z <= area.maxZ(); z++) { + int originalY = Math.max(area.minY(), Math.min(area.maxY(), surfaceHeight.applyAsInt(x, z))); + long column = NativeStructureTemplateOccupancy.columnKey(x, z); + FlattenResolution resolution = resolveFlattenSurface(footprint.anchors(), x, z, originalY); + int projectedRange = footprint.projectedSupport().getOrDefault(column, 0); + int range = Math.max(resolution.range(), projectedRange); + if (range == 0 || originalY < area.maxY() + && !world.getBlockState(position.set(x, originalY + 1, z)).getFluidState().isEmpty()) { + continue; + } + int targetY = Math.max(area.minY(), Math.min(area.maxY(), resolution.targetY())); + int minimumY = Math.max(area.minY(), targetY - range); + applySurfaceColumn(world, position, x, z, originalY, targetY, + minimumY, Math.min(area.maxY(), originalY + range)); + fillFlattenFoundation(world, position, x, z, targetY, minimumY); + heights.put(column, targetY + 1); + } + } + return Map.copyOf(heights); + } + + private static void fillFlattenFoundation( + WorldGenLevel world, BlockPos.MutableBlockPos position, + int x, int z, int targetY, int minimumY) { + SurfaceMaterials materials = resolveSurfaceMaterials(world, position, x, z, targetY, minimumY); + if (!isTerrainBlock(world.getBlockState(position.set(x, targetY, z)))) { + world.setBlock(position, materials.surface(), 2); + } + for (int y = targetY - 1; y >= minimumY; y--) { + BlockState existing = world.getBlockState(position.set(x, y, z)); + if (!existing.getFluidState().isEmpty() + || NativeStructureVegetationClearer.isTreeBlock(existing)) { + break; + } + if (!isTerrainBlock(existing)) { + world.setBlock(position, materials.subsurface(), 2); + } + } + } + + 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 occupiedCells, int x, int z, int minimumY, int maximumY) { + for (int y = minimumY; y <= maximumY; y++) { + if (occupiedCells.contains(BlockPos.asLong(x, y, z))) { + return true; + } + } + return false; + } + + private static SurfaceResolution resolveVacuumSurface( + VacuumFootprint footprint, int worldX, int worldZ, int originalY) { + long column = NativeStructureTemplateOccupancy.columnKey(worldX, worldZ); + Integer cap = footprint.columnCaps().get(column); + VacuumAnchor local = footprint.anchors().get(column); + if (local != null && local.strength() > 1) { + int localTargetY = cap == null + ? local.surfaceY() : Math.min(local.surfaceY(), cap); + if (localTargetY <= originalY) { + return new SurfaceResolution(originalY, false); + } + return new SurfaceResolution( + localTargetY, localTargetY == local.surfaceY()); + } + long totalInfluence = 0L; + long weightedSurfaceY = 0L; + long maximumInfluence = 0L; + int radiusSquared = SURFACE_TERRAIN_RADIUS * SURFACE_TERRAIN_RADIUS; + for (int offsetX = -SURFACE_TERRAIN_RADIUS; + offsetX <= SURFACE_TERRAIN_RADIUS; offsetX++) { + for (int offsetZ = -SURFACE_TERRAIN_RADIUS; + offsetZ <= SURFACE_TERRAIN_RADIUS; offsetZ++) { + int horizontalDistanceSquared = offsetX * offsetX + offsetZ * offsetZ; + if (horizontalDistanceSquared > radiusSquared) { + continue; + } + VacuumAnchor anchor = footprint.anchors().get( + NativeStructureTemplateOccupancy.columnKey( + worldX + offsetX, worldZ + offsetZ)); + if (anchor == null || anchor.surfaceY() <= originalY) { + continue; + } + int verticalDistance = Math.abs(anchor.surfaceY() + 1 - originalY); + long distanceSquared = horizontalDistanceSquared + + (long) verticalDistance * verticalDistance; + double factor = 0D; + if (distanceSquared <= radiusSquared) { + factor = surfaceFactor(Math.sqrt(distanceSquared)); + } + if (anchor.strength() > 1 + && verticalDistance > SURFACE_TERRAIN_RADIUS) { + double rescueProgress = Math.min(1D, + (verticalDistance - SURFACE_TERRAIN_RADIUS) + / (double) SURFACE_TERRAIN_RADIUS); + double rescueWeight = rescueProgress * rescueProgress + * (3D - 2D * rescueProgress); + factor = Math.max(factor, + surfaceFactor(Math.sqrt(horizontalDistanceSquared)) + * rescueWeight); + } + long influence = Math.round( + factor * SURFACE_TERRAIN_INFLUENCE_SCALE); + if (influence <= 0L) { + continue; + } + long weightedInfluence = influence * Math.max(1, anchor.strength()); + totalInfluence += weightedInfluence; + weightedSurfaceY += weightedInfluence * anchor.surfaceY(); + maximumInfluence = Math.max(maximumInfluence, influence); + } + } + if (totalInfluence == 0L) { + return new SurfaceResolution(originalY, false); + } + double blendedSurfaceY = weightedSurfaceY / (double) totalInfluence; + double factor = maximumInfluence / (double) SURFACE_TERRAIN_INFLUENCE_SCALE; + int targetY = (int) Math.round( + originalY + ((blendedSurfaceY - originalY) * factor)); + if (cap != null) { + targetY = Math.min(targetY, cap); + } + return new SurfaceResolution(Math.max(originalY, targetY), false); + } + + private static double surfaceFactor(double distance) { + return Math.pow(1D - distance / SURFACE_TERRAIN_RADIUS, + SURFACE_TERRAIN_FALLOFF); + } + + static void applySurfaceColumn(WorldGenLevel world, BlockPos.MutableBlockPos position, + int x, int z, int originalY, int targetY, + int worldMinY, int worldMaxY) { + applySurfaceColumn(world, position, x, z, originalY, targetY, + worldMinY, worldMaxY, false); + } + + static void applySurfaceColumn(WorldGenLevel world, BlockPos.MutableBlockPos position, + int x, int z, int originalY, int targetY, + int worldMinY, int worldMaxY, + boolean requireRigidBaseSupport) { + if (targetY == originalY) { + if (requireRigidBaseSupport) { + ensureRigidBaseTerrain(world, position, x, z, targetY, worldMinY); + } + return; + } + SurfaceMaterials materials = resolveSurfaceMaterials(world, position, x, z, originalY, worldMinY); + if (targetY < originalY) { + BlockState clearedState = clearSurfaceDecorationAndResolveFill( + world, position, x, z, originalY, worldMaxY); + for (int y = originalY; y > targetY; y--) { + world.setBlock(position.set(x, y, z), clearedState, 2); + } + world.setBlock(position.set(x, targetY, z), materials.surface(), 2); + if (requireRigidBaseSupport) { + ensureRigidBaseTerrain(world, position, x, z, targetY, worldMinY, materials); + } + return; + } + for (int y = originalY + 1; y < targetY; y++) { + position.set(x, y, z); + if (!NativeStructureVegetationClearer.isTreeBlock(world.getBlockState(position))) { + world.setBlock(position, materials.subsurface(), 2); + } + } + position.set(x, targetY, z); + if (!NativeStructureVegetationClearer.isTreeBlock(world.getBlockState(position))) { + world.setBlock(position, materials.surface(), 2); + } + if (requireRigidBaseSupport) { + ensureRigidBaseTerrain(world, position, x, z, targetY, worldMinY, materials); + } + } + + private static void ensureRigidBaseTerrain(WorldGenLevel world, + BlockPos.MutableBlockPos position, + int x, int z, int targetY, int worldMinY) { + int supportY = targetY - 1; + boolean targetIsTerrain = isTerrainBlock(world.getBlockState(position.set(x, targetY, z))); + boolean supportIsTerrain = supportY < worldMinY + || isTerrainBlock(world.getBlockState(position.set(x, supportY, z))); + if (targetIsTerrain && supportIsTerrain) { + return; + } + SurfaceMaterials materials = resolveSurfaceMaterials( + world, position, x, z, targetY, worldMinY); + ensureRigidBaseTerrain(world, position, x, z, targetY, worldMinY, materials); + } + + private static void ensureRigidBaseTerrain(WorldGenLevel world, + BlockPos.MutableBlockPos position, + int x, int z, int targetY, int worldMinY, + SurfaceMaterials materials) { + int supportY = targetY - 1; + position.set(x, targetY, z); + if (!isTerrainBlock(world.getBlockState(position))) { + world.setBlock(position, materials.surface(), 2); + } + if (supportY < worldMinY) { + return; + } + position.set(x, supportY, z); + if (!isTerrainBlock(world.getBlockState(position))) { + world.setBlock(position, materials.subsurface(), 2); + } + } + + private static BlockState clearSurfaceDecorationAndResolveFill( + WorldGenLevel world, BlockPos.MutableBlockPos position, + int x, int z, int originalY, int worldMaxY) { + BlockState air = Blocks.AIR.defaultBlockState(); + for (int y = originalY + 1; y <= worldMaxY; y++) { + BlockState state = world.getBlockState(position.set(x, y, z)); + if (state.isAir()) { + return air; + } + if (!state.getFluidState().isEmpty()) { + BlockState fluid = state.getFluidState().createLegacyBlock(); + if (state != fluid) { + world.setBlock(position, fluid, 2); + } + return fluid; + } + if (state.isSolid() && !state.is(Blocks.CACTUS) + || NativeStructureVegetationClearer.isTreeBlock(state)) { + return air; + } + world.setBlock(position, air, 2); + } + return air; + } + + private static SurfaceMaterials resolveSurfaceMaterials(WorldGenLevel world, + BlockPos.MutableBlockPos position, + int x, int z, int originalY, + int worldMinY) { + BlockState surface = world.getBlockState(position.set(x, originalY, z)); + BlockState subsurface = null; + for (int y = originalY - 1; y >= worldMinY; y--) { + BlockState candidate = world.getBlockState(position.set(x, y, z)); + if (isTerrainBlock(candidate)) { + subsurface = candidate; + break; + } + } + if (subsurface == null) { + subsurface = isTerrainBlock(surface) ? surface : Blocks.STONE.defaultBlockState(); + } + if (!isTerrainBlock(surface)) { + surface = subsurface; + } + return new SurfaceMaterials(surface, subsurface); + } + + private static boolean isTerrainBlock(BlockState state) { + return state.isSolid() && !NativeStructureVegetationClearer.isTreeBlock(state); + } + + record SurfaceAnchor(int minX, int maxX, int minZ, int maxZ, int meetY, int strength) { + } + + record FlattenAnchor(SurfaceAnchor source, int radius, int range) { + } + + record FlattenResolution(int targetY, int range) { + } + + private record FlattenFootprint(List anchors, Map projectedSupport) { + } + + private record SurfaceMaterials(BlockState surface, BlockState subsurface) { + } + + private record SurfaceResolution(int targetY, boolean rigidBaseSupport) { + } + + private record VacuumFootprint( + Map anchors, Map columnCaps, + Set foundationBases, Set occupiedCells) { + } + + private record VacuumAnchor(int surfaceY, int strength) { + } + + public record SurfaceTerrainPlan( + List foundationBases, Set occupiedCells, Map flattenedHeights) { + private static final SurfaceTerrainPlan EMPTY = + new SurfaceTerrainPlan(List.of(), Set.of(), Map.of()); + + public SurfaceTerrainPlan { + foundationBases = List.copyOf(foundationBases); + occupiedCells = Set.copyOf(occupiedCells); + flattenedHeights = Map.copyOf(flattenedHeights); + } + + private static SurfaceTerrainPlan empty() { + return EMPTY; + } + + private static SurfaceTerrainPlan create( + VacuumFootprint vacuum, BoundingBox area, Map flattenedHeights) { + Set packedBases = vacuum.foundationBases(); + if (packedBases.isEmpty() && flattenedHeights.isEmpty()) { + return EMPTY; + } + List bases = new ArrayList<>(packedBases.size()); + Set 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() && flattenedHeights.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 occupancy = new HashSet<>(); + for (long packedCell : vacuum.occupiedCells()) { + BlockPos cell = BlockPos.of(packedCell); + if (columns.contains(NativeStructureTemplateOccupancy.columnKey( + cell.getX(), cell.getZ()))) { + occupancy.add(packedCell); + } + } + return new SurfaceTerrainPlan( + bases, occupancy, flattenedHeights); + } + + public void primeHeightmaps(ChunkAccess chunk) { + if (flattenedHeights.isEmpty()) { + return; + } + Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG); + Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG); + WorldgenTerrainHeightmaps.primeTerrain(chunk, + (x, z) -> flattenedHeights.getOrDefault(NativeStructureTemplateOccupancy.columnKey(x, z), + surface.getFirstAvailable(x & 15, z & 15)), + (x, z) -> flattenedHeights.getOrDefault(NativeStructureTemplateOccupancy.columnKey(x, z), + floor.getFirstAvailable(x & 15, z & 15))); + } + } + + private static final class TemplateCellBudget { + private final int maximum; + private int consumed; + + private TemplateCellBudget(int maximum) { + if (maximum < 0) { + throw new IllegalArgumentException( + "Native structure surface template budget cannot be negative"); + } + this.maximum = maximum; + } + + private void consume(int amount) { + if (amount < 0 || consumed > maximum - amount) { + throw TemplateBudgetExceeded.INSTANCE; + } + consumed += amount; + } + } + + private static final class TemplateBudgetExceeded extends RuntimeException { + private static final TemplateBudgetExceeded INSTANCE = + new TemplateBudgetExceeded(); + + private TemplateBudgetExceeded() { + super(null, null, false, false); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureSurfaceSupportBuilder.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureSurfaceSupportBuilder.java new file mode 100644 index 000000000..627adb3dd --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureSurfaceSupportBuilder.java @@ -0,0 +1,305 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.structure.placement.IrisStructureTerrainMode; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +final class NativeStructureSurfaceSupportBuilder { + private static final int MAX_BATCH_CELLS = 524_288; + private static final int MAX_BRIDGE_SPAN = 2; + + private NativeStructureSurfaceSupportBuilder() { + } + + static Set bridgeRigidPieceSupport( + WorldGenLevel world, BoundingBox area, + List targets, + Supplier templates) { + if (targets == null || targets.isEmpty()) { + return Set.of(); + } + Map planned; + try { + BatchCellBudget budget = new BatchCellBudget(); + List requests = new ArrayList<>(); + Set seenStarts = Collections.newSetFromMap( + new IdentityHashMap<>()); + for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) { + if (target == null + || !NativeStructureSurfaceFitter.requiresSurfaceTerrain(target) + || target.terrain().resolvedMode() == IrisStructureTerrainMode.FLATTEN + && !NativeStructureSurfaceFitter.hasExposedFlattenPiece( + target.start(), (x, z) -> world.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - 1, + target.terrain().resolvedFlattenRange()) + || !seenStarts.add(target.start())) { + continue; + } + collectTargetRequests( + world, area, target, templates, budget, requests); + } + planned = planWrites(world, area, requests, budget); + } catch (BatchBudgetExceeded ignored) { + return Set.of(); + } + if (planned.isEmpty()) { + return Set.of(); + } + List> ordered = new ArrayList<>(planned.entrySet()); + ordered.sort(Map.Entry.comparingByKey()); + Set written = new HashSet<>(); + for (Map.Entry entry : ordered) { + BlockPos position = BlockPos.of(entry.getKey()); + BlockState existing = world.getBlockState(position); + if (!existing.isSolid() + && !NativeStructureVegetationClearer.isTreeBlock(existing) + && existing.getFluidState().isEmpty() + && world.setBlock(position, entry.getValue(), 2)) { + written.add(entry.getKey()); + } + } + return Set.copyOf(written); + } + + private static void collectTargetRequests( + WorldGenLevel world, BoundingBox area, + NativeStructureTerrainIntegrator.TerrainTarget target, + Supplier templates, + BatchCellBudget budget, List requests) { + if (!NativeStructureSurfaceFitter.requiresSurfaceTerrain(target)) { + return; + } + List anchors = rigidAnchors(target.start()); + if (anchors.size() < 2) { + return; + } + BlockPos referencePos = referencePosition(target.start()); + for (RigidAnchor upper : anchors) { + Map lowerMeets = candidateLowerMeets( + area, upper, anchors, budget); + if (lowerMeets.isEmpty()) { + continue; + } + NativeStructureTemplateOccupancy.OccupancyResult occupancy = + NativeStructureTemplateOccupancy.resolve( + world, upper.piece(), referencePos, area, templates, + position -> retain(position, area, lowerMeets), budget::consume); + if (!occupancy.resolved()) { + continue; + } + Map lowest = + NativeStructureTemplateOccupancy.lowestCells(occupancy.cells()); + for (Map.Entry entry + : lowest.entrySet()) { + NativeStructureTemplateOccupancy.LowestCell cell = entry.getValue(); + Integer lowerMeet = lowerMeets.get(entry.getKey()); + if (lowerMeet == null || cell.occupancy().blocker() + || !NativeStructureTemplateOccupancy.isSolidBase( + cell.occupancy().state()) + || cell.y() - lowerMeet < 2 + || cell.y() - lowerMeet > MAX_BRIDGE_SPAN) { + continue; + } + budget.consume(1); + requests.add(new SupportRequest( + cell.x(), cell.z(), lowerMeet, cell.y())); + } + } + } + + private static List rigidAnchors(StructureStart start) { + List anchors = new ArrayList<>(); + for (StructurePiece piece : start.getPieces()) { + if (!(piece instanceof PoolElementStructurePiece poolPiece) + || poolPiece.getElement().getProjection() + != StructureTemplatePool.Projection.RIGID) { + continue; + } + BoundingBox bounds = poolPiece.getBoundingBox(); + int meetY = bounds.minY() + poolPiece.getGroundLevelDelta() - 1; + anchors.add(new RigidAnchor(poolPiece, bounds, meetY)); + } + return List.copyOf(anchors); + } + + private static Map candidateLowerMeets( + BoundingBox area, RigidAnchor upper, List anchors, + BatchCellBudget budget) { + Map lowerMeets = new HashMap<>(); + for (RigidAnchor lower : anchors) { + budget.consume(1); + int span = upper.meetY() - lower.meetY(); + if (lower.piece() == upper.piece() || span < 1 + || span > MAX_BRIDGE_SPAN + || lower.meetY() < area.minY() || lower.meetY() > area.maxY()) { + continue; + } + int minimumX = Math.max(area.minX(), + Math.max(upper.bounds().minX(), lower.bounds().minX())); + int maximumX = Math.min(area.maxX(), + Math.min(upper.bounds().maxX(), lower.bounds().maxX())); + int minimumZ = Math.max(area.minZ(), + Math.max(upper.bounds().minZ(), lower.bounds().minZ())); + int maximumZ = Math.min(area.maxZ(), + Math.min(upper.bounds().maxZ(), lower.bounds().maxZ())); + if (minimumX > maximumX || minimumZ > maximumZ) { + continue; + } + for (int x = minimumX; x <= maximumX; x++) { + for (int z = minimumZ; z <= maximumZ; z++) { + budget.consume(1); + lowerMeets.merge(columnKey(x, z), lower.meetY(), Math::max); + } + } + } + return lowerMeets; + } + + private static boolean retain( + BlockPos position, BoundingBox area, Map lowerMeets) { + if (!area.isInside(position)) { + return false; + } + Integer lowerMeet = lowerMeets.get(columnKey(position.getX(), position.getZ())); + return lowerMeet != null && position.getY() <= lowerMeet + MAX_BRIDGE_SPAN; + } + + private static Map planWrites( + WorldGenLevel world, BoundingBox area, + List requests, BatchCellBudget budget) { + List eligible = new ArrayList<>(); + Set materialKeys = new HashSet<>(); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (SupportRequest request : requests) { + BlockState lowerTerrain = world.getBlockState(position.set( + request.x(), request.lowerMeetY(), request.z())); + if (!isTerrainSupport(lowerTerrain) + || !gapIsClear(world, area, position, request, budget)) { + continue; + } + eligible.add(request); + materialKeys.add(new MaterialKey( + request.x(), request.z(), request.lowerMeetY())); + } + Map materials = new HashMap<>(); + for (MaterialKey key : materialKeys) { + materials.put(key, resolveSupportState( + world, position, key.x(), key.z(), key.lowerMeetY(), budget)); + } + Map planned = new HashMap<>(); + Set conflicts = new HashSet<>(); + eligible.sort(Comparator.comparingInt(SupportRequest::x) + .thenComparingInt(SupportRequest::z) + .thenComparingInt(SupportRequest::lowerMeetY) + .thenComparingInt(SupportRequest::baseY)); + for (SupportRequest request : eligible) { + MaterialKey materialKey = new MaterialKey( + request.x(), request.z(), request.lowerMeetY()); + BlockState material = materials.get(materialKey); + for (int y = request.lowerMeetY() + 1; y < request.baseY(); y++) { + budget.consume(1); + long positionKey = BlockPos.asLong(request.x(), y, request.z()); + if (conflicts.contains(positionKey)) { + continue; + } + BlockState existing = planned.putIfAbsent(positionKey, material); + if (existing != null && !existing.equals(material)) { + planned.remove(positionKey); + conflicts.add(positionKey); + } + } + } + return planned; + } + + private static boolean gapIsClear( + WorldGenLevel world, BoundingBox area, BlockPos.MutableBlockPos position, + SupportRequest request, BatchCellBudget budget) { + for (int y = request.lowerMeetY() + 1; y < request.baseY(); y++) { + budget.consume(1); + if (!area.isInside(position.set(request.x(), y, request.z()))) { + return false; + } + BlockState state = world.getBlockState(position); + if (state.isSolid() || NativeStructureVegetationClearer.isTreeBlock(state) + || !state.getFluidState().isEmpty()) { + return false; + } + } + return true; + } + + private static BlockState resolveSupportState( + WorldGenLevel world, BlockPos.MutableBlockPos position, + int x, int z, int lowerMeetY, BatchCellBudget budget) { + budget.consume(1); + BlockState below = world.getBlockState(position.set(x, lowerMeetY - 1, z)); + if (isTerrainSupport(below)) { + return below; + } + return world.getBlockState(position.set(x, lowerMeetY, z)); + } + + private static boolean isTerrainSupport(BlockState state) { + return state.isSolid() + && !NativeStructureVegetationClearer.isTreeBlock(state) + && state.getFluidState().isEmpty(); + } + + private static BlockPos referencePosition(StructureStart start) { + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + BlockPos center = bounds.getCenter(); + return new BlockPos(center.getX(), bounds.minY(), center.getZ()); + } + + private static long columnKey(int x, int z) { + return (long) x << 32 ^ z & 0xffffffffL; + } + + private record RigidAnchor( + PoolElementStructurePiece piece, BoundingBox bounds, int meetY) { + } + + private record SupportRequest(int x, int z, int lowerMeetY, int baseY) { + } + + private record MaterialKey(int x, int z, int lowerMeetY) { + } + + private static final class BatchCellBudget { + private int consumed; + + private void consume(int amount) { + if (amount < 0 || consumed > MAX_BATCH_CELLS - amount) { + throw BatchBudgetExceeded.INSTANCE; + } + consumed += amount; + } + } + + private static final class BatchBudgetExceeded extends RuntimeException { + private static final BatchBudgetExceeded INSTANCE = new BatchBudgetExceeded(); + + private BatchBudgetExceeded() { + super(null, null, false, false); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureTemplateOccupancy.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureTemplateOccupancy.java new file mode 100644 index 000000000..e399a91b4 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureTemplateOccupancy.java @@ -0,0 +1,165 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +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 templates, + PositionFilter filter, CellBudget budget) { + List leaves = new ArrayList<>(); + if (!flattenSingles(piece.getElement(), leaves)) { + return new OccupancyResult(false, Map.of()); + } + List placements = new ArrayList<>(leaves.size()); + Map occupancy = new HashMap<>(); + for (SinglePoolElement leaf : leaves) { + StructurePlaceSettings settings = NativeStructureReflection.resolvePlacementSettings( + leaf, piece, processingArea); + StructureTemplate template = NativeStructureReflection.resolveTemplate(leaf, templates); + List 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 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 lowestCells(Map occupancy) { + Map lowest = new HashMap<>(); + for (Map.Entry 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 Map lowestProcessedSolidCells(Map occupancy) { + Map lowest = new HashMap<>(); + for (Map.Entry entry : occupancy.entrySet()) { + OccupancyCell cell = entry.getValue(); + if (cell.blocker() || !isSolidBase(cell.state())) { + continue; + } + 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 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 cells) { + } + + record OccupancyCell(BlockState state, boolean blocker) { + } + + record LowestCell(int x, int y, int z, OccupancyCell occupancy) { + } + + private record LeafPlacement( + StructurePlaceSettings settings, + List rawBlocks, + StructureTemplate template) { + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureTemplatePoolBounds.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureTemplatePoolBounds.java new file mode 100644 index 000000000..815199590 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureTemplatePoolBounds.java @@ -0,0 +1,228 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import com.mojang.datafixers.util.Pair; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.FeaturePoolElement; +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.alias.DirectPoolAlias; +import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding; +import net.minecraft.world.level.levelgen.structure.pools.alias.RandomGroupPoolAlias; +import net.minecraft.world.level.levelgen.structure.pools.alias.RandomPoolAlias; +import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +public final class NativeStructureTemplatePoolBounds { + private NativeStructureTemplatePoolBounds() { + } + + public static int sourceHorizontalSpan(RegistryAccess registryAccess, + StructureTemplateManager templateManager, + JigsawStructure source) { + Registry pools = registryAccess.lookupOrThrow(Registries.TEMPLATE_POOL); + return sourceHorizontalSpan( + templateManager, + source.getStartPool(), + source.getPoolAliases(), + key -> pools.getValue(key.identifier())); + } + + public static int horizontalSpan(RegistryAccess registryAccess, + StructureTemplateManager templateManager, + String templatePoolKey) { + Identifier identifier = Identifier.tryParse(templatePoolKey); + if (identifier == null) { + throw new IllegalArgumentException("Invalid registered template pool key: " + templatePoolKey); + } + Registry pools = registryAccess.lookupOrThrow(Registries.TEMPLATE_POOL); + StructureTemplatePool pool = pools.getValue(identifier); + if (pool == null) { + throw new IllegalArgumentException("Registered template pool does not exist: " + templatePoolKey); + } + return horizontalSpan(pool, templateManager); + } + + public static int horizontalSpan(RegistryAccess registryAccess, + StructureTemplateManager templateManager, + JigsawStructure source, + String templatePoolKey) { + Identifier identifier = Identifier.tryParse(templatePoolKey); + if (identifier == null) { + throw new IllegalArgumentException("Invalid registered template pool key: " + templatePoolKey); + } + Registry pools = registryAccess.lookupOrThrow(Registries.TEMPLATE_POOL); + ResourceKey startPoolKey = ResourceKey.create( + Registries.TEMPLATE_POOL, identifier); + StructureTemplatePool startPool = pools.getValue(identifier); + if (startPool == null) { + throw new IllegalArgumentException("Registered template pool does not exist: " + templatePoolKey); + } + return sourceHorizontalSpan( + templateManager, + startPool, + startPoolKey, + source.getPoolAliases(), + key -> pools.getValue(key.identifier())); + } + + static int sourceHorizontalSpan(StructureTemplateManager templateManager, + Holder startPool, + List aliases, + Function, StructureTemplatePool> poolLookup) { + return sourceHorizontalSpan(templateManager, startPool.value(), + startPool.unwrapKey().orElse(null), aliases, poolLookup); + } + + static int sourceHorizontalSpan(StructureTemplateManager templateManager, + StructureTemplatePool startPool, + ResourceKey startPoolKey, + List aliases, + Function, StructureTemplatePool> poolLookup) { + int maximumSpan = directHorizontalSpan(startPool, templateManager); + if (startPoolKey == null) { + return maximumSpan; + } + Set> targets = new HashSet<>(); + for (PoolAliasBinding binding : aliases) { + collectTargets(binding, startPoolKey, targets); + } + for (ResourceKey target : targets) { + StructureTemplatePool targetPool = poolLookup.apply(target); + if (targetPool == null) { + throw new IllegalStateException("Jigsaw start-pool alias target does not exist: " + + target.identifier()); + } + maximumSpan = Math.max(maximumSpan, directHorizontalSpan(targetPool, templateManager)); + } + return maximumSpan; + } + + private static void collectTargets(PoolAliasBinding binding, + ResourceKey startPoolKey, + Set> targets) { + if (binding instanceof DirectPoolAlias direct) { + if (direct.alias().equals(startPoolKey)) { + targets.add(direct.target()); + } + return; + } + if (binding instanceof RandomPoolAlias random) { + if (random.alias().equals(startPoolKey)) { + random.targets().unwrap().forEach(entry -> targets.add(entry.value())); + } + return; + } + if (binding instanceof RandomGroupPoolAlias group) { + group.groups().unwrap().forEach(entry -> entry.value().forEach( + nested -> collectTargets(nested, startPoolKey, targets))); + return; + } + throw new IllegalStateException("Unsupported jigsaw pool alias type: " + + binding.getClass().getName()); + } + + static int horizontalSpan(StructureTemplatePool pool, + StructureTemplateManager templateManager) { + Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + return horizontalSpan(pool, templateManager, visited); + } + + private static int horizontalSpan(StructureTemplatePool pool, + StructureTemplateManager templateManager, + Set visited) { + if (!visited.add(pool)) { + return 0; + } + int maximumSpan = directHorizontalSpan(pool, templateManager); + Holder fallback = pool.getFallback(); + if (fallback == null || !fallback.isBound()) { + throw new IllegalStateException("Template pool has an unresolved fallback"); + } + return Math.max(maximumSpan, horizontalSpan(fallback.value(), templateManager, visited)); + } + + private static int directHorizontalSpan(StructureTemplatePool pool, + StructureTemplateManager templateManager) { + int maximumSpan = 0; + for (Pair entry : pool.getTemplates()) { + StructurePoolElement element = entry.getFirst(); + validateElement(element, templateManager); + maximumSpan = Math.max(maximumSpan, horizontalSpan(element, templateManager)); + } + return maximumSpan; + } + + private static void validateElement(StructurePoolElement element, + StructureTemplateManager templateManager) { + if (element == EmptyPoolElement.INSTANCE || element instanceof FeaturePoolElement) { + return; + } + if (element instanceof SinglePoolElement single) { + Identifier template; + try { + template = single.getTemplateLocation(); + } catch (RuntimeException inlineTemplate) { + return; + } + if (templateManager.get(template).isEmpty()) { + throw new IllegalStateException("Template pool element does not resolve a structure template: " + + template); + } + return; + } + if (element instanceof ListPoolElement list) { + for (StructurePoolElement child : list.getElements()) { + validateElement(child, templateManager); + } + return; + } + } + + private static int horizontalSpan(StructurePoolElement element, + StructureTemplateManager templateManager) { + if (element == EmptyPoolElement.INSTANCE) { + return 0; + } + long maximumSpan = 0L; + for (Rotation rotation : Rotation.values()) { + BoundingBox box; + try { + box = element.getBoundingBox(templateManager, BlockPos.ZERO, rotation); + } catch (RuntimeException | LinkageError error) { + throw new IllegalStateException("Template pool element " + + element.getClass().getName() + + " could not provide bounded geometry for rotation " + rotation, error); + } + if (box == null) { + throw new IllegalStateException("Template pool element " + + element.getClass().getName() + + " returned no bounded geometry for rotation " + rotation); + } + long xSpan = (long) box.maxX() - box.minX() + 1L; + long zSpan = (long) box.maxZ() - box.minZ() + 1L; + maximumSpan = Math.max(maximumSpan, Math.max(xSpan, zSpan)); + } + if (maximumSpan < 0L || maximumSpan > Integer.MAX_VALUE) { + throw new IllegalStateException("Template pool element has an unbounded horizontal span"); + } + return (int) maximumSpan; + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureTerrainIntegrator.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureTerrainIntegrator.java new file mode 100644 index 000000000..e16922700 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureTerrainIntegrator.java @@ -0,0 +1,1096 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.generation.mantle.StructureCarveEnvelope; +import art.arcane.iris.generation.mantle.StructureCarvingFootprint; +import art.arcane.iris.generation.terrain.IrisMaterialPalette; +import art.arcane.iris.structure.placement.IrisStructureCarveShape; +import art.arcane.iris.structure.placement.IrisStructureTerrain; +import art.arcane.iris.structure.placement.IrisStructureTerrainMode; +import art.arcane.volmlib.util.noise.CNG; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.dimension.BuiltinDimensionTypes; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction; +import net.minecraft.world.level.levelgen.structure.pools.LegacySinglePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +public final class NativeStructureTerrainIntegrator { + private static final long CARVE_CEILING_ROLL_SIGNATURE = 0x2A17L; + private static final long CARVE_FLOOR_ROLL_SIGNATURE = 0x5B3DL; + private static final long CARVE_LOBE_SIGNATURE = 0x7C41L; + private static final int MAX_CACHED_CARVE_CELLS = 2_000_000; + private static final int MAX_CARVE_COLUMNS = 2_000_000; + private static final int MAX_TEMPLATE_OCCUPANCY_CELLS = 4_194_304; + private static final int SOURCE_BURY_HORIZONTAL_RADIUS = 6; + private static final int SOURCE_BURY_VERTICAL_RADIUS = 12; + private static final int SOURCE_ENCAPSULATE_RADIUS = 12; + private static final int SOURCE_JUNCTION_RADIUS = 12; + private static final int SOURCE_MATERIAL_SAMPLE_RADIUS = 8; + private static final List TEMPLATE_VOID_BLOCKS = List.of(Blocks.AIR, Blocks.STRUCTURE_VOID); + private static final Set VANILLA_SOURCE_TERRAIN_BLOCKS = Set.of( + Blocks.STONE, Blocks.GRANITE, Blocks.DIORITE, Blocks.ANDESITE, + Blocks.TUFF, Blocks.DEEPSLATE, Blocks.NETHERRACK, Blocks.BASALT, + Blocks.BLACKSTONE, Blocks.DIRT, Blocks.COARSE_DIRT, Blocks.ROOTED_DIRT, + Blocks.GRASS_BLOCK, Blocks.PODZOL, Blocks.MYCELIUM, Blocks.MUD, + Blocks.MOSS_BLOCK, Blocks.SAND, Blocks.RED_SAND, Blocks.TERRACOTTA, + Blocks.CRIMSON_NYLIUM, Blocks.WARPED_NYLIUM, Blocks.SNOW_BLOCK, + Blocks.ICE, Blocks.PACKED_ICE, Blocks.BLUE_ICE, Blocks.SOUL_SAND, + Blocks.SOUL_SOIL, Blocks.END_STONE, Blocks.GRAVEL, Blocks.CLAY, + Blocks.CALCITE, Blocks.DRIPSTONE_BLOCK, Blocks.SANDSTONE, + Blocks.RED_SANDSTONE, Blocks.SCULK); + // Weakly keyed on the StructureStart so retired starts (world unload, generation done) + // are collectable instead of pinned forever by a process-wide static; values hold only + // primitives, so there is no value-to-key cycle defeating the weak keys. + private static final Map> CARVE_FOOTPRINTS = + new WeakHashMap<>(16); + private static final ConcurrentHashMap> + CARVE_FOOTPRINT_BUILDS = new ConcurrentHashMap<>(); + private static int cachedCarveCells; + + private NativeStructureTerrainIntegrator() { + } + + public static IrisStructureTerrain resolveNativeTerrain(StructureStart start, + IrisStructureTerrain configuredTerrain) { + return configuredTerrain == null + ? new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE) + : configuredTerrain; + } + + static void integrateTerrain(WorldGenLevel world, BoundingBox area, String structureId, + StructureStart start, IrisStructureTerrain configuredTerrain, + NativeStructurePostProcessor.PaletteBlockResolver paletteBlockResolver) { + SourceTerrainSnapshot sourceTerrain = requiresSourceTerrainFill(start, configuredTerrain) + ? captureSourceTerrain(world, area, start, configuredTerrain) : null; + integrateTerrain(world, area, structureId, start, configuredTerrain, + paletteBlockResolver, sourceTerrain); + } + + static void integrateTerrain(WorldGenLevel world, BoundingBox area, String structureId, + StructureStart start, IrisStructureTerrain configuredTerrain, + NativeStructurePostProcessor.PaletteBlockResolver paletteBlockResolver, + SourceTerrainSnapshot sourceTerrain) { + IrisStructureTerrain terrain = configuredTerrain == null + ? new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE) + : configuredTerrain; + IrisStructureTerrainMode mode = terrain.resolvedMode(); + if (mode == IrisStructureTerrainMode.SOURCE) { + integrateSourceTerrain(world, area, start, sourceTerrain); + return; + } + if (mode == IrisStructureTerrainMode.PRESERVE) { + return; + } + if (mode == IrisStructureTerrainMode.VACUUM || mode == IrisStructureTerrainMode.FLATTEN) { + return; + } + if (mode == IrisStructureTerrainMode.ENCASE) { + encasePieces(world, area, structureId, start, terrain, paletteBlockResolver); + return; + } + if (mode != IrisStructureTerrainMode.BORE && mode != IrisStructureTerrainMode.FORCE_CARVE) { + throw new IllegalStateException("Native structure terrain mode " + mode + + " is not implemented for '" + structureId + "'"); + } + IrisStructureCarveShape shape = mode == IrisStructureTerrainMode.BORE + ? IrisStructureCarveShape.BOX : terrain.resolvedShape(); + if (shape == IrisStructureCarveShape.BOX) { + carvePieceBoxes(world, area, start, terrain); + return; + } + carveOrganicColumns(world, area, organicCarve( + carveFootprint(start, Math.max(0, terrain.getHorizontalPadding()), + () -> world.getLevel().getStructureTemplateManager()), + terrain, shape, carveNoiseIdentity(world, structureId, start))); + } + + static SourceTerrainSnapshot captureSourceTerrain( + WorldGenLevel world, BoundingBox area, List targets) { + BitSet requiredLayers = new BitSet(area.getYSpan()); + boolean requiresSnapshot = false; + for (TerrainTarget target : targets) { + if (target != null && requiresSourceTerrainFill(target.start(), target.terrain())) { + requiresSnapshot = true; + markSourceTerrainLayers(area, target.start(), target.terrain(), requiredLayers); + } + } + return requiresSnapshot ? SourceTerrainSnapshot.capture(world, area, requiredLayers) : null; + } + + private static SourceTerrainSnapshot captureSourceTerrain( + WorldGenLevel world, BoundingBox area, StructureStart start, + IrisStructureTerrain configuredTerrain) { + BitSet requiredLayers = new BitSet(area.getYSpan()); + markSourceTerrainLayers(area, start, configuredTerrain, requiredLayers); + return SourceTerrainSnapshot.capture(world, area, requiredLayers); + } + + private static void markSourceTerrainLayers( + BoundingBox area, StructureStart start, IrisStructureTerrain configuredTerrain, + BitSet requiredLayers) { + if (!requiresSourceTerrainFill(start, configuredTerrain)) { + return; + } + TerrainAdjustment adjustment = start.getStructure().terrainAdaptation(); + for (StructurePiece piece : start.getPieces()) { + if (!isSourceRigidPiece(piece)) { + continue; + } + BoundingBox bounds = piece.getBoundingBox(); + long horizontalDistanceSquared = minimumHorizontalDistanceSquared(area, bounds); + if (adjustment == TerrainAdjustment.BURY) { + long remainingDistanceSquared = (long) SOURCE_BURY_VERTICAL_RADIUS + * SOURCE_BURY_VERTICAL_RADIUS - 1L - horizontalDistanceSquared * 4L; + if (remainingDistanceSquared < 0L) { + continue; + } + int verticalRadius = (int) Math.floor(Math.sqrt(remainingDistanceSquared)); + long groundY = bounds.minY(); + if (piece instanceof PoolElementStructurePiece poolPiece) { + groundY += poolPiece.getGroundLevelDelta(); + } + markLayers(area, requiredLayers, + groundY - verticalRadius, groundY + verticalRadius); + } else { + long remainingDistanceSquared = (long) SOURCE_ENCAPSULATE_RADIUS + * SOURCE_ENCAPSULATE_RADIUS - 1L - horizontalDistanceSquared; + if (remainingDistanceSquared < 0L) { + continue; + } + int verticalRadius = (int) Math.floor(Math.sqrt(remainingDistanceSquared)); + markLayers(area, requiredLayers, + (long) bounds.minY() - verticalRadius, + (long) bounds.maxY() + verticalRadius); + } + } + markSourceJunctionLayers(area, start, requiredLayers); + } + + private static void markSourceJunctionLayers( + BoundingBox area, StructureStart start, BitSet requiredLayers) { + Set anchors = sourceJunctionAnchors(start); + for (JunctionAnchor anchor : anchors) { + long deltaX = intervalDistance(area.minX(), area.maxX(), anchor.x(), anchor.x()); + long deltaZ = intervalDistance(area.minZ(), area.maxZ(), anchor.z(), anchor.z()); + long horizontalDistanceSquared = deltaX * deltaX + deltaZ * deltaZ; + long remainingDistanceSquared = (long) SOURCE_JUNCTION_RADIUS + * SOURCE_JUNCTION_RADIUS - 1L - horizontalDistanceSquared; + if (remainingDistanceSquared < 1L) { + continue; + } + int verticalRadius = (int) Math.floor(Math.sqrt(remainingDistanceSquared)); + markLayers(area, requiredLayers, + (long) anchor.y() - verticalRadius, (long) anchor.y() - 1L); + } + } + + private static long minimumHorizontalDistanceSquared(BoundingBox area, BoundingBox bounds) { + long deltaX = intervalDistance(area.minX(), area.maxX(), bounds.minX(), bounds.maxX()); + long deltaZ = intervalDistance(area.minZ(), area.maxZ(), bounds.minZ(), bounds.maxZ()); + return deltaX * deltaX + deltaZ * deltaZ; + } + + private static long intervalDistance(int firstMin, int firstMax, int secondMin, int secondMax) { + if (firstMax < secondMin) { + return (long) secondMin - firstMax; + } + if (firstMin > secondMax) { + return (long) firstMin - secondMax; + } + return 0L; + } + + private static void markLayers( + BoundingBox area, BitSet requiredLayers, long minimumY, long maximumY) { + long clippedMinimumY = Math.max(area.minY(), minimumY); + long clippedMaximumY = Math.min(area.maxY(), maximumY); + if (clippedMinimumY > clippedMaximumY) { + return; + } + int fromIndex = Math.toIntExact(clippedMinimumY - area.minY()); + int toIndex = Math.toIntExact(clippedMaximumY - area.minY() + 1L); + requiredLayers.set(fromIndex, toIndex); + } + + private static boolean requiresSourceTerrainFill( + StructureStart start, IrisStructureTerrain configuredTerrain) { + if (start == null || !start.isValid()) { + return false; + } + IrisStructureTerrain terrain = configuredTerrain == null + ? new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE) + : configuredTerrain; + if (terrain.resolvedMode() != IrisStructureTerrainMode.SOURCE) { + return false; + } + TerrainAdjustment adjustment = start.getStructure().terrainAdaptation(); + return adjustment == TerrainAdjustment.BURY + || adjustment == TerrainAdjustment.ENCAPSULATE; + } + + static boolean clearsLegacyTemplateAir(StructureStart start, IrisStructureTerrain terrain) { + if (start == null || !start.isValid() || terrain == null) { + return false; + } + IrisStructureTerrainMode mode = terrain.resolvedMode(); + return mode == IrisStructureTerrainMode.ENCASE + || mode == IrisStructureTerrainMode.SOURCE + && start.getStructure().terrainAdaptation() != TerrainAdjustment.NONE; + } + + private static void integrateSourceTerrain(WorldGenLevel world, BoundingBox area, + StructureStart start, + SourceTerrainSnapshot sourceTerrain) { + if (start == null || !start.isValid()) { + return; + } + TerrainAdjustment adjustment = start.getStructure().terrainAdaptation(); + if (adjustment == TerrainAdjustment.BURY) { + fillBuriedTerrain(world, area, start, requireSourceTerrain( + world, area, start, sourceTerrain)); + } else if (adjustment == TerrainAdjustment.ENCAPSULATE) { + fillEncapsulatedTerrain(world, area, start, requireSourceTerrain( + world, area, start, sourceTerrain)); + } + } + + private static SourceTerrainSnapshot requireSourceTerrain( + WorldGenLevel world, BoundingBox area, StructureStart start, + SourceTerrainSnapshot sourceTerrain) { + return sourceTerrain == null + ? captureSourceTerrain(world, area, start, null) : sourceTerrain; + } + + private static void fillBuriedTerrain(WorldGenLevel world, BoundingBox area, + StructureStart start, + SourceTerrainSnapshot sourceTerrain) { + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (StructurePiece piece : start.getPieces()) { + if (!isSourceRigidPiece(piece)) { + continue; + } + BoundingBox bounds = piece.getBoundingBox(); + int groundY = bounds.minY(); + if (piece instanceof PoolElementStructurePiece poolPiece) { + groundY += poolPiece.getGroundLevelDelta(); + } + int minX = Math.max(area.minX(), bounds.minX() - SOURCE_BURY_HORIZONTAL_RADIUS); + int maxX = Math.min(area.maxX(), bounds.maxX() + SOURCE_BURY_HORIZONTAL_RADIUS); + int minY = Math.max(area.minY(), groundY - SOURCE_BURY_VERTICAL_RADIUS); + int maxY = Math.min(area.maxY(), groundY + SOURCE_BURY_VERTICAL_RADIUS); + int minZ = Math.max(area.minZ(), bounds.minZ() - SOURCE_BURY_HORIZONTAL_RADIUS); + int maxZ = Math.min(area.maxZ(), bounds.maxZ() + SOURCE_BURY_HORIZONTAL_RADIUS); + for (int x = minX; x <= maxX; x++) { + int outX = outset(x, bounds.minX(), bounds.maxX()); + for (int z = minZ; z <= maxZ; z++) { + int outZ = outset(z, bounds.minZ(), bounds.maxZ()); + for (int y = minY; y <= maxY; y++) { + int vertical = Math.abs(y - groundY); + if (!insideBurialEnvelope(outX, vertical, outZ)) { + continue; + } + fillEncaseable(world, position.set(x, y, z), sourceTerrain); + } + } + } + } + fillSourceJunctionTerrain(world, area, start, position, sourceTerrain); + } + + private static void fillEncapsulatedTerrain(WorldGenLevel world, BoundingBox area, + StructureStart start, + SourceTerrainSnapshot sourceTerrain) { + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (StructurePiece piece : start.getPieces()) { + if (!isSourceRigidPiece(piece)) { + continue; + } + BoundingBox bounds = piece.getBoundingBox(); + int minX = Math.max(area.minX(), bounds.minX() - SOURCE_ENCAPSULATE_RADIUS); + int maxX = Math.min(area.maxX(), bounds.maxX() + SOURCE_ENCAPSULATE_RADIUS); + int minY = Math.max(area.minY(), bounds.minY() - SOURCE_ENCAPSULATE_RADIUS); + int maxY = Math.min(area.maxY(), bounds.maxY() + SOURCE_ENCAPSULATE_RADIUS); + int minZ = Math.max(area.minZ(), bounds.minZ() - SOURCE_ENCAPSULATE_RADIUS); + int maxZ = Math.min(area.maxZ(), bounds.maxZ() + SOURCE_ENCAPSULATE_RADIUS); + for (int x = minX; x <= maxX; x++) { + int outX = outset(x, bounds.minX(), bounds.maxX()); + for (int z = minZ; z <= maxZ; z++) { + int outZ = outset(z, bounds.minZ(), bounds.maxZ()); + for (int y = minY; y <= maxY; y++) { + int outY = outset(y, bounds.minY(), bounds.maxY()); + if (!insideEncapsulationEnvelope(outX, outY, outZ)) { + continue; + } + fillEncaseable(world, position.set(x, y, z), sourceTerrain); + } + } + } + } + fillSourceJunctionTerrain(world, area, start, position, sourceTerrain); + } + + static boolean insideBurialEnvelope(int outX, int verticalDistance, int outZ) { + long horizontalSquared = (long) outX * outX + (long) outZ * outZ; + long verticalSquared = (long) verticalDistance * verticalDistance; + return horizontalSquared * 4L + verticalSquared + < (long) SOURCE_BURY_VERTICAL_RADIUS * SOURCE_BURY_VERTICAL_RADIUS; + } + + static boolean insideEncapsulationEnvelope(int outX, int outY, int outZ) { + return (long) outX * outX + (long) outY * outY + (long) outZ * outZ + < (long) SOURCE_ENCAPSULATE_RADIUS * SOURCE_ENCAPSULATE_RADIUS; + } + + static boolean isSourceRigidPiece(StructurePiece piece) { + if (piece == null) { + return false; + } + return !(piece instanceof PoolElementStructurePiece poolPiece) + || poolPiece.getElement().getProjection() == StructureTemplatePool.Projection.RIGID; + } + + static boolean insideSourceJunctionEnvelope(int deltaX, int deltaY, int deltaZ) { + if (deltaY >= 0) { + return false; + } + return (long) deltaX * deltaX + (long) deltaY * deltaY + (long) deltaZ * deltaZ + < (long) SOURCE_JUNCTION_RADIUS * SOURCE_JUNCTION_RADIUS; + } + + private static void fillSourceJunctionTerrain(WorldGenLevel world, BoundingBox area, + StructureStart start, + BlockPos.MutableBlockPos position, + SourceTerrainSnapshot sourceTerrain) { + Set anchors = sourceJunctionAnchors(start); + for (JunctionAnchor anchor : anchors) { + int minX = Math.max(area.minX(), anchor.x() - SOURCE_JUNCTION_RADIUS + 1); + int maxX = Math.min(area.maxX(), anchor.x() + SOURCE_JUNCTION_RADIUS - 1); + int minY = Math.max(area.minY(), anchor.y() - SOURCE_JUNCTION_RADIUS + 1); + int maxY = Math.min(area.maxY(), anchor.y() - 1); + int minZ = Math.max(area.minZ(), anchor.z() - SOURCE_JUNCTION_RADIUS + 1); + int maxZ = Math.min(area.maxZ(), anchor.z() + SOURCE_JUNCTION_RADIUS - 1); + for (int x = minX; x <= maxX; x++) { + int deltaX = x - anchor.x(); + for (int z = minZ; z <= maxZ; z++) { + int deltaZ = z - anchor.z(); + for (int y = minY; y <= maxY; y++) { + if (insideSourceJunctionEnvelope(deltaX, y - anchor.y(), deltaZ)) { + fillEncaseable(world, position.set(x, y, z), sourceTerrain); + } + } + } + } + } + } + + private static Set sourceJunctionAnchors(StructureStart start) { + Set anchors = new HashSet<>(); + for (StructurePiece piece : start.getPieces()) { + if (!(piece instanceof PoolElementStructurePiece poolPiece)) { + continue; + } + for (JigsawJunction junction : poolPiece.getJunctions()) { + anchors.add(new JunctionAnchor( + junction.getSourceX(), junction.getSourceGroundY(), junction.getSourceZ())); + } + } + return anchors; + } + + private static int outset(int value, int minimum, int maximum) { + if (value < minimum) { + return minimum - value; + } + return Math.max(0, value - maximum); + } + + private static void fillEncaseable(WorldGenLevel world, BlockPos position, + SourceTerrainSnapshot sourceTerrain) { + if (isEncaseable(world.getBlockState(position))) { + world.setBlock(position, sourceEncaseBlock(world, position, sourceTerrain), 2); + } + } + + static BlockState sourceEncaseBlock(WorldGenLevel world, BlockPos position) { + int minX = Math.subtractExact(position.getX(), SOURCE_MATERIAL_SAMPLE_RADIUS); + int minZ = Math.subtractExact(position.getZ(), SOURCE_MATERIAL_SAMPLE_RADIUS); + int maxX = Math.addExact(position.getX(), SOURCE_MATERIAL_SAMPLE_RADIUS); + int maxZ = Math.addExact(position.getZ(), SOURCE_MATERIAL_SAMPLE_RADIUS); + BoundingBox sampleArea = new BoundingBox(minX, position.getY(), minZ, + maxX, position.getY(), maxZ); + BitSet requiredLayers = new BitSet(1); + requiredLayers.set(0); + SourceTerrainSnapshot sourceTerrain = SourceTerrainSnapshot.capture( + world, sampleArea, requiredLayers); + return sourceEncaseBlock(world, position, sourceTerrain); + } + + static BlockState sourceEncaseBlock(WorldGenLevel world, BlockPos position, + SourceTerrainSnapshot sourceTerrain) { + BlockPos.MutableBlockPos probe = new BlockPos.MutableBlockPos(); + for (int distance = 1; distance <= SOURCE_MATERIAL_SAMPLE_RADIUS; distance++) { + BlockState sampled = sourceTerrain.stateAt(probe.set( + position.getX() - distance, position.getY(), position.getZ())); + if (sampled != null) { + return sampled; + } + sampled = sourceTerrain.stateAt(probe.set( + position.getX() + distance, position.getY(), position.getZ())); + if (sampled != null) { + return sampled; + } + sampled = sourceTerrain.stateAt(probe.set( + position.getX(), position.getY(), position.getZ() - distance)); + if (sampled != null) { + return sampled; + } + sampled = sourceTerrain.stateAt(probe.set( + position.getX(), position.getY(), position.getZ() + distance)); + if (sampled != null) { + return sampled; + } + } + return defaultEncaseBlock(world, position.getY()); + } + + private static BlockState sourceTerrainBlock(BlockState state) { + if (!state.isSolid() || NativeStructureVegetationClearer.isTreeBlock(state)) { + return null; + } + return VANILLA_SOURCE_TERRAIN_BLOCKS.contains(state.getBlock()) + || state.is(BlockTags.BASE_STONE_OVERWORLD) + || state.is(BlockTags.BASE_STONE_NETHER) + || state.is(BlockTags.SUBSTRATE_OVERWORLD) + || state.is(BlockTags.DIRT) + || state.is(BlockTags.SAND) + || state.is(BlockTags.TERRACOTTA) + || state.is(BlockTags.MUD) + || state.is(BlockTags.MOSS_BLOCKS) + || state.is(BlockTags.GRASS_BLOCKS) + || state.is(BlockTags.NYLIUM) + || state.is(BlockTags.SNOW) + || state.is(BlockTags.ICE) + || state.is(BlockTags.CORAL_BLOCKS) + || state.is(BlockTags.SOUL_FIRE_BASE_BLOCKS) + ? state : null; + } + + static List contentPieceBounds(StructureStart start) { + List bounds = new ArrayList<>(start.getPieces().size()); + for (StructurePiece piece : start.getPieces()) { + bounds.add(piece.getBoundingBox()); + } + return List.copyOf(bounds); + } + + /** + * Per-column occupancy of the whole start, cached because a single structure spans many chunks and + * every one of them carves against the same shrinkwrapped footprint. + */ + static StructureCarvingFootprint carveFootprint(StructureStart start, int horizontalPadding, + Supplier templates) { + CarveFootprintKey key = new CarveFootprintKey(start, horizontalPadding); + StructureCarvingFootprint cached = cachedCarveFootprint(key); + if (cached != null) { + return cached; + } + CompletableFuture build = new CompletableFuture<>(); + CompletableFuture active = CARVE_FOOTPRINT_BUILDS.putIfAbsent(key, build); + if (active != null) { + return awaitCarveFootprint(active); + } + // Close the check-then-claim window: a racer whose cache check missed before the + // previous builder cached can claim the build slot after that builder retired it, + // and would rebuild a second instance without this re-check. + StructureCarvingFootprint published = cachedCarveFootprint(key); + if (published != null) { + build.complete(published); + CARVE_FOOTPRINT_BUILDS.remove(key, build); + return published; + } + try { + StructureCarvingFootprint footprint = StructureCarvingFootprint.fromColumns( + sink -> emitCarveColumns(start, templates, sink), horizontalPadding, MAX_CARVE_COLUMNS); + if (footprint == null) { + throw new IllegalStateException("Native structure carve footprint is empty or exceeds " + + MAX_CARVE_COLUMNS + " columns"); + } + cacheCarveFootprint(key, footprint); + build.complete(footprint); + return footprint; + } catch (RuntimeException | Error error) { + build.completeExceptionally(error); + throw error; + } finally { + CARVE_FOOTPRINT_BUILDS.remove(key, build); + } + } + + static int cachedCarveFootprintCells() { + synchronized (CARVE_FOOTPRINTS) { + cachedCarveCells = liveCarveCells(); + return cachedCarveCells; + } + } + + private static int liveCarveCells() { + int total = 0; + for (Map perStart : CARVE_FOOTPRINTS.values()) { + for (CachedCarveFootprint cached : perStart.values()) { + total += cached.cells(); + } + } + return total; + } + + static int maximumCachedCarveFootprintCells() { + return MAX_CACHED_CARVE_CELLS; + } + + private static StructureCarvingFootprint cachedCarveFootprint(CarveFootprintKey key) { + synchronized (CARVE_FOOTPRINTS) { + Map perStart = CARVE_FOOTPRINTS.get(key.start()); + CachedCarveFootprint cached = perStart == null ? null : perStart.get(key.padding()); + return cached == null ? null : cached.footprint(); + } + } + + private static StructureCarvingFootprint awaitCarveFootprint( + CompletableFuture future) { + return NativeBuildFutures.awaitBuild(future, "Native structure carve footprint build"); + } + + private static void cacheCarveFootprint(CarveFootprintKey key, + StructureCarvingFootprint footprint) { + int cells = Math.multiplyExact(footprint.width(), footprint.depth()); + synchronized (CARVE_FOOTPRINTS) { + // Weak keys expunge silently as the GC drops retired starts; re-derive the live + // total so the cell budget tracks reality instead of drifting. + cachedCarveCells = liveCarveCells(); + Map perStart = + CARVE_FOOTPRINTS.computeIfAbsent(key.start(), ignored -> new LinkedHashMap<>(4)); + CachedCarveFootprint previous = perStart.remove(key.padding()); + if (previous != null) { + cachedCarveCells -= previous.cells(); + } + Iterator>> eviction = + CARVE_FOOTPRINTS.entrySet().iterator(); + while (cachedCarveCells + cells > MAX_CACHED_CARVE_CELLS && eviction.hasNext()) { + Map.Entry> victim = eviction.next(); + if (victim.getKey() == key.start()) { + continue; + } + for (CachedCarveFootprint evicted : victim.getValue().values()) { + cachedCarveCells -= evicted.cells(); + } + eviction.remove(); + } + perStart.put(key.padding(), new CachedCarveFootprint(footprint, cells)); + cachedCarveCells += cells; + } + } + + static OrganicCarve organicCarve(StructureCarvingFootprint footprint, IrisStructureTerrain terrain, + IrisStructureCarveShape shape, long identity) { + double strength = terrain.resolvedErosionStrength(); + double lobeStrength = terrain.resolvedLobeStrength(); + RNG noiseRng = new RNG(identity); + CNG blob = null; + CNG ceilingRoll = null; + CNG floorRoll = null; + CNG lobe = null; + if (shape == IrisStructureCarveShape.ERODED && strength > 0D) { + blob = CNG.signature(noiseRng); + ceilingRoll = CNG.signature(noiseRng.nextParallelRNG(CARVE_CEILING_ROLL_SIGNATURE)); + floorRoll = CNG.signature(noiseRng.nextParallelRNG(CARVE_FLOOR_ROLL_SIGNATURE)); + } + if (shape == IrisStructureCarveShape.ERODED && lobeStrength > 0D) { + // A plain single octave channel: the fractured signature noise has no usable low frequency band. + lobe = new CNG(noiseRng.nextParallelRNG(CARVE_LOBE_SIGNATURE), 1D, 1); + } + return new OrganicCarve(footprint, shape, + Math.max(0, terrain.getHorizontalPadding()), + Math.max(0, terrain.getCeilingPadding()), + Math.max(0, terrain.getFloorPadding()), + strength, terrain.resolvedErosionFrequency(), blob, ceilingRoll, floorRoll, + lobe, terrain.resolvedLobeFrequency(), lobeStrength); + } + + static void carveOrganicColumns(WorldGenLevel world, BoundingBox area, OrganicCarve carve) { + StructureCarvingFootprint footprint = carve.footprint(); + int minX = Math.max(area.minX(), footprint.minX()); + int maxX = Math.min(area.maxX(), footprint.maxX()); + int minZ = Math.max(area.minZ(), footprint.minZ()); + int maxZ = Math.min(area.maxZ(), footprint.maxZ()); + if (minX > maxX || minZ > maxZ) { + return; + } + boolean eroded = carve.blob() != null; + BlockState air = Blocks.AIR.defaultBlockState(); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (int z = minZ; z <= maxZ; z++) { + for (int x = minX; x <= maxX; x++) { + int index = footprint.indexAt(x, z); + long horizontalDistanceSquared = footprint.distanceSquaredAt(index); + double sideReach = StructureCarveEnvelope.lobedSideReach(carve.lobe(), + carve.lobeFrequency(), carve.lobeStrength(), x, z, carve.horizontalPadding()); + double sideReachSquared = sideReach * sideReach; + if (horizontalDistanceSquared > sideReachSquared) { + continue; + } + double normalizedHorizontal = sideReachSquared == 0D + ? 0D : horizontalDistanceSquared / sideReachSquared; + int sourceMinY = footprint.sourceMinYAt(index); + int sourceMaxY = footprint.sourceMaxYAt(index); + double upReach = eroded + ? StructureCarveEnvelope.lobedUpReach(carve.lobe(), carve.lobeFrequency(), + carve.lobeStrength(), x, z, + StructureCarveEnvelope.erodedUpReach(carve.ceilingRoll(), + carve.frequency(), carve.strength(), x, z, + carve.ceilingPadding())) + : Math.max(1D, carve.ceilingPadding()); + double floorReach = eroded + ? StructureCarveEnvelope.erodedDownReach(carve.floorRoll(), carve.frequency(), + carve.strength(), x, z, carve.floorPadding()) + : carve.floorPadding(); + int columnMinY = Math.max(area.minY(), sourceMinY - (int) Math.floor(floorReach)); + int columnMaxY = Math.min(area.maxY(), + sourceMaxY + (eroded ? (int) Math.ceil(upReach) : carve.ceilingPadding())); + double downReach = Math.max(1D, floorReach); + for (int y = columnMinY; y <= columnMaxY; y++) { + double normalizedVertical = StructureCarveEnvelope.normalizedVerticalDistance( + y, sourceMinY, sourceMaxY, upReach, downReach); + double distanceSquared = normalizedHorizontal + + normalizedVertical * normalizedVertical; + if (distanceSquared > 1D) { + continue; + } + if (distanceSquared > 0D && eroded) { + double noise = carve.blob().fitDouble(0D, 1D, + x * carve.frequency(), y * carve.frequency(), z * carve.frequency()); + if (!StructureCarveEnvelope.shouldCarveOverboreCell( + carve.shape(), distanceSquared, noise, carve.strength())) { + continue; + } + } + world.setBlock(position.set(x, y, z), air, 2); + } + } + } + } + + private static boolean emitCarveColumns(StructureStart start, + Supplier templates, + StructureCarvingFootprint.ColumnSink sink) { + for (StructurePiece piece : start.getPieces()) { + BoundingBox bounds = piece.getBoundingBox(); + if (!(piece instanceof PoolElementStructurePiece poolPiece) + || !emitTemplateColumns(pieceTemplates(poolPiece, templates), + poolPiece.getPosition(), poolPiece.getRotation(), bounds, sink)) { + emitBoxColumns(bounds, sink); + } + } + return true; + } + + /** + * Derives per-column vertical extents from the complement of the template's air and structure-void + * cells, so a carve tracks the actual silhouette instead of the piece's rectangular bounding box. + */ + static boolean emitTemplateColumns(List templates, BlockPos position, + Rotation rotation, BoundingBox bounds, + StructureCarvingFootprint.ColumnSink sink) { + if (templates.isEmpty()) { + return false; + } + int width = bounds.getXSpan(); + int depth = bounds.getZSpan(); + long cells = (long) width * depth * bounds.getYSpan(); + if (cells < 1L || cells > MAX_TEMPLATE_OCCUPANCY_CELLS) { + return false; + } + StructurePlaceSettings settings = new StructurePlaceSettings().setRotation(rotation); + boolean[] voidCells = null; + for (StructureTemplate template : templates) { + boolean[] templateVoid = new boolean[(int) cells]; + for (Block ignored : TEMPLATE_VOID_BLOCKS) { + for (StructureTemplate.StructureBlockInfo info + : template.filterBlocks(position, settings, ignored)) { + BlockPos voidPosition = info.pos(); + if (bounds.isInside(voidPosition)) { + templateVoid[templateCellIndex(bounds, width, depth, + voidPosition.getX(), voidPosition.getY(), voidPosition.getZ())] = true; + } + } + } + if (voidCells == null) { + voidCells = templateVoid; + continue; + } + for (int cell = 0; cell < voidCells.length; cell++) { + voidCells[cell] &= templateVoid[cell]; + } + } + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + int minY = Integer.MAX_VALUE; + int maxY = Integer.MIN_VALUE; + for (int y = bounds.minY(); y <= bounds.maxY(); y++) { + if (voidCells[templateCellIndex(bounds, width, depth, x, y, z)]) { + continue; + } + if (minY == Integer.MAX_VALUE) { + minY = y; + } + maxY = y; + } + if (minY != Integer.MAX_VALUE) { + sink.column(x, z, minY, maxY); + } + } + } + return true; + } + + private static void emitBoxColumns(BoundingBox bounds, StructureCarvingFootprint.ColumnSink sink) { + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + sink.column(x, z, bounds.minY(), bounds.maxY()); + } + } + } + + private static int templateCellIndex(BoundingBox bounds, int width, int depth, + int x, int y, int z) { + return ((y - bounds.minY()) * depth + z - bounds.minZ()) * width + x - bounds.minX(); + } + + private static List pieceTemplates(PoolElementStructurePiece poolPiece, + Supplier templates) { + List resolved = new ArrayList<>(1); + collectElementTemplates(poolPiece.getElement(), templates, resolved); + return resolved; + } + + private static void collectElementTemplates(StructurePoolElement element, + Supplier templates, + List resolved) { + if (element instanceof ListPoolElement listElement) { + for (StructurePoolElement child : listElement.getElements()) { + collectElementTemplates(child, templates, resolved); + } + return; + } + if (element instanceof SinglePoolElement singleElement) { + resolved.add(NativeStructureReflection.resolveTemplate(singleElement, templates)); + } + } + + private static long carveNoiseIdentity(WorldGenLevel world, String structureId, + StructureStart start) { + return world.getSeed() + ^ ((long) start.getChunkPos().x() * 341873128712L) + ^ ((long) start.getChunkPos().z() * 132897987541L) + ^ (structureId == null ? 0 : structureId.hashCode()); + } + + private static void encasePieces(WorldGenLevel world, BoundingBox area, String structureId, + StructureStart start, IrisStructureTerrain terrain, + NativeStructurePostProcessor.PaletteBlockResolver paletteBlockResolver) { + IrisMaterialPalette palette = terrain.getEncasePalette(); + RNG rng = null; + if (palette != null) { + Objects.requireNonNull(paletteBlockResolver, + "Native structure encase palette requires a platform block resolver"); + rng = new RNG(world.getSeed() ^ (structureId == null ? 0 : structureId.hashCode())); + } + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (BoundingBox bounds : contentPieceBounds(start)) { + BoundingBox shell = paddedPieceArea(area, bounds, terrain); + if (shell == null) { + continue; + } + for (int x = shell.minX(); x <= shell.maxX(); x++) { + for (int z = shell.minZ(); z <= shell.maxZ(); z++) { + for (int y = shell.minY(); y <= shell.maxY(); y++) { + BlockState existing = world.getBlockState(position.set(x, y, z)); + if (!isEncaseable(existing)) { + continue; + } + BlockState fill = palette == null + ? defaultEncaseBlock(world, y) + : Objects.requireNonNull( + paletteBlockResolver.resolve(palette, rng, x, y, z), + "Encase palette returned no block for " + structureId + " at " + + x + "," + y + "," + z); + world.setBlock(position, fill, 2); + } + } + } + } + } + + static boolean isEncaseable(BlockState state) { + return state.isAir() || !state.getFluidState().isEmpty(); + } + + static BlockState defaultEncaseBlock(int y) { + return y < 0 ? Blocks.DEEPSLATE.defaultBlockState() : Blocks.STONE.defaultBlockState(); + } + + static BlockState defaultEncaseBlock(WorldGenLevel world, int y) { + ServerLevel level = world == null ? null : world.getLevel(); + if (level == null) { + return defaultEncaseBlock(y); + } + Holder dimensionType = level.dimensionTypeRegistration(); + ResourceKey dimensionTypeKey = dimensionType.unwrapKey().orElse(null); + return defaultEncaseBlock(level.dimension(), dimensionTypeKey, dimensionType.value(), y); + } + + static BlockState defaultEncaseBlock(ResourceKey dimension, int y) { + return defaultEncaseBlock(dimension, null, null, y); + } + + static BlockState defaultEncaseBlock(ResourceKey dimension, + ResourceKey dimensionTypeKey, + DimensionType dimensionType, int y) { + if (Level.NETHER.equals(dimension)) { + return Blocks.NETHERRACK.defaultBlockState(); + } + if (Level.END.equals(dimension)) { + return Blocks.END_STONE.defaultBlockState(); + } + if (BuiltinDimensionTypes.NETHER.equals(dimensionTypeKey) + || dimensionType != null && dimensionType.hasCeiling() && !dimensionType.hasSkyLight()) { + return Blocks.NETHERRACK.defaultBlockState(); + } + if (BuiltinDimensionTypes.END.equals(dimensionTypeKey) + || dimensionType != null && (dimensionType.hasEnderDragonFight() + || dimensionType.hasEndFlashes())) { + return Blocks.END_STONE.defaultBlockState(); + } + return defaultEncaseBlock(y); + } + + private static void carvePieceBoxes(WorldGenLevel world, BoundingBox area, StructureStart start, + IrisStructureTerrain terrain) { + BlockState air = Blocks.AIR.defaultBlockState(); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (BoundingBox bounds : contentPieceBounds(start)) { + BoundingBox carve = paddedPieceArea(area, bounds, terrain); + if (carve == null) { + continue; + } + for (int x = carve.minX(); x <= carve.maxX(); x++) { + for (int z = carve.minZ(); z <= carve.maxZ(); z++) { + for (int y = carve.minY(); y <= carve.maxY(); y++) { + world.setBlock(position.set(x, y, z), air, 2); + } + } + } + } + } + + private static BoundingBox paddedPieceArea(BoundingBox area, BoundingBox bounds, + IrisStructureTerrain terrain) { + int horizontalPadding = Math.max(0, terrain.getHorizontalPadding()); + int minX = Math.max(area.minX(), bounds.minX() - horizontalPadding); + int minY = Math.max(area.minY(), bounds.minY() - Math.max(0, terrain.getFloorPadding())); + int minZ = Math.max(area.minZ(), bounds.minZ() - horizontalPadding); + int maxX = Math.min(area.maxX(), bounds.maxX() + horizontalPadding); + int maxY = Math.min(area.maxY(), bounds.maxY() + Math.max(0, terrain.getCeilingPadding())); + int maxZ = Math.min(area.maxZ(), bounds.maxZ() + horizontalPadding); + if (minX > maxX || minY > maxY || minZ > maxZ) { + return null; + } + return new BoundingBox(minX, minY, minZ, maxX, maxY, maxZ); + } + + static void clearLegacyTemplateAir(WorldGenLevel world, BoundingBox area, + StructureStart start, + Supplier templates) { + for (StructurePiece piece : start.getPieces()) { + if (!(piece instanceof PoolElementStructurePiece poolPiece) + || poolPiece.getElement().getProjection() != StructureTemplatePool.Projection.RIGID + || !intersects(poolPiece.getBoundingBox(), area)) { + continue; + } + int groundY = poolPiece.getBoundingBox().minY() + poolPiece.getGroundLevelDelta(); + StructurePlaceSettings settings = new StructurePlaceSettings() + .setRotation(poolPiece.getRotation()) + .setBoundingBox(area); + clearLegacyTemplateAir(world, poolPiece.getElement(), poolPiece.getPosition(), + groundY, settings, templates); + } + } + + private static void clearLegacyTemplateAir(WorldGenLevel world, StructurePoolElement element, + BlockPos position, int groundY, + StructurePlaceSettings settings, + Supplier templates) { + if (element instanceof ListPoolElement listElement) { + for (StructurePoolElement child : listElement.getElements()) { + clearLegacyTemplateAir(world, child, position, groundY, settings, templates); + } + return; + } + if (!(element instanceof LegacySinglePoolElement legacyElement)) { + return; + } + StructureTemplate template = NativeStructureReflection.resolveTemplate(legacyElement, templates); + clearTemplateAir(world, template, position, groundY, settings); + } + + static void clearTemplateAir(WorldGenLevel world, StructureTemplate template, + BlockPos position, int groundY, + StructurePlaceSettings settings) { + List airBlocks = template.filterBlocks( + position, settings, Blocks.AIR); + BlockState air = Blocks.AIR.defaultBlockState(); + for (StructureTemplate.StructureBlockInfo airBlock : airBlocks) { + BlockPos airPosition = airBlock.pos(); + BlockState existingState = world.getBlockState(airPosition); + if (shouldClearLegacyAir( + airPosition.getY(), groundY, existingState.isAir()) + && !NativeStructureVegetationClearer.isTreeBlock(existingState)) { + world.setBlock(airPosition, air, 2); + } + } + } + + static boolean shouldClearLegacyAir(int airY, int groundY, boolean existingAir) { + return airY >= groundY && !existingAir; + } + + static boolean intersects(BoundingBox first, BoundingBox second) { + return first.maxX() >= second.minX() && first.minX() <= second.maxX() + && first.maxY() >= second.minY() && first.minY() <= second.maxY() + && first.maxZ() >= second.minZ() && first.minZ() <= second.maxZ(); + } + + // StructureStart has no value equality, so the key pins the exact start instance a chunk carves against. + private record CarveFootprintKey(StructureStart start, int padding) { + } + + private record CachedCarveFootprint(StructureCarvingFootprint footprint, int cells) { + } + + private record JunctionAnchor(int x, int y, int z) { + } + + static final class SourceTerrainSnapshot { + private final BoundingBox area; + private final int width; + private final BlockState[][] statesByLayer; + private final int sampledCells; + + private SourceTerrainSnapshot(BoundingBox area, int width, + BlockState[][] statesByLayer, int sampledCells) { + this.area = area; + this.width = width; + this.statesByLayer = statesByLayer; + this.sampledCells = sampledCells; + } + + static SourceTerrainSnapshot capture( + WorldGenLevel world, BoundingBox area, BitSet requiredLayers) { + Objects.requireNonNull(world, "Source terrain snapshot requires a generation level"); + Objects.requireNonNull(area, "Source terrain snapshot requires writable bounds"); + Objects.requireNonNull(requiredLayers, "Source terrain snapshot requires sampled layers"); + int width = area.getXSpan(); + int depth = area.getZSpan(); + int height = area.getYSpan(); + int horizontalCells = Math.multiplyExact(width, depth); + BlockState[][] statesByLayer = new BlockState[height][]; + int sampledCells = 0; + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (int layer = requiredLayers.nextSetBit(0); + layer >= 0; layer = requiredLayers.nextSetBit(layer + 1)) { + if (layer >= height) { + throw new IllegalArgumentException( + "Source terrain snapshot layer exceeds writable bounds: " + layer); + } + BlockState[] states = new BlockState[horizontalCells]; + statesByLayer[layer] = states; + sampledCells = Math.addExact(sampledCells, horizontalCells); + int y = Math.addExact(area.minY(), layer); + for (int z = area.minZ(); z <= area.maxZ(); z++) { + for (int x = area.minX(); x <= area.maxX(); x++) { + int index = (z - area.minZ()) * width + x - area.minX(); + states[index] = sourceTerrainBlock( + world.getBlockState(position.set(x, y, z))); + } + } + } + return new SourceTerrainSnapshot(new BoundingBox( + area.minX(), area.minY(), area.minZ(), + area.maxX(), area.maxY(), area.maxZ()), width, + statesByLayer, sampledCells); + } + + BlockState stateAt(BlockPos position) { + if (!area.isInside(position)) { + return null; + } + BlockState[] states = statesByLayer[position.getY() - area.minY()]; + if (states == null) { + return null; + } + int index = (position.getZ() - area.minZ()) * width + + position.getX() - area.minX(); + return states[index]; + } + + int sampledCells() { + return sampledCells; + } + } + + record OrganicCarve(StructureCarvingFootprint footprint, IrisStructureCarveShape shape, + int horizontalPadding, int ceilingPadding, int floorPadding, + double strength, double frequency, CNG blob, CNG ceilingRoll, CNG floorRoll, + CNG lobe, double lobeFrequency, double lobeStrength) { + } + + public record TerrainTarget(String structureId, StructureStart start, + IrisStructureTerrain terrain) { + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureVanillaLocator.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureVanillaLocator.java new file mode 100644 index 000000000..aab945dcc --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureVanillaLocator.java @@ -0,0 +1,224 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import com.mojang.datafixers.util.Pair; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureCheckResult; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class NativeStructureVanillaLocator { + private NativeStructureVanillaLocator() { + } + + public static Candidate predict(ServerLevel level, HolderSet holders, + BlockPos origin, int radius, boolean findUnexplored) { + if (SharedConstants.DEBUG_DISABLE_FEATURES + || !level.getServer().getWorldGenSettings().options().generateStructures()) { + return null; + } + ChunkGeneratorStructureState state = level.getChunkSource().getGeneratorState(); + Map>> byPlacement = new LinkedHashMap<>(); + for (Holder holder : holders) { + for (StructurePlacement placement : state.getPlacementsForStructure(holder)) { + byPlacement.computeIfAbsent(placement, ignored -> new LinkedHashSet<>()).add(holder); + } + } + if (byPlacement.isEmpty()) { + return null; + } + + StructureManager structureManager = level.structureManager(); + Candidate best = null; + double bestDistance = Double.MAX_VALUE; + List>>> randomPlacements = + new ArrayList<>(byPlacement.size()); + for (Map.Entry>> entry : byPlacement.entrySet()) { + StructurePlacement placement = entry.getKey(); + if (placement instanceof ConcentricRingsStructurePlacement concentric) { + Candidate candidate = predictConcentric( + entry.getValue(), level, structureManager, origin, findUnexplored, + state, concentric); + if (candidate != null) { + double distance = origin.distSqr(candidate.result().getFirst()); + if (distance < bestDistance) { + best = candidate; + bestDistance = distance; + } + } + } else if (placement instanceof RandomSpreadStructurePlacement randomSpread) { + randomPlacements.add(Map.entry(randomSpread, entry.getValue())); + } + } + + int centerChunkX = SectionPos.blockToSectionCoord(origin.getX()); + int centerChunkZ = SectionPos.blockToSectionCoord(origin.getZ()); + int searchRadius = Math.max(0, radius); + for (int ring = 0; ring <= searchRadius; ring++) { + boolean foundInRing = false; + for (Map.Entry>> entry + : randomPlacements) { + Candidate candidate = predictRandomSpread( + entry.getValue(), level, structureManager, + centerChunkX, centerChunkZ, ring, findUnexplored, + state.getLevelSeed(), entry.getKey()); + if (candidate == null) { + continue; + } + foundInRing = true; + double distance = origin.distSqr(candidate.result().getFirst()); + if (distance < bestDistance) { + best = candidate; + bestDistance = distance; + } + } + if (foundInRing) { + return best; + } + } + return best; + } + + private static Candidate predictConcentric(Set> holders, + ServerLevel level, + StructureManager structureManager, + BlockPos origin, + boolean findUnexplored, + ChunkGeneratorStructureState state, + ConcentricRingsStructurePlacement placement) { + List positions = state.getRingPositionsFor(placement); + if (positions == null) { + throw new IllegalStateException( + "Tried to locate structures for an unavailable concentric placement"); + } + Candidate best = null; + double bestDistance = Double.MAX_VALUE; + for (ChunkPos position : positions) { + BlockPos locatePosition = new BlockPos( + SectionPos.sectionToBlockCoord(position.x(), 8), + 32, + SectionPos.sectionToBlockCoord(position.z(), 8)); + double distance = locatePosition.distSqr(origin); + if (best != null && distance >= bestDistance) { + continue; + } + Candidate candidate = predictAt( + holders, level, structureManager, findUnexplored, placement, position); + if (candidate != null) { + best = candidate; + bestDistance = distance; + } + } + return best; + } + + private static Candidate predictRandomSpread(Set> holders, + ServerLevel level, + StructureManager structureManager, + int centerChunkX, int centerChunkZ, + int ring, boolean findUnexplored, + long seed, + RandomSpreadStructurePlacement placement) { + int spacing = placement.spacing(); + for (int offsetX = -ring; offsetX <= ring; offsetX++) { + boolean edgeX = offsetX == -ring || offsetX == ring; + for (int offsetZ = -ring; offsetZ <= ring; offsetZ++) { + boolean edgeZ = offsetZ == -ring || offsetZ == ring; + if (!edgeX && !edgeZ) { + continue; + } + int gridChunkX = centerChunkX + spacing * offsetX; + int gridChunkZ = centerChunkZ + spacing * offsetZ; + ChunkPos candidatePosition = placement.getPotentialStructureChunk( + seed, gridChunkX, gridChunkZ); + Candidate candidate = predictAt( + holders, level, structureManager, + findUnexplored, placement, candidatePosition); + if (candidate != null) { + return candidate; + } + } + } + return null; + } + + private static Candidate predictAt(Set> holders, + ServerLevel level, + StructureManager structureManager, + boolean findUnexplored, + StructurePlacement placement, + ChunkPos candidatePosition) { + for (Holder holder : holders) { + Structure structure = holder.value(); + StructureCheckResult result = structureManager.checkStructurePresence( + candidatePosition, structure, placement, findUnexplored); + if (result == StructureCheckResult.START_NOT_PRESENT) { + continue; + } + if (!findUnexplored && result == StructureCheckResult.START_PRESENT) { + return new Candidate( + Pair.of(placement.getLocatePos(candidatePosition), holder), null); + } + ChunkAccess chunk = level.getChunk( + candidatePosition.x(), candidatePosition.z(), ChunkStatus.STRUCTURE_STARTS); + StructureStart start = structureManager.getStartForStructure( + structure, chunk); + if (start == null || !start.isValid() + || findUnexplored && !start.canBeReferenced()) { + continue; + } + return new Candidate( + Pair.of(placement.getLocatePos(start.getChunkPos()), holder), + findUnexplored ? start : null); + } + return null; + } + + public static final class Candidate { + private final Pair> result; + private final StructureStart referenceStart; + private final AtomicBoolean committed; + + private Candidate(Pair> result, + StructureStart referenceStart) { + this.result = result; + this.referenceStart = referenceStart; + this.committed = new AtomicBoolean(); + } + + public Pair> result() { + return result; + } + + public boolean reference(StructureManager structureManager) { + if (referenceStart == null || !committed.compareAndSet(false, true)) { + return false; + } + if (!referenceStart.canBeReferenced()) { + return false; + } + structureManager.addReference(referenceStart); + return true; + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureVegetationClearer.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureVegetationClearer.java new file mode 100644 index 000000000..5eeb76029 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureVegetationClearer.java @@ -0,0 +1,174 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.tags.BlockTags; +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.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.List; + +public final class NativeStructureVegetationClearer { + private NativeStructureVegetationClearer() { + } + + public static boolean isUndergroundStep(GenerationStep.Decoration step) { + return step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES + || step == GenerationStep.Decoration.UNDERGROUND_DECORATION + || step == GenerationStep.Decoration.STRONGHOLDS; + } + + public static void clearIntersectingVegetation(WorldGenLevel world, ChunkAccess chunk, BoundingBox area, + List starts) { + if (starts == null || starts.isEmpty()) { + return; + } + VegetationSnapshot snapshot = captureVegetation(chunk, area); + if (snapshot.treeBlockCount() == 0) { + return; + } + int columns = area.getXSpan() * area.getZSpan(); + int[] clearMinY = new int[columns]; + int[] clearMaxY = new int[columns]; + Arrays.fill(clearMinY, Integer.MAX_VALUE); + Arrays.fill(clearMaxY, Integer.MIN_VALUE); + for (StructureStart start : starts) { + if (shouldProcessStart(start)) { + markVegetationColumns(area, snapshot, start, clearMinY, clearMaxY); + } + } + clearVegetationColumns(world, area, snapshot, clearMinY, clearMaxY); + } + + static boolean shouldProcessStart(StructureStart start) { + return start != null && start.isValid(); + } + + private static VegetationSnapshot captureVegetation(ChunkAccess chunk, BoundingBox area) { + int width = area.getXSpan(); + int depth = area.getZSpan(); + BitSet[] columns = new BitSet[width * depth]; + int treeBlockCount = 0; + LevelChunkSection[] sections = chunk.getSections(); + int chunkMinX = chunk.getPos().getMinBlockX(); + int chunkMinZ = chunk.getPos().getMinBlockZ(); + for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { + LevelChunkSection section = sections[sectionIndex]; + int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4; + int minY = Math.max(area.minY(), sectionMinY); + int maxY = Math.min(area.maxY(), sectionMinY + 15); + if (minY > maxY || section.hasOnlyAir() + || !section.maybeHas(NativeStructureVegetationClearer::isTreeBlock)) { + continue; + } + for (int y = minY; y <= maxY; y++) { + int localY = y - sectionMinY; + for (int z = area.minZ(); z <= area.maxZ(); z++) { + int localZ = z - chunkMinZ; + for (int x = area.minX(); x <= area.maxX(); x++) { + int localX = x - chunkMinX; + if (!isTreeBlock(section.getBlockState(localX, localY, localZ))) { + continue; + } + int column = (z - area.minZ()) * width + x - area.minX(); + BitSet treeBlocks = columns[column]; + if (treeBlocks == null) { + treeBlocks = new BitSet(area.getYSpan()); + columns[column] = treeBlocks; + } + treeBlocks.set(y - area.minY()); + treeBlockCount++; + } + } + } + } + return new VegetationSnapshot(columns, treeBlockCount); + } + + private static void markVegetationColumns(BoundingBox area, VegetationSnapshot snapshot, + StructureStart start, int[] clearMinY, int[] clearMaxY) { + int width = area.getXSpan(); + int[] pieceTops = new int[clearMinY.length]; + int[] pieceBottoms = new int[clearMinY.length]; + Arrays.fill(pieceTops, Integer.MIN_VALUE); + Arrays.fill(pieceBottoms, Integer.MAX_VALUE); + for (StructurePiece piece : start.getPieces()) { + BoundingBox bounds = piece.getBoundingBox(); + int minX = Math.max(area.minX(), bounds.minX()); + int maxX = Math.min(area.maxX(), bounds.maxX()); + int minZ = Math.max(area.minZ(), bounds.minZ()); + int maxZ = Math.min(area.maxZ(), bounds.maxZ()); + if (minX > maxX || minZ > maxZ) { + continue; + } + for (int z = minZ; z <= maxZ; z++) { + for (int x = minX; x <= maxX; x++) { + int column = (z - area.minZ()) * width + x - area.minX(); + pieceTops[column] = Math.max(pieceTops[column], bounds.maxY()); + pieceBottoms[column] = Math.min(pieceBottoms[column], bounds.minY()); + } + } + } + for (int column = 0; column < pieceTops.length; column++) { + if (snapshot.columns()[column] == null || pieceTops[column] == Integer.MIN_VALUE) { + continue; + } + clearMinY[column] = Math.min(clearMinY[column], pieceBottoms[column] - 1); + clearMaxY[column] = Math.max(clearMaxY[column], pieceTops[column]); + } + } + + private static void clearVegetationColumns(WorldGenLevel world, BoundingBox area, + VegetationSnapshot snapshot, int[] clearMinY, int[] clearMaxY) { + int width = area.getXSpan(); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + BlockState air = Blocks.AIR.defaultBlockState(); + 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(); + BitSet treeBlocks = snapshot.columns()[column]; + if (treeBlocks == null || clearMaxY[column] < clearMinY[column]) { + continue; + } + int firstBit = Math.max(0, clearMinY[column] - area.minY()); + int lastBit = Math.min(area.getYSpan() - 1, clearMaxY[column] - area.minY()); + if (firstBit > lastBit) { + continue; + } + for (int bit = treeBlocks.nextSetBit(firstBit); bit >= 0 && bit <= lastBit; + bit = treeBlocks.nextSetBit(bit + 1)) { + int y = area.minY() + bit; + position.set(x, y, z); + BlockState state = world.getBlockState(position); + if (isTreeBlock(state)) { + world.setBlock(position, air, 2); + } + } + } + } + } + + static boolean isTreeBlock(BlockState state) { + if (state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES)) { + return true; + } + // Field reads instead of a registry byValue map hit — this predicate runs per block + // probe on the hottest native-structure scans. + String path = state.getBlock().builtInRegistryHolder().key().identifier().getPath(); + return path.endsWith("_log") || path.endsWith("_wood") + || path.endsWith("_stem") || path.endsWith("_hyphae") + || path.endsWith("_leaves"); + } + + private record VegetationSnapshot(BitSet[] columns, int treeBlockCount) { + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureVerticalPlacer.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureVerticalPlacer.java new file mode 100644 index 000000000..60aeead42 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureVerticalPlacer.java @@ -0,0 +1,384 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.structure.placement.StructureVerticalBounds; +import art.arcane.iris.structure.placement.IrisStructureYBand; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +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.structures.DesertPyramidPiece; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure; +import net.minecraft.world.level.levelgen.structure.structures.JungleTemplePiece; +import net.minecraft.world.level.levelgen.structure.structures.JungleTempleStructure; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.IntBinaryOperator; + +public final class NativeStructureVerticalPlacer { + private static final String DESERT_PYRAMID_ID = "minecraft:desert_pyramid"; + private static final String JUNGLE_PYRAMID_ID = "minecraft:jungle_pyramid"; + private static final int MAX_BURIAL_COLUMNS = 2_000_000; + private static final int MONUMENT_BASE_BELOW_SEA_LEVEL = 24; + private static final String OCEAN_MONUMENT_ID = "minecraft:monument"; + private static final int UNDERGROUND_SURFACE_CLEARANCE = 1; + + private NativeStructureVerticalPlacer() { + } + + public static int applyVerticalPlacement(StructureStart start, String structureId, int requestedOffset, + int seaLevel, int worldMinY, int worldMaxYExclusive, + boolean underground, boolean preserveSourceY, + IrisStructureYBand yBand, + IntBinaryOperator surfaceHeight) { + if (isOceanMonument(start, structureId)) { + return alignOceanMonumentToSeaLevel( + start, requestedOffset, seaLevel, worldMinY, worldMaxYExclusive); + } + if (isAdjustedScatteredStructure(start, structureId)) { + return alignScatteredStructureToSurface( + start, structureId, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight); + } + return applyVerticalShift(start, requestedOffset, worldMinY, worldMaxYExclusive, + underground, preserveSourceY, yBand, surfaceHeight); + } + + public static StructureStart relocateToMinY(StructureStart start, Structure source, int targetMinY, + LevelHeightAccessor heightAccessor) { + Objects.requireNonNull(start, "Native structure start must not be null"); + Objects.requireNonNull(source, "Native structure source must not be null"); + Objects.requireNonNull(heightAccessor, "Native structure height accessor must not be null"); + if (!start.isValid()) { + return StructureStart.INVALID_START; + } + List pieces = start.getPieces(); + int minY = Integer.MAX_VALUE; + for (StructurePiece piece : pieces) { + minY = Math.min(minY, piece.getBoundingBox().minY()); + } + if (minY == Integer.MAX_VALUE) { + return StructureStart.INVALID_START; + } + int offsetY = Math.subtractExact(targetMinY, minY); + if (offsetY != 0) { + for (StructurePiece piece : pieces) { + moveStructurePiece(piece, offsetY); + } + } + int worldMinY = heightAccessor.getMinY() + 1; + int worldMaxYExclusive = Math.addExact( + heightAccessor.getMinY(), heightAccessor.getHeight()); + for (StructurePiece piece : pieces) { + BoundingBox bounds = piece.getBoundingBox(); + if (bounds.minY() < worldMinY || bounds.maxY() >= worldMaxYExclusive) { + throw new IllegalStateException("Native structure cannot fit target minimum Y " + + targetMinY + " inside world bounds [" + worldMinY + "," + + worldMaxYExclusive + ")"); + } + } + return new StructureStart( + source, + start.getChunkPos(), + start.getReferences(), + new PiecesContainer(List.copyOf(pieces)) + ); + } + + static int alignScatteredStructureToSurface(StructureStart start, String structureId, + int configuredOffset, int worldMinY, + int worldMaxYExclusive, + IntBinaryOperator surfaceHeight) { + Objects.requireNonNull(surfaceHeight, "Scattered native structure requires a terrain height resolver"); + ScatteredFeaturePiece piece = requireAdjustedScatteredPiece(start, structureId); + BoundingBox bounds = start.getBoundingBox(); + BoundingBox pieceBounds = piece.getBoundingBox(); + int surfaceY = representativeScatteredSurfaceY(structureId, pieceBounds, surfaceHeight); + int targetMinY = Math.addExact(Math.addExact(surfaceY, 1), configuredOffset); + int requestedMove = Math.subtractExact(targetMinY, pieceBounds.minY()); + int offsetY = StructureVerticalBounds.clampOffset( + bounds.minY(), bounds.maxY(), requestedMove, worldMinY, worldMaxYExclusive); + if (offsetY != 0) { + moveStructureStart(start, bounds, offsetY); + } + setScatteredHeightPosition(piece, Math.max(0, piece.getBoundingBox().minY())); + return offsetY; + } + + public static int applyVerticalShift(StructureStart start, int requestedOffset, int worldMinY, + int worldMaxYExclusive, boolean underground, + boolean preserveSourceY, IrisStructureYBand yBand, + IntBinaryOperator surfaceHeight) { + BoundingBox bounds = start.getBoundingBox(); + int resolvedOffset = resolveShiftOffset(start, bounds, requestedOffset, worldMinY, + worldMaxYExclusive, underground, preserveSourceY, yBand, surfaceHeight); + int offsetY = StructureVerticalBounds.clampOffset( + bounds.minY(), bounds.maxY(), resolvedOffset, worldMinY, worldMaxYExclusive); + if (offsetY == 0) { + return 0; + } + moveStructureStart(start, bounds, offsetY); + return offsetY; + } + + private static int resolveShiftOffset(StructureStart start, BoundingBox bounds, int requestedOffset, + int worldMinY, int worldMaxYExclusive, boolean underground, + boolean preserveSourceY, IrisStructureYBand yBand, + IntBinaryOperator surfaceHeight) { + if (preserveSourceY) { + return requestedOffset; + } + if (yBand != null) { + return resolveYBandOffset(bounds, yBand, start.getChunkPos()); + } + if (underground) { + return resolveBuriedOffset( + bounds, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight); + } + return requestedOffset; + } + + static int alignOceanMonumentToSeaLevel(StructureStart start, int configuredOffset, int seaLevel, + int worldMinY, int worldMaxYExclusive) { + OceanMonumentPieces.MonumentBuilding building = requireOceanMonumentBuilding(start); + BoundingBox bounds = start.getBoundingBox(); + int targetMinY = Math.addExact( + Math.subtractExact(seaLevel, MONUMENT_BASE_BELOW_SEA_LEVEL), configuredOffset); + int requestedOffset = Math.subtractExact(targetMinY, building.getBoundingBox().minY()); + int offsetY = StructureVerticalBounds.clampOffset( + bounds.minY(), bounds.maxY(), requestedOffset, worldMinY, worldMaxYExclusive); + if (offsetY != requestedOffset) { + throw new IllegalStateException("Ocean monument cannot align to sea level " + seaLevel + + " with configured offset " + configuredOffset + " inside world bounds [" + + worldMinY + "," + worldMaxYExclusive + ")"); + } + if (offsetY == 0) { + return 0; + } + moveStructureStart(start, bounds, offsetY); + return offsetY; + } + + static void ensureMonumentSeaLevelAlignment(StructureStart start, String structureId, + int configuredOffset, int seaLevel, + int worldMinY, int worldMaxYExclusive) { + if (!isOceanMonument(start, structureId)) { + return; + } + synchronized (start) { + alignOceanMonumentToSeaLevel( + start, configuredOffset, seaLevel, worldMinY, worldMaxYExclusive); + } + } + + private static boolean isOceanMonument(StructureStart start, String structureId) { + return OCEAN_MONUMENT_ID.equals(structureId) + && start != null + && start.getStructure() instanceof OceanMonumentStructure; + } + + private static boolean isAdjustedScatteredStructure(StructureStart start, String structureId) { + if (start == null) { + return false; + } + Structure source = start.getStructure(); + return (DESERT_PYRAMID_ID.equals(structureId) && source instanceof DesertPyramidStructure) + || (JUNGLE_PYRAMID_ID.equals(structureId) && source instanceof JungleTempleStructure); + } + + private static ScatteredFeaturePiece requireAdjustedScatteredPiece(StructureStart start, + String structureId) { + Objects.requireNonNull(start, "Scattered native structure start must not be null"); + List pieces = start.getPieces(); + if (pieces.size() != 1) { + throw new IllegalStateException(structureId + " must contain exactly one scattered piece, found " + + pieces.size()); + } + StructurePiece piece = pieces.get(0); + if (DESERT_PYRAMID_ID.equals(structureId) && piece instanceof DesertPyramidPiece desertPyramid) { + return desertPyramid; + } + if (JUNGLE_PYRAMID_ID.equals(structureId) && piece instanceof JungleTemplePiece jungleTemple) { + return jungleTemple; + } + throw new IllegalStateException(structureId + " contains unexpected piece " + + piece.getClass().getName()); + } + + private static int representativeScatteredSurfaceY(String structureId, BoundingBox bounds, + IntBinaryOperator surfaceHeight) { + if (DESERT_PYRAMID_ID.equals(structureId)) { + return lowestSurfaceY(bounds, surfaceHeight); + } + if (JUNGLE_PYRAMID_ID.equals(structureId)) { + return averageSurfaceY(bounds, surfaceHeight); + } + throw new IllegalStateException("Unsupported scattered native structure " + structureId); + } + + private static int lowestSurfaceY(BoundingBox bounds, IntBinaryOperator surfaceHeight) { + int lowestY = Integer.MAX_VALUE; + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + lowestY = Math.min(lowestY, surfaceHeight.applyAsInt(x, z)); + } + } + if (lowestY == Integer.MAX_VALUE) { + throw new IllegalStateException("Scattered native structure has an empty terrain footprint"); + } + return lowestY; + } + + private static int averageSurfaceY(BoundingBox bounds, IntBinaryOperator surfaceHeight) { + long totalY = 0L; + long columns = 0L; + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + totalY += surfaceHeight.applyAsInt(x, z); + columns++; + } + } + if (columns == 0L) { + throw new IllegalStateException("Scattered native structure has an empty terrain footprint"); + } + return Math.toIntExact(totalY / columns); + } + + private static void setScatteredHeightPosition(ScatteredFeaturePiece piece, int heightPosition) { + try { + NativeStructureReflection.ScatteredHeightPositionAccess.FIELD.setInt(piece, heightPosition); + } catch (IllegalAccessException error) { + throw new IllegalStateException("Cannot lock scattered native structure height", error); + } + } + + private static OceanMonumentPieces.MonumentBuilding requireOceanMonumentBuilding(StructureStart start) { + Objects.requireNonNull(start, "Ocean monument start must not be null"); + List pieces = start.getPieces(); + OceanMonumentPieces.MonumentBuilding building = null; + for (StructurePiece piece : pieces) { + if (piece instanceof OceanMonumentPieces.MonumentBuilding monumentBuilding) { + building = monumentBuilding; + } + } + if (pieces.size() != 1 || building == null) { + throw new IllegalStateException("minecraft:monument must contain exactly one MonumentBuilding, found " + + pieces.size() + " pieces"); + } + return building; + } + + private static void moveStructureStart(StructureStart start, BoundingBox cachedBounds, int offsetY) { + for (StructurePiece piece : start.getPieces()) { + moveStructurePiece(piece, offsetY); + } + cachedBounds.move(0, offsetY, 0); + } + + private static void moveStructurePiece(StructurePiece piece, int offsetY) { + piece.move(0, offsetY, 0); + if (piece instanceof OceanMonumentPieces.MonumentBuilding building) { + for (StructurePiece child : monumentChildPieces(building)) { + child.move(0, offsetY, 0); + } + } + if (piece instanceof PoolElementStructurePiece poolPiece) { + List junctions = poolPiece.getJunctions(); + for (int i = 0; i < junctions.size(); i++) { + JigsawJunction junction = junctions.get(i); + junctions.set(i, new JigsawJunction( + junction.getSourceX(), + junction.getSourceGroundY() + offsetY, + junction.getSourceZ(), + junction.getDeltaY(), + junction.getDestProjection())); + } + } + } + + static List monumentChildPieces(OceanMonumentPieces.MonumentBuilding building) { + Object value; + try { + value = NativeStructureReflection.MonumentChildPiecesAccess.FIELD.get(building); + } catch (IllegalAccessException error) { + throw new IllegalStateException("Cannot read Ocean Monument child pieces", error); + } + if (!(value instanceof List children)) { + throw new IllegalStateException("Ocean Monument child-pieces field is not a list"); + } + List pieces = new ArrayList<>(children.size()); + for (Object child : children) { + if (!(child instanceof StructurePiece monumentPiece) + || child.getClass().getEnclosingClass() != OceanMonumentPieces.class) { + throw new IllegalStateException("Ocean Monument child-pieces list contains " + + (child == null ? "null" : child.getClass().getName())); + } + pieces.add(monumentPiece); + } + return List.copyOf(pieces); + } + + static int resolveYBandOffset(BoundingBox bounds, IrisStructureYBand yBand, ChunkPos startChunk) { + Objects.requireNonNull(bounds, "Native structure bounds must not be null"); + Objects.requireNonNull(startChunk, "Native structure Y band requires a start chunk"); + int target = yBandTargetMidpointY( + bounds.minY(), bounds.maxY(), yBand.resolvedMin(), yBand.resolvedMax(), startChunk); + return Math.subtractExact(target, midpointY(bounds.minY(), bounds.maxY())); + } + + static int yBandTargetMidpointY(int minY, int maxY, int bandMin, int bandMax, ChunkPos startChunk) { + int height = Math.subtractExact(maxY, minY); + int lowest = Math.addExact(bandMin, height / 2); + int highest = Math.subtractExact(bandMax, height - height / 2); + if (lowest > highest) { + // The band is shorter than the structure, so only its midpoint can be honoured. + return Math.floorDiv(Math.addExact(bandMin, bandMax), 2); + } + int span = highest - lowest + 1; + if (span == 1) { + return lowest; + } + long identity = (long) startChunk.x() * 341873128712L ^ (long) startChunk.z() * 132897987541L; + return lowest + new RNG(identity).nextInt(span); + } + + private static int midpointY(int minY, int maxY) { + return minY + (maxY - minY) / 2; + } + + static int resolveBuriedOffset(BoundingBox bounds, int requestedOffset, int worldMinY, + int worldMaxYExclusive, IntBinaryOperator surfaceHeight) { + Objects.requireNonNull(bounds, "Native structure bounds must not be null"); + Objects.requireNonNull(surfaceHeight, "Underground native structure requires a terrain height resolver"); + long width = (long) bounds.maxX() - bounds.minX() + 1L; + long depth = (long) bounds.maxZ() - bounds.minZ() + 1L; + if (width <= 0L || depth <= 0L || width > MAX_BURIAL_COLUMNS / depth) { + throw new IllegalStateException("Underground native structure burial footprint is invalid or exceeds " + + MAX_BURIAL_COLUMNS + " columns"); + } + int maximumOffset = requestedOffset; + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + int allowedTopY = surfaceHeight.applyAsInt(x, z) - UNDERGROUND_SURFACE_CLEARANCE; + maximumOffset = Math.min(maximumOffset, allowedTopY - bounds.maxY()); + } + } + int clampedOffset = StructureVerticalBounds.clampOffset( + bounds.minY(), bounds.maxY(), maximumOffset, worldMinY, worldMaxYExclusive); + if (clampedOffset > maximumOffset) { + IrisLogging.warn("Native structure burial at " + bounds.minX() + "," + bounds.minZ() + + " clamped to world floor: wanted " + maximumOffset + ", used " + clampedOffset); + } + return clampedOffset; + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureVolumeIndex.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureVolumeIndex.java new file mode 100644 index 000000000..024c3bb69 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeStructureVolumeIndex.java @@ -0,0 +1,643 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import net.minecraft.world.level.levelgen.densityfunction.SamplerContext; +import art.arcane.iris.generation.runtime.IrisEngine; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy; +import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord; +import art.arcane.iris.structure.nativegen.NativeStructurePlacementPlanner; +import art.arcane.iris.structure.nativegen.NativeStructureStartPlan; +import art.arcane.iris.structure.nativegen.NativeStructureVolume; +import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter; +import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.IntBinaryOperator; +import java.util.function.IntConsumer; +import java.util.function.Supplier; + +/** + * Resolves the world-space piece bounds of every native structure that will generate around a query rect. + * + *

The answer is derived from the seed, the registries and this pack's structure policy only: candidate origins + * come from the placement grid and from vanilla structure placements, and each candidate is assembled through the + * same {@link NativeStructureFactory} path chunk generation uses. Nothing here reads chunk state, ownership records + * or generation progress, so the same query answers identically no matter which chunks already exist. + */ +public final class NativeStructureVolumeIndex { + private static final int ORIGIN_REACH_CHUNKS = NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS; + private static final int ORIGIN_WINDOW_DIAMETER = ORIGIN_REACH_CHUNKS * 2 + 1; + private static final int ORIGIN_WINDOW_STRIPE_COUNT = Long.SIZE; + private static final int MAX_CACHED_ORIGIN_CHUNKS = 16_384; + private static final int MAX_CACHED_QUERY_CHUNKS = 4_096; + private static final Map INDEXES = + Collections.synchronizedMap(new WeakHashMap<>()); + private static final Set WARNED_RESOLUTION_FAILURES = ConcurrentHashMap.newKeySet(); + + private final Context context; + private final OriginResolver origins; + private final Map> originCache = lru(MAX_CACHED_ORIGIN_CHUNKS); + private final Map> queryCache = lru(MAX_CACHED_QUERY_CHUNKS); + private final ConcurrentHashMap>> originBuilds = + new ConcurrentHashMap<>(); + private final ConcurrentHashMap>> queryBuilds = + new ConcurrentHashMap<>(); + private final ReentrantLock[] originWindowStripes = createOriginWindowStripes(); + private final IntConsumer retirementListener = this::evictRuntime; + + private NativeStructureVolumeIndex(Context context) { + this.context = Objects.requireNonNull(context, "Native structure volume context must not be null"); + this.origins = this::buildOriginVolumes; + } + + private NativeStructureVolumeIndex(OriginResolver origins) { + this.context = null; + this.origins = Objects.requireNonNull(origins, "Native structure origin resolver must not be null"); + } + + public static void install(Engine engine, Context context) { + Engine requiredEngine = Objects.requireNonNull(engine, "Native structure volume index requires an engine"); + NativeStructureVolumeIndex installed = new NativeStructureVolumeIndex(context); + synchronized (INDEXES) { + NativeStructureVolumeIndex previous = INDEXES.put(requiredEngine, installed); + if (previous != null) { + previous.detachRetirementListener(requiredEngine); + } + installed.attachRetirementListener(requiredEngine); + } + } + + public static void uninstall(Engine engine) { + if (engine != null) { + synchronized (INDEXES) { + NativeStructureVolumeIndex removed = INDEXES.remove(engine); + if (removed != null) { + removed.detachRetirementListener(engine); + } + } + } + } + + public static void invalidate(Engine engine) { + if (engine == null) { + return; + } + synchronized (INDEXES) { + NativeStructureVolumeIndex current = INDEXES.get(engine); + if (current != null) { + NativeStructureVolumeIndex replacement = current.fresh(); + current.detachRetirementListener(engine); + INDEXES.put(engine, replacement); + replacement.attachRetirementListener(engine); + } + } + } + + public static KList volumes(Engine engine, int minX, int minZ, int maxX, int maxZ) { + NativeStructureVolumeIndex index = engine == null ? null : INDEXES.get(engine); + return index == null ? NativeStructureVolume.NONE : index.resolve(engine, minX, minZ, maxX, maxZ); + } + + static NativeStructureVolumeIndex forTesting(OriginResolver origins) { + return new NativeStructureVolumeIndex(origins); + } + + static int originReachChunks() { + return ORIGIN_REACH_CHUNKS; + } + + private NativeStructureVolumeIndex fresh() { + return context == null + ? new NativeStructureVolumeIndex(origins) + : new NativeStructureVolumeIndex(context); + } + + KList resolve(Engine engine, int minX, int minZ, int maxX, int maxZ) { + int runtimeId = runtimeId(engine); + int fromChunkX = Math.min(minX, maxX) >> 4; + int toChunkX = Math.max(minX, maxX) >> 4; + int fromChunkZ = Math.min(minZ, maxZ) >> 4; + int toChunkZ = Math.max(minZ, maxZ) >> 4; + KList matches = null; + for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) { + for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) { + for (NativeStructureVolume volume : chunkVolumes(engine, runtimeId, chunkX, chunkZ)) { + if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) { + continue; + } + if (matches == null) { + matches = new KList<>(); + } + if (!matches.contains(volume)) { + matches.add(volume); + } + } + } + } + return matches == null ? NativeStructureVolume.NONE : matches; + } + + private KList chunkVolumes( + Engine engine, + int runtimeId, + int chunkX, + int chunkZ + ) { + return cached(queryCache, queryBuilds, new RuntimeChunkKey(runtimeId, chunkKey(chunkX, chunkZ)), + () -> buildChunkVolumes(engine, chunkX, chunkZ)); + } + + private KList buildChunkVolumes(Engine engine, int chunkX, int chunkZ) { + long stripeMask = originWindowStripeMask(chunkX, chunkZ); + lockOriginWindow(stripeMask); + try { + return buildChunkVolumesCoordinated(engine, chunkX, chunkZ); + } finally { + unlockOriginWindow(stripeMask); + } + } + + private KList buildChunkVolumesCoordinated(Engine engine, int chunkX, int chunkZ) { + int minX = chunkX << 4; + int minZ = chunkZ << 4; + int maxX = minX + 15; + int maxZ = minZ + 15; + Integer pinnedRuntimeId = pinnedRuntimeId(engine); + KList volumes = null; + for (int originX = chunkX - ORIGIN_REACH_CHUNKS; originX <= chunkX + ORIGIN_REACH_CHUNKS; originX++) { + for (int originZ = chunkZ - ORIGIN_REACH_CHUNKS; originZ <= chunkZ + ORIGIN_REACH_CHUNKS; originZ++) { + for (NativeStructureVolume volume : scopedOriginVolumes(engine, originX, originZ, pinnedRuntimeId)) { + if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) { + continue; + } + if (volumes == null) { + volumes = new KList<>(); + } + volumes.add(volume); + } + } + } + return volumes == null ? NativeStructureVolume.NONE : volumes; + } + + private static Integer pinnedRuntimeId(Engine engine) { + if (!(engine instanceof IrisEngine irisEngine) || !irisEngine.hasGenerationRuntimeScope()) { + return null; + } + GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null); + if (router == null) { + return null; + } + GenerationHistoryRuntimeRouter.RuntimeOwnership ownership = router.currentRuntimeOwnership().orElse(null); + int runtimeId = runtimeId(engine); + return ownership != null && ownership.binding().runtimeId() == runtimeId ? runtimeId : null; + } + + private KList scopedOriginVolumes( + Engine engine, int chunkX, int chunkZ, Integer pinnedRuntimeId + ) { + if (pinnedRuntimeId != null) { + synchronized (originCache) { + KList cached = originCache.get( + new RuntimeChunkKey(pinnedRuntimeId, chunkKey(chunkX, chunkZ))); + if (cached != null) { + return cached; + } + } + } + return originVolumes(engine, chunkX, chunkZ); + } + + static long originWindowStripeMask(int chunkX, int chunkZ) { + int minCellX = Math.floorDiv(chunkX - ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER); + int maxCellX = Math.floorDiv(chunkX + ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER); + int minCellZ = Math.floorDiv(chunkZ - ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER); + int maxCellZ = Math.floorDiv(chunkZ + ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER); + long mask = 0L; + for (int cellX = minCellX; cellX <= maxCellX; cellX++) { + for (int cellZ = minCellZ; cellZ <= maxCellZ; cellZ++) { + mask |= 1L << originWindowStripe(cellX, cellZ); + } + } + return mask; + } + + KList originVolumes(Engine engine, int chunkX, int chunkZ) { + try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = + openHistoryCoordinateScope(engine, chunkX, chunkZ)) { + int runtimeId = runtimeId(engine); + return cached( + originCache, + originBuilds, + new RuntimeChunkKey(runtimeId, chunkKey(chunkX, chunkZ)), + () -> origins.volumesAt(engine, chunkX, chunkZ) + ); + } + } + + private KList buildOriginVolumes(Engine engine, int chunkX, int chunkZ) { + ChunkGeneratorStructureState state = context.structureState().get(); + ChunkGenerator generator = context.generator().get(); + BiomeSource biomeSource = context.biomeSource().get(); + if (state == null || generator == null || biomeSource == null) { + return NativeStructureVolume.NONE; + } + Registry registry = context.registryAccess().lookupOrThrow(Registries.STRUCTURE); + Bindings bindings = new Bindings(state, generator, biomeSource); + KList volumes = appendPlannedVolumes(engine, bindings, registry, chunkX, chunkZ, null); + volumes = appendVanillaVolumes(engine, bindings, registry, chunkX, chunkZ, volumes); + return volumes == null ? NativeStructureVolume.NONE : volumes; + } + + private KList appendPlannedVolumes(Engine engine, Bindings bindings, + Registry registry, int chunkX, int chunkZ, + KList volumes) { + KList plans; + try { + plans = NativeStructurePlacementPlanner.plansAt(engine, chunkX, chunkZ); + } catch (RuntimeException | Error error) { + warnOnce("iris-placements", error); + return volumes; + } + + KList target = volumes; + for (NativeStructureStartPlan plan : plans) { + IrisNativeStructureDecision decision = NativeStructurePlacementPlanner.decisionFor(plan); + if (!decision.generate()) { + continue; + } + Identifier identifier = Identifier.tryParse(plan.source().getStructure()); + if (identifier == null) { + continue; + } + Structure structure = registry.getValue(identifier); + if (structure == null) { + continue; + } + String structureId = identifier.toString(); + try { + StructureStart generated = NativeStructureFactory.generate( + generationContext(engine, bindings), registry.wrapAsHolder(structure), plan, 0); + target = appendPieces(target, structureId, generated); + } catch (RuntimeException | Error error) { + warnOnce(structureId, error); + } + } + return target; + } + + private KList appendVanillaVolumes(Engine engine, Bindings bindings, + Registry registry, int chunkX, int chunkZ, + KList volumes) { + ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); + IntBinaryOperator surfaceHeight = surfaceHeight(engine); + int worldMinY = context.heightAccessor().getMinY(); + int worldMaxYExclusive = worldMinY + context.heightAccessor().getHeight(); + KList target = volumes; + for (Holder setHolder : bindings.state().possibleStructureSets()) { + StructureSet set = setHolder.value(); + if (!set.placement().isStructureChunk(bindings.state(), chunkX, chunkZ)) { + continue; + } + for (StructureSet.StructureSelectionEntry entry : set.structures()) { + Holder holder = entry.structure(); + Structure structure = holder.value(); + Identifier identifier = registry.getKey(structure); + if (identifier == null) { + continue; + } + String structureId = identifier.toString(); + boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step()); + try { + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve( + engine, structureId, undergroundStep); + if (!decision.generate()) { + continue; + } + StructureStart generated = structure.generate( + holder, + context.levelKey(), + context.registryAccess(), + bindings.generator(), + bindings.biomeSource(), + bindings.state().randomState().createClimateSampler(SamplerContext.EMPTY_UNCACHED), + bindings.state().randomState(), + context.templateManager(), + bindings.state().getLevelSeed(), + chunkPos, + 0, + context.heightAccessor(), + structure.biomes()::contains + ); + if (generated == null || !generated.isValid()) { + continue; + } + NativeStructureVerticalPlacer.applyVerticalPlacement( + generated, + structureId, + decision.yShift(), + bindings.generator().getSeaLevel(), + worldMinY, + worldMaxYExclusive, + undergroundStep, + decision.preserveSourceY(), + decision.yBand(), + surfaceHeight); + target = appendPieces(target, structureId, generated); + } catch (RuntimeException | Error error) { + warnOnce(structureId, error); + } + } + } + return target; + } + + private NativeStructureFactory.GenerationContext generationContext(Engine engine, Bindings bindings) { + return new NativeStructureFactory.GenerationContext( + context.registryAccess(), + bindings.generator(), + bindings.biomeSource(), + bindings.state().randomState(), + context.templateManager(), + bindings.state().getLevelSeed(), + context.levelKey(), + context.heightAccessor(), + biome -> true, + bindings.generator().getSeaLevel(), + surfaceHeight(engine) + ); + } + + private record Bindings(ChunkGeneratorStructureState state, ChunkGenerator generator, BiomeSource biomeSource) { + } + + @FunctionalInterface + interface OriginResolver { + KList volumesAt(Engine engine, int chunkX, int chunkZ); + } + + private static IntBinaryOperator surfaceHeight(Engine engine) { + return (x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight(); + } + + static KList appendPieces(KList volumes, + String structureId, StructureStart start) { + if (start == null || !start.isValid()) { + return volumes; + } + KList target = volumes; + for (StructurePiece piece : start.getPieces()) { + BoundingBox bounds = piece.getBoundingBox(); + if (target == null) { + target = new KList<>(); + } + target.add(new NativeStructureVolume(structureId, + bounds.minX(), bounds.minY(), bounds.minZ(), + bounds.maxX(), bounds.maxY(), bounds.maxZ())); + } + return target; + } + + void evictRuntime(int runtimeId) { + synchronized (originCache) { + originBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId); + originCache.keySet().removeIf(key -> key.runtimeId() == runtimeId); + } + synchronized (queryCache) { + queryBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId); + queryCache.keySet().removeIf(key -> key.runtimeId() == runtimeId); + } + } + + private KList cached( + Map> cache, + ConcurrentHashMap>> builds, + RuntimeChunkKey key, + Supplier> loader + ) { + synchronized (cache) { + KList hit = cache.get(key); + if (hit != null) { + return hit; + } + } + + CompletableFuture> future = new CompletableFuture<>(); + CompletableFuture> existing = builds.putIfAbsent(key, future); + if (existing != null) { + return NativeBuildFutures.awaitBuild(existing, "Native structure volume build"); + } + + // Close the check-then-claim window: a racer whose cache check missed before the + // previous builder cached could claim the build slot after it retired and rebuild. + synchronized (cache) { + KList published = cache.get(key); + if (published != null) { + future.complete(published); + builds.remove(key, future); + return published; + } + } + + try { + KList built = loader.get(); + synchronized (cache) { + if (builds.get(key) == future) { + cache.put(key, built); + } + } + future.complete(built); + return built; + } catch (RuntimeException | Error error) { + future.completeExceptionally(error); + throw error; + } finally { + builds.remove(key, future); + } + } + + private static void warnOnce(String structureId, Throwable error) { + if (WARNED_RESOLUTION_FAILURES.add(structureId)) { + IrisLogging.reportError("Native structure volume resolution failed for '" + structureId + + "'; objects will not be vetoed against it.", error); + } + } + + private static long chunkKey(int chunkX, int chunkZ) { + return ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL); + } + + private static int originWindowStripe(int cellX, int cellZ) { + long mixed = chunkKey(cellX, cellZ); + mixed ^= mixed >>> 33; + mixed *= 0xff51afd7ed558ccdL; + mixed ^= mixed >>> 33; + mixed *= 0xc4ceb9fe1a85ec53L; + mixed ^= mixed >>> 33; + return (int) mixed & (ORIGIN_WINDOW_STRIPE_COUNT - 1); + } + + private static ReentrantLock[] createOriginWindowStripes() { + ReentrantLock[] stripes = new ReentrantLock[ORIGIN_WINDOW_STRIPE_COUNT]; + for (int i = 0; i < stripes.length; i++) { + stripes[i] = new ReentrantLock(); + } + return stripes; + } + + private void lockOriginWindow(long stripeMask) { + long pending = stripeMask; + while (pending != 0L) { + int stripe = Long.numberOfTrailingZeros(pending); + lockOriginStripe(originWindowStripes[stripe]); + pending &= pending - 1L; + } + } + + private static void lockOriginStripe(ReentrantLock lock) { + if (lock.tryLock()) { + return; + } + if (!ForkJoinTask.inForkJoinPool()) { + lock.lock(); + return; + } + OriginLockBlocker blocker = new OriginLockBlocker(lock); + try { + ForkJoinPool.managedBlock(blocker); + } catch (InterruptedException interruption) { + Thread.currentThread().interrupt(); + blocker.block(); + } catch (RejectedExecutionException exhaustedPool) { + blocker.block(); + } + } + + private void unlockOriginWindow(long stripeMask) { + long pending = stripeMask; + while (pending != 0L) { + int stripe = Long.SIZE - 1 - Long.numberOfLeadingZeros(pending); + originWindowStripes[stripe].unlock(); + pending &= ~(1L << stripe); + } + } + + private static final class OriginLockBlocker implements ForkJoinPool.ManagedBlocker { + private final ReentrantLock lock; + private boolean acquired; + + private OriginLockBlocker(ReentrantLock lock) { + this.lock = lock; + } + + @Override + public boolean block() { + if (!acquired) { + lock.lock(); + acquired = true; + } + return true; + } + + @Override + public boolean isReleasable() { + if (!acquired) { + acquired = lock.tryLock(); + } + return acquired; + } + } + + private static GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope( + Engine engine, + int chunkX, + int chunkZ + ) { + if (!(engine instanceof IrisEngine irisEngine)) { + return null; + } + try { + return irisEngine.openGenerationHistoryCoordinateScope(chunkX << 4, chunkZ << 4); + } catch (IOException failure) { + throw new IllegalStateException("Unable to route native structure origin " + + chunkX + "," + chunkZ + " through generation history.", failure); + } + } + + private static int runtimeId(Engine engine) { + return engine == null ? 0 : engine.getCacheID(); + } + + private void attachRetirementListener(Engine engine) { + if (engine instanceof IrisEngine irisEngine) { + irisEngine.addGenerationRuntimeRetirementListener(retirementListener); + } + } + + private void detachRetirementListener(Engine engine) { + if (engine instanceof IrisEngine irisEngine) { + irisEngine.removeGenerationRuntimeRetirementListener(retirementListener); + } + } + + private static Map> lru(int capacity) { + return new LinkedHashMap<>(16, 0.75F, true) { + @Override + protected boolean removeEldestEntry( + Map.Entry> eldest + ) { + return size() > capacity; + } + }; + } + + private record RuntimeChunkKey(int runtimeId, long chunkKey) { + } + + /** + * The generator, biome source and structure state arrive as suppliers so an installed index never holds the + * level (and through it the engine) strongly. That keeps the weakly keyed registry collectable on world unload. + */ + public record Context( + RegistryAccess registryAccess, + StructureTemplateManager templateManager, + ResourceKey levelKey, + LevelHeightAccessor heightAccessor, + Supplier generator, + Supplier biomeSource, + Supplier structureState + ) { + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeTerrainHeightCache.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeTerrainHeightCache.java new file mode 100644 index 000000000..bd54b6f61 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeTerrainHeightCache.java @@ -0,0 +1,42 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; +import net.minecraft.world.level.levelgen.Heightmap; + +import java.util.OptionalInt; +import java.util.function.Supplier; + +public final class NativeTerrainHeightCache { + private static final int MAXIMUM_CACHED_QUERIES = 65_536; + + private final Object2IntLinkedOpenHashMap heights = new Object2IntLinkedOpenHashMap<>(); + + public OptionalInt resolvedHeight(Query query, Supplier resolver) { + synchronized (heights) { + if (heights.containsKey(query)) { + return OptionalInt.of(heights.getAndMoveToFirst(query)); + } + } + OptionalInt resolved = resolver.get(); + if (resolved.isEmpty()) { + return resolved; + } + int height = resolved.getAsInt(); + synchronized (heights) { + heights.putAndMoveToFirst(query, height); + while (heights.size() > MAXIMUM_CACHED_QUERIES) { + heights.removeLastInt(); + } + } + return resolved; + } + + public void evictRuntime(int runtimeId) { + synchronized (heights) { + heights.keySet().removeIf(query -> query.runtimeId() == runtimeId); + } + } + + public record Query(int runtimeId, int x, int z, Heightmap.Types type, int minimumY, int height) { + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeTransitionColumn.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeTransitionColumn.java new file mode 100644 index 000000000..50b3138ad --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/NativeTransitionColumn.java @@ -0,0 +1,63 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.world.history.TerrainBoundarySignature; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBlockState; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.commands.arguments.blocks.BlockStateParser; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.NoiseColumn; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; + +import java.util.HashMap; +import java.util.Map; + +public final class NativeTransitionColumn { + private NativeTransitionColumn() { + } + + public static NoiseColumn column(TerrainBoundarySignature signature, LevelHeightAccessor height) { + BlockState[] states = new BlockState[height.getHeight()]; + Map palette = new HashMap<>(); + for (int offset = 0; offset < states.length; offset++) { + String state = signature.geometry().voxelAt(height.getMinY() + offset).stateKey(); + states[offset] = palette.computeIfAbsent(state, NativeTransitionColumn::parse); + } + return new NoiseColumn(height.getMinY(), states); + } + + public static int height(TerrainBoundarySignature signature, Heightmap.Types type, LevelHeightAccessor height) { + Map palette = new HashMap<>(); + for (int offset = height.getHeight() - 1; offset >= 0; offset--) { + String state = signature.geometry().voxelAt(height.getMinY() + offset).stateKey(); + if (type.isOpaque().test(palette.computeIfAbsent(state, NativeTransitionColumn::parse))) { + return height.getMinY() + offset + 1; + } + } + return height.getMinY(); + } + + private static BlockState parse(String key) { + String nativeKey = key; + if (!key.startsWith("minecraft:") && IrisPlatforms.isBound()) { + PlatformBlockState logicalState = IrisPlatforms.get().registries().blockOrNull(key); + PlatformBlockState baseState = logicalState == null ? null : logicalState.placementBaseState(); + if (baseState != null) { + nativeKey = baseState.key(); + } + } + StringReader reader = new StringReader(nativeKey); + try { + BlockState state = BlockStateParser.parseForBlock(BuiltInRegistries.BLOCK, reader, false).blockState(); + if (reader.canRead()) { + throw new IllegalArgumentException("Unexpected terrain block state suffix: " + reader.getRemaining()); + } + return state; + } catch (CommandSyntaxException failure) { + throw new IllegalArgumentException("Saved terrain block state cannot be resolved: " + key, failure); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/WorldgenTerrainHeightmaps.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/WorldgenTerrainHeightmaps.java new file mode 100644 index 000000000..d02958a48 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/nativegen/v26_3_R1/WorldgenTerrainHeightmaps.java @@ -0,0 +1,113 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import net.minecraft.core.SectionPos; +import net.minecraft.util.Mth; +import net.minecraft.util.SimpleBitStorage; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.StructureStart; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.IntBinaryOperator; + +/** + * Writes WORLD_SURFACE_WG and OCEAN_FLOOR_WG from Iris engine heights. + * + * Iris writes its terrain straight into LevelChunkSection, so neither vanilla's noise fill nor + * ProtoChunk.setBlockState ever updates the two worldgen heightmaps. Without these writes + * ChunkAccess.getHeight lazily primes the map from finished blocks - Iris trees, leaves and snow + * included - and vanilla pieces that self-snap against WORLD_SURFACE_WG (igloo) or OCEAN_FLOOR_WG + * (ocean ruins, buried treasure, mineshaft interiors) anchor to the canopy instead of the terrain. + * + * Both resolvers take world block coordinates and return the absolute first free Y, matching + * Heightmap's internal "first available" semantics: WORLD_SURFACE_WG counts fluid, OCEAN_FLOOR_WG + * does not. + */ +public final class WorldgenTerrainHeightmaps { + private static final int COLUMNS = 256; + private static final int PLACEMENT_CHUNK_MARGIN = 1; + + private WorldgenTerrainHeightmaps() { + } + + public static void primeTerrain(ChunkAccess chunk, IntBinaryOperator surfaceFirstFreeY, + IntBinaryOperator floorFirstFreeY) { + Objects.requireNonNull(chunk, "Iris worldgen heightmap priming requires a chunk"); + Objects.requireNonNull(surfaceFirstFreeY, + "Iris worldgen heightmap priming requires a surface height resolver"); + Objects.requireNonNull(floorFirstFreeY, + "Iris worldgen heightmap priming requires an ocean floor height resolver"); + write(chunk, Heightmap.Types.WORLD_SURFACE_WG, surfaceFirstFreeY); + write(chunk, Heightmap.Types.OCEAN_FLOOR_WG, floorFirstFreeY); + } + + public static void primeStructurePlacement(WorldGenLevel world, ChunkPos generationCenter, + List starts, + IntBinaryOperator surfaceFirstFreeY, + IntBinaryOperator floorFirstFreeY) { + Objects.requireNonNull(world, + "Iris worldgen heightmap priming requires a generation level"); + Objects.requireNonNull(generationCenter, + "Iris worldgen heightmap priming requires a generation center"); + if (starts == null || starts.isEmpty()) { + return; + } + Set primed = new HashSet<>(); + for (StructureStart start : starts) { + if (start == null || !start.isValid()) { + continue; + } + BoundingBox bounds = start.getBoundingBox(); + int minChunkX = SectionPos.blockToSectionCoord(bounds.minX()) - PLACEMENT_CHUNK_MARGIN; + int maxChunkX = SectionPos.blockToSectionCoord(bounds.maxX()) + PLACEMENT_CHUNK_MARGIN; + int minChunkZ = SectionPos.blockToSectionCoord(bounds.minZ()) - PLACEMENT_CHUNK_MARGIN; + int maxChunkZ = SectionPos.blockToSectionCoord(bounds.maxZ()) + PLACEMENT_CHUNK_MARGIN; + for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { + for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { + if (Math.abs(chunkX - generationCenter.x()) > PLACEMENT_CHUNK_MARGIN + || Math.abs(chunkZ - generationCenter.z()) > PLACEMENT_CHUNK_MARGIN) { + continue; + } + if (!primed.add(ChunkPos.pack(chunkX, chunkZ))) { + continue; + } + if (!world.hasChunk(chunkX, chunkZ)) { + continue; + } + primeTerrain(world.getChunk(chunkX, chunkZ), surfaceFirstFreeY, floorFirstFreeY); + } + } + } + } + + private static void write(ChunkAccess chunk, Heightmap.Types type, IntBinaryOperator firstFreeY) { + ChunkPos pos = chunk.getPos(); + int minY = chunk.getMinY(); + int height = chunk.getHeight(); + int baseX = pos.getMinBlockX(); + int baseZ = pos.getMinBlockZ(); + SimpleBitStorage storage = new SimpleBitStorage(Mth.ceillog2(height + 1), COLUMNS); + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + int firstFree = firstFreeY.applyAsInt(baseX + x, baseZ + z) - minY; + storage.set(x + z * 16, Mth.clamp(firstFree, 0, height)); + } + } + Heightmap heightmap = chunk.getOrCreateHeightmapUnprimed(type); + long[] packed = storage.getRaw(); + int expected = heightmap.getRawData().length; + if (expected != packed.length) { + throw new IllegalStateException("Iris cannot prime " + type + " for chunk " + + pos.x() + "," + pos.z() + ": packed " + packed.length + + " words for a heightmap storing " + expected + + "; Heightmap.setRawData would silently fall back to block scanning"); + } + heightmap.setRawData(chunk, type, packed); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/CustomBiomeSource.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/CustomBiomeSource.java new file mode 100644 index 000000000..d243a7510 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/CustomBiomeSource.java @@ -0,0 +1,1287 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import net.minecraft.world.level.biome.BiomeResolver; +import net.minecraft.world.level.levelgen.RandomState; +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.MapCodec; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.generation.runtime.DimensionStackContext; +import art.arcane.iris.generation.runtime.DimensionStackLayout; +import art.arcane.iris.generation.runtime.DimensionTerrainContext; +import art.arcane.iris.generation.cache.AtomicCache; +import art.arcane.iris.generation.runtime.IrisEngine; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.GenerationSessionException; +import art.arcane.iris.generation.runtime.GenerationSessionLease; +import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter; +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.biome.IrisBiomeCustom; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver; +import art.arcane.iris.platform.generation.BukkitChunkGenerator; +import art.arcane.iris.generation.context.IrisContext; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.core.QuartPos; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.util.RandomSource; +import net.minecraft.util.Mth; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.biome.Climate; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.CraftWorld; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Stream; + +public class CustomBiomeSource extends BiomeSource implements BiomeResolver { + private static final int NOISE_BIOME_CACHE_MAX = 262144; + private static final int STRONGHOLD_RING_SEARCH_Y = 0; + private static final int STRONGHOLD_RING_SEARCH_RADIUS = 112; + private static final int STRONGHOLD_RING_SEARCH_QUART_STEP = 4; + + private final long seed; + private final Engine engine; + private final BukkitChunkGenerator platformGenerator; + private final Registry biomeCustomRegistry; + private final Registry biomeRegistry; + private final AtomicCache registryAccess = new AtomicCache<>(); + private final Holder fallbackBiome; + private final ConcurrentHashMap> noiseBiomeCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> structureBiomeCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> surfaceStructureBiomeCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> naturalSurfaceStructureBiomeCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap runtimeBiomeStates = new ConcurrentHashMap<>(); + + public CustomBiomeSource(long seed, Engine engine, World world) { + this.engine = engine; + this.platformGenerator = world.getGenerator() instanceof BukkitChunkGenerator generator + ? generator + : null; + this.seed = seed; + this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null); + this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null); + this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry); + if (engine instanceof IrisEngine irisEngine) { + irisEngine.addGenerationRuntimeRetirementListener(this::evictRuntimeCaches); + } + runtimeBiomeState(); + } + + private static List> getAllBiomes( + Registry customRegistry, + Registry registry, + Engine engine, + boolean includeDimensionStack + ) { + LinkedHashSet> biomes = new LinkedHashSet<>(); + + for (OwnedBiome ownedBiome : getHostOwnedBiomes(engine)) { + IrisBiome i = ownedBiome.biome(); + Holder vanillaHolder = resolveBiomeHolder(registry, i.getStructureDerivativeKey()); + if (vanillaHolder == null) { + throw new IllegalStateException("Iris structure biome derivative '" + + i.getStructureDerivativeKey() + "' is not registered for biome '" + i.getLoadKey() + "'"); + } + biomes.add(vanillaHolder); + } + + if (includeDimensionStack) { + biomes.addAll(getVisibleBiomes( + customRegistry, + registry, + engine, + resolveFallbackBiome(registry, customRegistry) + )); + } + + if (biomes.isEmpty()) { + throw new IllegalStateException("Iris pack '" + engine.getName() + + "' has no registered structure biomes"); + } + return new ArrayList<>(biomes); + } + + private static List> getVisibleBiomes( + Registry customRegistry, + Registry registry, + Engine engine, + Holder fallback + ) { + LinkedHashSet> biomes = new LinkedHashSet<>(); + boolean fallbackPossible = false; + for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) { + IrisBiome irisBiome = ownedBiome.biome(); + if (irisBiome.isCustom()) { + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + Holder customHolder = resolveCustomBiomeHolder( + customRegistry, engine, ownedBiome.dimension(), customBiome.getId()); + if (customHolder == null) { + throw new IllegalStateException("Iris custom visible biome '" + + customBiomeKey(ownedBiome.dimension(), customBiome.getId()) + + "' is not registered"); + } + biomes.add(customHolder); + } + continue; + } + fallbackPossible |= !addVisibleBiomeHolder( + biomes, registry, irisBiome.getDerivativeKey()); + for (String scatter : irisBiome.getBiomeScatter()) { + fallbackPossible |= !addVisibleBiomeHolder(biomes, registry, scatter); + } + for (String scatter : irisBiome.getBiomeSkyScatter()) { + fallbackPossible |= !addVisibleBiomeHolder(biomes, registry, scatter); + } + } + if (fallbackPossible && fallback != null) { + biomes.add(fallback); + } + return new ArrayList<>(biomes); + } + + private static boolean addVisibleBiomeHolder( + Set> biomes, + Registry registry, + String biomeKey + ) { + Holder holder = resolveBiomeHolder(registry, biomeKey); + if (holder == null) { + return false; + } + biomes.add(holder); + return true; + } + + private static Object getFor(Class type, Object source) { + Object o = fieldFor(type, source); + + if (o != null) { + return o; + } + + o = invokeFor(type, source); + + if (o != null) { + return o; + } + + throw new IllegalStateException("Iris cannot resolve a " + type.getName() + + " from " + source.getClass().getName() + " on this server version"); + } + + private static Object fieldFor(Class returns, Object in) { + for (Class sourceType = in.getClass(); sourceType != null; sourceType = sourceType.getSuperclass()) { + Object o = fieldForClass(returns, sourceType, in); + + if (o != null) { + return o; + } + } + + return null; + } + + private static Object invokeFor(Class returns, Object in) { + for (Method i : in.getClass().getMethods()) { + if (i.getReturnType().equals(returns)) { + i.setAccessible(true); + try { + IrisLogging.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()"); + return i.invoke(in); + } catch (ReflectiveOperationException | RuntimeException e) { + throw new IllegalStateException("Iris failed to invoke " + in.getClass().getName() + "." + + i.getName() + "() for " + returns.getName(), e); + } + } + } + + return null; + } + + @SuppressWarnings("unchecked") + private static T fieldForClass(Class returnType, Class sourceType, Object in) { + for (Field i : sourceType.getDeclaredFields()) { + if (i.getType().equals(returnType)) { + i.setAccessible(true); + try { + IrisLogging.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName()); + return (T) i.get(in); + } catch (IllegalAccessException | RuntimeException e) { + throw new IllegalStateException("Iris failed to read " + sourceType.getName() + "." + + i.getName() + " for " + returnType.getName(), e); + } + } + } + return null; + } + + @Override + protected Stream> collectPossibleBiomes() { + return possibleBiomes(true).stream(); + } + + @Override + public Set> possibleBiomes() { + return possibleBiomes(true); + } + + Set> possibleStructureBiomes() { + return possibleBiomes(false); + } + + private Set> possibleBiomes(boolean includeDimensionStack) { + GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_possible_biomes"); + if (lease == null) { + throw new IllegalStateException("Iris possible biome lookup was rejected during an engine transition"); + } + try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + if (!isRuntimeAvailable()) { + throw new IllegalStateException("Iris possible biome lookup has no active engine runtime"); + } + runtimeBiomeState(); + World world = BukkitWorldBinding.world(engine.getWorld()); + if (world == null) { + throw new IllegalStateException("Iris biome source has no bound Bukkit world"); + } + Registry customRegistry = ((RegistryAccess) getFor( + RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())) + .lookup(Registries.BIOME).orElse(null); + Registry worldRegistry = ((CraftWorld) world).getHandle().registryAccess() + .lookup(Registries.BIOME).orElse(null); + return Set.copyOf(getAllBiomes( + customRegistry, worldRegistry, engine, includeDimensionStack)); + } + } + + private KMap> fillCustomBiomes(Registry customRegistry, Engine engine, Holder fallback) { + KMap> m = new KMap<>(); + if (customRegistry == null) { + return m; + } + + for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) { + IrisBiome i = ownedBiome.biome(); + if (i.isCustom()) { + for (IrisBiomeCustom j : i.getCustomDerivitives()) { + String key = customBiomeKey(ownedBiome.dimension(), j.getId()); + Holder holder = resolveCustomBiomeHolder( + customRegistry, engine, ownedBiome.dimension(), j.getId()); + if (holder == null) { + if (fallback != null) { + m.put(key, fallback); + } + IrisLogging.error("Cannot find biome for IrisBiomeCustom " + key + + " from engine " + engine.getName()); + continue; + } + m.put(key, holder); + } + } + } + + return m; + } + + private Map> fillVanillaSpawnBiomes(Registry customRegistry, Registry registry, Engine engine) { + IdentityHashMap> spawnBiomes = new IdentityHashMap<>(); + if (customRegistry == null || registry == null) { + return Collections.unmodifiableMap(spawnBiomes); + } + + for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) { + IrisBiome irisBiome = ownedBiome.biome(); + if (!irisBiome.isCustom()) { + continue; + } + Holder vanillaHolder = NMSBinding.biomeToBiomeBase(registry, irisBiome.getVanillaDerivative()); + if (vanillaHolder == null) { + continue; + } + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + Holder customHolder = resolveCustomBiomeHolder( + customRegistry, engine, ownedBiome.dimension(), customBiome.getId()); + if (customHolder != null) { + spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder); + } + } + } + + return Collections.unmodifiableMap(spawnBiomes); + } + + Holder getRetainedVanillaSpawnBiome(String derivativeKey) { + return resolveBiomeHolder(biomeRegistry, derivativeKey); + } + + Holder getVanillaSpawnBiome(Holder biome) { + if (biome == null) { + return null; + } + GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_spawn_biome"); + if (lease == null) { + return null; + } + try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + if (!isRuntimeAvailable()) { + return null; + } + return runtimeBiomeState().vanillaSpawnBiomes().get(biome.value()); + } + } + + Holder getVisibleSurfaceBiome(int blockX, int blockZ) { + GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_surface_spawn_biome"); + if (lease == null) { + return null; + } + try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + if (!isRuntimeAvailable()) { + return null; + } + if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) { + return fallbackBiome; + } + runtimeBiomeState(); + int quartX = QuartPos.fromBlock(blockX); + int quartZ = QuartPos.fromBlock(blockZ); + int sampleX = QuartPos.toBlock(quartX); + int sampleZ = QuartPos.toBlock(quartZ); + boolean cacheable = isBiomeCacheable(engine, sampleX, sampleZ); + DimensionStackContext stackContext = engine.getDimensionStackContext(); + DimensionStackLayout.Layer layer = stackContext == null + ? null + : stackContext.getLayout(sampleX, sampleZ).surfaceLayer(); + int internalY = layer == null + ? Engine.hostHeight(engine, sampleX, sampleZ, true) + : layer.clippedSurfaceY(); + int quartY = QuartPos.fromBlock(internalY + engine.getWorld().minHeight()); + return getVisibleNoiseBiomeWithActiveGenerationLease( + quartX, quartY, quartZ, null, null, cacheable); + } + } + + private RegistryAccess registry() { + RegistryAccess access = registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())); + + if (access == null) { + throw new IllegalStateException("Iris cannot resolve the Minecraft registry access on this server version"); + } + + return access; + } + + @Override + protected MapCodec codec() { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public BiomeResolver createResolver(Climate.Sampler sampler) { + return this; + } + + @Override + public Holder getNoiseBiome(int x, int y, int z) { + int blockX = x << 2; + int blockZ = z << 2; + try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = + openHistoryCoordinateScope(blockX, blockZ, "bukkit_structure_biome"); + GenerationSessionLease lease = requireGenerationLease( + "bukkit_structure_biome", + "Iris structure biome lookup was rejected during an engine transition"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + if (!isRuntimeAvailable()) { + throw new IllegalStateException("Iris structure biome lookup has no active engine runtime"); + } + return getStructureNoiseBiomeWithActiveGenerationLease(x, y, z); + } + } + + private Holder getStructureNoiseBiomeWithActiveGenerationLease(int x, int y, int z) { + if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) { + return fallbackBiome; + } + if (isGuaranteedSurfaceBiome(y)) { + return getSurfaceStructureBiomeHolder(x, z); + } + + boolean cacheable = isBiomeCacheable(x, z); + RuntimeNoiseKey cacheKey = new RuntimeNoiseKey( + engine.getCacheID(), packNoiseKey(x, y, z)); + if (cacheable) { + Holder cachedHolder = structureBiomeCache.get(cacheKey); + if (cachedHolder != null) { + return cachedHolder; + } + } + + Holder resolvedHolder = resolveStructureBiomeHolder(x, y, z); + if (!cacheable) { + return resolvedHolder; + } + Holder existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder); + if (existingHolder != null) { + return existingHolder; + } + + if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) { + structureBiomeCache.clear(); + } + + return resolvedHolder; + } + + @Override + public Pair> findBiomeHorizontal( + int x, + int y, + int z, + int searchRadius, + Predicate> allowed, + RandomSource random, + RandomState sampler + ) { + int quartStep = horizontalBiomeSearchQuartStep(y, searchRadius); + if (quartStep == 1) { + return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler); + } + GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_structure_ring_biome"); + if (lease == null) { + throw new IllegalStateException("Iris structure ring biome lookup was rejected during an engine transition"); + } + try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + if (!isRuntimeAvailable()) { + throw new IllegalStateException("Iris structure ring biome lookup has no active engine runtime"); + } + return findNaturalSurfaceBiomeHorizontal( + x, y, z, searchRadius, quartStep, allowed, random); + } + } + + @Override + public Pair> findClosestBiome3d( + BlockPos origin, + int searchRadius, + int sampleResolutionHorizontal, + int sampleResolutionVertical, + Predicate> allowed, + RandomState sampler, + LevelReader level + ) { + GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_locate_visible_biome"); + if (lease == null) { + throw new IllegalStateException("Iris visible biome search was rejected during an engine transition"); + } + try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + prepareVisibleBiomeBatch(); + DimensionStackContext stackContext = engine.getDimensionStackContext(); + if (stackContext == null) { + return super.findClosestBiome3d( + origin, searchRadius, sampleResolutionHorizontal, sampleResolutionVertical, + allowed, sampler, level); + } + Set> candidates = new HashSet<>(); + for (Holder biome : getVisibleBiomes( + biomeCustomRegistry, biomeRegistry, engine, fallbackBiome)) { + if (allowed.test(biome)) { + candidates.add(biome); + } + } + if (candidates.isEmpty()) { + return null; + } + int sampleRadius = Math.floorDiv(searchRadius, sampleResolutionHorizontal); + int[] sampleYs = Mth.outFromOrigin( + origin.getY(), level.getMinY() + 1, level.getMaxY() + 1, sampleResolutionVertical).toArray(); + for (BlockPos.MutableBlockPos sampleColumn : BlockPos.spiralAround( + BlockPos.ZERO, sampleRadius, Direction.EAST, Direction.SOUTH)) { + int blockX = origin.getX() + sampleColumn.getX() * sampleResolutionHorizontal; + int blockZ = origin.getZ() + sampleColumn.getZ() * sampleResolutionHorizontal; + int quartX = QuartPos.fromBlock(blockX); + int quartZ = QuartPos.fromBlock(blockZ); + DimensionStackLayout stackLayout = stackContext.sample( + QuartPos.toBlock(quartX), QuartPos.toBlock(quartZ)); + for (int blockY : sampleYs) { + int quartY = QuartPos.fromBlock(blockY); + Holder visibleBiome = resolveVisibleBiomeHolder( + quartX, quartY, quartZ, null, stackLayout); + if (candidates.contains(visibleBiome)) { + return Pair.of(new BlockPos(blockX, blockY, blockZ), visibleBiome); + } + } + } + return null; + } + } + + static int horizontalBiomeSearchQuartStep(int blockY, int searchRadius) { + return blockY == STRONGHOLD_RING_SEARCH_Y && searchRadius == STRONGHOLD_RING_SEARCH_RADIUS + ? STRONGHOLD_RING_SEARCH_QUART_STEP + : 1; + } + + @Override + public Set> getBiomesWithin(int x, int y, int z, int radius) { + GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_biomes_within"); + if (lease == null) { + throw new IllegalStateException("Iris biome radius lookup was rejected during an engine transition"); + } + try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + if (!isRuntimeAvailable()) { + throw new IllegalStateException("Iris biome radius lookup has no active engine runtime"); + } + int minQuartY = QuartPos.fromBlock(y - radius); + boolean monumentQuery = radius == 29 + && y == engine.getMinHeight() + engine.getDimension().getFluidHeight(); + if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY)) { + return BiomeResolver.super.getBiomesWithin(x, y, z, radius); + } + int minQuartX = QuartPos.fromBlock(x - radius); + int maxQuartX = QuartPos.fromBlock(x + radius); + int minQuartZ = QuartPos.fromBlock(z - radius); + int maxQuartZ = QuartPos.fromBlock(z + radius); + int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1); + Set> biomes = new HashSet<>(columns); + for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) { + for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) { + biomes.add(getSurfaceStructureBiomeHolder(quartX, quartZ)); + } + } + return biomes; + } + } + + private Holder getSurfaceStructureBiomeHolder(int x, int z) { + int blockX = x << 2; + int blockZ = z << 2; + try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = + openHistoryCoordinateScope(blockX, blockZ, "bukkit_surface_structure_biome")) { + RuntimeColumnKey columnKey = new RuntimeColumnKey( + engine.getCacheID(), packColumnKey(x, z)); + Holder surfaceHolder = surfaceStructureBiomeCache.get(columnKey); + if (surfaceHolder != null) { + return surfaceHolder; + } + Holder resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z); + Holder existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent( + columnKey, resolvedSurfaceHolder); + if (existingSurfaceHolder != null) { + return existingSurfaceHolder; + } + if (surfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) { + surfaceStructureBiomeCache.clear(); + } + return resolvedSurfaceHolder; + } + } + + private Pair> findNaturalSurfaceBiomeHorizontal( + int x, + int y, + int z, + int searchRadius, + int quartStep, + Predicate> allowed, + RandomSource random + ) { + int centerQuartX = QuartPos.fromBlock(x); + int centerQuartZ = QuartPos.fromBlock(z); + int quartRadius = QuartPos.fromBlock(searchRadius); + Pair> selected = null; + int matches = 0; + for (int radius = 0; radius <= quartRadius; radius += quartStep) { + for (int offsetZ = -radius; offsetZ <= radius; offsetZ += quartStep) { + for (int offsetX = -radius; offsetX <= radius; offsetX += quartStep) { + int quartX = centerQuartX + offsetX; + int quartZ = centerQuartZ + offsetZ; + Holder holder = getNaturalSurfaceStructureBiomeHolder(quartX, quartZ); + if (!allowed.test(holder)) { + continue; + } + if (selected == null || random.nextInt(matches + 1) == 0) { + selected = Pair.of(new BlockPos( + QuartPos.toBlock(quartX), + y, + QuartPos.toBlock(quartZ)), holder); + } + matches++; + } + } + } + return selected; + } + + private Holder getNaturalSurfaceStructureBiomeHolder(int x, int z) { + int blockX = x << 2; + int blockZ = z << 2; + try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = + openHistoryCoordinateScope(blockX, blockZ, "bukkit_natural_structure_biome")) { + RuntimeColumnKey columnKey = new RuntimeColumnKey( + engine.getCacheID(), packColumnKey(x, z)); + Holder cachedHolder = naturalSurfaceStructureBiomeCache.get(columnKey); + if (cachedHolder != null) { + return cachedHolder; + } + Holder resolvedHolder = resolveNaturalSurfaceStructureBiomeHolder(x, z); + Holder existingHolder = naturalSurfaceStructureBiomeCache.putIfAbsent( + columnKey, resolvedHolder); + if (existingHolder != null) { + return existingHolder; + } + if (naturalSurfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) { + naturalSurfaceStructureBiomeCache.clear(); + } + return resolvedHolder; + } + } + + private boolean isGuaranteedSurfaceBiome(int quartY) { + if (engine == null || engine.isClosed() || engine.getComplex() == null) { + return false; + } + int worldMinHeight = engine.getWorld().minHeight(); + int internalY = (quartY << 2) - worldMinHeight; + int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40); + return internalY > caveSwitchInternalY; + } + + private Holder resolveSurfaceStructureBiomeHolder(int x, int z) { + int blockX = x << 2; + int blockZ = z << 2; + IrisBiome irisBiome = resolveSurfaceStructureBiome(engine, blockX, blockZ); + if (irisBiome == null) { + throw new IllegalStateException("Iris returned no surface structure biome at block " + + blockX + "," + blockZ); + } + Holder holder = resolveBiomeHolder(biomeRegistry, irisBiome.getStructureDerivativeKey()); + if (holder == null) { + throw new IllegalStateException("Iris structure biome derivative '" + + irisBiome.getStructureDerivativeKey() + "' is not registered at block " + + blockX + "," + blockZ); + } + return holder; + } + + private Holder resolveNaturalSurfaceStructureBiomeHolder(int x, int z) { + int blockX = x << 2; + int blockZ = z << 2; + IrisBiome irisBiome = engine.getComplex().getNaturalTrueBiomeStream().get(blockX, blockZ); + if (irisBiome == null) { + throw new IllegalStateException("Iris returned no natural structure biome at block " + + blockX + "," + blockZ); + } + Holder holder = resolveBiomeHolder(biomeRegistry, irisBiome.getStructureDerivativeKey()); + if (holder == null) { + throw new IllegalStateException("Iris natural structure biome derivative '" + + irisBiome.getStructureDerivativeKey() + "' is not registered at block " + + blockX + "," + blockZ); + } + return holder; + } + + public Holder getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) { + try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope( + x << 2, z << 2, "bukkit_visible_biome"); + GenerationSessionLease lease = requireGenerationLease( + "bukkit_visible_biome", + "Iris visible biome lookup was rejected during an engine transition"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + prepareVisibleBiomeBatch(); + return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler); + } + } + + void prepareVisibleBiomeBatch() { + if (!isRuntimeAvailable()) { + throw new IllegalStateException("Iris visible biome lookup has no active engine runtime"); + } + runtimeBiomeState(); + } + + Holder getVisibleNoiseBiomeWithActiveGenerationLease( + int x, + int y, + int z, + Climate.Sampler sampler + ) { + return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler, null); + } + + Holder getVisibleNoiseBiomeWithActiveGenerationLease( + int x, + int y, + int z, + Climate.Sampler sampler, + IrisDimensionCarvingResolver.State resolverState + ) { + boolean cacheable = isBiomeCacheable(x, z); + return getVisibleNoiseBiomeWithActiveGenerationLease( + x, y, z, sampler, resolverState, cacheable); + } + + private Holder getVisibleNoiseBiomeWithActiveGenerationLease( + int x, + int y, + int z, + Climate.Sampler sampler, + IrisDimensionCarvingResolver.State resolverState, + boolean cacheable + ) { + if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) { + return fallbackBiome; + } + RuntimeNoiseKey cacheKey = new RuntimeNoiseKey( + engine.getCacheID(), packNoiseKey(x, y, z)); + if (cacheable) { + Holder cachedHolder = noiseBiomeCache.get(cacheKey); + if (cachedHolder != null) { + return cachedHolder; + } + } + + Holder resolvedHolder = resolveVisibleBiomeHolder(x, y, z, resolverState); + if (!cacheable) { + return resolvedHolder; + } + Holder existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder); + if (existingHolder != null) { + return existingHolder; + } + + if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) { + noiseBiomeCache.clear(); + } + + return resolvedHolder; + } + + private boolean isBiomeCacheable(int quartX, int quartZ) { + return isBiomeCacheable( + engine, + QuartPos.toBlock(quartX), + QuartPos.toBlock(quartZ) + ); + } + + static boolean isBiomeCacheable(Engine engine, int blockX, int blockZ) { + return !engine.answersFromNaturalTerrain(blockX, blockZ); + } + + private GenerationSessionLease tryAcquireGenerationLease(String operation) { + if (engine.isClosed()) { + return null; + } + try { + return engine.acquireGenerationLease(operation); + } catch (GenerationSessionException e) { + if (engine.isClosing() || e.isExpectedTeardown()) { + return null; + } + throw new IllegalStateException("Iris biome source could not acquire generation session for " + + operation + ".", e); + } + } + + private GenerationSessionLease requireGenerationLease(String operation, String rejectionMessage) { + GenerationSessionLease lease = tryAcquireGenerationLease(operation); + if (lease == null) { + throw new IllegalStateException(rejectionMessage); + } + return lease; + } + + private boolean isRuntimeAvailable() { + return !engine.isClosed() && engine.getComplex() != null; + } + + private GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope( + int blockX, + int blockZ, + String operation + ) { + if (!(engine instanceof IrisEngine irisEngine)) { + return null; + } + try { + return irisEngine.openGenerationHistoryCoordinateScope(blockX, blockZ); + } catch (IOException failure) { + throw new IllegalStateException("Iris " + operation + + " could not route generation history at " + + blockX + "," + blockZ + ".", failure); + } + } + + private void evictRuntimeCaches(int runtimeId) { + runtimeBiomeStates.remove(runtimeId); + noiseBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId); + structureBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId); + surfaceStructureBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId); + naturalSurfaceStructureBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId); + } + + private RuntimeBiomeState runtimeBiomeState() { + IrisDimension dimension = engine.getDimension(); + int runtimeId = engine.getCacheID(); + RuntimeBiomeState existing = runtimeBiomeStates.get(runtimeId); + if (existing != null) { + if (existing.dimension() != dimension) { + throw new IllegalStateException("Iris biome runtime cache ID is shared by different dimensions."); + } + return existing; + } + synchronized (runtimeBiomeStates) { + existing = runtimeBiomeStates.get(runtimeId); + if (existing != null) { + if (existing.dimension() != dimension) { + throw new IllegalStateException("Iris biome runtime cache ID is shared by different dimensions."); + } + return existing; + } + KMap> refreshedCustomBiomes = fillCustomBiomes( + biomeCustomRegistry, engine, fallbackBiome); + Map> refreshedSpawnBiomes = fillVanillaSpawnBiomes( + biomeCustomRegistry, biomeRegistry, engine); + RuntimeBiomeState created = new RuntimeBiomeState( + dimension, + refreshedCustomBiomes, + refreshedSpawnBiomes); + runtimeBiomeStates.put(runtimeId, created); + return created; + } + } + + private Holder resolveStructureBiomeHolder(int x, int y, int z) { + BiomeResolution resolution = resolveStructureBiomeResolution(x, y, z); + if (resolution == null) { + throw new IllegalStateException("Iris returned no structure biome at quart " + + x + "," + y + "," + z); + } + + Holder holder = resolveBiomeHolder( + biomeRegistry, resolution.irisBiome.getStructureDerivativeKey()); + if (holder == null) { + throw new IllegalStateException("Iris structure biome derivative '" + + resolution.irisBiome.getStructureDerivativeKey() + "' is not registered at block " + + resolution.blockX + "," + resolution.blockY + "," + resolution.blockZ); + } + return holder; + } + + private Holder resolveVisibleBiomeHolder( + int x, + int y, + int z, + IrisDimensionCarvingResolver.State resolverState + ) { + return resolveVisibleBiomeHolder(x, y, z, resolverState, null); + } + + private Holder resolveVisibleBiomeHolder( + int x, + int y, + int z, + IrisDimensionCarvingResolver.State resolverState, + DimensionStackLayout stackLayout + ) { + int blockX = x << 2; + int blockY = y << 2; + int blockZ = z << 2; + Optional historicalKey = engine.getComplex().historicalPhysicalBiomeKeyAt( + blockX, blockY, blockZ); + if (historicalKey.isPresent()) { + return resolvePhysicalBiomeHolder(historicalKey.get()); + } + BiomeResolution resolution = resolveBiomeResolution( + x, y, z, resolverState, true, stackLayout); + if (resolution == null) { + return getFallbackBiome(); + } + + return resolveVisibleBiomeHolder(resolution); + } + + private Holder resolveVisibleBiomeHolder(BiomeResolution resolution) { + if (resolution.irisBiome.isCustom()) { + return resolveCustomHolder(resolution); + } + + org.bukkit.block.Biome vanillaBiome = resolution.underground + ? resolution.irisBiome.getGroundBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ) + : resolution.irisBiome.getSkyBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ); + Holder holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome); + if (holder != null) { + return holder; + } + + return getFallbackBiome(); + } + + private Holder resolveCustomHolder(BiomeResolution resolution) { + IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ); + if (customBiome != null) { + Holder holder = runtimeBiomeState().customBiomes().get(customBiomeKey( + resolution.dimension, customBiome.getId())); + if (holder != null) { + return holder; + } + } + + return getFallbackBiome(); + } + + private BiomeResolution resolveStructureBiomeResolution(int x, int y, int z) { + return resolveBiomeResolution(x, y, z, null, false); + } + + private Holder resolvePhysicalBiomeHolder(String physicalKey) { + Holder holder = resolveBiomeHolder(biomeCustomRegistry, physicalKey); + if (holder == null) { + holder = resolveBiomeHolder(biomeRegistry, physicalKey); + } + if (holder == null) { + throw new IllegalStateException("Historical Iris biome '" + physicalKey + + "' is not registered in the active world registry."); + } + return holder; + } + + private BiomeResolution resolveBiomeResolution( + int x, + int y, + int z, + IrisDimensionCarvingResolver.State resolverState + ) { + return resolveBiomeResolution(x, y, z, resolverState, true); + } + + private BiomeResolution resolveBiomeResolution( + int x, + int y, + int z, + IrisDimensionCarvingResolver.State resolverState, + boolean includeDimensionStack + ) { + return resolveBiomeResolution( + x, y, z, resolverState, includeDimensionStack, null); + } + + private BiomeResolution resolveBiomeResolution( + int x, + int y, + int z, + IrisDimensionCarvingResolver.State resolverState, + boolean includeDimensionStack, + DimensionStackLayout stackLayout + ) { + if (engine == null || engine.isClosed()) { + return null; + } + + if (engine.getComplex() == null) { + return null; + } + + int blockX = x << 2; + int blockZ = z << 2; + int blockY = y << 2; + int worldMinHeight = engine.getWorld().minHeight(); + int internalY = blockY - worldMinHeight; + int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40); + DimensionStackLayout.Layer stackLayer = includeDimensionStack + ? stackLayout == null + ? resolveDimensionStackLayer(engine, blockX, internalY, blockZ) + : stackLayout.layerAt(internalY) + : null; + IrisDimension owningDimension = stackLayer == null + ? engine.getDimension() + : stackLayer.terrainContext().getDimension(); + if (stackLayer != null && !stackLayer.terrainContext().isSelfReferencing()) { + IrisBiome stackedBiome = stackLayer.biome(); + return stackedBiome == null + ? null + : createBiomeResolution( + stackedBiome, false, owningDimension, blockX, blockY, blockZ); + } + boolean deepUnderground = internalY <= caveSwitchInternalY; + boolean underground = false; + IrisBiome irisBiome; + if (stackLayer != null) { + int surfaceInternalY = stackLayer.surfaceY(); + underground = internalY <= surfaceInternalY - 8; + irisBiome = underground + ? engine.getCaveBiome( + blockX, + internalY, + blockZ, + resolverState, + stackLayer.biome(), + surfaceInternalY + ) + : stackLayer.biome(); + } else if (deepUnderground) { + int surfaceInternalY = Engine.hostHeight(engine, blockX, blockZ, true); + underground = internalY <= surfaceInternalY - 8; + irisBiome = underground + ? engine.getCaveBiome(blockX, internalY, blockZ, resolverState) + : engine.getHostSurfaceBiome(blockX, blockZ); + } else { + irisBiome = engine.getHostSurfaceBiome(blockX, blockZ); + } + if (irisBiome == null && underground) { + irisBiome = stackLayer == null + ? engine.getHostSurfaceBiome(blockX, blockZ) + : stackLayer.biome(); + } + if (irisBiome == null) { + return null; + } + + return createBiomeResolution( + irisBiome, underground, owningDimension, blockX, blockY, blockZ); + } + + private BiomeResolution createBiomeResolution( + IrisBiome irisBiome, + boolean underground, + IrisDimension dimension, + int blockX, + int blockY, + int blockZ + ) { + RNG noiseRng = new RNG(seed + ^ (((long) blockX) * 341873128712L) + ^ (((long) blockY) * 132897987541L) + ^ (((long) blockZ) * 42317861L)); + return new BiomeResolution( + irisBiome, underground, dimension, blockX, blockY, blockZ, noiseRng); + } + + private Holder getFallbackBiome() { + if (fallbackBiome != null) { + return fallbackBiome; + } + + Holder holder = resolveFallbackBiome(biomeRegistry, biomeCustomRegistry); + if (holder != null) { + return holder; + } + + throw new IllegalStateException("Unable to resolve any biome holder fallback for Iris biome source"); + } + + private static long packNoiseKey(int x, int y, int z) { + return (((long) x & 67108863L) << 38) + | (((long) z & 67108863L) << 12) + | ((long) y & 4095L); + } + + private static long packColumnKey(int x, int z) { + return ((long) x << 32) ^ ((long) z & 4294967295L); + } + + private static Holder resolveCustomBiomeHolder( + Registry customRegistry, + Engine engine, + IrisDimension dimension, + String customBiomeId + ) { + if (customRegistry == null || engine == null || dimension == null + || customBiomeId == null || customBiomeId.isBlank()) { + return null; + } + + String physicalKey = engine.getData().customBiomeResourceKey(dimension, customBiomeId); + Identifier resourceLocation = Identifier.tryParse(physicalKey); + if (resourceLocation == null) { + throw new IllegalStateException("Invalid Iris custom biome resource key '" + physicalKey + "'."); + } + Biome biome = customRegistry.getValue(resourceLocation); + if (biome == null) { + return null; + } + + Optional> optionalBiomeKey = customRegistry.getResourceKey(biome); + if (optionalBiomeKey.isEmpty()) { + return null; + } + + Optional> optionalReferenceHolder = customRegistry.get(optionalBiomeKey.get()); + if (optionalReferenceHolder.isEmpty()) { + return null; + } + + return optionalReferenceHolder.get(); + } + + static String customBiomeKey(IrisDimension dimension, String customBiomeId) { + return dimension.getCustomBiomeKey(customBiomeId); + } + + private static List getOwnedBiomes(Engine engine) { + ArrayList biomes = new ArrayList<>(getHostOwnedBiomes(engine)); + DimensionStackContext stackContext = engine.getDimensionStackContext(); + if (stackContext == null) { + return biomes; + } + for (DimensionTerrainContext terrainContext : stackContext.getLayersBottomToTop()) { + if (terrainContext.isSelfReferencing()) { + continue; + } + IrisDimension dimension = terrainContext.getDimension(); + addOwnedBiomes( + biomes, + dimension, + dimension.getReachableBiomes(terrainContext) + ); + } + return biomes; + } + + private static List getHostOwnedBiomes(Engine engine) { + IrisDimension hostDimension = engine.getDimension(); + ArrayList biomes = new ArrayList<>(); + addOwnedBiomes(biomes, hostDimension, hostDimension.getReachableBiomes(engine)); + return biomes; + } + + private static void addOwnedBiomes( + List target, + IrisDimension dimension, + Iterable biomes + ) { + for (IrisBiome biome : biomes) { + if (biome != null) { + target.add(new OwnedBiome(dimension, biome)); + } + } + } + + private static DimensionStackLayout.Layer resolveDimensionStackLayer( + Engine engine, + int blockX, + int internalY, + int blockZ + ) { + DimensionStackContext stackContext = engine.getDimensionStackContext(); + return stackContext == null ? null : stackContext.getLayerAt(blockX, internalY, blockZ); + } + + private static IrisBiome resolveSurfaceStructureBiome(Engine engine, int blockX, int blockZ) { + return engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); + } + + private static Holder resolveBiomeHolder(Registry registry, String biomeKey) { + if (registry == null || biomeKey == null || biomeKey.isBlank()) { + return null; + } + Identifier identifier = Identifier.tryParse(biomeKey); + if (identifier == null) { + return null; + } + return registry.get(ResourceKey.create(Registries.BIOME, identifier)).orElse(null); + } + + private static Holder resolveFallbackBiome(Registry registry, Registry customRegistry) { + Holder plains = NMSBinding.biomeToBiomeBase(registry, org.bukkit.block.Biome.PLAINS); + if (plains != null) { + return plains; + } + + Holder vanilla = firstHolder(registry); + if (vanilla != null) { + return vanilla; + } + + return firstHolder(customRegistry); + } + + private static Holder firstHolder(Registry registry) { + if (registry == null) { + return null; + } + + for (Biome biome : registry) { + Optional> optionalBiomeKey = registry.getResourceKey(biome); + if (optionalBiomeKey.isEmpty()) { + continue; + } + + Optional> optionalHolder = registry.get(optionalBiomeKey.get()); + if (optionalHolder.isPresent()) { + return optionalHolder.get(); + } + } + + return null; + } + + private static final class BiomeResolution { + private final IrisBiome irisBiome; + private final boolean underground; + private final IrisDimension dimension; + private final int blockX; + private final int blockY; + private final int blockZ; + private final RNG rng; + + private BiomeResolution( + IrisBiome irisBiome, + boolean underground, + IrisDimension dimension, + int blockX, + int blockY, + int blockZ, + RNG rng + ) { + this.irisBiome = irisBiome; + this.underground = underground; + this.dimension = dimension; + this.blockX = blockX; + this.blockY = blockY; + this.blockZ = blockZ; + this.rng = rng; + } + } + + private record OwnedBiome(IrisDimension dimension, IrisBiome biome) { + } + + private record RuntimeNoiseKey(int runtimeId, long coordinateKey) { + } + + private record RuntimeColumnKey(int runtimeId, long coordinateKey) { + } + + private record RuntimeBiomeState( + IrisDimension dimension, + KMap> customBiomes, + Map> vanillaSpawnBiomes + ) { + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/DatapackStructureStateFilter.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/DatapackStructureStateFilter.java new file mode 100644 index 000000000..cfcdb820a --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/DatapackStructureStateFilter.java @@ -0,0 +1,513 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement; +import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex; +import art.arcane.iris.structure.nativegen.NativeStructureFrequencyScale; +import art.arcane.iris.structure.nativegen.IrisImportedStructureControl; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderOwner; +import net.minecraft.core.Vec3i; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +final class DatapackStructureStateFilter { + private DatapackStructureStateFilter() { + } + + static Selection filter( + List> structureSets, + DatapackStructureScopeIndex scopeIndex, + Set declaredSources + ) { + return filter(structureSets, scopeIndex, declaredSources, new IrisImportedStructureControl()); + } + + static Selection filter( + List> structureSets, + DatapackStructureScopeIndex scopeIndex, + Set declaredSources, + IrisImportedStructureControl importedStructures + ) { + return filter( + structureSets, + scopeIndex, + declaredSources, + importedStructures, + keyIndex(structureSets)); + } + + static Selection filter( + List> structureSets, + DatapackStructureScopeIndex scopeIndex, + Set declaredSources, + IrisImportedStructureControl importedStructures, + StructureSetKeyIndex keyIndex + ) { + if (scopeIndex.isEmpty()) { + return new Selection( + scaleFrequencyOverrides(structureSets, importedStructures, keyIndex), + 0, + 0); + } + Map> holdersByKey = new HashMap<>(); + for (Holder holder : structureSets) { + String key = structureSetKey(holder, keyIndex); + if (key != null) { + holdersByKey.putIfAbsent(key, holder); + } + } + Map, Holder> scopedByIdentity = new IdentityHashMap<>(); + Set> visiting = Collections.newSetFromMap(new IdentityHashMap<>()); + List> filteredSets = new ArrayList<>(structureSets.size()); + int retainedManagedSets = 0; + int excludedManagedSets = 0; + for (Holder holder : structureSets) { + String setKey = structureSetKey(holder, keyIndex); + boolean managedSet = setKey != null && scopeIndex.isManagedStructureSet(setKey); + Holder scopedHolder = scopeHolder( + holder, + scopeIndex, + declaredSources, + holdersByKey, + scopedByIdentity, + visiting, + keyIndex); + if (scopedHolder == null) { + if (managedSet) { + excludedManagedSets++; + } + continue; + } + if (managedSet) { + retainedManagedSets++; + } + filteredSets.add(scopedHolder); + } + return new Selection( + scaleFrequencyOverrides( + List.copyOf(filteredSets), importedStructures, keyIndex), + retainedManagedSets, + excludedManagedSets); + } + + static String structureSetKey(Holder holder) { + return structureSetKey(holder, keyIndex(List.of(holder))); + } + + static StructureSetKeyIndex keyIndex( + Iterable> registeredStructureSets + ) { + return StructureSetKeyIndex.create(registeredStructureSets); + } + + private static String structureSetKey( + Holder holder, + StructureSetKeyIndex keyIndex + ) { + return keyIndex.resolve(holder) + .map(key -> key.identifier().toString()) + .orElse(null); + } + + private static List filterEntries( + List entries, + DatapackStructureScopeIndex scopeIndex, + Set declaredSources + ) { + List filtered = new ArrayList<>(entries.size()); + for (StructureSet.StructureSelectionEntry entry : entries) { + String structureKey = entry.structure().unwrapKey() + .map(key -> key.identifier().toString()) + .orElse(null); + if (structureKey == null || scopeIndex.allowsStructure(structureKey, declaredSources)) { + filtered.add(entry); + } + } + return filtered; + } + + private static Holder scopeHolder( + Holder holder, + DatapackStructureScopeIndex scopeIndex, + Set declaredSources, + Map> holdersByKey, + Map, Holder> scopedByIdentity, + Set> visiting, + StructureSetKeyIndex keyIndex + ) { + if (scopedByIdentity.containsKey(holder)) { + return scopedByIdentity.get(holder); + } + if (!visiting.add(holder)) { + return null; + } + try { + String setKey = structureSetKey(holder, keyIndex); + if (setKey != null && scopeIndex.isManagedStructureSet(setKey) + && !scopeIndex.allowsStructureSet(setKey, declaredSources)) { + scopedByIdentity.put(holder, null); + return null; + } + StructureSet originalSet = holder.value(); + List entries = filterEntries( + originalSet.structures(), scopeIndex, declaredSources); + if (entries.isEmpty()) { + scopedByIdentity.put(holder, null); + return null; + } + StructurePlacement placement = scopePlacement( + originalSet.placement(), + scopeIndex, + declaredSources, + holdersByKey, + scopedByIdentity, + visiting, + keyIndex); + if (entries.size() == originalSet.structures().size() + && placement == originalSet.placement()) { + scopedByIdentity.put(holder, holder); + return holder; + } + Holder scoped = replacementHolder( + holder, new StructureSet(entries, placement), keyIndex); + scopedByIdentity.put(holder, scoped); + return scoped; + } finally { + visiting.remove(holder); + } + } + + static Optional exclusionZone(StructurePlacement placement) { + if (!(placement instanceof AbstractSpreadingStructurePlacement)) { + return Optional.empty(); + } + Object value = declaredFieldValue(AbstractSpreadingStructurePlacement.class, placement, Optional.class); + if (value instanceof Optional optional && (optional.isEmpty() + || optional.get() instanceof AbstractSpreadingStructurePlacement.ExclusionZone)) { + @SuppressWarnings("unchecked") + Optional resolved = + (Optional) optional; + return resolved; + } + throw new IllegalStateException("Could not read structure placement exclusion zone from " + + placement.getClass().getName()); + } + + private static StructurePlacement scopePlacement( + StructurePlacement placement, + DatapackStructureScopeIndex scopeIndex, + Set declaredSources, + Map> holdersByKey, + Map, Holder> scopedByIdentity, + Set> visiting, + StructureSetKeyIndex keyIndex + ) { + Optional currentZone = exclusionZone(placement); + if (currentZone.isEmpty()) { + return placement; + } + + Holder target = currentZone.get().otherSet(); + String targetKey = structureSetKey(target, keyIndex); + if (targetKey != null) { + target = holdersByKey.getOrDefault(targetKey, target); + } + Holder scopedTarget = scopeHolder( + target, + scopeIndex, + declaredSources, + holdersByKey, + scopedByIdentity, + visiting, + keyIndex); + Optional scopedZone = Optional.empty(); + if (scopedTarget != null) { + scopedZone = Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone( + scopedTarget, + currentZone.get().chunkCount())); + } + if (scopedTarget == currentZone.get().otherSet()) { + return placement; + } + return copyPlacement(placement, scopedZone, 1D); + } + + private static List> scaleFrequencyOverrides( + List> structureSets, + IrisImportedStructureControl importedStructures, + StructureSetKeyIndex keyIndex + ) { + if (!importedStructures.hasFrequencyOverrides()) { + return structureSets; + } + Map> holdersByKey = new HashMap<>(); + for (Holder holder : structureSets) { + String key = structureSetKey(holder, keyIndex); + if (key != null) { + holdersByKey.putIfAbsent(key, holder); + } + } + Map, Holder> scaledByIdentity = new IdentityHashMap<>(); + List> scaledSets = new ArrayList<>(structureSets.size()); + boolean changed = false; + for (Holder holder : structureSets) { + Holder scaled = scaleFrequencyHolder( + holder, + importedStructures, + holdersByKey, + scaledByIdentity, + keyIndex); + scaledSets.add(scaled); + changed |= scaled != holder; + } + return changed ? List.copyOf(scaledSets) : structureSets; + } + + private static Holder scaleFrequencyHolder( + Holder holder, + IrisImportedStructureControl importedStructures, + Map> holdersByKey, + Map, Holder> scaledByIdentity, + StructureSetKeyIndex keyIndex + ) { + if (scaledByIdentity.containsKey(holder)) { + return scaledByIdentity.get(holder); + } + if (!dependsOnFrequencyOverride( + holder, importedStructures, holdersByKey, keyIndex)) { + scaledByIdentity.put(holder, holder); + return holder; + } + + ResourceKey holderKey = keyIndex.resolve(holder).orElseThrow(() -> + new IllegalStateException( + "An affected native structure-set exclusion graph has an unkeyed holder")); + ReboundStructureSetHolder scaledHolder = new ReboundStructureSetHolder(holderKey); + scaledByIdentity.put(holder, scaledHolder); + + StructureSet originalSet = holder.value(); + StructurePlacement originalPlacement = originalSet.placement(); + Optional originalZone = exclusionZone(originalPlacement); + Optional scaledZone = originalZone; + if (originalZone.isPresent()) { + Holder target = canonicalHolder( + originalZone.get().otherSet(), holdersByKey, keyIndex); + Holder scaledTarget = scaleFrequencyHolder( + target, + importedStructures, + holdersByKey, + scaledByIdentity, + keyIndex); + if (scaledTarget != originalZone.get().otherSet()) { + scaledZone = Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone( + scaledTarget, originalZone.get().chunkCount())); + } + } + + double multiplier = importedStructures.frequencyMultiplier( + structureSetKey(holder, keyIndex)); + StructurePlacement scaledPlacement = multiplier == 1D && scaledZone.equals(originalZone) + ? originalPlacement + : copyPlacement(originalPlacement, scaledZone, multiplier); + scaledHolder.bind(new StructureSet(originalSet.structures(), scaledPlacement)); + return scaledHolder; + } + + private static boolean dependsOnFrequencyOverride( + Holder holder, + IrisImportedStructureControl importedStructures, + Map> holdersByKey, + StructureSetKeyIndex keyIndex + ) { + Set> visited = Collections.newSetFromMap(new IdentityHashMap<>()); + Holder current = holder; + while (visited.add(current)) { + if (importedStructures.frequencyMultiplier( + structureSetKey(current, keyIndex)) != 1D) { + return true; + } + Optional zone = + exclusionZone(current.value().placement()); + if (zone.isEmpty()) { + return false; + } + current = canonicalHolder(zone.get().otherSet(), holdersByKey, keyIndex); + } + return false; + } + + private static Holder canonicalHolder( + Holder holder, + Map> holdersByKey, + StructureSetKeyIndex keyIndex + ) { + String key = structureSetKey(holder, keyIndex); + return key == null ? holder : holdersByKey.getOrDefault(key, holder); + } + + private static Holder replacementHolder( + Holder original, + StructureSet replacement, + StructureSetKeyIndex keyIndex + ) { + Optional> key = keyIndex.resolve(original); + if (key.isEmpty()) { + return Holder.direct(replacement); + } + ReboundStructureSetHolder holder = new ReboundStructureSetHolder(key.get()); + holder.bind(replacement); + return holder; + } + + private static StructurePlacement copyPlacement( + StructurePlacement placement, + Optional exclusionZone, + double frequencyMultiplier + ) { + Vec3i locateOffset = (Vec3i) declaredFieldValue( + AbstractSpreadingStructurePlacement.class, placement, Vec3i.class); + AbstractSpreadingStructurePlacement.FrequencyReductionMethod frequencyReductionMethod = + (AbstractSpreadingStructurePlacement.FrequencyReductionMethod) declaredFieldValue( + AbstractSpreadingStructurePlacement.class, + placement, + AbstractSpreadingStructurePlacement.FrequencyReductionMethod.class); + float frequency = (float) declaredFieldValue( + AbstractSpreadingStructurePlacement.class, placement, float.class); + int salt = (int) declaredFieldValue( + AbstractSpreadingStructurePlacement.class, placement, int.class); + + if (placement instanceof RandomSpreadStructurePlacement randomSpread) { + NativeStructureFrequencyScale scale = NativeStructureFrequencyScale.randomSpread( + frequency, randomSpread.spacing(), randomSpread.separation(), frequencyMultiplier); + return new RandomSpreadStructurePlacement( + locateOffset, + frequencyReductionMethod, + scale.frequency(), + salt, + exclusionZone, + scale.spacing(), + randomSpread.separation(), + randomSpread.spreadType()); + } + if (placement.getClass() == ConcentricRingsStructurePlacement.class) { + ConcentricRingsStructurePlacement rings = + (ConcentricRingsStructurePlacement) placement; + float scaledFrequency = NativeStructureFrequencyScale.probability( + frequency, frequencyMultiplier); + return new ConcentricRingsStructurePlacement( + locateOffset, + frequencyReductionMethod, + scaledFrequency, + salt, + exclusionZone, + rings.distance(), + rings.spread(), + rings.count(), + rings.preferredBiomes()); + } + throw new IllegalStateException("Unsupported native structure placement in an affected " + + "frequency-override or datapack-scope graph: " + + placement.getClass().getName()); + } + + private static Object declaredFieldValue( + Class declaringType, + Object target, + Class fieldType + ) { + Field match = null; + for (Field field : declaringType.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || field.getType() != fieldType) { + continue; + } + if (match != null) { + throw new IllegalStateException("Ambiguous " + fieldType.getName() + " field on " + + declaringType.getName()); + } + match = field; + } + if (match == null) { + throw new IllegalStateException("Missing " + fieldType.getName() + " field on " + + declaringType.getName()); + } + try { + match.setAccessible(true); + return match.get(target); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Could not read " + fieldType.getName() + " field on " + + declaringType.getName(), e); + } + } + + static final class StructureSetKeyIndex { + private final Map, ResourceKey> + keysByEntries; + + private StructureSetKeyIndex( + Map, ResourceKey> + keysByEntries + ) { + this.keysByEntries = keysByEntries; + } + + private static StructureSetKeyIndex create( + Iterable> registeredStructureSets + ) { + Map, ResourceKey> keys = + new IdentityHashMap<>(); + for (Holder holder : registeredStructureSets) { + Optional> key = holder.unwrapKey(); + if (key.isEmpty()) { + continue; + } + List entries = holder.value().structures(); + ResourceKey previous = keys.put(entries, key.get()); + if (previous != null && !previous.equals(key.get())) { + throw new IllegalStateException( + "Multiple registered structure sets share the same entry list"); + } + } + return new StructureSetKeyIndex(keys); + } + + private Optional> resolve(Holder holder) { + Optional> key = holder.unwrapKey(); + if (key.isPresent()) { + return key; + } + return Optional.ofNullable(keysByEntries.get(holder.value().structures())); + } + } + + record Selection( + List> structureSets, + int retainedManagedSets, + int excludedManagedSets + ) { + } + + private static final class ReboundStructureSetHolder extends Holder.Reference { + private ReboundStructureSetHolder(ResourceKey key) { + super(Type.STAND_ALONE, new HolderOwner<>() { + }, key, null); + } + + private void bind(StructureSet structureSet) { + bindValue(structureSet); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/ImportedFeatureStage.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/ImportedFeatureStage.java new file mode 100644 index 000000000..a5d81510a --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/ImportedFeatureStage.java @@ -0,0 +1,441 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import net.minecraft.world.level.levelgen.placement.FeaturePlacer; +import art.arcane.iris.generation.runtime.DimensionStackContext; +import art.arcane.iris.generation.runtime.DimensionStackLayout; +import art.arcane.iris.nativegen.v26_3_R1.NativeGenerationWriteGuard; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.structure.nativegen.NativeFeatureGenerationPolicy; +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.biome.IrisBiomeCustom; +import art.arcane.iris.generation.decoration.IrisDecorationStep; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl; +import art.arcane.iris.structure.object.IrisStaticObjectLayer; +import art.arcane.iris.spi.IrisLogging; +import it.unimi.dsi.fastutil.ints.IntArraySet; +import it.unimi.dsi.fastutil.ints.IntSet; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeGenerationSettings; +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.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; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.IntBinaryOperator; + +/** + * Bukkit twin of the modded imported-feature stage. Same control, same semantics, same seeds: with + * {@code importedFeatures.enabled} the vanilla placed-feature pass runs over Iris terrain, and with the + * control off nothing here allocates or runs. + * + *

Iris does its own native structure pass and calls the delegate with {@code addVanillaDecorations=false}, + * so the vanilla feature half never runs on its own. This reproduces that half only - structures are never + * placed twice. + * + *

Threading: called from {@code applyBiomeDecoration} on the worldgen thread that owns the chunk. The + * FEATURES chunk step is not parallel-safe. + */ +final class ImportedFeatureStage { + private static final String CYCLE_MARKER = "Feature order cycle found"; + + private final Engine engine; + private final ConcurrentHashMap runtimeStates = new ConcurrentHashMap<>(); + + ImportedFeatureStage(Engine engine) { + this.engine = engine; + } + + /** + * Generation settings for one biome holder, mapping Iris custom biomes onto their vanilla derivative. + * Pass-through while the control is off, which is what keeps {@code BiomeFilter} behaving exactly as it + * does today. + */ + BiomeGenerationSettings generationSettings(Holder biome) { + RuntimeFeatureState state = currentState(); + FeatureTable table = state == null ? null : state.featureTable(); + if (table == null) { + return null; + } + Holder mapped = table.derivatives().get(holderKey(biome)); + return mapped == null ? null : mapped.value().getGenerationSettings(); + } + + /** + * Builds the table on the first decorated chunk, or leaves the stage inert. A feature-order cycle is + * reported once here and degrades to features-off; it never reaches chunk generation as a crash. + */ + void prepare(WorldGenLevel level) { + IrisDimension dimension = engine.getDimension(); + int runtimeId = engine.getCacheID(); + if (settled(runtimeStates.get(runtimeId), dimension)) { + return; + } + synchronized (this) { + if (settled(runtimeStates.get(runtimeId), dimension)) { + return; + } + runtimeStates.put(runtimeId, build(level, dimension)); + } + } + + void evictRuntime(int runtimeId) { + runtimeStates.remove(runtimeId); + } + + private RuntimeFeatureState currentState() { + IrisDimension dimension = engine.getDimension(); + RuntimeFeatureState state = runtimeStates.get(engine.getCacheID()); + return settled(state, dimension) ? state : null; + } + + private static boolean settled(RuntimeFeatureState state, IrisDimension dimension) { + return state != null && state.dimension() == dimension; + } + + private RuntimeFeatureState build(WorldGenLevel level, IrisDimension dimension) { + IrisImportedFeatureControl control; + try { + control = NativeFeatureGenerationPolicy.control(engine); + } catch (RuntimeException error) { + IrisLogging.error("Iris could not read importedFeatures for this dimension; features off: " + + error); + return new RuntimeFeatureState(dimension, null); + } + if (!control.shouldGenerateFeatures()) { + return new RuntimeFeatureState(dimension, null); + } + FeatureTable built; + try { + built = buildTable(level, control, dimension); + } catch (Throwable error) { + IrisLogging.error("Iris importedFeatures is off for " + dimensionKey() + + ": feature table construction failed: " + error); + return new RuntimeFeatureState(dimension, null); + } + if (built == null) { + return new RuntimeFeatureState(dimension, null); + } + IrisLogging.info("Iris importedFeatures on for " + dimensionKey() + ": " + built.biomes().size() + + " biomes, " + built.steps().size() + " steps, " + built.derivatives().size() + + " custom-biome derivative maps"); + return new RuntimeFeatureState(dimension, built); + } + + private FeatureTable buildTable(WorldGenLevel level, IrisImportedFeatureControl control, + IrisDimension dimension) { + Registry registry = level.registryAccess().lookupOrThrow(Registries.BIOME); + Set visibleKeys = visibleBiomeKeys(); + // Registry-ordered walk, never a hash-ordered set: FeatureSorter's cycle detection walks this list and + // an unordered walk makes detection depend on JVM hash order. + List> biomes = new ArrayList<>(); + Map> byKey = new HashMap<>(); + registry.listElements().forEach((Holder.Reference reference) -> { + String key = holderKey(reference); + if (key != null && visibleKeys.contains(key)) { + biomes.add(reference); + byKey.put(key, reference); + } + }); + if (biomes.isEmpty()) { + IrisLogging.error("Iris importedFeatures is on but " + dimensionKey() + + " exposes no registered biomes; features off"); + return null; + } + Map> derivatives = customBiomeDerivatives(registry, byKey); + List steps; + try { + steps = FeatureSorter.buildFeaturesPerStep(biomes, + (Holder biome) -> settingsFor(biome, derivatives).features(), true); + } catch (IllegalStateException error) { + String message = error.getMessage(); + if (message == null || !message.contains(CYCLE_MARKER)) { + throw error; + } + IrisLogging.error("Iris importedFeatures is off for " + dimensionKey() + + ": the registered placed features cannot be ordered. " + message + + ". Remove or reorder the conflicting content, or leave importedFeatures.enabled false."); + return null; + } + boolean filtered = control.getDisabled() != null && !control.getDisabled().isEmpty(); + return new FeatureTable(dimension, control, List.copyOf(biomes), Set.copyOf(biomes), + Map.copyOf(byKey), steps, Map.copyOf(derivatives), filtered); + } + + /** + * Every biome key Iris can write into a chunk section: the structure derivative, the raw derivative, every + * scatter entry, and every generated custom biome. + */ + private Set visibleBiomeKeys() { + Set keys = new LinkedHashSet<>(); + for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) { + addKey(keys, irisBiome.getStructureDerivativeKey()); + addKey(keys, irisBiome.getDerivativeKey()); + addKey(keys, irisBiome.getVanillaDerivativeKey()); + for (String scatter : irisBiome.getBiomeScatter()) { + addKey(keys, scatter); + } + for (String scatter : irisBiome.getBiomeSkyScatter()) { + addKey(keys, scatter); + } + if (!irisBiome.isCustom()) { + continue; + } + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + addKey(keys, engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome)); + } + } + return keys; + } + + /** + * Maps every generated Iris custom biome key onto the registry holder of its Iris biome's vanilla + * derivative. The custom biome's own datapack JSON carries no features by design. + */ + private Map> customBiomeDerivatives(Registry registry, + Map> byKey) { + Map> derivatives = new HashMap<>(); + for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) { + if (!irisBiome.isCustom()) { + continue; + } + String derivativeKey = normalizeKey(irisBiome.getVanillaDerivativeKey()); + // Resolve from the registry, not only from the visible biome set: a sea or shore biome's structure + // derivative is rewritten away from its vanilla derivative, so the derivative whose features we + // want is not always a biome Iris can emit. + Holder derivative = byKey.get(derivativeKey); + if (derivative == null) { + derivative = resolveHolder(registry, derivativeKey); + } + if (derivative == null) { + IrisLogging.warn("Iris importedFeatures: vanilla derivative " + derivativeKey + " of biome " + + irisBiome.getLoadKey() + + " is not registered; its custom biomes generate no imported features"); + continue; + } + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + derivatives.put( + engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome), + derivative + ); + } + } + return derivatives; + } + + private static Holder resolveHolder(Registry registry, String key) { + if (registry == null || key == null || key.isBlank()) { + return null; + } + Identifier identifier = Identifier.tryParse(key); + if (identifier == null) { + return null; + } + return registry.get(identifier).>map((Holder.Reference reference) -> reference) + .orElse(null); + } + + private static BiomeGenerationSettings settingsFor(Holder biome, + Map> derivatives) { + Holder mapped = derivatives.get(holderKey(biome)); + return mapped == null + ? biome.value().getGenerationSettings() + : mapped.value().getGenerationSettings(); + } + + /** + * Runs the vanilla placed-feature pass for one chunk on the calling worldgen thread. A no-op while the + * control is disabled or the table degraded. + */ + void run(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator owner) { + RuntimeFeatureState state = currentState(); + FeatureTable table = state == null ? null : state.featureTable(); + if (table == null) { + return; + } + ChunkPos centerPos = chunk.getPos(); + SectionPos sectionPos = SectionPos.of(centerPos, level.getMinSectionY()); + BlockPos origin = sectionPos.origin(); + Registry featureRegistry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE); + List steps = table.steps(); + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); + long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ()); + IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData()); + int staticMinY = engine.getMinHeight(); + DimensionStackContext stackContext = engine.getDimensionStackContext(); + Set> chunkBiomes = chunkBiomes(level, sectionPos, table, stackContext); + IntBinaryOperator surfaceFirstFreeY = stackContext == null + ? (x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) + : (x, z) -> Engine.hostHeight(engine, x, z, false) + staticMinY + 1; + IntBinaryOperator floorFirstFreeY = stackContext == null + ? (x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z) + : (x, z) -> Engine.hostHeight(engine, x, z, true) + staticMinY + 1; + WorldGenLevel placementLevel = staticObjects.isEmpty() && stackContext == null + ? level + : NativeStructureWorldgenAccess.create( + level, centerPos, + surfaceFirstFreeY, + floorFirstFreeY, + stackContext != null, + NativeGenerationWriteGuard.protectedPositions(staticObjects, stackContext, staticMinY)); + + try { + for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) { + if (!table.control().shouldGenerateStep(IrisDecorationStep.byOrdinal(stepIndex))) { + continue; + } + placeStep(placementLevel, table, steps.get(stepIndex), featureRegistry, chunkBiomes, owner, + random, decorationSeed, origin, stepIndex); + } + } catch (Throwable error) { + throw new IllegalStateException("Iris imported feature placement failed for chunk " + + centerPos.x() + "," + centerPos.z(), error); + } finally { + level.setCurrentlyGenerating(null); + } + } + + private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData, + Registry featureRegistry, Set> chunkBiomes, + ChunkGenerator owner, WorldgenRandom random, long decorationSeed, + BlockPos origin, int stepIndex) { + IntSet stepFeatures = new IntArraySet(); + for (Holder biome : chunkBiomes) { + List> biomeFeatures = settingsFor(biome, table.derivatives()).features(); + if (stepIndex >= biomeFeatures.size()) { + continue; + } + for (Holder feature : biomeFeatures.get(stepIndex)) { + stepFeatures.add(stepData.indexMapping().applyAsInt(feature.value())); + } + } + if (stepFeatures.isEmpty()) { + return; + } + // Sorted global indices: identical ordering to vanilla, and each feature's seed comes from its own + // global index, so denying one feature never shifts another. + int[] featureIndices = stepFeatures.toIntArray(); + Arrays.sort(featureIndices); + for (int globalIndex : featureIndices) { + PlacedFeature feature = stepData.features().get(globalIndex); + if (table.filtered()) { + Identifier featureId = featureRegistry.getKey(feature); + if (featureId != null && !table.control().shouldGenerate(featureId.toString())) { + continue; + } + } + random.setFeatureSeed(decorationSeed, globalIndex, stepIndex); + level.setCurrentlyGenerating(() -> describeFeature(featureRegistry, feature)); + new FeaturePlacer(level, owner).placeWithBiomeCheck(feature, random, origin); + } + } + + private static String describeFeature(Registry registry, PlacedFeature feature) { + Identifier id = registry.getKey(feature); + return id == null ? feature.toString() : id.toString(); + } + + private Set> chunkBiomes( + WorldGenLevel level, + SectionPos sectionPos, + FeatureTable table, + DimensionStackContext stackContext + ) { + List> collected = new ArrayList<>(); + ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> { + ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z()); + LevelChunkSection[] sections = neighbour.getSections(); + for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { + LevelChunkSection section = sections[sectionIndex]; + if (stackContext == null) { + section.getBiomes().getAll(collected::add); + continue; + } + int sectionMinimumY = neighbour.getSectionYFromSectionIndex(sectionIndex) << 4; + for (int quartY = 0; quartY < 4; quartY++) { + int internalY = sectionMinimumY + (quartY << 2) - level.getMinY(); + for (int quartX = 0; quartX < 4; quartX++) { + int blockX = chunkPos.getMinBlockX() + (quartX << 2); + for (int quartZ = 0; quartZ < 4; quartZ++) { + int blockZ = chunkPos.getMinBlockZ() + (quartZ << 2); + DimensionStackLayout.Layer layer = stackContext.getLayerAt( + blockX, internalY, blockZ); + if (layer.terrainContext().isSelfReferencing()) { + collected.add(section.getBiomes().get(quartX, quartY, quartZ)); + } + } + } + } + } + }); + Set> present = new LinkedHashSet<>(); + for (Holder biome : collected) { + if (table.biomeSet().contains(biome)) { + present.add(biome); + continue; + } + String key = holderKey(biome); + Holder canonical = key == null ? null : table.byKey().get(key); + if (canonical != null) { + present.add(canonical); + } + } + return present; + } + + private static void addKey(Set keys, String key) { + String normalized = normalizeKey(key); + if (normalized != null) { + keys.add(normalized); + } + } + + private String dimensionKey() { + IrisDimension dimension = engine.getDimension(); + return dimension == null ? "" : dimension.getLoadKey(); + } + + private static String holderKey(Holder holder) { + return holder.unwrapKey() + .map(key -> key.identifier().toString().toLowerCase(Locale.ROOT)) + .orElse(null); + } + + private static String normalizeKey(String key) { + Identifier identifier = key == null ? null : Identifier.tryParse(key); + return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT); + } + + private record FeatureTable(IrisDimension dimension, IrisImportedFeatureControl control, + List> biomes, Set> biomeSet, + Map> byKey, + List steps, + Map> derivatives, boolean filtered) { + } + + private record RuntimeFeatureState(IrisDimension dimension, FeatureTable featureTable) { + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/InitialMobSpawner.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/InitialMobSpawner.java new file mode 100644 index 000000000..e04bdbd37 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/InitialMobSpawner.java @@ -0,0 +1,132 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import art.arcane.iris.spi.IrisLogging; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.util.random.WeightedList; +import net.minecraft.world.attribute.EnvironmentAttributes; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.entity.SpawnGroupData; +import net.minecraft.world.entity.SpawnPlacements; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.MobSpawnSettings; +import net.minecraft.world.level.gamerules.GameRules; +import org.bukkit.event.entity.CreatureSpawnEvent; +import java.util.Optional; + +final class InitialMobSpawner { + private InitialMobSpawner() { + } + + static void spawn( + WorldGenRegion level, Holder biome, ChunkPos chunkPos, RandomSource random + ) { + if (level.getLevel().getGameRules().get(GameRules.SPAWN_MOBS)) { + MobSpawnSettings mobSettings = biome.value().getAttributes().applyModifier( + EnvironmentAttributes.NATURAL_MOB_SPAWNS, EnvironmentAttributes.NATURAL_MOB_SPAWNS.defaultValue()); + WeightedList mobs = mobSettings.getMobsToSpawn(MobCategory.CREATURE); + if (!mobs.isEmpty()) { + float creatureProbability = biome.value().getAttributes().applyModifier( + EnvironmentAttributes.CREATURE_WORLD_GEN_SPAWN_PROBABILITY, + EnvironmentAttributes.CREATURE_WORLD_GEN_SPAWN_PROBABILITY.defaultValue()); + int xo = chunkPos.getMinBlockX(); + int zo = chunkPos.getMinBlockZ(); + + while (random.nextFloat() < creatureProbability) { + Optional nextSpawnerData = mobs.getRandom(random); + if (!nextSpawnerData.isEmpty()) { + MobSpawnSettings.SpawnerData spawnerData = nextSpawnerData.get(); + int count = spawnerData.count().sample(random); + SpawnGroupData groupSpawnData = null; + int x = xo + random.nextInt(16); + int z = zo + random.nextInt(16); + int startX = x; + int startZ = z; + + for (int i = 0; i < count; i++) { + boolean success = false; + + for (int attempts = 0; !success && attempts < 4; attempts++) { + BlockPos pos = getTopNonCollidingPos(level, spawnerData.type(), x, z); + if (spawnerData.type().canSummon() && SpawnPlacements.isSpawnPositionOk(spawnerData.type(), level, pos)) { + float width = spawnerData.type().getWidth(); + double fx = Mth.clamp(x, (double)xo + width, xo + 16.0 - width); + double fz = Mth.clamp(z, (double)zo + width, zo + 16.0 - width); + if (!level.noCollision(spawnerData.type().getSpawnAABB(fx, pos.getY(), fz)) + || !SpawnPlacements.checkSpawnRules( + spawnerData.type(), + level, + EntitySpawnReason.CHUNK_GENERATION, + BlockPos.containing(fx, pos.getY(), fz), + level.getRandom() + )) { + continue; + } + + Entity entity; + try { + entity = spawnerData.type().create(level.getLevel(), EntitySpawnReason.NATURAL); + } catch (Exception e) { + IrisLogging.reportError("Failed to create initial mob at chunk " + chunkPos, e); + continue; + } + + if (entity == null) { + continue; + } + + entity.snapTo(fx, pos.getY(), fz, random.nextFloat() * 360.0F, 0.0F); + if (entity instanceof Mob mob + && mob.checkSpawnRules(level, EntitySpawnReason.CHUNK_GENERATION) + && mob.checkSpawnObstruction(level)) { + groupSpawnData = mob.finalizeSpawn( + level, level.getCurrentDifficultyAt(mob.blockPosition()), EntitySpawnReason.CHUNK_GENERATION, groupSpawnData + ); + level.addFreshEntityWithPassengers(mob, CreatureSpawnEvent.SpawnReason.CHUNK_GEN); + success = true; + } + } + + x += random.nextInt(5) - random.nextInt(5); + + for (z += random.nextInt(5) - random.nextInt(5); + x < xo || x >= xo + 16 || z < zo || z >= zo + 16; + z = startZ + random.nextInt(5) - random.nextInt(5) + ) { + x = startX + random.nextInt(5) - random.nextInt(5); + } + } + } + } + } + } + } + } + + private static BlockPos getTopNonCollidingPos(LevelReader level, EntityType type, int x, int z) { + int levelHeight = level.getHeight(SpawnPlacements.getHeightmapType(type), x, z); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(x, levelHeight, z); + if (level.dimensionType().hasCeiling()) { + do { + pos.move(Direction.DOWN); + } while (!level.getBlockState(pos).isAir()); + + do { + pos.move(Direction.DOWN); + } while (level.getBlockState(pos).isAir() && pos.getY() > level.getMinY()); + } + + return SpawnPlacements.getPlacementType(type).adjustSpawnPosition(level, pos.immutable()); + } + +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/IrisChunkGenerator.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/IrisChunkGenerator.java new file mode 100644 index 000000000..25e5a9234 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/IrisChunkGenerator.java @@ -0,0 +1,1621 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import net.minecraft.world.level.levelgen.densityfunction.SamplerContext; +import net.minecraft.world.attribute.EnvironmentAttributes; +import net.minecraft.world.level.Level; +import art.arcane.iris.nativegen.v26_3_R1.NativeTransitionColumn; +import art.arcane.iris.nativegen.v26_3_R1.NativeTerrainHeightCache; +import art.arcane.iris.world.history.TerrainBoundarySignature; +import java.util.Optional; +import art.arcane.iris.nativegen.v26_3_R1.NativeGenerationWriteGuard; +import java.util.function.LongPredicate; +import art.arcane.iris.generation.runtime.DimensionStackContext; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.EngineLifecycleTasks; +import art.arcane.iris.generation.runtime.GenerationSessionException; +import art.arcane.iris.generation.runtime.GenerationSessionLease; +import art.arcane.iris.structure.placement.IrisStructureLocator; +import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord; +import art.arcane.iris.structure.nativegen.NativeStructureOwnershipStore; +import art.arcane.iris.structure.nativegen.NativeStructureStartPlan; +import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy; +import art.arcane.iris.generation.runtime.IrisEngine; +import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter; +import art.arcane.iris.world.history.NativeTerrainReceipt; +import art.arcane.iris.world.history.NativeBiomeSpawnSelection; +import art.arcane.iris.world.history.SavedTerrainChunk; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataType; +import art.arcane.iris.platform.generation.BukkitChunkGenerator; +import art.arcane.iris.studio.generation.JigsawStudioGenerator; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver; +import art.arcane.iris.generation.terrain.IrisMaterialPalette; +import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision; +import art.arcane.iris.structure.object.IrisStaticObjectLayer; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureGenerationException; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureStartInjector; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureReferenceEnvelope; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureLocateResults; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureLocatePersistence; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureOwnershipRecovery; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructurePostProcessor; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureReferenceRepair; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureSurfaceFitter; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureTerrainIntegrator; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureVegetationClearer; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureVerticalPlacer; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureVanillaLocator; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureVolumeIndex; +import art.arcane.iris.nativegen.v26_3_R1.WorldgenTerrainHeightmaps; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.generation.block.IrisCustomData; +import art.arcane.iris.platform.reflect.WrappedField; +import art.arcane.iris.platform.reflect.WrappedReturningMethod; +import art.arcane.iris.generation.context.IrisContext; +import art.arcane.volmlib.util.math.RNG; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.util.random.Weighted; +import net.minecraft.util.random.WeightedList; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.NaturalSpawner; +import net.minecraft.world.level.NoiseColumn; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeGenerationSettings; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.biome.MobSpawnSettings; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.RandomSupport; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.XoroshiroRandomSource; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobCategory; +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.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.blending.Blender; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.block.data.CraftBlockData; +import org.bukkit.craftbukkit.generator.CustomChunkGenerator; +import org.bukkit.block.data.BlockData; +import org.spigotmc.SpigotWorldConfig; + +import javax.annotation.Nullable; +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.OptionalInt; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.IntBinaryOperator; +import java.util.function.Predicate; + +public class IrisChunkGenerator extends CustomChunkGenerator implements LongPredicate { + private static final Set AUTHORING_HEIGHTMAPS = Set.of( + Heightmap.Types.WORLD_SURFACE_WG, + Heightmap.Types.OCEAN_FLOOR_WG, + Heightmap.Types.WORLD_SURFACE, + Heightmap.Types.OCEAN_FLOOR, + Heightmap.Types.MOTION_BLOCKING, + Heightmap.Types.MOTION_BLOCKING_NO_LEAVES); + private static final WrappedField BIOME_SOURCE; + private static final WrappedReturningMethod SET_HEIGHT; + private static final Runnable NO_OP = () -> { + }; + private final ChunkGenerator delegate; + private final Engine engine; + private final CustomBiomeSource customBiomeSource; + private final ServerLevel runtimeLevel; + private final @Nullable BukkitChunkGenerator platformGenerator; + private final int runtimeMinY; + private final int runtimeHeight; + private final int runtimeSeaLevel; + private final ConcurrentHashMap> mergedSpawnTables = new ConcurrentHashMap<>(); + private final ImportedFeatureStage importedFeatures; + private final NativeTerrainHeightCache terrainHeights = new NativeTerrainHeightCache(); + private final AtomicReference retainedStudioStructureState = new AtomicReference<>(); + private volatile ReachableStructureCache reachableStructureCache; + private volatile StructureStepCache structureStepCache; + + public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) { + this(delegate, engine, world, new CustomBiomeSource(seed, engine, world)); + } + + private IrisChunkGenerator(ChunkGenerator delegate, Engine engine, World world, CustomBiomeSource customBiomeSource) { + super(((CraftWorld) world).getHandle(), edit(delegate, customBiomeSource), world.getGenerator()); + this.delegate = delegate; + this.engine = engine; + this.customBiomeSource = customBiomeSource; + this.importedFeatures = new ImportedFeatureStage(engine); + ServerLevel level = ((CraftWorld) world).getHandle(); + this.runtimeLevel = level; + this.platformGenerator = world.getGenerator() instanceof BukkitChunkGenerator bukkitGenerator + ? bukkitGenerator + : null; + this.runtimeMinY = level.getMinY(); + this.runtimeHeight = level.getHeight(); + this.runtimeSeaLevel = runtimeMinY + engine.getDimension().getFluidHeight(); + if (engine instanceof IrisEngine irisEngine) { + irisEngine.addGenerationRuntimeRetirementListener(this::evictRuntimeCaches); + } + installNativeStructureVolumeIndex(level); + } + + private void evictRuntimeCaches(int runtimeId) { + terrainHeights.evictRuntime(runtimeId); + importedFeatures.evictRuntime(runtimeId); + mergedSpawnTables.keySet().removeIf(key -> key.runtimeId() == runtimeId); + ReachableStructureCache reachable = reachableStructureCache; + if (reachable != null && reachable.runtimeId() == runtimeId) { + reachableStructureCache = null; + } + StructureStepCache steps = structureStepCache; + if (steps != null && steps.runtimeId() == runtimeId) { + structureStepCache = null; + } + } + + private void installNativeStructureVolumeIndex(ServerLevel level) { + WeakReference levelReference = new WeakReference<>(level); + WeakReference generatorReference = new WeakReference<>(this); + WeakReference biomeSourceReference = new WeakReference<>(customBiomeSource); + NativeStructureVolumeIndex.install(engine, new NativeStructureVolumeIndex.Context( + level.registryAccess(), + level.getServer().getStructureTemplateManager(), + level.dimension(), + LevelHeightAccessor.create(runtimeMinY, runtimeHeight), + generatorReference::get, + biomeSourceReference::get, + () -> { + ServerLevel active = levelReference.get(); + return active == null ? null : active.getChunkSource().getGeneratorState(); + })); + } + + @Override + public @Nullable Pair> findNearestMapStructure(ServerLevel level, HolderSet holders, BlockPos pos, int radius, boolean findUnexplored) { + if (platformGenerator != null && !platformGenerator.shouldGenerateStructures()) { + return null; + } + if (level != runtimeLevel || level.getChunkSource().getGenerator() != this) { + return null; + } + try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute( + SectionPos.blockToSectionCoord(pos.getX()), + SectionPos.blockToSectionCoord(pos.getZ()), + "bukkit_nms_structure_locate"); + GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route); + GenerationSessionLease lease = requireGenerationLease("bukkit_nms_structure_locate"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + HolderSet reachable = filterReachableStructures(level, holders); + NativeStructureVanillaLocator.Candidate nativeCandidate = + reachable == null || reachable.size() == 0 ? null + : NativeStructureVanillaLocator.predict( + level, reachable, pos, radius, findUnexplored); + return findNearestIrisStructure( + level, holders, pos, Math.max(0, radius), + findUnexplored, nativeCandidate); + } + } + + private Pair> findNearestIrisStructure(ServerLevel level, + HolderSet holders, + BlockPos pos, int radius, + boolean findUnexplored, + NativeStructureVanillaLocator.Candidate nativeCandidate) { + Pair> nativeLocated = + nativeCandidate == null ? null : nativeCandidate.result(); + Runnable nativeReference = () -> { + if (nativeCandidate != null) { + nativeCandidate.reference(level.structureManager()); + } + }; + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + List searches = new ArrayList<>(holders.size()); + NativeStructureLocatePersistence.ProbeBudget budget = NativeStructureLocatePersistence.probeBudget(); + for (Holder holder : holders) { + Object id = registry.getKey(holder.value()); + if (id == null) { + throw new IllegalStateException("Native structure locate received an unregistered structure holder"); + } + String structureId = id.toString(); + if (!IrisStructureLocator.hasNativePlacement(engine, structureId)) { + continue; + } + NativeStructureLocatePersistence.Probe probe = NativeStructureLocatePersistence.probe( + level, holder.value(), findUnexplored, budget); + searches.add(new IrisNativeLocateSearch( + holder, structureId, NativeStructureLocatePersistence.search( + engine, structureId, pos.getX(), pos.getZ(), radius, probe))); + } + searches.sort(Comparator.comparing(IrisNativeLocateSearch::structureId)); + for (int attempt = 0; attempt < NativeStructureLocatePersistence.MAX_SELECTED_CANDIDATE_RETRIES; attempt++) { + IrisNativeLocateSearch bestSearch = null; + IrisStructureLocator.LocateResult bestResult = null; + long bestDistance = Long.MAX_VALUE; + for (IrisNativeLocateSearch search : searches) { + IrisStructureLocator.LocateResult result = search.search().predict(); + if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) { + throw new IllegalStateException("Iris structure locate reached its safety limit for " + + search.structureId() + " within " + radius + " placement rings"); + } + if (!result.found()) { + continue; + } + long dx = (long) result.originX() - pos.getX(); + long dz = (long) result.originZ() - pos.getZ(); + long distance = dx * dx + dz * dz; + if (distance < bestDistance) { + bestDistance = distance; + bestSearch = search; + bestResult = result; + } + } + if (bestSearch == null) { + return NativeStructureLocateResults.selectAndReference( + pos, null, () -> { }, nativeLocated, nativeReference); + } + Pair> predicted = Pair.of( + new BlockPos(bestResult.originX(), bestResult.baseY(), bestResult.originZ()), + bestSearch.holder()); + if (NativeStructureLocateResults.nearest(pos, predicted, nativeLocated) != predicted) { + return NativeStructureLocateResults.selectAndReference( + pos, predicted, () -> { }, nativeLocated, nativeReference); + } + NativeStructureLocatePersistence.VerifiedStart verified = + bestSearch.search().verify(bestResult); + if (verified == null) { + bestSearch.search().reject(bestResult); + continue; + } + BlockPos located = new BlockPos( + bestResult.originX(), verified.ownership().locatorY(), + bestResult.originZ()); + Pair> irisLocated = Pair.of(located, bestSearch.holder()); + IrisNativeLocateSearch selectedSearch = bestSearch; + NativeStructureLocatePersistence.VerifiedStart selectedStart = verified; + return NativeStructureLocateResults.selectAndReference( + pos, irisLocated, () -> selectedSearch.search().reference(selectedStart), + nativeLocated, nativeReference); + } + throw new IllegalStateException("Iris structure locate rejected too many selected candidates within " + + radius + " placement rings"); + } + + private HolderSet filterReachableStructures(ServerLevel level, HolderSet holders) { + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + List candidates = new ArrayList<>(holders.size()); + for (Holder holder : holders) { + Object id = registry.getKey(holder.value()); + if (id == null) { + throw new IllegalStateException("Native structure filtering received an unregistered structure holder"); + } + String key = id.toString(); + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, + key, NativeStructureVegetationClearer.isUndergroundStep(holder.value().step())); + if (!decision.generate()) { + continue; + } + candidates.add(new NativeLocateCandidate(holder, key)); + } + if (candidates.isEmpty()) { + return HolderSet.direct(List.of()); + } + Set reachable = reachableStructureKeys(level); + List> kept = new ArrayList<>(candidates.size()); + for (NativeLocateCandidate candidate : candidates) { + if (reachable.contains(candidate.key())) { + kept.add(candidate.holder()); + } + } + if (kept.size() == holders.size()) { + return holders; + } + return HolderSet.direct(kept); + } + + private Set reachableStructureKeys(ServerLevel level) { + IrisDimension dimension = engine.getDimension(); + int runtimeId = engine.getCacheID(); + ReachableStructureCache cached = reachableStructureCache; + if (cached != null && cached.dimension() == dimension && cached.runtimeId() == runtimeId) { + return cached.keys(); + } + synchronized (this) { + cached = reachableStructureCache; + if (cached != null && cached.dimension() == dimension && cached.runtimeId() == runtimeId) { + return cached.keys(); + } + Set reachable = Set.copyOf( + VanillaStructureBiomes.reachableStructureKeys(level, customBiomeSource)); + reachableStructureCache = new ReachableStructureCache(dimension, runtimeId, reachable); + return reachable; + } + } + + @Override + protected MapCodec codec() { + return MapCodec.unit(null); + } + + @Override + public ChunkGenerator getDelegate() { + if (delegate instanceof CustomChunkGenerator chunkGenerator) + return chunkGenerator.getDelegate(); + return delegate; + } + + @Override + public int getMinY() { + return runtimeMinY; + } + + @Override + public int getSeaLevel() { + return runtimeSeaLevel; + } + + @Override + public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey levelKey) { + if (platformGenerator != null && !platformGenerator.shouldGenerateStructures()) { + return; + } + if (runtimeLevel.getChunkSource().getGenerator() != this + || runtimeLevel.getChunkSource().getGeneratorState() != structureState) { + return; + } + ChunkPos chunkPos = access.getPos(); + try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_structures"); + GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute( + chunkPos.x(), chunkPos.z(), "bukkit_nms_create_structures"); + GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route); + GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_structures"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + if (!allowsRoutedDiscreteGeneration(access, ChunkStatus.STRUCTURE_STARTS)) { + return; + } + Map previousStarts = new HashMap<>(access.getAllStarts()); + super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey); + Map configuredStarts = NativeStructureStartInjector.inject( + new NativeStructureStartInjector.InjectionContext( + engine, + registryAccess, + structureState, + structureManager, + access, + templateManager, + levelKey, + this, + customBiomeSource + )); + adjustGeneratedStructures( + registryAccess, access, previousStarts, configuredStarts, templateManager); + if (route != null) { + access.persistentDataContainer.set(new NamespacedKey("iris", "structure_activation"), + PersistentDataType.LONG, route.activation().activationId()); + access.markUnsaved(); + } + } + } + + private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess access, + Map previousStarts, + Map configuredStarts, + StructureTemplateManager templateManager) { + Registry registry = registryAccess.lookupOrThrow(Registries.STRUCTURE); + ChunkPos chunkPos = access.getPos(); + for (Map.Entry entry : access.getAllStarts().entrySet()) { + Structure structure = entry.getKey(); + StructureStart start = entry.getValue(); + if (!start.isValid() || previousStarts.get(structure) == start) { + continue; + } + Identifier id = registry.getKey(structure); + String structureId = id == null ? null : id.toString(); + if (structureId == null) { + throw NativeStructureGenerationException.failure( + "resolution", null, chunkPos.x(), chunkPos.z()); + } + BoundingBox footprint = start.getBoundingBox(); + if (!engine.getComplex().allowsNewGenerationFootprint( + footprint.minX(), + footprint.minZ(), + footprint.maxX(), + footprint.maxZ())) { + access.setStartForStructure(structure, StructureStart.INVALID_START); + if (configuredStarts.containsKey(structure)) { + NativeStructureOwnershipStore.discard( + engine, structureId, chunkPos.x(), chunkPos.z()); + } + continue; + } + if (configuredStarts.containsKey(structure)) { + continue; + } + boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step()); + IrisNativeStructureDecision decision; + try { + decision = NativeStructureGenerationPolicy.resolve(engine, + structureId, undergroundStep); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "policy resolution", structureId, chunkPos.x(), chunkPos.z(), error); + } + if (!decision.generate()) { + access.setStartForStructure(structure, StructureStart.INVALID_START); + continue; + } + try { + NativeStructureVerticalPlacer.applyVerticalPlacement( + start, + structureId, + decision.yShift(), + getSeaLevel(), + access.getMinY(), + access.getMinY() + access.getHeight(), + undergroundStep, + decision.preserveSourceY(), + decision.yBand(), + (x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight()); + StructureStart wrapped = NativeStructureReferenceEnvelope.wrapForPublication( + start, structure, start.getReferences(), + NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()), + structureId); + access.setStartForStructure(structure, wrapped); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error); + } + } + } + + @Override + public ChunkGeneratorStructureState createState(HolderLookup holderlookup, RandomState randomstate, long i, SpigotWorldConfig conf) { + return delegate.createState(holderlookup, randomstate, i, conf); + } + + void retainStudioStructureState( + ServerLevel level, + ChunkMap chunkMap, + ChunkGeneratorStructureState structureState + ) { + requireCurrentStructureOwner(level, chunkMap); + StudioStructureState retained = new StudioStructureState( + level, + chunkMap, + Objects.requireNonNull(structureState, "Studio native structure state")); + if (!retainedStudioStructureState.compareAndSet(null, retained)) { + throw new IllegalStateException("Studio native structure state is already retained."); + } + } + + StudioStructureState retainedStudioStructureState(ServerLevel level, ChunkMap chunkMap) { + StudioStructureState retained = retainedStudioStructureState.get(); + if (retained == null) { + return null; + } + requireCurrentStructureOwner(level, chunkMap); + if (retained.level() != level || retained.chunkMap() != chunkMap) { + throw new IllegalStateException("Retained Studio native structure state belongs to another world runtime."); + } + if (level.getChunkSource().getGeneratorState() != retained.structureState()) { + throw new IllegalStateException("Studio native structure state is no longer current."); + } + return retained; + } + + void claimStudioStructureState(StudioStructureState retained) { + if (!retainedStudioStructureState.compareAndSet(retained, null)) { + throw new IllegalStateException("Studio native structure state changed before activation began."); + } + } + + void abandonStudioStructureState() { + retainedStudioStructureState.set(null); + } + + CompletableFuture initializeAndPublishStructureState( + ChunkGeneratorStructureState structureState, + StructureStatePublisher publisher + ) { + return startStructureStateBootstrap( + structureState, + NO_OP, + () -> publishStructureState(publisher)); + } + + CompletableFuture activateStudioStructureState(StudioStructureState retained) { + Objects.requireNonNull(retained, "Retained Studio native structure state"); + return startStructureStateBootstrap( + retained.structureState(), + () -> { + StudioStructureState current = retainedStudioStructureState( + retained.level(), retained.chunkMap()); + if (current != retained) { + throw new IllegalStateException("Studio native structure state changed before activation."); + } + claimStudioStructureState(retained); + }, + NO_OP); + } + + private CompletableFuture startStructureStateBootstrap( + ChunkGeneratorStructureState structureState, + Runnable claim, + Runnable activation + ) { + if (!(engine instanceof IrisEngine irisEngine)) { + throw new IllegalStateException("Native structure bootstrap requires an IrisEngine runtime."); + } + AtomicReference> registeredCompletion = new AtomicReference<>(); + CompletableFuture completion = irisEngine.startNativeStructureBootstrap( + claim, + () -> { + CompletableFuture rings = initializeStructureState(structureState); + registeredCompletion.set(rings); + return rings; + }, + () -> { + CompletableFuture rings = Objects.requireNonNull( + registeredCompletion.get(), + "Registered native structure ring completion"); + if (rings.isCompletedExceptionally()) { + throw new IllegalStateException( + "Minecraft native structure ring bootstrap failed before activation."); + } + activation.run(); + }); + completion.whenComplete((ignored, failure) -> { + if (failure != null) { + Throwable cause = failure instanceof CompletionException && failure.getCause() != null + ? failure.getCause() + : failure; + IrisLogging.reportError("Native structure ring bootstrap failed for world '" + + runtimeLevel.getWorld().getName() + "'.", cause); + } + }); + return completion; + } + + private CompletableFuture initializeStructureState(ChunkGeneratorStructureState structureState) { + Map ringPositions = structureRingPositions(structureState); + structureState.ensureStructuresGenerated(); + return structureRingCompletion(ringPositions); + } + + private Map structureRingPositions(ChunkGeneratorStructureState structureState) { + try { + Field field = structureRingPositionsField(); + field.setAccessible(true); + Object value = field.get(structureState); + if (!(value instanceof Map ringPositions)) { + throw new IllegalStateException("Minecraft native structure ring state is unavailable."); + } + return ringPositions; + } catch (IllegalAccessException e) { + throw new IllegalStateException("Could not bind Minecraft native structure ring completions.", e); + } + } + + private CompletableFuture structureRingCompletion(Map ringPositions) { + List> futures = new ArrayList<>(ringPositions.size()); + for (Object candidate : ringPositions.values()) { + if (candidate instanceof CompletableFuture future) { + futures.add(future); + } else { + throw new IllegalStateException( + "Minecraft native structure ring completion is not a future."); + } + } + CompletableFuture[] completions = futures.toArray(new CompletableFuture[0]); + return CompletableFuture.allOf(completions); + } + + private Field structureRingPositionsField() { + List candidates = new ArrayList<>(1); + for (Field field : ChunkGeneratorStructureState.class.getDeclaredFields()) { + if (!Map.class.isAssignableFrom(field.getType())) { + continue; + } + Type genericType = field.getGenericType(); + if (!(genericType instanceof ParameterizedType parameterizedType)) { + continue; + } + Type[] arguments = parameterizedType.getActualTypeArguments(); + if (arguments.length == 2 + && arguments[1].getTypeName().contains(CompletableFuture.class.getName())) { + candidates.add(field); + } + } + if (candidates.size() != 1) { + throw new IllegalStateException("Expected one Minecraft native structure ring-future map, found " + + candidates.size() + "."); + } + return candidates.getFirst(); + } + + private void requireCurrentStructureOwner(ServerLevel level, ChunkMap chunkMap) { + if (runtimeLevel != level + || level.getChunkSource().chunkMap != chunkMap + || level.getChunkSource().getGenerator() != this) { + throw new IllegalStateException("Iris native structure state no longer belongs to the active world runtime."); + } + } + + private void publishStructureState(StructureStatePublisher publisher) { + try { + publisher.publish(); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Could not publish Minecraft native structure state.", e); + } + } + + @Override + public void createReferences(WorldGenLevel generatoraccessseed, StructureManager structuremanager, ChunkAccess ichunkaccess) { + if (platformGenerator != null && !platformGenerator.shouldGenerateStructures()) { + return; + } + if (runtimeLevel.getChunkSource().getGenerator() != this) { + return; + } + ChunkPos chunkPos = ichunkaccess.getPos(); + try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_references"); + GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute( + chunkPos.x(), chunkPos.z(), "bukkit_nms_create_references"); + GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route); + GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_references"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + NativeStructureReferenceRepair.createReferences( + engine, generatoraccessseed, structuremanager, ichunkaccess); + } + } + + @Override + public CompletableFuture createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) { + ChunkPos chunkPos = ichunkaccess.getPos(); + try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_biomes"); + GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute( + chunkPos.x(), chunkPos.z(), "bukkit_nms_create_biomes"); + GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route); + GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + customBiomeSource.prepareVisibleBiomeBatch(); + IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); + ichunkaccess.fillBiomesFromNoise( + (x, y, z) -> customBiomeSource.getVisibleNoiseBiomeWithActiveGenerationLease( + x, y, z, null, resolverState)); + return CompletableFuture.completedFuture(ichunkaccess); + } + } + + @Override + public CompletableFuture buildTerrain(ChunkAccess ichunkaccess, Blender blender, RandomState randomstate, StructureManager structuremanager, BiomeManager biomeManager, WorldGenRegion carverBiomeRegion, Set> possibleBiomes) { + ChunkPos chunkPos = ichunkaccess.getPos(); + BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_chunk_pipeline"); + GenerationHistoryRuntimeRouter.RuntimeRoute route; + try { + route = openHistoryRoute(chunkPos.x(), chunkPos.z(), "bukkit_nms_chunk_pipeline"); + } catch (RuntimeException | Error failure) { + stage.close(); + throw failure; + } + GenerationSessionLease lease; + try { + lease = requireGenerationLease("bukkit_nms_chunk_pipeline"); + } catch (RuntimeException | Error failure) { + closeHistoryRoute(route, failure); + stage.close(); + throw failure; + } + try { + CompletableFuture delegatePipeline; + try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + delegatePipeline = delegate.buildTerrain( + ichunkaccess, blender, randomstate, structuremanager, biomeManager, carverBiomeRegion, possibleBiomes); + } + CompletableFuture pipeline = delegatePipeline + .thenApply(filled -> { + try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = + openHistoryRuntimeScope(route); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + claimGeneratedSemantics(route, filled, engine.getMinHeight()); + persistNaturalTerrain(filled, route); + primeWorldgenHeightmaps(filled); + return filled; + } + }); + CompletableFuture completion = new CompletableFuture<>(); + pipeline.whenComplete((filled, failure) -> { + boolean cancelled = isCancellationFailure(failure); + Throwable completionFailure = closeNoisePipelineResources( + failure, lease, route, stage); + if (completionFailure == null) { + completion.complete(filled); + } else if (cancelled) { + completion.cancel(false); + } else { + completion.completeExceptionally(completionFailure); + } + }); + lease.detachThread(); + if (route != null) { + route.detachThread(); + } + return completion; + } catch (RuntimeException | Error failure) { + lease.close(); + closeHistoryRoute(route, failure); + stage.close(); + throw failure; + } + } + + private static void persistNaturalTerrain(ChunkAccess chunk, GenerationHistoryRuntimeRouter.RuntimeRoute route) { + if (route == null || route.naturalTerrain().isEmpty()) { + return; + } + try { + SavedTerrainChunk terrain = route.naturalTerrain().orElseThrow(); + chunk.persistentDataContainer.set(new NamespacedKey("iris", "natural_terrain"), PersistentDataType.BYTE_ARRAY, + NativeTerrainReceipt.encode(terrain, route.activation().activationId(), route.epoch().epochId())); + } catch (IOException failure) { + throw new IllegalStateException("Unable to persist natural terrain for chunk " + chunk.getPos(), failure); + } + } + + private static Throwable closeNoisePipelineResources( + Throwable failure, + GenerationSessionLease lease, + GenerationHistoryRuntimeRouter.RuntimeRoute route, + BukkitChunkGenerator.GenerationStagePermit stage + ) { + Throwable result = failure; + try { + lease.close(); + } catch (Throwable closeFailure) { + result = appendFailure(result, closeFailure); + } + try { + if (route != null) { + route.close(); + } + } catch (Throwable closeFailure) { + result = appendFailure(result, closeFailure); + } + try { + stage.close(); + } catch (Throwable closeFailure) { + result = appendFailure(result, closeFailure); + } + return result; + } + + private static void closeHistoryRoute( + GenerationHistoryRuntimeRouter.RuntimeRoute route, + Throwable failure + ) { + if (route == null) { + return; + } + try { + route.close(); + } catch (Throwable closeFailure) { + if (failure != closeFailure) { + failure.addSuppressed(closeFailure); + } + } + } + + private static Throwable appendFailure(Throwable failure, Throwable closeFailure) { + if (failure == null) { + return closeFailure; + } + if (failure != closeFailure) { + failure.addSuppressed(closeFailure); + } + return failure; + } + + private static boolean isCancellationFailure(Throwable failure) { + Throwable current = failure; + while (current instanceof CompletionException && current.getCause() != null) { + current = current.getCause(); + } + return current instanceof CancellationException; + } + + private void primeWorldgenHeightmaps(ChunkAccess chunkAccess) { + if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) { + primeAuthoringHeightmaps(chunkAccess); + return; + } + WorldgenTerrainHeightmaps.primeTerrain(chunkAccess, worldgenSurfaceHeight(), worldgenFloorHeight()); + } + + static void primeAuthoringHeightmaps(ChunkAccess chunkAccess) { + Heightmap.primeHeightmaps(chunkAccess, AUTHORING_HEIGHTMAPS); + } + + private IntBinaryOperator worldgenSurfaceHeight() { + return (x, z) -> engine.getHeight(x, z, false) + runtimeMinY + 1; + } + + private IntBinaryOperator worldgenFloorHeight() { + return (x, z) -> engine.getHeight(x, z, true) + runtimeMinY + 1; + } + + private IntBinaryOperator hostWorldgenSurfaceHeight() { + return (x, z) -> Engine.hostHeight(engine, x, z, false) + runtimeMinY + 1; + } + + private IntBinaryOperator hostWorldgenFloorHeight() { + return (x, z) -> Engine.hostHeight(engine, x, z, true) + runtimeMinY + 1; + } + + @Override + public WeightedList getMobsAt(Level level, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) { + Holder holder = level.getBiome(blockposition); + return EngineLifecycleTasks.call(engine, "bukkit_nms_mob_spawns", + () -> getAdmittedMobsAt(holder, structuremanager, enumcreaturetype, blockposition), + WeightedList.of(List.of())); + } + + private WeightedList getAdmittedMobsAt( + Holder holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) { + NativeBiomeSpawnSelection selection = NativeBiomeSpawnSelection.at( + engine, blockposition.getX(), blockposition.getY(), blockposition.getZ(), + holder.unwrapKey().map(key -> key.identifier().toString()).orElse("")); + if (selection.mode() == NativeBiomeSpawnSelection.Mode.LOADING) { + return WeightedList.of(List.of()); + } + try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope( + blockposition.getX(), blockposition.getZ(), "bukkit_nms_mob_spawns")) { + return getMobsAtWithActiveRuntime(holder, structuremanager, enumcreaturetype, blockposition, selection); + } + } + + private WeightedList getMobsAtWithActiveRuntime( + Holder holder, + StructureManager structuremanager, + MobCategory enumcreaturetype, + BlockPos blockposition, + NativeBiomeSpawnSelection selection + ) { + Holder vanillaSpawnBiome = switch (selection.mode()) { + case RETAINED -> customBiomeSource.getRetainedVanillaSpawnBiome(selection.derivativeKey()); + case CURRENT -> customBiomeSource.getVanillaSpawnBiome(holder); + case NONE, LOADING -> null; + }; + if (vanillaSpawnBiome == null) { + return delegate.getMobsAt(runtimeLevel, structuremanager, enumcreaturetype, blockposition); + } + + WeightedList vanillaSpawns = biomeSpawns(vanillaSpawnBiome, enumcreaturetype); + WeightedList resolvedSpawns = delegate.getMobsAt( + runtimeLevel, structuremanager, enumcreaturetype, blockposition); + WeightedList environmentSpawns = runtimeLevel.environmentAttributes() + .getValue(EnvironmentAttributes.NATURAL_MOB_SPAWNS, blockposition).getMobsToSpawn(enumcreaturetype); + if (resolvedSpawns != environmentSpawns) { + return resolvedSpawns; + } + + WeightedList explicitSpawns = biomeSpawns(holder, enumcreaturetype); + if (explicitSpawns.isEmpty()) { + return vanillaSpawns; + } + if (vanillaSpawns.isEmpty()) { + return explicitSpawns; + } + + int spawnRuntimeId = engine.getCacheID(); + SpawnTableKey key = new SpawnTableKey(spawnRuntimeId, holder.value(), vanillaSpawnBiome.value(), enumcreaturetype); + return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns)); + } + + @Override + public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager) { + applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, true); + } + + private static WeightedList biomeSpawns(Holder biome, MobCategory category) { + return biome.value().getAttributes().applyModifier(EnvironmentAttributes.NATURAL_MOB_SPAWNS, + EnvironmentAttributes.NATURAL_MOB_SPAWNS.defaultValue()).getMobsToSpawn(category); + } + + @Override + public void addDebugScreenInfo(List list, RandomState randomstate, BlockPos blockposition, SamplerContext samplerContext) { + delegate.addDebugScreenInfo(list, randomstate, blockposition, samplerContext); + } + + @Override + public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) { + ChunkPos chunkPos = ichunkaccess.getPos(); + try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_biome_decoration"); + GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute( + chunkPos.x(), chunkPos.z(), "bukkit_nms_biome_decoration"); + GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route); + GenerationSessionLease lease = requireGenerationLease("bukkit_nms_biome_decoration"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + boolean flatStudioTerrain = platformGenerator != null && platformGenerator.usesFlatStudioTerrain(); + if (!flatStudioTerrain && !ichunkaccess.getPersistedStatus().isOrAfter(ChunkStatus.FEATURES)) { + placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager); + } + if (allowsRoutedDiscreteGeneration(ichunkaccess, ChunkStatus.FEATURES) + && NativeGenerationWriteGuard.allowsDecoration(engine, generatoraccessseed, chunkPos)) { + if (!flatStudioTerrain) { + // Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a + // feature-order cycle is reported once and degraded to features-off. + importedFeatures.prepare(generatoraccessseed); + } + addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager); + if (!flatStudioTerrain) { + // Vanilla's placed-feature pass, on THIS thread. The delegate is still called with + // addVanillaDecorations=false below, so the vanilla half never runs twice. Inert unless the + // dimension set importedFeatures.enabled. + importedFeatures.run(generatoraccessseed, ichunkaccess, this); + } + delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false); + } + claimGeneratedSemantics(route, ichunkaccess, engine.getMinHeight()); + } + } + + /** + * Iris custom biomes carry no features in their datapack JSON by design; when importedFeatures is on they + * inherit the generation settings of the vanilla biome their Iris biome derives from. This is the gate + * {@code BiomeFilter} consults, so it has to agree with the feature pass. With the control off this is + * exactly the inherited behaviour. + */ + @Override + public BiomeGenerationSettings getBiomeGenerationSettings(Holder holder) { + BiomeGenerationSettings imported = importedFeatures.generationSettings(holder); + return imported == null ? super.getBiomeGenerationSettings(holder) : imported; + } + + private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) { + if (!structureManager.shouldGenerateStructures()) { + ChunkPos disabledChunk = chunk.getPos(); + throw new IllegalStateException("Iris cannot generate native structures in chunk " + + disabledChunk.x() + "," + disabledChunk.z() + + " because structure generation is disabled outside the pack; enable native structure generation " + + "and deny families through importedStructures.disabled or complete keys through importedStructures.disabledExact"); + } + ChunkPos chunkPos = chunk.getPos(); + SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY()); + BlockPos origin = sectionPos.origin(); + Registry registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE); + List> byStep = structuresByStep(registry); + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); + long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ()); + BoundingBox area = writableArea(chunk); + int steps = GenerationStep.Decoration.values().length; + List placementGroups = new ArrayList<>(); + List heightmapStarts = new ArrayList<>(); + List vegetationTargets = new ArrayList<>(); + List terrainTargets = new ArrayList<>(); + for (int step = 0; step < steps; step++) { + int index = 0; + for (Structure structure : byStep.get(step)) { + Object id = registry.getKey(structure); + String structureId = id == null ? null : id.toString(); + if (structureId == null) { + throw NativeStructureGenerationException.failure( + "resolution", null, chunkPos.x(), chunkPos.z()); + } + try { + IrisNativeStructureDecision sourceDecision = NativeStructureGenerationPolicy.resolve(engine, + structureId, NativeStructureVegetationClearer.isUndergroundStep(structure.step())); + List starts = structureManager.startsForStructure(sectionPos.x(), sectionPos.z(), structure); + List resolvedPlacements = new ArrayList<>(starts.size()); + for (StructureStart start : starts) { + BoundingBox footprint = start.getBoundingBox(); + if (!isHistoricalStructureStart(world, start) + && !engine.getComplex().allowsNewGenerationFootprint( + footprint.minX(), + footprint.minZ(), + footprint.maxX(), + footprint.maxZ())) { + continue; + } + NativeStructureOwnershipRecord ownership = + NativeStructureOwnershipRecovery.resolve( + engine, world.getLevel(), structureId, structure, start); + IrisNativeStructureDecision decision = + ownership == null ? sourceDecision : ownership.restoredDecision(); + if (!decision.generate()) { + continue; + } + resolvedPlacements.add(new NativePlacement(start, decision)); + heightmapStarts.add(start); + terrainTargets.add(new NativeStructureTerrainIntegrator.TerrainTarget( + structureId, start, + NativeStructureTerrainIntegrator.resolveNativeTerrain( + start, decision.terrain()))); + vegetationTargets.add(start); + } + if (!resolvedPlacements.isEmpty()) { + placementGroups.add(new NativePlacementGroup( + structureId, index, step, List.copyOf(resolvedPlacements))); + } + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "resolution", structureId, chunkPos.x(), chunkPos.z(), error); + } + index++; + } + } + try { + WorldgenTerrainHeightmaps.primeStructurePlacement( + world, chunkPos, heightmapStarts, + hostWorldgenSurfaceHeight(), hostWorldgenFloorHeight()); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "heightmap priming", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData()); + Predicate protectedPosition = NativeGenerationWriteGuard.protectedPositions( + staticObjects, engine.getDimensionStackContext(), engine.getMinHeight()); + WorldGenLevel boundedWorld = NativeStructureWorldgenAccess.create( + world, chunkPos, hostWorldgenSurfaceHeight(), hostWorldgenFloorHeight(), + engine.getDimensionStackContext() != null, + protectedPosition); + try { + NativeStructureVegetationClearer.clearIntersectingVegetation( + boundedWorld, chunk, area, vegetationTargets); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "vegetation cleanup", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + NativeStructureSurfaceFitter.SurfaceTerrainPlan surfaceTerrainPlan; + try { + surfaceTerrainPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures( + boundedWorld, area, terrainTargets, + (x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight()); + surfaceTerrainPlan.primeHeightmaps(chunk); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "terrain integration", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + try { + NativeStructurePostProcessor.prepareTerrain( + boundedWorld, area, terrainTargets, this::resolvePaletteBlock); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "terrain preparation", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + for (NativePlacementGroup group : placementGroups) { + random.setFeatureSeed(decoSeed, group.featureIndex(), group.step()); + try { + for (NativePlacement placement : group.placements()) { + placeVanillaStructure(boundedWorld, structureManager, random, area, chunkPos, + group.structureId(), placement.start(), placement.decision()); + } + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "placement", group.structureId(), chunkPos.x(), chunkPos.z(), error); + } + } + try { + NativeStructureSurfaceFitter.repairVacuumFoundations( + boundedWorld, area, surfaceTerrainPlan); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "foundation repair", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + } + + private boolean isHistoricalStructureStart(WorldGenLevel world, StructureStart start) { + ChunkPos origin = start.getChunkPos(); + if (!engine.getComplex().allowsMantleChunkWrite(origin.x(), origin.z())) { + return true; + } + ChunkAccess source = world.getChunk(origin.x(), origin.z(), ChunkStatus.EMPTY, false); + if (source == null) { + return false; + } + long activation = source.persistentDataContainer.getOrDefault( + new NamespacedKey("iris", "structure_activation"), PersistentDataType.LONG, 0L); + return NativeGenerationWriteGuard.isHistoricalStructure(engine, activation); + } + + private static String nativeStructureBatchContext(List placementGroups) { + if (placementGroups.isEmpty()) { + return ""; + } + StringBuilder context = new StringBuilder("["); + for (int i = 0; i < placementGroups.size(); i++) { + if (i > 0) { + context.append(", "); + } + context.append(placementGroups.get(i).structureId()); + } + return context.append(']').toString(); + } + + private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager, WorldgenRandom random, + BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start, + IrisNativeStructureDecision decision) { + WorldGenLevel boundedWorld = world instanceof NativeStructureWorldgenAccess ? world : NativeStructureWorldgenAccess.create( + world, + chunkPos, + hostWorldgenSurfaceHeight(), + hostWorldgenFloorHeight(), + engine.getDimensionStackContext() != null, + NativeGenerationWriteGuard.protectedPositions( + engine.getDimension().getStaticObjectLayer(engine.getData()), + engine.getDimensionStackContext(), engine.getMinHeight())); + world.setCurrentlyGenerating(() -> "Iris native structure " + structureId); + try { + NativeStructurePostProcessor.place(boundedWorld, structureManager, this, random, area, chunkPos, + structureId, start, decision, this::resolvePaletteBlock, + (x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight()); + } finally { + world.setCurrentlyGenerating(null); + } + } + + private List> structuresByStep(Registry registry) { + int runtimeId = engine.getCacheID(); + StructureStepCache cached = structureStepCache; + if (cached != null && cached.runtimeId() == runtimeId && cached.registry() == registry) { + return cached.structures(); + } + synchronized (this) { + cached = structureStepCache; + if (cached != null && cached.runtimeId() == runtimeId && cached.registry() == registry) { + return cached.structures(); + } + int steps = GenerationStep.Decoration.values().length; + List> grouped = new ArrayList<>(steps); + for (int step = 0; step < steps; step++) { + grouped.add(new ArrayList<>()); + } + for (Structure structure : registry) { + grouped.get(structure.step().ordinal()).add(structure); + } + for (int step = 0; step < steps; step++) { + grouped.set(step, List.copyOf(grouped.get(step))); + } + List> resolved = List.copyOf(grouped); + structureStepCache = new StructureStepCache(runtimeId, registry, resolved); + return resolved; + } + } + + private BlockState resolvePaletteBlock(IrisMaterialPalette palette, RNG rng, int x, int y, int z) { + PlatformBlockState platformState = palette.get(rng, x, y, z, engine.getData()); + if (platformState == null || !(platformState.nativeHandle() instanceof BlockData blockData)) { + throw new IllegalStateException("Configured native structure palette did not resolve a Bukkit block at " + + x + "," + y + "," + z); + } + if (blockData instanceof IrisCustomData customData) { + blockData = customData.getBase(); + } + if (blockData instanceof CraftBlockData craftBlockData) { + return craftBlockData.getState(); + } + throw new IllegalStateException("Configured native structure palette resolved unsupported Bukkit block data " + + blockData.getClass().getName() + " at " + x + "," + y + "," + z); + } + + private BoundingBox writableArea(ChunkAccess chunk) { + ChunkPos cp = chunk.getPos(); + int i = cp.getMinBlockX(); + int j = cp.getMinBlockZ(); + int minY = chunk.getMinY() + 1; + int maxY = chunk.getMinY() + chunk.getHeight() - 1; + return new BoundingBox(i, minY, j, i + 15, maxY, j + 15); + } + + @Override + public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) { + ChunkPos chunkPos = chunkAccess.getPos(); + try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope( + chunkPos.getMinBlockX(), chunkPos.getMinBlockZ(), "bukkit_nms_heightmaps"); + GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_heightmaps"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY()); + BlockPos blockPos = sectionPos.origin(); + + primeWorldgenHeightmaps(chunkAccess); + if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) { + return; + } + + Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING); + Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES); + int minHeight = engine.getMinHeight(); + + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + int wX = x + blockPos.getX(); + int wZ = z + blockPos.getZ(); + + int terrainTop = engine.getHeight(wX, wZ, false) + minHeight + 1; + SET_HEIGHT.invoke(motion, x, z, terrainTop); + SET_HEIGHT.invoke(motionNoLeaves, x, z, terrainTop); + } + } + + Heightmap.primeHeightmaps(chunkAccess, ChunkStatus.FINAL_HEIGHTMAPS); + } catch (GenerationSessionException e) { + throw new IllegalStateException("Iris heightmap generation could not acquire its engine runtime.", e); + } + } + + @Override + public void spawnOriginalMobs(WorldGenRegion region) { + ChunkPos center = region.getCenter(); + try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_spawn_original_mobs"); + GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute( + center.x(), center.z(), "bukkit_nms_spawn_original_mobs"); + GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route); + GenerationSessionLease lease = requireGenerationLease("bukkit_nms_spawn_original_mobs"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + if (!allowsRoutedDiscreteGeneration(region.getChunk(center.x(), center.z()), ChunkStatus.SPAWN)) { + return; + } + Holder visibleBiome; + if (engine.getDimensionStackContext() == null) { + visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY())); + } else { + visibleBiome = customBiomeSource.getVisibleSurfaceBiome( + center.getMinBlockX() + 8, + center.getMinBlockZ() + 8); + if (visibleBiome == null) { + visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY())); + } + } + Holder vanillaBiome = customBiomeSource.getVanillaSpawnBiome(visibleBiome); + WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed())); + random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ()); + InitialMobSpawner.spawn( + region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random); + } + } + + @Override + public boolean test(long packedChunk) { + return !engine.isClosing() && !engine.isClosed() + && engine.getComplex().allowsMantleChunkWrite(ChunkPos.getX(packedChunk), ChunkPos.getZ(packedChunk)); + } + + private boolean allowsRoutedDiscreteGeneration(ChunkAccess chunk, ChunkStatus stage) { + if (chunk.getPersistedStatus().isOrAfter(stage)) { + return false; + } + ChunkPos chunkPos = chunk.getPos(); + return NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, stage) + || engine.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z()); + } + + private static WeightedList mergeSpawnTables( + WeightedList vanillaSpawns, + WeightedList explicitSpawns) { + List> entries = new ArrayList<>( + vanillaSpawns.unwrap().size() + explicitSpawns.unwrap().size()); + Set> explicitTypes = new HashSet<>(); + for (Weighted entry : explicitSpawns.unwrap()) { + explicitTypes.add(entry.value().type()); + } + for (Weighted entry : vanillaSpawns.unwrap()) { + if (!explicitTypes.contains(entry.value().type())) { + entries.add(entry); + } + } + entries.addAll(explicitSpawns.unwrap()); + return WeightedList.of(entries); + } + + @Override + public int getSpawnHeight(LevelHeightAccessor levelheightaccessor) { + return delegate.getSpawnHeight(levelheightaccessor); + } + + @Override + public int getGenDepth() { + return runtimeHeight; + } + + @Override + public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) { + if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) { + return platformGenerator.getAuthoringBaseHeight(); + } + try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope( + i, j, "bukkit_nms_base_height"); + GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + NativeTerrainHeightCache.Query query = new NativeTerrainHeightCache.Query( + engine.getCacheID(), i, j, heightmap_type, + levelheightaccessor.getMinY(), levelheightaccessor.getHeight()); + OptionalInt resolved = terrainHeights.resolvedHeight(query, + () -> resolvedBaseHeight(i, j, heightmap_type, levelheightaccessor)); + if (resolved.isPresent()) { + return resolved.getAsInt(); + } + boolean ignoreFluid = !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState()); + int height = engine.getDimensionStackContext() == null + ? engine.getHeight(i, j, ignoreFluid) + : Engine.hostHeight(engine, i, j, ignoreFluid); + return levelheightaccessor.getMinY() + height + 1; + } catch (GenerationSessionException e) { + throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", e); + } + } + + private OptionalInt resolvedBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor) { + Optional resolved = engine.getComplex().resolvedTerrainColumn(x, z); + return resolved.isPresent() + ? OptionalInt.of(NativeTransitionColumn.height(resolved.get(), type, heightAccessor)) + : OptionalInt.empty(); + } + + @Override + public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) { + if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) { + BlockState[] column = new BlockState[levelheightaccessor.getHeight()]; + int floorIndex = platformGenerator.getAuthoringFloorY() - levelheightaccessor.getMinY(); + BlockState floor = platformGenerator.isJigsawStudioActive() && JigsawStudioGenerator.isLightFloor(i, j) + ? Blocks.SMOOTH_STONE.defaultBlockState() + : Blocks.POLISHED_DEEPSLATE.defaultBlockState(); + for (int index = 0; index < column.length; index++) { + column[index] = index == floorIndex + ? floor + : Blocks.AIR.defaultBlockState(); + } + return new NoiseColumn(levelheightaccessor.getMinY(), column); + } + try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope( + i, j, "bukkit_nms_base_column"); + GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + Optional resolved = engine.getComplex().resolvedTerrainColumn(i, j); + if (resolved.isPresent()) { + return NativeTransitionColumn.column(resolved.get(), levelheightaccessor); + } + boolean dimensionStack = engine.getDimensionStackContext() != null; + int block = dimensionStack + ? Engine.hostHeight(engine, i, j, true) + : engine.getHeight(i, j, true); + int water = dimensionStack + ? Engine.hostHeight(engine, i, j, false) + : engine.getHeight(i, j, false); + BlockState[] column = new BlockState[levelheightaccessor.getHeight()]; + for (int k = 0; k < column.length; k++) { + if (k <= block) { + column[k] = Blocks.STONE.defaultBlockState(); + } else if (k <= water) { + column[k] = Blocks.WATER.defaultBlockState(); + } else { + column[k] = Blocks.AIR.defaultBlockState(); + } + } + return new NoiseColumn(levelheightaccessor.getMinY(), column); + } catch (GenerationSessionException e) { + throw new IllegalStateException("Iris base column query could not acquire its engine runtime.", e); + } + } + + private GenerationSessionLease requireGenerationLease(String operation) { + try { + return engine.acquireGenerationLease(operation); + } catch (GenerationSessionException exception) { + throw new IllegalStateException("Iris " + operation + " could not acquire its engine runtime.", exception); + } + } + + private GenerationHistoryRuntimeRouter.RuntimeRoute openHistoryRoute( + int chunkX, + int chunkZ, + String operation + ) { + if (!(engine instanceof IrisEngine irisEngine)) { + return null; + } + GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null); + if (router == null) { + if (platformGenerator != null && platformGenerator.getGenerationHistory() != null) { + throw new IllegalStateException("Iris " + operation + + " has no generation-history runtime router."); + } + return null; + } + try { + return router.openRoute(chunkX, chunkZ); + } catch (IOException failure) { + throw new IllegalStateException("Iris " + operation + + " could not route generation history for chunk " + + chunkX + "," + chunkZ + ".", failure); + } + } + + private GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope( + int blockX, + int blockZ, + String operation + ) { + if (!(engine instanceof IrisEngine irisEngine)) { + return null; + } + try { + return irisEngine.openGenerationHistoryCoordinateScope(blockX, blockZ); + } catch (IOException failure) { + throw new IllegalStateException("Iris " + operation + + " could not route generation history at " + + blockX + "," + blockZ + ".", failure); + } + } + + private static GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope openHistoryRuntimeScope( + GenerationHistoryRuntimeRouter.RuntimeRoute route + ) { + return route == null ? null : route.openRuntimeScope(); + } + + private static void claimGeneratedSemantics( + GenerationHistoryRuntimeRouter.RuntimeRoute route, + ChunkAccess chunk, + int minimumY + ) { + if (route == null) { + return; + } + try { + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + route.claimGeneratedSemantics((x, y, z) -> { + BlockState state = chunk.getBlockState(position.set(x, minimumY + y, z)); + return state.isAir() || state.liquid(); + }); + } catch (IOException failure) { + throw new IllegalStateException("Iris could not durably claim generated chunk semantics.", failure); + } + } + + private BukkitChunkGenerator.GenerationStagePermit requireGenerationStage(String operation) { + return platformGenerator == null + ? BukkitChunkGenerator.GenerationStagePermit.noop() + : platformGenerator.acquireGenerationStage(operation); + } + + @Override + public Optional getTypeNameForDataFixer() { + return delegate.getTypeNameForDataFixer(); + } + + @Override + public void validate() { + delegate.validate(); + } + + static { + List biomeSources = new ArrayList<>(1); + for (Field field : ChunkGenerator.class.getDeclaredFields()) { + if (!field.getType().equals(BiomeSource.class)) + continue; + biomeSources.add(field); + } + if (biomeSources.size() != 1) + throw new IllegalStateException("Expected exactly one BiomeSource field in ChunkGenerator, found " + + biomeSources.size() + " " + biomeSources.stream().map(Field::getName).toList()); + Field biomeSource = biomeSources.getFirst(); + + List setHeights = new ArrayList<>(1); + for (Method method : Heightmap.class.getDeclaredMethods()) { + Class[] types = method.getParameterTypes(); + if (!method.getName().equals("setHeight") + || !Arrays.equals(types, new Class[]{int.class, int.class, int.class}) + || !method.getReturnType().equals(void.class)) + continue; + setHeights.add(method); + } + if (setHeights.size() != 1) + throw new IllegalStateException("Expected exactly one Heightmap.setHeight(int,int,int) method, found " + + setHeights.size()); + Method setHeight = setHeights.getFirst(); + + BIOME_SOURCE = new WrappedField<>(ChunkGenerator.class, biomeSource.getName()); + SET_HEIGHT = new WrappedReturningMethod<>(Heightmap.class, setHeight.getName(), setHeight.getParameterTypes()); + } + + private static ChunkGenerator edit(ChunkGenerator generator, BiomeSource source) { + try { + BIOME_SOURCE.set(generator, source); + if (generator instanceof CustomChunkGenerator custom) + BIOME_SOURCE.set(custom.getDelegate(), source); + + return generator; + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private record SpawnTableKey(int runtimeId, Biome biome, Biome vanillaBiome, MobCategory category) { + } + + private record ReachableStructureCache(IrisDimension dimension, int runtimeId, Set keys) { + } + + private record StructureStepCache(int runtimeId, Registry registry, + List> structures) { + } + + record StudioStructureState( + ServerLevel level, + ChunkMap chunkMap, + ChunkGeneratorStructureState structureState + ) { + } + + @FunctionalInterface + interface StructureStatePublisher { + void publish() throws IllegalAccessException; + } + + private record NativePlacement(StructureStart start, IrisNativeStructureDecision decision) { + } + + private record NativePlacementGroup(String structureId, int featureIndex, int step, + List placements) { + } + + private record NativeLocateCandidate(Holder holder, String key) { + } + + private record IrisNativeLocateSearch(Holder holder, String structureId, + NativeStructureLocatePersistence.Search search) { + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NMSBinding.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NMSBinding.java new file mode 100644 index 000000000..c44e3b841 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NMSBinding.java @@ -0,0 +1,1645 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import net.minecraft.world.level.levelgen.densityfunction.SamplerContext; +import net.minecraft.util.ARGB; +import org.joml.Vector3fc; +import art.arcane.iris.world.history.SavedTerrainChunk; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex; +import art.arcane.iris.platform.bukkit.nms.DatapackStructureScopeResult; +import art.arcane.iris.structure.nativegen.IrisImportedStructureControl; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.platform.bukkit.nms.INMSBinding; +import art.arcane.iris.platform.bukkit.nms.MinecraftVersion; +import art.arcane.iris.platform.bukkit.nms.ServerShutdownBoundary; +import art.arcane.iris.platform.bukkit.nms.container.BiomeColor; +import art.arcane.iris.platform.bukkit.nms.container.Pair; +import art.arcane.iris.platform.bukkit.nms.container.BlockProperty; +import art.arcane.iris.platform.bukkit.nms.datapack.DataVersion; +import art.arcane.iris.generation.cache.AtomicCache; +import art.arcane.iris.generation.chunk.TerrainChunk; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.structure.nativegen.NativeStructureVolume; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureVolumeIndex; +import art.arcane.iris.generation.terrain.IrisDimensionRuntimeContract; +import art.arcane.iris.platform.generation.BukkitChunkGenerator; +import art.arcane.iris.platform.generation.PlatformChunkGenerator; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureFactory; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureGenerationException; +import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.hunk.Hunk; +import art.arcane.iris.generation.chunk.ChunkDataHunkHolder; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformGenerationRegistry; +import art.arcane.iris.generation.block.IrisCustomData; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.math.Vector3d; +import art.arcane.volmlib.util.scheduling.FoliaScheduler; +import art.arcane.volmlib.util.matter.MatterBiomeInject; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.world.storage.region.NBTWorld; +import art.arcane.volmlib.util.nbt.mca.palette.MCABiomeContainer; +import art.arcane.volmlib.util.nbt.mca.palette.MCAChunkBiomeContainer; +import art.arcane.volmlib.util.nbt.mca.palette.MCAGlobalPalette; +import art.arcane.volmlib.util.nbt.mca.palette.MCAIdMap; +import art.arcane.volmlib.util.nbt.mca.palette.MCAIdMapper; +import art.arcane.volmlib.util.nbt.mca.palette.MCAPalette; +import art.arcane.volmlib.util.nbt.mca.palette.MCAPaletteAccess; +import art.arcane.volmlib.util.nbt.mca.palette.MCAPalettedContainer; +import art.arcane.volmlib.util.nbt.mca.palette.MCAWrappedPalettedContainer; +import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import art.arcane.iris.world.task.J; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.IdMapper; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.Registry; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.CollectionTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.EndTag; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.ShortTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.TagParser; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.commands.data.BlockDataAccessor; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ThreadedLevelLightEngine; +import net.minecraft.tags.TagKey; +import net.minecraft.world.attribute.EnvironmentAttributes; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.status.WorldGenContext; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.levelgen.FlatLevelSource; +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.structure.StructureCheck; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure; +import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature; +import net.minecraft.world.level.levelgen.feature.FallenTreeFeature; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.TreeFeature; +import net.minecraft.world.level.levelgen.flat.FlatLayerInfo; +import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Difficulty; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.CraftChunk; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.block.CraftBlockState; +import org.bukkit.craftbukkit.block.CraftBlockStates; +import org.bukkit.craftbukkit.block.data.CraftBlockData; +import org.bukkit.craftbukkit.generator.CraftChunkData; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.craftbukkit.util.CraftMagicNumbers; +import org.bukkit.craftbukkit.util.CraftNamespacedKey; +import org.bukkit.entity.Entity; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.awt.Color; +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +public class NMSBinding implements INMSBinding { + private final KMap baseBiomeCache = new KMap<>(); + private volatile DataVersion dataVersion; + private final BlockData AIR = Material.AIR.createBlockData(); + private final AtomicCache> biomeMapCache = new AtomicCache<>(); + private final NmsWorldLifecycle worldLifecycle = new NmsWorldLifecycle(); + private final AtomicCache> registryCache = new AtomicCache<>(); + private final AtomicCache> globalCache = new AtomicCache<>(); + private final AtomicCache registryAccess = new AtomicCache<>(); + private final AtomicCache byIdRef = new AtomicCache<>(); + + @Override + public PlatformGenerationRegistry generationRegistry() { + return new NmsGenerationRegistry( + this::registry, + "bukkit-generation-registry-v1", + "bukkit-v26_3_R1-generated-registry-json-v1" + ); + } + + private static Object getFor(Class type, Object source) { + Object o = fieldFor(type, source); + + if (o != null) { + return o; + } + + o = invokeFor(type, source); + + if (o != null) { + return o; + } + + throw new IllegalStateException("Iris cannot resolve a " + type.getName() + + " from " + source.getClass().getName() + " on this server version"); + } + + private static Object invokeFor(Class returns, Object in) { + for (Method i : in.getClass().getMethods()) { + if (i.getReturnType().equals(returns)) { + i.setAccessible(true); + try { + IrisLogging.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()"); + return i.invoke(in); + } catch (ReflectiveOperationException | RuntimeException e) { + throw new IllegalStateException("Iris failed to invoke " + in.getClass().getName() + "." + + i.getName() + "() for " + returns.getName(), e); + } + } + } + + return null; + } + + private static Object fieldFor(Class returns, Object in) { + for (Class sourceType = in.getClass(); sourceType != null; sourceType = sourceType.getSuperclass()) { + Object o = fieldForClass(returns, sourceType, in); + + if (o != null) { + return o; + } + } + + return null; + } + + @SuppressWarnings("unchecked") + private static T fieldForClass(Class returnType, Class sourceType, Object in) { + for (Field i : sourceType.getDeclaredFields()) { + if (i.getType().equals(returnType)) { + i.setAccessible(true); + try { + IrisLogging.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName()); + return (T) i.get(in); + } catch (IllegalAccessException | RuntimeException e) { + throw new IllegalStateException("Iris failed to read " + sourceType.getName() + "." + + i.getName() + " for " + returnType.getName(), e); + } + } + } + return null; + } + + private static Class getClassType(Class type, int ordinal) { + return type.getDeclaredClasses()[ordinal]; + } + + @Override + public boolean hasTile(Material material) { + return !CraftBlockState.class.equals(CraftBlockStates.getBlockStateType(material)); + } + + @Override + public String getEntitySpawnCategory(String key) { + Identifier identifier = Identifier.parse(key); + if (!BuiltInRegistries.ENTITY_TYPE.containsKey(identifier)) { + throw new IllegalArgumentException("Unknown native entity type: " + key); + } + EntityType type = BuiltInRegistries.ENTITY_TYPE.getValue(identifier); + return type.getCategory().getSerializedName().toLowerCase(Locale.ROOT); + } + + @Override + public boolean hasTile(Location l) { + return ((CraftWorld) l.getWorld()).getHandle().getBlockEntity(new BlockPos(l.getBlockX(), l.getBlockY(), l.getBlockZ())) != null; + } + + @Override + @SuppressWarnings("unchecked") + public KMap serializeTile(Location location) { + BlockEntity e = ((CraftWorld) location.getWorld()).getHandle().getBlockEntity(new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ())); + + if (e == null) { + return null; + } + + net.minecraft.nbt.CompoundTag tag = e.saveWithoutMetadata(registry()); + return (KMap) convertFromTag(tag, 0, 64); + } + + @Contract(value = "null, _, _ -> null", pure = true) + private Object convertFromTag(Tag tag, int depth, int maxDepth) { + if (tag == null || depth > maxDepth) return null; + return switch (tag) { + case CollectionTag collection -> { + KList list = new KList<>(); + + for (Object i : collection) { + if (i instanceof Tag t) + list.add(convertFromTag(t, depth + 1, maxDepth)); + else list.add(i); + } + yield list; + } + case net.minecraft.nbt.CompoundTag compound -> { + KMap map = new KMap<>(); + + for (String key : compound.keySet()) { + Tag child = compound.get(key); + if (child == null) continue; + Object value = convertFromTag(child, depth + 1, maxDepth); + if (value == null) continue; + map.put(key, value); + } + yield map; + } + case NumericTag numeric -> numeric.box(); + default -> tag.asString().orElse(null); + }; + } + + @Override + public void deserializeTile(KMap map, Location pos) { + if (map == null || pos == null || pos.getWorld() == null) { + return; + } + + Tag converted = convertToTag(map, 0, 64); + if (!(converted instanceof net.minecraft.nbt.CompoundTag tag)) { + return; + } + + ServerLevel level = ((CraftWorld) pos.getWorld()).getHandle(); + BlockPos blockPos = new BlockPos(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ()); + int chunkX = pos.getBlockX() >> 4; + int chunkZ = pos.getBlockZ() >> 4; + if (J.isOwnedByCurrentRegion(pos.getWorld(), chunkX, chunkZ)) { + merge(level, blockPos, tag); + return; + } + if (!J.runAt(pos, () -> merge(level, blockPos, tag))) { + IrisLogging.warn("[NMS] Failed to schedule tile deserialize at " + blockPos + " in world " + pos.getWorld().getName()); + } + } + + private void merge(ServerLevel level, BlockPos blockPos, net.minecraft.nbt.CompoundTag tag) { + if (level == null || blockPos == null || tag == null) { + return; + } + + try { + BlockEntity blockEntity = level.getBlockEntity(blockPos); + if (blockEntity == null) { + IrisLogging.warn("[NMS] BlockEntity not found at " + blockPos); + BlockState state = level.getBlockState(blockPos); + if (!state.hasBlockEntity()) { + return; + } + + blockEntity = ((EntityBlock) state.getBlock()) + .newBlockEntity(blockPos, state); + } + + BlockDataAccessor accessor = new BlockDataAccessor(blockEntity, blockPos); + accessor.setData(accessor.getData().merge(tag)); + } catch (Throwable e) { + IrisLogging.warn("[NMS] Failed to merge tile data at " + blockPos + ": " + e.getMessage()); + IrisLogging.reportError(e); + } + } + + private Tag convertToTag(Object object, int depth, int maxDepth) { + if (object == null || depth > maxDepth) return EndTag.INSTANCE; + return switch (object) { + case Map map -> { + net.minecraft.nbt.CompoundTag tag = new net.minecraft.nbt.CompoundTag(); + for (Map.Entry i : map.entrySet()) { + tag.put(i.getKey().toString(), convertToTag(i.getValue(), depth + 1, maxDepth)); + } + yield tag; + } + case List list -> { + ListTag tag = new ListTag(); + for (Object i : list) { + tag.add(convertToTag(i, depth + 1, maxDepth)); + } + yield tag; + } + case Byte number -> ByteTag.valueOf(number); + case Short number -> ShortTag.valueOf(number); + case Integer number -> IntTag.valueOf(number); + case Long number -> LongTag.valueOf(number); + case Float number -> FloatTag.valueOf(number); + case Double number -> DoubleTag.valueOf(number); + case String string -> StringTag.valueOf(string); + default -> EndTag.INSTANCE; + }; + } + + @Override + public CompoundTag serializeEntity(Entity location) { + return null;// TODO: + } + + @Override + public Entity deserializeEntity(CompoundTag s, Location newPosition) { + return null;// TODO: + } + + @Override + public boolean supportsCustomHeight() { + return true; + } + + private RegistryAccess registry() { + RegistryAccess access = registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())); + + if (access == null) { + throw new IllegalStateException("Iris cannot resolve the Minecraft registry access on this server version"); + } + + return access; + } + + private Registry getCustomBiomeRegistry() { + return registry().lookup(Registries.BIOME).orElseThrow(() -> new IllegalStateException( + "Iris cannot resolve the Minecraft biome registry on this server version")); + } + + private Registry getBlockRegistry() { + return registry().lookup(Registries.BLOCK).orElse(null); + } + + @Override + public Object getBiomeBaseFromId(int id) { + Object raw = getCustomBiomeRegistry().get(id); + if (raw instanceof java.util.Optional opt) { + raw = opt.orElse(null); + } + if (raw instanceof Holder holder) { + raw = holder.value(); + } + return raw; + } + + @Override + public int getMinHeight(World world) { + return world.getMinHeight(); + } + + @Override + public boolean supportsCustomBiomes() { + return true; + } + + @Override + public boolean supportsIrisWorldGeneration() { + return true; + } + + @Override + public int getTrueBiomeBaseId(Object biomeBase) { + return getCustomBiomeRegistry().getId(((Holder) biomeBase).value()); + } + + @Override + public Object getTrueBiomeBase(Location location) { + return ((CraftWorld) location.getWorld()).getHandle().getBiome(new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ())); + } + + @Override + public String getTrueBiomeBaseKey(Location location) { + return getKeyForBiomeBase(getTrueBiomeBase(location)); + } + + @Override + public Object getCustomBiomeBaseFor(String mckey) { + return getCustomBiomeRegistry().getValue(net.minecraft.resources.Identifier.parse(mckey)); + } + + @Override + public Object getCustomBiomeBaseHolderFor(String mckey) { + return getCustomBiomeRegistry().get(getTrueBiomeBaseId(getCustomBiomeRegistry().get(net.minecraft.resources.Identifier.parse(mckey)))).orElse(null); + } + + public int getBiomeBaseIdForKey(String key) { + return getCustomBiomeRegistry().getId(getCustomBiomeRegistry().get(net.minecraft.resources.Identifier.parse(key)).map(Holder::value).orElse(null)); + } + + @Override + public String getKeyForBiomeBase(Object biomeBase) { + net.minecraft.world.level.biome.Biome biome; + if (biomeBase instanceof Holder holder) { + Object value = holder.value(); + if (!(value instanceof net.minecraft.world.level.biome.Biome held)) { + throw new IllegalArgumentException("Iris cannot read a biome key from holder value " + + (value == null ? "null" : value.getClass().getName())); + } + biome = held; + } else if (biomeBase instanceof net.minecraft.world.level.biome.Biome direct) { + biome = direct; + } else { + throw new IllegalArgumentException("Iris cannot read a biome key from " + + (biomeBase == null ? "null" : biomeBase.getClass().getName())); + } + + Identifier key = getCustomBiomeRegistry().getKey(biome); + if (key == null) { + throw new IllegalStateException("Iris found no registry key for biome " + biome); + } + return key.getPath(); // something, not something:something + } + + @Override + public Object getBiomeBase(World world, Biome biome) { + return biomeToBiomeBase(((CraftWorld) world).getHandle() + .registryAccess().lookup(Registries.BIOME).orElse(null), biome); + } + + @Override + public Object getBiomeBase(Object registry, Biome biome) { + Object v = baseBiomeCache.get(biome); + + if (v != null) { + return v; + } + //noinspection unchecked + v = biomeToBiomeBase((Registry) registry, biome); + if (v == null) { + // Ok so there is this new biome name called "CUSTOM" in Paper's new releases. + // But, this does NOT exist within CraftBukkit which makes it return an error. + // So, we will just return the ID that the plains biome returns instead. + //noinspection unchecked + return biomeToBiomeBase((Registry) registry, Biome.PLAINS); + } + baseBiomeCache.put(biome, v); + return v; + } + + @Override + public KList getBiomes() { + KList biomes = new KList<>(); + for (Biome biome : org.bukkit.Registry.BIOME) { + biomes.add(biome); + } + return biomes; + } + + @Override + public KList getStructureKeys() { + KList keys = new KList<>(); + try { + registry().lookupOrThrow(Registries.STRUCTURE).keySet().forEach(k -> keys.add(k.toString())); + } catch (RuntimeException e) { + throw new IllegalStateException("Iris failed to read registered structure keys from the Minecraft registry", e); + } + return keys; + } + + @Override + public KList getJigsawStructureKeys() { + KList keys = new KList<>(); + try { + Registry structures = registry().lookupOrThrow(Registries.STRUCTURE); + for (Map.Entry, Structure> entry : structures.entrySet()) { + if (entry.getValue() instanceof JigsawStructure) { + keys.add(entry.getKey().identifier().toString()); + } + } + } catch (RuntimeException e) { + throw new IllegalStateException( + "Iris failed to read registered jigsaw structure keys from the Minecraft registry", e); + } + return keys; + } + + @Override + public KList getTemplatePoolKeys() { + KList keys = new KList<>(); + try { + registry().lookupOrThrow(Registries.TEMPLATE_POOL).keySet() + .forEach(key -> keys.add(key.toString())); + } catch (RuntimeException e) { + throw new IllegalStateException( + "Iris failed to read registered template pool keys from the Minecraft registry", e); + } + return keys; + } + + @Override + public JigsawSourceMetadata getJigsawSourceMetadata(String structureKey) { + try { + Identifier identifier = Identifier.tryParse(structureKey); + if (identifier == null) { + throw new IllegalArgumentException("Invalid registered structure key: " + structureKey); + } + Registry structures = registry().lookupOrThrow(Registries.STRUCTURE); + Structure structure = structures.getValue(identifier); + if (structure == null) { + throw new IllegalArgumentException("Registered structure does not exist: " + structureKey); + } + if (!(structure instanceof JigsawStructure jigsaw)) { + throw new IllegalArgumentException("Registered structure is not a jigsaw: " + structureKey); + } + MinecraftServer server = ((CraftServer) Bukkit.getServer()).getHandle().getServer(); + return NativeStructureFactory.sourceMetadata( + registry(), server.getStructureTemplateManager(), jigsaw); + } catch (RuntimeException error) { + throw new IllegalStateException("Iris failed to resolve live jigsaw metadata for registered structure '" + + structureKey + "'", error); + } + } + + @Override + public int getTemplatePoolHorizontalSpan(String templatePoolKey) { + try { + MinecraftServer server = ((CraftServer) Bukkit.getServer()).getHandle().getServer(); + return NativeStructureFactory.templatePoolHorizontalSpan( + registry(), server.getStructureTemplateManager(), templatePoolKey); + } catch (RuntimeException error) { + throw new IllegalStateException("Iris failed to resolve the live horizontal span for registered " + + "template pool '" + templatePoolKey + "'", error); + } + } + + @Override + public int getJigsawStartPoolHorizontalSpan(String structureKey, String templatePoolKey) { + try { + Identifier identifier = Identifier.tryParse(structureKey); + if (identifier == null) { + throw new IllegalArgumentException("Invalid registered structure key: " + structureKey); + } + Structure structure = registry().lookupOrThrow(Registries.STRUCTURE).getValue(identifier); + if (!(structure instanceof JigsawStructure jigsaw)) { + throw new IllegalArgumentException("Registered structure is not a jigsaw: " + structureKey); + } + MinecraftServer server = ((CraftServer) Bukkit.getServer()).getHandle().getServer(); + return NativeStructureFactory.jigsawStartPoolHorizontalSpan( + registry(), server.getStructureTemplateManager(), jigsaw, templatePoolKey); + } catch (RuntimeException error) { + throw new IllegalStateException("Iris failed to resolve the effective start-pool span for registered " + + "jigsaw structure '" + structureKey + "' and pool '" + templatePoolKey + "'", error); + } + } + + @Override + public KList getStructureSetKeys() { + KList keys = new KList<>(); + try { + registry().lookupOrThrow(Registries.STRUCTURE_SET).keySet().forEach(k -> keys.add(k.toString())); + } catch (RuntimeException e) { + throw new IllegalStateException("Iris failed to read registered structure-set keys from the Minecraft registry", e); + } + return keys; + } + + @Override + public KList getReachableStructureKeys(World world) { + KList keys = new KList<>(); + try { + ServerLevel level = ((CraftWorld) world).getHandle(); + BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource(); + keys.addAll(VanillaStructureBiomes.reachableStructureKeys(level, source)); + } catch (RuntimeException e) { + throw new IllegalStateException("Iris failed to resolve reachable structures for Bukkit world '" + + (world == null ? "" : world.getName()) + "'", e); + } + return keys; + } + + @Override + public KList getStructureBiomeKeys(String structureKey) { + KList keys = new KList<>(); + try { + RegistryAccess access = registry(); + if (access == null) { + throw new IllegalStateException("Minecraft registry access is unavailable"); + } + keys.addAll(VanillaStructureBiomes.structureBiomeKeys(access, structureKey)); + } catch (RuntimeException e) { + throw new IllegalStateException("Iris failed to resolve biome keys for registered structure '" + + structureKey + "'", e); + } + return keys; + } + + @Override + public KList getPossibleBiomeKeys(World world) { + KList keys = new KList<>(); + try { + ServerLevel level = ((CraftWorld) world).getHandle(); + BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource(); + keys.addAll(VanillaStructureBiomes.possibleBiomeKeys(source)); + } catch (RuntimeException e) { + throw new IllegalStateException("Iris failed to resolve possible structure biome keys for Bukkit world '" + + (world == null ? "" : world.getName()) + "'", e); + } + return keys; + } + + @Override + public KList getObjectFeatureKeys() { + KList keys = new KList<>(); + try { + Registry reg = registry().lookupOrThrow(Registries.FEATURE); + for (Identifier id : reg.keySet()) { + Feature cf = reg.getValue(id); + if (cf == null) { + continue; + } + String group = classifyFeature(cf); + if (group == null) { + continue; + } + keys.add(group + "|" + id); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + return keys; + } + + private static String classifyFeature(Feature feature) { + if (feature instanceof TreeFeature) { + return "trees"; + } + if (feature instanceof FallenTreeFeature) { + return "fallen_trees"; + } + if (feature instanceof AbstractHugeMushroomFeature) { + return "mushrooms"; + } + return null; + } + + @Override + public boolean placeFeature(World world, int x, int y, int z, String featureKey, long seed) { + try { + ServerLevel level = ((CraftWorld) world).getHandle(); + net.minecraft.world.level.chunk.ChunkGenerator generator = level.getChunkSource().getGenerator(); + Registry reg = registry().lookupOrThrow(Registries.FEATURE); + Feature cf = reg.getValue(Identifier.parse(featureKey)); + if (cf == null) { + return false; + } + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(seed)); + return cf.place(level, generator, random, new BlockPos(x, y, z)); + } catch (Throwable e) { + IrisLogging.reportError(e); + return false; + } + } + + @Override + public int[] placeStructure(World world, int chunkX, int chunkZ, String structureKey, long seed, int maxSpan) { + try { + ServerLevel level = ((CraftWorld) world).getHandle(); + net.minecraft.world.level.chunk.ChunkGenerator generator = level.getChunkSource().getGenerator(); + Registry reg = registry().lookupOrThrow(Registries.STRUCTURE); + net.minecraft.world.level.levelgen.structure.Structure structure = reg.getValue(Identifier.parse(structureKey)); + if (structure == null) { + return null; + } + net.minecraft.core.Holder holder = reg.wrapAsHolder(structure); + net.minecraft.world.level.levelgen.RandomState randomState = level.getChunkSource().randomState(); + net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager templateManager = level.getStructureTemplateManager(); + net.minecraft.world.level.StructureManager structureManager = level.structureManager(); + net.minecraft.world.level.biome.BiomeSource biomeSource = generator.getBiomeSource(); + net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ); + + net.minecraft.world.level.levelgen.structure.StructureStart start = structure.generate( + holder, + level.dimension(), + level.registryAccess(), + generator, + biomeSource, + randomState.createClimateSampler(SamplerContext.EMPTY_UNCACHED), + randomState, + templateManager, + seed, + chunkPos, + 0, + level, + biome -> true); + + if (start == null || !start.isValid()) { + return null; + } + + BoundingBox box = start.getBoundingBox(); + int spanX = box.maxX() - box.minX() + 1; + int spanY = box.maxY() - box.minY() + 1; + int spanZ = box.maxZ() - box.minZ() + 1; + if (maxSpan > 0 && (spanX > maxSpan || spanZ > maxSpan || spanY > maxSpan)) { + return null; + } + + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(seed)); + int minCX = box.minX() >> 4; + int maxCX = box.maxX() >> 4; + int minCZ = box.minZ() >> 4; + int maxCZ = box.maxZ() >> 4; + for (int cx = minCX; cx <= maxCX; cx++) { + for (int cz = minCZ; cz <= maxCZ; cz++) { + level.getChunk(cx, cz); + net.minecraft.world.level.ChunkPos cp = new net.minecraft.world.level.ChunkPos(cx, cz); + BoundingBox chunkBox = new BoundingBox( + cp.getMinBlockX(), box.minY(), cp.getMinBlockZ(), + cp.getMaxBlockX(), box.maxY(), cp.getMaxBlockZ()); + start.placeInChunk(level, structureManager, generator, random, chunkBox, cp); + } + } + return new int[]{box.minX(), box.minY(), box.minZ(), box.maxX(), box.maxY(), box.maxZ()}; + } catch (RuntimeException e) { + throw NativeStructureGenerationException.failure( + "capture placement", structureKey, chunkX, chunkZ, e); + } + } + + @Override + public boolean supportsStructureCapture() { + return true; + } + + @Override + public KList nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) { + return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ); + } + + @Override + public void invalidateNativeStructureVolumeIndex(Engine engine) { + NativeStructureVolumeIndex.invalidate(engine); + } + + @Override + public int getBiomeId(Biome biome) { + for (World i : Bukkit.getWorlds()) { + if (i.getEnvironment().equals(World.Environment.NORMAL)) { + Registry registry = ((CraftWorld) i).getHandle().registryAccess().lookup(Registries.BIOME).orElse(null); + if (registry == null) { + continue; + } + Object baseRaw = getBiomeBase(registry, biome); + net.minecraft.world.level.biome.Biome base; + if (baseRaw instanceof Holder holder) { + Object value = holder.value(); + if (!(value instanceof net.minecraft.world.level.biome.Biome resolved)) { + continue; + } + base = resolved; + } else if (baseRaw instanceof net.minecraft.world.level.biome.Biome resolved) { + base = resolved; + } else { + continue; + } + return registry.getId(base); + } + } + + List biomes = new ArrayList<>(); + for (Biome entry : org.bukkit.Registry.BIOME) { + biomes.add(entry); + } + int index = biomes.indexOf(biome); + return Math.max(index, 0); + } + + private MCAIdMap getBiomeMapping() { + return biomeMapCache.aquire(() -> new MCAIdMap<>() { + @NotNull + @Override + public Iterator iterator() { + return getCustomBiomeRegistry().iterator(); + } + + @Override + public int getId(net.minecraft.world.level.biome.Biome paramT) { + return getCustomBiomeRegistry().getId(paramT); + } + + @Override + public net.minecraft.world.level.biome.Biome byId(int paramInt) { + return (net.minecraft.world.level.biome.Biome) getBiomeBaseFromId(paramInt); + } + }); + } + + @NotNull + private MCABiomeContainer getBiomeContainerInterface(MCAIdMap biomeMapping, MCAChunkBiomeContainer base) { + return new MCABiomeContainer() { + @Override + public int[] getData() { + return base.writeBiomes(); + } + + @Override + public void setBiome(int x, int y, int z, int id) { + base.setBiome(x, y, z, biomeMapping.byId(id)); + } + + @Override + public int getBiome(int x, int y, int z) { + return biomeMapping.getId(base.getBiome(x, y, z)); + } + }; + } + + @Override + public MCABiomeContainer newBiomeContainer(int min, int max) { + MCAChunkBiomeContainer base = new MCAChunkBiomeContainer<>(getBiomeMapping(), min, max); + return getBiomeContainerInterface(getBiomeMapping(), base); + } + + @Override + public MCABiomeContainer newBiomeContainer(int min, int max, int[] data) { + MCAChunkBiomeContainer base = new MCAChunkBiomeContainer<>(getBiomeMapping(), min, max, data); + return getBiomeContainerInterface(getBiomeMapping(), base); + } + + @Override + public int countCustomBiomes() { + AtomicInteger a = new AtomicInteger(0); + + getCustomBiomeRegistry().keySet().forEach((i) -> { + if (i.getNamespace().equals("minecraft")) { + return; + } + + a.incrementAndGet(); + IrisLogging.debug("Custom Biome: " + i); + }); + + return a.get(); + } + + public boolean supportsDataPacks() { + return true; + } + + public void setBiomes(int cx, int cz, World world, Hunk biomes) { + LevelChunk c = ((CraftWorld) world).getHandle().getChunk(cx, cz); + biomes.iterateSync((x, y, z, b) -> c.setNoiseBiome(x, y, z, (Holder) b)); + c.markUnsaved(); + } + + @Override + public MCAPaletteAccess createPalette() { + MCAIdMapper registry = registryCache.aquireNastyPrint(() -> { + Field cf = IdMapper.class.getDeclaredField("tToId"); + Field df = IdMapper.class.getDeclaredField("idToT"); + Field bf = IdMapper.class.getDeclaredField("nextId"); + cf.setAccessible(true); + df.setAccessible(true); + bf.setAccessible(true); + IdMapper blockData = Block.BLOCK_STATE_REGISTRY; + int b = bf.getInt(blockData); + Object2IntMap c = (Object2IntMap) cf.get(blockData); + List d = (List) df.get(blockData); + return new MCAIdMapper(c, d, b); + }); + if (registry == null) { + throw new IllegalStateException("Iris cannot mirror the Minecraft block state id map on this server version"); + } + MCAPalette global = globalCache.aquireNastyPrint(() -> new MCAGlobalPalette<>(registry, ((CraftBlockData) AIR).getState())); + if (global == null) { + throw new IllegalStateException("Iris cannot build the global block state palette on this server version"); + } + java.util.Map, BlockState> innerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64); + java.util.Map outerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64); + MCAPalettedContainer container = new MCAPalettedContainer<>(global, registry, + i -> innerDecodeCache.computeIfAbsent(i, t -> ((CraftBlockData) NBTWorld.getBlockData(t)).getState()), + i -> NBTWorld.getCompound(CraftBlockData.createData(i)), + ((CraftBlockData) AIR).getState()); + return new MCAWrappedPalettedContainer<>(container, + i -> NBTWorld.getCompound(CraftBlockData.createData(i)), + i -> outerDecodeCache.computeIfAbsent(i, t -> ((CraftBlockData) NBTWorld.getBlockData(t)).getState())); + } + + @Override + public void injectBiomesFromMantle(Chunk e, Mantle mantle) { + ChunkAccess chunk = ((CraftChunk) e).getHandle(ChunkStatus.FULL); + AtomicInteger c = new AtomicInteger(); + AtomicInteger r = new AtomicInteger(); + mantle.iterateChunk(e.getX(), e.getZ(), MatterBiomeInject.class, (x, y, z, b) -> { + if (b != null) { + if (b.isCustom()) { + chunk.setNoiseBiome(x, y, z, getCustomBiomeRegistry().get(b.getBiomeId()).get()); + c.getAndIncrement(); + } else { + chunk.setNoiseBiome(x, y, z, getCustomBiomeRegistry() + .get(net.minecraft.resources.Identifier.parse(b.getBiomeKey())) + .orElseGet(() -> getCustomBiomeRegistry() + .get(net.minecraft.resources.Identifier.parse("minecraft:plains")) + .orElseThrow())); + r.getAndIncrement(); + } + } + }); + } + + @Override + public boolean applyChunkBlocks(Chunk bukkitChunk, TerrainChunk data) { + if (!(data.getChunkData() instanceof CraftChunkData chunkData)) { + return false; + } + + try { + ServerLevel level = ((CraftWorld) bukkitChunk.getWorld()).getHandle(); + LevelChunk chunk = level.getChunk(bukkitChunk.getX(), bukkitChunk.getZ()); + ChunkAccess source = chunkData.getHandle(); + removeBlockEntities(chunk); + + int minY = level.getMinY(); + int baseX = chunk.getPos().getMinBlockX(); + int baseZ = chunk.getPos().getMinBlockZ(); + for (int i = 0; i < chunk.getSectionsCount(); i++) { + LevelChunkSection target = chunk.getSection(i); + LevelChunkSection from = source.getSection(i); + if (from.hasOnlyAir() && target.hasOnlyAir()) { + continue; + } + + int sectionBaseY = minY + (i << 4); + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + BlockState state = from.getBlockState(x, y, z); + target.setBlockState(x, y, z, state, false); + if (state.hasBlockEntity() && state.getBlock() instanceof EntityBlock entityBlock) { + BlockPos pos = new BlockPos(baseX + x, sectionBaseY + y, baseZ + z); + BlockEntity entity = entityBlock.newBlockEntity(pos, state); + if (entity != null) { + chunk.setBlockEntity(entity); + } + } + } + } + } + } + + finishChunkRewrite(level, chunk); + return true; + } catch (Throwable e) { + IrisLogging.reportError(e); + return false; + } + } + + @Override + public boolean applyChunkDataBlocks(ChunkGenerator.ChunkData chunkData, Hunk data) { + if (!(chunkData instanceof CraftChunkData craftChunkData)) { + return false; + } + + try { + ChunkAccess access = craftChunkData.getHandle(); + int minY = craftChunkData.getMinHeight(); + int height = craftChunkData.getMaxHeight() - minY; + int accessMinY = access.getMinY(); + int accessMaxY = accessMinY + access.getHeight(); + int yStart = Math.max(0, accessMinY - minY); + int yEnd = Math.min(height, accessMaxY - minY); + int baseX = access.getPos().getMinBlockX(); + int baseZ = access.getPos().getMinBlockZ(); + ChunkDataHunkHolder holder = data instanceof ChunkDataHunkHolder chunkDataHolder ? chunkDataHolder : null; + for (int z = 0; z < 16; z++) { + for (int y = yStart; y < yEnd; y++) { + int blockY = y + minY; + int sectionIndex = (blockY - accessMinY) >> 4; + LevelChunkSection section = access.getSection(sectionIndex); + int sectionY = blockY & 15; + for (int x = 0; x < 16; x++) { + PlatformBlockState platformState = holder == null ? data.getRaw(x, y, z) : holder.getStoredRaw(x, y, z); + if (platformState == null) { + continue; + } + + BlockData blockData = (BlockData) platformState.nativeHandle(); + if (blockData instanceof IrisCustomData customData) { + blockData = customData.getBase(); + } + if (!(blockData instanceof CraftBlockData craftBlockData)) { + return false; + } + + BlockState state = craftBlockData.getState(); + BlockState oldState = section.setBlockState(x, sectionY, z, state, false); + if (state.hasBlockEntity()) { + BlockPos pos = new BlockPos(baseX + x, blockY, baseZ + z); + BlockEntity entity = ((EntityBlock) state.getBlock()).newBlockEntity(pos, state); + if (entity == null) { + access.removeBlockEntity(pos); + } else { + access.setBlockEntity(entity); + } + } else if (oldState != null && oldState.hasBlockEntity()) { + access.removeBlockEntity(new BlockPos(baseX + x, blockY, baseZ + z)); + } + } + } + } + + return true; + } catch (Throwable e) { + IrisLogging.reportError(e); + return false; + } + } + + @Override + public boolean forceEvictChunk(World world, int chunkX, int chunkZ) { + try { + if (!world.isChunkLoaded(chunkX, chunkZ)) { + return true; + } + return world.unloadChunk(chunkX, chunkZ, true); + } catch (Throwable e) { + IrisLogging.reportError(e); + return false; + } + } + + @Override + public CompletableFuture flushSavedTerrainCapture(World world) { + return NmsSavedTerrainCapture.flush(world); + } + + @Override + public CompletableFuture captureSavedTerrainChunk(World world, int chunkX, int chunkZ, + int minimumY, int height) { + return NmsSavedTerrainCapture.capture(world, chunkX, chunkZ, minimumY, height); + } + + @Override + public boolean saveAndUnloadChunk(World world, int x, int z) { + try { + ServerLevel level = ((CraftWorld) world).getHandle(); + ChunkHolderManager chm = level.moonrise$getChunkTaskScheduler().chunkHolderManager; + chm.processTicketUpdates(); + NewChunkHolder holder = chm.getChunkHolder(x, z); + if (holder != null) { + holder.save(false); + } + chm.processUnloads(); + return true; + } catch (Throwable e) { + IrisLogging.reportError(e); + return false; + } + } + + @Override + public boolean pollChunkTask(World world) { + if (FoliaScheduler.isRegionizedRuntime(Bukkit.getServer())) { + return false; + } + ServerLevel level = ((CraftWorld) world).getHandle(); + if (Thread.currentThread() != level.getServer().getRunningThread()) { + throw new IllegalStateException("Native chunk tasks require the server lifecycle thread"); + } + return level.getChunkSource().pollTask(); + } + + @Override + public void flushChunkIO(World world) { + ServerLevel level = ((CraftWorld) world).getHandle(); + MoonriseRegionFileIO.flush(level); + } + + @Override + public void reconcileNativeStructurePois(Chunk chunk) { + NativeStructurePoiUpdates.reconcile((LevelChunk) ((CraftChunk) chunk).getHandle(ChunkStatus.FULL)); + } + + @Override + public boolean clearChunkBlocks(Chunk bukkitChunk) { + try { + ServerLevel level = ((CraftWorld) bukkitChunk.getWorld()).getHandle(); + LevelChunk chunk = level.getChunk(bukkitChunk.getX(), bukkitChunk.getZ()); + removeBlockEntities(chunk); + + BlockState air = ((CraftBlockData) AIR).getState(); + for (int i = 0; i < chunk.getSectionsCount(); i++) { + LevelChunkSection section = chunk.getSection(i); + if (section.hasOnlyAir()) { + continue; + } + + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + section.setBlockState(x, y, z, air, false); + } + } + } + } + + finishChunkRewrite(level, chunk); + return true; + } catch (Throwable e) { + IrisLogging.reportError(e); + return false; + } + } + + private void removeBlockEntities(LevelChunk chunk) { + for (BlockPos pos : new ArrayList<>(chunk.getBlockEntities().keySet())) { + chunk.removeBlockEntity(pos); + } + } + + private void finishChunkRewrite(ServerLevel level, LevelChunk chunk) { + Heightmap.primeHeightmaps(chunk, ChunkStatus.FULL.heightmapsAfter()); + chunk.markUnsaved(); + ThreadedLevelLightEngine lightEngine = (ThreadedLevelLightEngine) level.getChunkSource().getLightEngine(); + lightEngine.starlight$serverRelightChunks(List.of(chunk.getPos()), p -> { + }, c -> { + }); + } + + public ItemStack applyCustomNbt(ItemStack itemStack, KMap customNbt) throws IllegalArgumentException { + if (customNbt != null && !customNbt.isEmpty()) { + net.minecraft.world.item.ItemStack s = CraftItemStack.asNMSCopy(itemStack); + + try { + net.minecraft.nbt.CompoundTag tag = TagParser.parseCompoundFully((new JSONObject(customNbt)).toString()); + tag.merge(s.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag()); + s.set(DataComponents.CUSTOM_DATA, CustomData.of(tag)); + } catch (CommandSyntaxException var5) { + throw new IllegalArgumentException(var5); + } + + return CraftItemStack.asBukkitCopy(s); + } else { + return itemStack; + } + } + + public void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException { + ServerLevel level = ((CraftWorld) world).getHandle(); + validateDimensionContract(engine, world, level); + + ChunkMap chunkMap = level.getChunkSource().chunkMap; + Field worldGenContextField = getField(chunkMap.getClass(), WorldGenContext.class); + worldGenContextField.setAccessible(true); + WorldGenContext worldGenContext = (WorldGenContext) worldGenContextField.get(chunkMap); + + IrisChunkGenerator irisGenerator = new IrisChunkGenerator(worldGenContext.generator(), seed, engine, world); + WorldGenContext newContext = new WorldGenContext( + worldGenContext.level(), irisGenerator, + worldGenContext.structureManager(), worldGenContext.lightEngine(), worldGenContext.mainThreadExecutor(), worldGenContext.unsavedListener()); + + worldGenContextField.set(chunkMap, newContext); + net.minecraft.world.level.chunk.ChunkGenerator activeGenerator = level.getChunkSource().getGenerator(); + if (activeGenerator != irisGenerator) { + throw new IllegalStateException("Iris generator injection did not become the active Paper chunk generator; active=" + + activeGenerator.getClass().getName()); + } + retargetStructureCheck(level, irisGenerator); + } + + @Override + public DatapackStructureScopeResult scopeDatapackStructures( + World world, + DatapackStructureScopeIndex scopeIndex, + Set declaredSources, + IrisImportedStructureControl importedStructures + ) throws NoSuchFieldException, IllegalAccessException { + ServerLevel level = ((CraftWorld) world).getHandle(); + ChunkMap chunkMap = level.getChunkSource().chunkMap; + ChunkGeneratorStructureState currentState = level.getChunkSource().getGeneratorState(); + net.minecraft.world.level.chunk.ChunkGenerator generator = level.getChunkSource().getGenerator(); + ChunkGeneratorStructureState scopedState = createStructureState(level, generator, currentState); + Registry structureSetRegistry = + level.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET); + DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter( + scopedState.possibleStructureSets(), + scopeIndex, + declaredSources, + importedStructures, + DatapackStructureStateFilter.keyIndex( + structureSetRegistry.listElements().toList())); + + Field possibleSetsField = getField(scopedState.getClass(), List.class); + possibleSetsField.setAccessible(true); + possibleSetsField.set(scopedState, selection.structureSets()); + + Field stateField = getField(chunkMap.getClass(), ChunkGeneratorStructureState.class); + stateField.setAccessible(true); + BukkitChunkGenerator platformGenerator = world.getGenerator() instanceof BukkitChunkGenerator bukkitGenerator + ? bukkitGenerator + : null; + boolean studioBootstrap = platformGenerator != null + && platformGenerator.isStudioEntryBootstrapActive(); + boolean authoringStudio = platformGenerator != null + && platformGenerator.isAuthoringStudio(); + if (authoringStudio) { + requireIrisGenerator(generator); + if (currentState.possibleStructureSets().isEmpty()) { + currentState.ensureStructuresGenerated(); + } else { + ChunkGeneratorStructureState bootstrapState = createStructureState(level, generator, currentState); + Field bootstrapSetsField = getField(bootstrapState.getClass(), List.class); + bootstrapSetsField.setAccessible(true); + bootstrapSetsField.set(bootstrapState, List.of()); + bootstrapState.ensureStructuresGenerated(); + stateField.set(chunkMap, bootstrapState); + } + } else if (studioBootstrap) { + IrisChunkGenerator irisGenerator = requireIrisGenerator(generator); + irisGenerator.retainStudioStructureState(level, chunkMap, scopedState); + try { + stateField.set(chunkMap, scopedState); + } catch (IllegalAccessException | RuntimeException | Error failure) { + irisGenerator.abandonStudioStructureState(); + throw failure; + } + } else { + initializeAndPublishStructureState( + generator, + scopedState, + () -> stateField.set(chunkMap, scopedState)); + } + return new DatapackStructureScopeResult( + selection.retainedManagedSets(), + selection.excludedManagedSets()); + } + + @Override + public CompletableFuture completeStudioStructureBootstrap(World world) throws NoSuchFieldException, IllegalAccessException { + ServerLevel level = ((CraftWorld) world).getHandle(); + ChunkMap chunkMap = level.getChunkSource().chunkMap; + IrisChunkGenerator generator = requireIrisGenerator(level.getChunkSource().getGenerator()); + IrisChunkGenerator.StudioStructureState retained = + generator.retainedStudioStructureState(level, chunkMap); + if (retained == null) { + return CompletableFuture.completedFuture(null); + } + return generator.activateStudioStructureState(retained); + } + + @Override + public void abandonStudioStructureBootstrap(World world) { + ServerLevel level = ((CraftWorld) world).getHandle(); + net.minecraft.world.level.chunk.ChunkGenerator generator = level.getChunkSource().getGenerator(); + if (generator instanceof IrisChunkGenerator irisGenerator) { + irisGenerator.abandonStudioStructureState(); + } + } + + private ChunkGeneratorStructureState createStructureState( + ServerLevel level, + net.minecraft.world.level.chunk.ChunkGenerator generator, + ChunkGeneratorStructureState currentState + ) { + return generator.createState( + level.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET), + currentState.randomState(), + currentState.getLevelSeed(), + level.spigotConfig); + } + + private void initializeAndPublishStructureState( + net.minecraft.world.level.chunk.ChunkGenerator generator, + ChunkGeneratorStructureState structureState, + IrisChunkGenerator.StructureStatePublisher publisher + ) throws IllegalAccessException { + if (generator instanceof IrisChunkGenerator irisGenerator) { + irisGenerator.initializeAndPublishStructureState(structureState, publisher); + return; + } + structureState.ensureStructuresGenerated(); + publisher.publish(); + } + + private IrisChunkGenerator requireIrisGenerator( + net.minecraft.world.level.chunk.ChunkGenerator generator + ) { + if (generator instanceof IrisChunkGenerator irisGenerator) { + return irisGenerator; + } + throw new IllegalStateException("Studio native structure state is not owned by the active Iris generator."); + } + + private void validateDimensionContract(Engine engine, World world, ServerLevel level) { + DimensionType actualType = level.dimensionType(); + String actualTypeKey = level.dimensionTypeRegistration().unwrapKey() + .map(key -> key.identifier().toString()) + .orElse(""); + IrisDimensionRuntimeContract expected = IrisDimensionRuntimeContract.expected(engine.getDimension(), "iris"); + IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract( + actualTypeKey, + actualType.minY(), + actualType.height(), + actualType.logicalHeight()); + String runtimeName = "Bukkit world '" + world.getName() + "'"; + expected.requireExact(runtimeName, actual); + expected.requireHeight(runtimeName, level.getMinY(), level.getHeight()); + expected.requireHeight(runtimeName, world.getMinHeight(), world.getMaxHeight() - world.getMinHeight()); + IrisLogging.debug("Loaded world " + world.getName() + " with exact Iris dimension type " + actualTypeKey); + } + + private static void retargetStructureCheck(ServerLevel level, IrisChunkGenerator generator) throws NoSuchFieldException, IllegalAccessException { + Field structureCheckField = getField(level.getClass(), StructureCheck.class); + structureCheckField.setAccessible(true); + Object structureCheck = structureCheckField.get(level); + if (structureCheck == null) { + return; + } + Field generatorField = getField(structureCheck.getClass(), net.minecraft.world.level.chunk.ChunkGenerator.class); + generatorField.setAccessible(true); + generatorField.set(structureCheck, generator); + Field biomeSourceField = getField(structureCheck.getClass(), BiomeSource.class); + biomeSourceField.setAccessible(true); + biomeSourceField.set(structureCheck, generator.getBiomeSource()); + } + + public Vector3d getBoundingbox(org.bukkit.entity.EntityType entity) { + if (entity == null) { + return null; + } + + try { + // Registry lookup instead of an EntityType static-field scan: 26.2 moved the constants + // to a separate EntityTypes holder class that 26.1.2 does not have, while the registry + // resolves identically on both. ENTITY_TYPE is a DefaultedRegistry, so guard containsKey + // to avoid silently resolving unknown keys to the default entry. + Identifier key = Identifier.fromNamespaceAndPath(entity.getKey().getNamespace(), entity.getKey().getKey()); + if (!BuiltInRegistries.ENTITY_TYPE.containsKey(key)) { + return null; + } + EntityType entityType = BuiltInRegistries.ENTITY_TYPE.getValue(key); + if (entityType == null) { + return null; + } + return new Vector3d(entityType.getWidth(), entityType.getHeight(), entityType.getWidth()); + } catch (Throwable e) { + IrisLogging.error("Unable to get entity dimensions for " + entity + "!"); + IrisLogging.reportError(e); + return null; + } + } + + + @Override + public Entity spawnEntity(Location location, org.bukkit.entity.EntityType type, CreatureSpawnEvent.SpawnReason reason) { + if (location == null || location.getWorld() == null || type == null || type.getEntityClass() == null) { + return null; + } + CraftWorld world = (CraftWorld) location.getWorld(); + if (world.getDifficulty() == Difficulty.PEACEFUL) { + EntityType nativeType = BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse(type.getKey().toString())); + if (nativeType == null || !nativeType.isAllowedInPeaceful()) { + return null; + } + } + return world.spawn(location, type.getEntityClass(), null, reason); + } + + @Override + public Color getBiomeColor(Location location, BiomeColor type) { + ServerLevel reader = ((CraftWorld) location.getWorld()).getHandle(); + BlockPos pos = new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + Holder holder = reader.getBiome(pos); + net.minecraft.world.level.biome.Biome biome = holder.value(); + if (biome == null) throw new IllegalArgumentException("Invalid biome: " + holder.unwrapKey().orElse(null)); + + net.minecraft.world.attribute.EnvironmentAttributeSystem attributes = reader.environmentAttributes(); + int rgba = switch (type) { + case FOG -> environmentColor(attributes.getValue(EnvironmentAttributes.FOG_COLOR, pos)); + case WATER -> biome.getWaterColor(); + case WATER_FOG -> environmentColor(attributes.getValue(EnvironmentAttributes.WATER_FOG_COLOR, pos)); + case SKY -> environmentColor(attributes.getValue(EnvironmentAttributes.SKY_COLOR, pos)); + case FOLIAGE -> biome.getFoliageColor(); + case GRASS -> biome.getGrassColor(location.getBlockX(), location.getBlockZ()); + }; + if (rgba == 0) { + if (BiomeColor.FOLIAGE == type && biome.getSpecialEffects().foliageColorOverride().isEmpty()) + return null; + if (BiomeColor.GRASS == type && biome.getSpecialEffects().grassColorOverride().isEmpty()) + return null; + } + return new Color(rgba, true); + } + + private static Field getField(Class clazz, Class fieldType) throws NoSuchFieldException { + try { + for (Field f : clazz.getDeclaredFields()) { + if (f.getType().equals(fieldType)) + return f; + } + throw new NoSuchFieldException(fieldType.getName()); + } catch (NoSuchFieldException var4) { + Class superClass = clazz.getSuperclass(); + if (superClass == null) { + throw var4; + } else { + return getField(superClass, fieldType); + } + } + } + + public static Holder biomeToBiomeBase(Registry registry, Biome biome) { + if (registry == null || biome == null) { + return null; + } + + NamespacedKey biomeKey = resolveBiomeKey(biome); + if (biomeKey == null) { + return null; + } + + ResourceKey key = ResourceKey.create(Registries.BIOME, CraftNamespacedKey.toMinecraft(biomeKey)); + return registry.get(key).orElse(null); + } + + private static NamespacedKey resolveBiomeKey(Biome biome) { + Object keyOrNullValue = invokeNoThrow(biome, "getKeyOrNull", new Class[0]); + if (keyOrNullValue instanceof NamespacedKey namespacedKey) { + return namespacedKey; + } + + Object keyOrThrowValue = invokeNoThrow(biome, "getKeyOrThrow", new Class[0]); + if (keyOrThrowValue instanceof NamespacedKey namespacedKey) { + return namespacedKey; + } + + Object keyValue = invokeNoThrow(biome, "getKey", new Class[0]); + if (keyValue instanceof NamespacedKey namespacedKey) { + return namespacedKey; + } + + return null; + } + + private static Object invokeNoThrow(Object target, String methodName, Class[] parameterTypes, Object... args) { + if (target == null) { + return null; + } + + try { + Method method = target.getClass().getMethod(methodName, parameterTypes); + return method.invoke(target, args); + } catch (Throwable ignored) { + return null; + } + } + + private static int environmentColor(Vector3fc color) { + return ARGB.colorFromFloat(1F, color.x(), color.y(), color.z()); + } + + @Override + public DataVersion getDataVersion() { + return DataVersion.V26_3; + } + + @Override + public int getSpawnChunkCount(World world) { + return 0; + } + + @Override + public boolean missingDimensionTypes(String... keys) { + Registry type = registry().lookupOrThrow(Registries.DIMENSION_TYPE); + return !Arrays.stream(keys) + .map(key -> Identifier.fromNamespaceAndPath("iris", key)) + .allMatch(type::containsKey); + } + + @Override + public boolean injectBukkit() { + return worldLifecycle.injectBukkit(); + } + + @Override + public void ensureServerLevelInjection() { + worldLifecycle.ensureServerLevelInjection(); + } + + @Override + public void uninjectBukkit() { + worldLifecycle.uninjectBukkit(); + } + + @Override + public boolean isServerStopping() { + return worldLifecycle.isServerStopping(); + } + + @Override + public ServerShutdownBoundary createServerShutdownBoundary() { + return worldLifecycle.createServerShutdownBoundary(); + } + + @Override + public void deferPluginClassLoaderClose() { + worldLifecycle.deferPluginClassLoaderClose(); + } + + @Override + public void releasePluginClassLoaderClose() { + worldLifecycle.releasePluginClassLoaderClose(); + } + + @Override + public KMap> getBlockProperties() { + KMap> states = new KMap<>(); + + for (Block block : registry().lookupOrThrow(Registries.BLOCK)) { + BlockState state = block.defaultBlockState(); + if (state == null) state = block.getStateDefinition().any(); + BlockState finalState = state; + + states.put(CraftMagicNumbers.getMaterial(block), block.getStateDefinition() + .getProperties() + .stream() + .map(p -> createProperty(p, finalState)) + .toList()); + } + return states; + } + + private > BlockProperty createProperty(Property property, BlockState state) { + return new BlockProperty(property.getName(), property.getValueClass(), state.getValue(property), property.getPossibleValues(), property::getName); + } + + @Override + public Object createRuntimeLevelStem(Object registryAccess, ChunkGenerator raw) { + if (!(registryAccess instanceof RegistryAccess access)) { + throw new IllegalStateException("Runtime LevelStem creation requires a RegistryAccess instance."); + } + if (!(raw instanceof PlatformChunkGenerator generator)) { + throw new IllegalStateException("Generator is not platform chunk generator!"); + } + + Identifier dimensionKey = Identifier.fromNamespaceAndPath("iris", generator.getTarget().getDimension().getDimensionTypeKey()); + Holder.Reference dimensionType = access.lookupOrThrow(Registries.DIMENSION_TYPE) + .getOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, dimensionKey)); + return new LevelStem(dimensionType, chunkGenerator(access)); + } + + private net.minecraft.world.level.chunk.ChunkGenerator chunkGenerator(RegistryAccess access) { + FlatLevelGeneratorSettings settings = new FlatLevelGeneratorSettings(Optional.empty(), access.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.THE_VOID), List.of()); + settings.getLayersInfo().add(new FlatLayerInfo(1, Blocks.AIR)); + settings.updateLayers(); + return new FlatLevelSource(settings); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructurePoiUpdates.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructurePoiUpdates.java new file mode 100644 index 000000000..254a4961b --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructurePoiUpdates.java @@ -0,0 +1,142 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import ca.spottedleaf.moonrise.common.util.TickThread; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import net.minecraft.world.entity.ai.village.poi.PoiSection; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.entity.ai.village.poi.PoiTypes; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ImposterProtoChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.ProtoChunk; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataType; + +import java.util.Arrays; +import java.util.Optional; + +final class NativeStructurePoiUpdates { + private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections"); + + private final int skipPoiFlag; + + NativeStructurePoiUpdates(int skipPoiFlag) { + this.skipPoiFlag = skipPoiFlag; + } + + static NativeStructurePoiUpdates create() { + try { + return new NativeStructurePoiUpdates(Block.class.getField("UPDATE_SKIP_POI").getInt(null)); + } catch (NoSuchFieldException e) { + return null; + } catch (IllegalAccessException e) { + throw new ExceptionInInitializerError(e); + } + } + + boolean setBlock(WorldGenLevel world, BlockPos position, BlockState state, int flags, int limit) { + if ((flags & skipPoiFlag) != 0) { + return world.setBlock(position, state, flags, limit); + } + ChunkAccess chunk = world.getChunk(position); + if (!(chunk instanceof ProtoChunk)) { + return world.setBlock(position, state, flags, limit); + } + if (!(chunk instanceof ImposterProtoChunk) + && !PoiTypes.forState(chunk.getBlockState(position)).equals(PoiTypes.forState(state))) { + markSection(chunk, chunk.getSectionIndex(position.getY())); + } + return world.setBlock(position, state, flags | skipPoiFlag, limit); + } + + static void reconcile(LevelChunk chunk) { + long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY); + if (sections == null) { + return; + } + validateSections(chunk, sections); + ServerLevel world = (ServerLevel) chunk.getLevel(); + TickThread.ensureTickThread(world, chunk.getPos(), "Native structure POIs require the chunk owning thread"); + PoiManager manager = world.getPoiManager(); + for (int word = 0; word < sections.length; word++) { + long remaining = sections[word]; + while (remaining != 0) { + int index = (word << 6) + Long.numberOfTrailingZeros(remaining); + reconcileSection(chunk, manager, index); + remaining &= remaining - 1; + } + } + chunk.persistentDataContainer.remove(DIRTY_SECTIONS); + chunk.markUnsaved(); + } + + private static void markSection(ChunkAccess chunk, int index) { + long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY); + int word = index >>> 6; + long bit = 1L << (index & 63); + if (sections == null) { + sections = new long[word + 1]; + } else { + validateSections(chunk, sections); + if (word < sections.length && (sections[word] & bit) != 0) { + return; + } + if (word >= sections.length) { + sections = Arrays.copyOf(sections, word + 1); + } + } + sections[word] |= bit; + chunk.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, sections); + } + + private static void validateSections(ChunkAccess chunk, long[] sections) { + int count = chunk.getSectionsCount(); + int words = (count + 63) >>> 6; + if (sections.length > words + || sections.length == words && (count & 63) != 0 + && (sections[words - 1] >>> (count & 63)) != 0) { + throw new IllegalArgumentException("Native POI repair sections exceed chunk height at " + chunk.getPos()); + } + } + + private static void reconcileSection(LevelChunk chunk, PoiManager manager, int index) { + SectionPos sectionPosition = SectionPos.of(chunk.getPos(), chunk.getSectionYFromSectionIndex(index)); + Optional existing = manager.getOrLoad(sectionPosition.asLong()); + if (existing.isPresent()) { + for (PoiRecord record : existing.get().getRecords(type -> true, PoiManager.Occupancy.ANY).toList()) { + if (!PoiTypes.forState(chunk.getBlockState(record.getPos())).equals(Optional.of(record.getPoiType()))) { + manager.remove(record.getPos()); + } + } + } + LevelChunkSection section = chunk.getSection(index); + if (!section.maybeHas(PoiTypes::hasPoi)) { + return; + } + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + Optional> type = PoiTypes.forState(section.getBlockState(x, y, z)); + if (type.isEmpty()) { + continue; + } + position.set(sectionPosition.minBlockX() + x, sectionPosition.minBlockY() + y, + sectionPosition.minBlockZ() + z); + if (!manager.getType(position).equals(type)) { + manager.add(position.immutable(), type.get()); + } + } + } + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructureWorldgenAccess.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructureWorldgenAccess.java new file mode 100644 index 000000000..6166e9210 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructureWorldgenAccess.java @@ -0,0 +1,606 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import art.arcane.iris.nativegen.v26_3_R1.WorldgenTerrainHeightmaps; +import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.core.QuartPos; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.attribute.EnvironmentAttributeReader; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.flag.FeatureFlagSet; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkSource; +import net.minecraft.world.level.chunk.EmptyLevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.lighting.LevelLightEngine; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.storage.LevelData; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.ticks.LevelTickAccess; +import net.minecraft.world.ticks.ScheduledTick; +import org.bukkit.event.entity.CreatureSpawnEvent; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.IntBinaryOperator; +import java.util.function.Predicate; +import java.util.function.Supplier; + +final class NativeStructureWorldgenAccess implements WorldGenLevel { + private static final int WRITE_RADIUS = 1; + private static final NativeStructurePoiUpdates POI_UPDATES = NativeStructurePoiUpdates.create(); + + private final WorldGenLevel delegate; + private final ChunkPos generationCenter; + private final IntBinaryOperator surfaceFirstFreeY; + private final IntBinaryOperator floorFirstFreeY; + private final boolean virtualizeHeights; + private final Predicate protectedPosition; + private final Holder fallbackBiome; + private final BiomeManager biomeManager; + private final AABB generationBounds; + private final LevelTickAccess blockTicks; + private final LevelTickAccess fluidTicks; + private final Long2LongOpenHashMap terrainHeights; + private final Long2ObjectOpenHashMap outsideChunks; + + private NativeStructureWorldgenAccess(WorldGenLevel delegate, Boundary boundary) { + this.delegate = Objects.requireNonNull(delegate, "Native structure world access requires a delegate"); + this.generationCenter = Objects.requireNonNull( + boundary.generationCenter(), "Native structure world access requires a generation center"); + this.surfaceFirstFreeY = Objects.requireNonNull( + 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.virtualizeHeights = boundary.virtualizeHeights(); + 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); + this.generationBounds = new AABB( + generationCenter.getMinBlockX() - 16, + delegate.getMinY(), + generationCenter.getMinBlockZ() - 16, + generationCenter.getMaxBlockX() + 17, + delegate.getMinY() + delegate.getHeight(), + generationCenter.getMaxBlockZ() + 17); + this.blockTicks = new BoundedTickAccess<>(delegate.getBlockTicks(), this::isWritable); + this.fluidTicks = new BoundedTickAccess<>(delegate.getFluidTicks(), this::isWritable); + this.terrainHeights = new Long2LongOpenHashMap(); + this.terrainHeights.defaultReturnValue(Long.MIN_VALUE); + this.outsideChunks = new Long2ObjectOpenHashMap<>(); + } + + static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter, + IntBinaryOperator surfaceFirstFreeY, + IntBinaryOperator floorFirstFreeY, + boolean virtualizeHeights, + Predicate protectedPosition) { + return new NativeStructureWorldgenAccess(delegate, new Boundary( + generationCenter, surfaceFirstFreeY, floorFirstFreeY, virtualizeHeights, protectedPosition)); + } + + @Override + public long getSeed() { + return delegate.getSeed(); + } + + @Override + public boolean ensureCanWrite(BlockPos position) { + return isWritable(position) && delegate.ensureCanWrite(position); + } + + @Override + public void setCurrentlyGenerating(Supplier description) { + delegate.setCurrentlyGenerating(description); + } + + @Override + public ServerLevel getLevel() { + return delegate.getLevel(); + } + + @Override + public DifficultyInstance getCurrentDifficultyAt(BlockPos position) { + if (isReadable(position)) { + return delegate.getCurrentDifficultyAt(position); + } + return delegate.getCurrentDifficultyAt(generationCenter.getMiddleBlockPosition( + Mth.clamp(position.getY(), getMinY(), getMaxY() - 1))); + } + + @Override + public long nextSubTickCount() { + return delegate.nextSubTickCount(); + } + + @Override + public LevelTickAccess getBlockTicks() { + return blockTicks; + } + + @Override + public LevelTickAccess getFluidTicks() { + return fluidTicks; + } + + @Override + public LevelData getLevelData() { + return delegate.getLevelData(); + } + + @Override + public MinecraftServer getServer() { + return delegate.getServer(); + } + + @Override + public ChunkSource getChunkSource() { + return delegate.getChunkSource(); + } + + @Override + public RandomSource getRandom() { + return delegate.getRandom(); + } + + @Override + public void updateNeighborsAt(BlockPos position, Block block) { + if (isWritableNeighbourhood(position)) { + delegate.updateNeighborsAt(position, block); + } + } + + @Override + public void neighborShapeChanged(Direction direction, BlockPos position, + BlockPos neighbourPosition, BlockState neighbourState, + int updateFlags, int updateLimit) { + if (isWritable(position) && isWritable(neighbourPosition)) { + delegate.neighborShapeChanged( + direction, position, neighbourPosition, neighbourState, updateFlags, updateLimit); + } + } + + @Override + public void playSound(Entity source, BlockPos position, SoundEvent sound, + SoundSource soundSource, float volume, float pitch) { + if (isWritable(position)) { + delegate.playSound(source, position, sound, soundSource, volume, pitch); + } + } + + @Override + public void addParticle(ParticleOptions particle, double x, double y, double z, + double velocityX, double velocityY, double velocityZ) { + if (isWritable(BlockPos.containing(x, y, z))) { + delegate.addParticle(particle, x, y, z, velocityX, velocityY, velocityZ); + } + } + + @Override + public void levelEvent(Entity source, int eventId, BlockPos position, int data) { + if (isWritable(position)) { + delegate.levelEvent(source, eventId, position, data); + } + } + + @Override + public void gameEvent(Holder event, Vec3 position, GameEvent.Context context) { + if (isWritable(BlockPos.containing(position))) { + delegate.gameEvent(event, position, context); + } + } + + @Override + public ChunkAccess getChunk(int chunkX, int chunkZ) { + if (isInsideGenerationRegion(chunkX, chunkZ)) { + return delegate.getChunk(chunkX, chunkZ); + } + return outsideChunk(chunkX, chunkZ); + } + + @Override + public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus status, boolean create) { + if (isInsideGenerationRegion(chunkX, chunkZ)) { + return delegate.getChunk(chunkX, chunkZ, status, create); + } + return create ? outsideChunk(chunkX, chunkZ) : null; + } + + @Override + public ChunkAccess getChunkIfLoadedImmediately(int chunkX, int chunkZ) { + if (!isInsideGenerationRegion(chunkX, chunkZ)) { + return null; + } + return delegate.getChunkIfLoadedImmediately(chunkX, chunkZ); + } + + @Override + public boolean hasChunk(int chunkX, int chunkZ) { + return isInsideGenerationRegion(chunkX, chunkZ) && delegate.hasChunk(chunkX, chunkZ); + } + + @Override + public int getHeight(Heightmap.Types type, int x, int z) { + if (!virtualizeHeights && isInsideGenerationRegion(x >> 4, z >> 4)) { + return delegate.getHeight(type, x, z); + } + return height(type, x, z); + } + + @Override + public int getSkyDarken() { + return delegate.getSkyDarken(); + } + + @Override + public BiomeManager getBiomeManager() { + return biomeManager; + } + + @Override + public Holder getNoiseBiome(int quartX, int quartY, int quartZ) { + return getUncachedNoiseBiome(quartX, quartY, quartZ); + } + + @Override + public Holder getUncachedNoiseBiome(int quartX, int quartY, int quartZ) { + if (isInsideGenerationRegion(QuartPos.toSection(quartX), QuartPos.toSection(quartZ))) { + return delegate.getUncachedNoiseBiome(quartX, quartY, quartZ); + } + return fallbackBiome; + } + + @Override + public boolean isClientSide() { + return delegate.isClientSide(); + } + + @Override + public int getSeaLevel() { + return delegate.getSeaLevel(); + } + + @Override + public DimensionType dimensionType() { + return delegate.dimensionType(); + } + + @Override + public int getMinY() { + return delegate.getMinY(); + } + + @Override + public int getHeight() { + return delegate.getHeight(); + } + + @Override + public RegistryAccess registryAccess() { + return delegate.registryAccess(); + } + + @Override + public FeatureFlagSet enabledFeatures() { + return delegate.enabledFeatures(); + } + + @Override + public EnvironmentAttributeReader environmentAttributes() { + return delegate.environmentAttributes(); + } + + @Override + public LevelLightEngine getLightEngine() { + return delegate.getLightEngine(); + } + + @Override + public int getBrightness(LightLayer layer, BlockPos position) { + if (isReadable(position)) { + return delegate.getBrightness(layer, position); + } + return layer == LightLayer.SKY && canSeeSky(position) ? 15 : 0; + } + + @Override + public int getRawBrightness(BlockPos position, int ambientDarkening) { + if (isReadable(position)) { + return delegate.getRawBrightness(position, ambientDarkening); + } + return Math.max(0, getBrightness(LightLayer.SKY, position) - ambientDarkening); + } + + @Override + public boolean canSeeSky(BlockPos position) { + if (isReadable(position)) { + return delegate.canSeeSky(position); + } + return position.getY() >= height(Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ()); + } + + @Override + public WorldBorder getWorldBorder() { + return delegate.getWorldBorder(); + } + + @Override + public BlockGetter getChunkForCollisions(int chunkX, int chunkZ) { + if (isInsideGenerationRegion(chunkX, chunkZ)) { + return delegate.getChunkForCollisions(chunkX, chunkZ); + } + return outsideChunk(chunkX, chunkZ); + } + + @Override + public BlockEntity getBlockEntity(BlockPos position) { + return isWritable(position) ? delegate.getBlockEntity(position) : null; + } + + @Override + public Optional getBlockEntity( + BlockPos position, BlockEntityType type) { + return isWritable(position) ? delegate.getBlockEntity(position, type) : Optional.empty(); + } + + @Override + public BlockState getBlockState(BlockPos position) { + return isReadable(position) ? delegate.getBlockState(position) : terrainState(position); + } + + @Override + public BlockState getBlockStateIfLoaded(BlockPos position) { + return isReadable(position) ? delegate.getBlockStateIfLoaded(position) : null; + } + + @Override + public FluidState getFluidState(BlockPos position) { + return isReadable(position) ? delegate.getFluidState(position) : terrainState(position).getFluidState(); + } + + @Override + public FluidState getFluidIfLoaded(BlockPos position) { + return isReadable(position) ? delegate.getFluidIfLoaded(position) : null; + } + + @Override + public List getEntities(Entity source, AABB area, Predicate predicate) { + AABB boundedArea = boundedArea(area); + return boundedArea == null ? List.of() : delegate.getEntities(source, boundedArea, predicate); + } + + @Override + public List getEntities( + EntityTypeTest type, + AABB area, Predicate predicate) { + AABB boundedArea = boundedArea(area); + return boundedArea == null ? List.of() : delegate.getEntities(type, boundedArea, predicate); + } + + @Override + public List players() { + return delegate.players(); + } + + @Override + public boolean isStateAtPosition(BlockPos position, Predicate predicate) { + return predicate.test(getBlockState(position)); + } + + @Override + public boolean isFluidAtPosition(BlockPos position, Predicate predicate) { + return predicate.test(getFluidState(position)); + } + + @Override + public BlockPos getHeightmapPos(Heightmap.Types type, BlockPos position) { + return position.atY(getHeight(type, position.getX(), position.getZ())); + } + + @Override + public boolean setBlock(BlockPos position, BlockState state, int updateFlags, int updateLimit) { + return isWritable(position) + && (POI_UPDATES != null + ? POI_UPDATES.setBlock(delegate, position, state, updateFlags, updateLimit) + : delegate.setBlock(position, state, updateFlags, updateLimit)); + } + + @Override + public boolean removeBlock(BlockPos position, boolean move) { + if (!isWritable(position)) { + return false; + } + return POI_UPDATES != null && delegate instanceof WorldGenRegion + ? setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, Block.UPDATE_LIMIT) + : delegate.removeBlock(position, move); + } + + @Override + public boolean destroyBlock(BlockPos position, boolean drop, Entity source, int updateLimit) { + if (!isWritable(position)) { + return false; + } + return POI_UPDATES != null && delegate instanceof WorldGenRegion + ? !delegate.getBlockState(position).isAir() + && setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, updateLimit) + : delegate.destroyBlock(position, drop, source, updateLimit); + } + + @Override + public boolean addFreshEntity(Entity entity) { + return isWritable(entity.blockPosition()) && delegate.addFreshEntity(entity); + } + + @Override + public boolean addFreshEntity(Entity entity, CreatureSpawnEvent.SpawnReason reason) { + return isWritable(entity.blockPosition()) && delegate.addFreshEntity(entity, reason); + } + + private boolean isReadable(BlockPos position) { + return isInsideGenerationRegion(position.getX() >> 4, position.getZ() >> 4) + && !isOutsideBuildHeight(position); + } + + private boolean isWritable(BlockPos position) { + return isReadable(position) && !protectedPosition.test(position); + } + + private boolean isWritableNeighbourhood(BlockPos position) { + return isWritable(position) + && isWritable(position.north()) + && isWritable(position.south()) + && isWritable(position.east()) + && isWritable(position.west()) + && isWritable(position.above()) + && isWritable(position.below()); + } + + private AABB boundedArea(AABB area) { + double minX = Math.max(area.minX, generationBounds.minX); + double minY = Math.max(area.minY, generationBounds.minY); + double minZ = Math.max(area.minZ, generationBounds.minZ); + double maxX = Math.min(area.maxX, generationBounds.maxX); + double maxY = Math.min(area.maxY, generationBounds.maxY); + double maxZ = Math.min(area.maxZ, generationBounds.maxZ); + if (minX >= maxX || minY >= maxY || minZ >= maxZ) { + return null; + } + if (minX == area.minX && minY == area.minY && minZ == area.minZ + && maxX == area.maxX && maxY == area.maxY && maxZ == area.maxZ) { + return area; + } + return new AABB(minX, minY, minZ, maxX, maxY, maxZ); + } + + boolean isInsideGenerationRegion(int chunkX, int chunkZ) { + return Math.abs(chunkX - generationCenter.x()) <= WRITE_RADIUS + && Math.abs(chunkZ - generationCenter.z()) <= WRITE_RADIUS; + } + + @Override + public boolean isOutsideBuildHeight(BlockPos position) { + return position.getY() < getMinY() || position.getY() >= getMaxY(); + } + + private BlockState terrainState(BlockPos position) { + if (isOutsideBuildHeight(position)) { + return Blocks.VOID_AIR.defaultBlockState(); + } + long heights = heights(position.getX(), position.getZ()); + int surface = (int) (heights >> 32); + int floor = (int) heights; + if (position.getY() < floor) { + return Blocks.STONE.defaultBlockState(); + } + if (position.getY() < surface) { + return Blocks.WATER.defaultBlockState(); + } + return Blocks.AIR.defaultBlockState(); + } + + private int height(Heightmap.Types type, int x, int z) { + long heights = heights(x, z); + if (type == Heightmap.Types.OCEAN_FLOOR || type == Heightmap.Types.OCEAN_FLOOR_WG) { + return (int) heights; + } + return (int) (heights >> 32); + } + + private long heights(int x, int z) { + long key = ((long) x << 32) ^ (z & 0xffffffffL); + long cached = terrainHeights.get(key); + if (cached != Long.MIN_VALUE) { + return cached; + } + int floor = Mth.clamp(floorFirstFreeY.applyAsInt(x, z), getMinY(), getMaxY()); + int surface = Mth.clamp(surfaceFirstFreeY.applyAsInt(x, z), floor, getMaxY()); + long heights = ((long) surface << 32) | (floor & 0xffffffffL); + terrainHeights.put(key, heights); + return heights; + } + + private ChunkAccess outsideChunk(int chunkX, int chunkZ) { + long key = ChunkPos.pack(chunkX, chunkZ); + ChunkAccess cached = outsideChunks.get(key); + if (cached != null) { + return cached; + } + EmptyLevelChunk chunk = new EmptyLevelChunk(getLevel(), new ChunkPos(chunkX, chunkZ), fallbackBiome); + WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY, floorFirstFreeY); + outsideChunks.put(key, chunk); + return chunk; + } + + private record Boundary(ChunkPos generationCenter, + IntBinaryOperator surfaceFirstFreeY, + IntBinaryOperator floorFirstFreeY, + boolean virtualizeHeights, + Predicate protectedPosition) { + } + + private static final class BoundedTickAccess implements LevelTickAccess { + private final LevelTickAccess delegate; + private final Predicate writable; + + private BoundedTickAccess(LevelTickAccess delegate, Predicate writable) { + this.delegate = delegate; + this.writable = writable; + } + + @Override + public void schedule(ScheduledTick tick) { + if (writable.test(tick.pos())) { + delegate.schedule(tick); + } + } + + @Override + public boolean hasScheduledTick(BlockPos position, T type) { + return writable.test(position) && delegate.hasScheduledTick(position, type); + } + + @Override + public int count() { + return delegate.count(); + } + + @Override + public boolean willTickThisTick(BlockPos position, T type) { + return writable.test(position) && delegate.willTickThisTick(position, type); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsGenerationHooks.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsGenerationHooks.java new file mode 100644 index 000000000..c6bba6b20 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsGenerationHooks.java @@ -0,0 +1,61 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import art.arcane.iris.platform.agent.Agent; +import it.unimi.dsi.fastutil.shorts.ShortList; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.matcher.ElementMatchers; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.ChunkPos; +import java.util.function.LongPredicate; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; + +import java.util.List; + +final class NmsGenerationHooks { + private NmsGenerationHooks() { + } + + static void install() { + ByteBuddy buddy = new ByteBuddy(); + for (Class clazz : List.of(ChunkAccess.class, ProtoChunk.class)) { + buddy.redefine(clazz) + .visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class)))) + .make() + .load(clazz.getClassLoader(), Agent.installed()); + } + buddy.redefine(WorldGenRegion.class) + .visit(Advice.to(WorldGenerationWriteAdvice.class).on(ElementMatchers.named("ensureCanWrite") + .and(ElementMatchers.takesArguments(BlockPos.class)))) + .make() + .load(WorldGenRegion.class.getClassLoader(), Agent.installed()); + } + + private static class WorldGenerationWriteAdvice { + @Advice.OnMethodExit + static void exit(@Advice.This WorldGenRegion region, @Advice.Argument(0) BlockPos position, + @Advice.Return(readOnly = false) boolean allowed) { + if (!allowed) { + return; + } + ChunkGenerator generator = region.getLevel().getChunkSource().getGenerator(); + if (generator instanceof LongPredicate predicate + && generator.getClass().getName().equals("art.arcane.iris.platform.bukkit.nms.v26_3_R1.IrisChunkGenerator") + && region.getChunk(position.getX() >> 4, position.getZ() >> 4) + .getPersistedStatus().isOrAfter(ChunkStatus.FULL)) { + allowed = predicate.test(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4)); + } + } + } + + private static class ChunkAccessAdvice { + @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class) + static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) { + return index >= access.getPostProcessing().length; + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsGenerationRegistry.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsGenerationRegistry.java new file mode 100644 index 000000000..4d5143de0 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsGenerationRegistry.java @@ -0,0 +1,182 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import art.arcane.iris.spi.PlatformGenerationRegistry; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.RegistryOps; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.dimension.DimensionType; + +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.function.Supplier; + +final class NmsGenerationRegistry implements PlatformGenerationRegistry { + private static final String BIOME_REGISTRY = "minecraft:worldgen/biome"; + private static final String STRUCTURE_REGISTRY = "minecraft:worldgen/structure"; + private static final String DIMENSION_TYPE_REGISTRY = "minecraft:dimension_type"; + private static final String BLOCK_REGISTRY = "minecraft:block"; + private static final String ENTITY_TYPE_REGISTRY = "minecraft:entity_type"; + + private final Supplier registryAccess; + private final String runtimeIdentity; + private final String rendererIdentity; + + NmsGenerationRegistry( + Supplier registryAccess, + String runtimeIdentity, + String rendererIdentity + ) { + this.registryAccess = Objects.requireNonNull(registryAccess, "registryAccess"); + this.runtimeIdentity = requireText(runtimeIdentity, "runtimeIdentity"); + this.rendererIdentity = requireText(rendererIdentity, "rendererIdentity"); + } + + @Override + public String runtimeIdentity() { + return runtimeIdentity; + } + + @Override + public String generatedDefinitionRendererIdentity() { + return rendererIdentity; + } + + @Override + public String customBiomeResourceKey(String identitySha256) { + return PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(identitySha256); + } + + @Override + public List legacyCustomBiomeResourceKeys( + String packName, + String dimensionKey, + String customBiomeId + ) { + return List.of(requireText(dimensionKey, "dimensionKey").toLowerCase(Locale.ROOT) + + ":" + requireText(customBiomeId, "customBiomeId").toLowerCase(Locale.ROOT)); + } + + @Override + public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) { + return "iris:" + requireText(dimensionTypeKey, "dimensionTypeKey").toLowerCase(Locale.ROOT); + } + + @Override + public Definition definition(String registryKey, String resourceKey) { + RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access"); + return switch (registryKey) { + case BIOME_REGISTRY -> presence(access, Registries.BIOME, registryKey, resourceKey); + case STRUCTURE_REGISTRY -> presence(access, Registries.STRUCTURE, registryKey, resourceKey); + case DIMENSION_TYPE_REGISTRY -> presence( + access, + Registries.DIMENSION_TYPE, + registryKey, + resourceKey + ); + case BLOCK_REGISTRY -> presence(access, Registries.BLOCK, registryKey, resourceKey); + case ENTITY_TYPE_REGISTRY -> presence(access, Registries.ENTITY_TYPE, registryKey, resourceKey); + default -> throw new IllegalArgumentException("Unsupported generation registry: " + registryKey); + }; + } + + @Override + public Definition canonicalDefinition(String registryKey, String resourceKey, String sourceJson) { + RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access"); + return switch (registryKey) { + case BIOME_REGISTRY -> canonicalize(access, Biome.DIRECT_CODEC, sourceJson); + case DIMENSION_TYPE_REGISTRY -> canonicalize(access, DimensionType.DIRECT_CODEC, sourceJson); + default -> throw new IllegalArgumentException( + "Registry does not accept generated JSON definitions: " + registryKey + ); + }; + } + + @Override + public Definition generatedDefinition(String registryKey, String resourceKey) { + RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access"); + return switch (registryKey) { + case BIOME_REGISTRY -> exactRegistered( + access, + Registries.BIOME, + Biome.DIRECT_CODEC, + resourceKey + ); + case DIMENSION_TYPE_REGISTRY -> exactRegistered( + access, + Registries.DIMENSION_TYPE, + DimensionType.DIRECT_CODEC, + resourceKey + ); + default -> throw new IllegalArgumentException( + "Registry does not contain Iris-generated definitions: " + registryKey + ); + }; + } + + private static Definition presence( + RegistryAccess access, + ResourceKey> registryKey, + String registryResourceKey, + String resourceKey + ) { + Registry registry = access.lookupOrThrow(registryKey); + Identifier identifier = requireIdentifier(resourceKey); + T value = registry.getValue(identifier); + if (value == null) { + throw new IllegalStateException("Missing registry definition " + registryKey.identifier() + + " / " + resourceKey + "."); + } + return Definition.resourceIdentity(registryResourceKey, resourceKey); + } + + private static Definition canonicalize(RegistryAccess access, Codec codec, String sourceJson) { + RegistryOps ops = RegistryOps.create(JsonOps.INSTANCE, access); + JsonElement source = JsonParser.parseString(Objects.requireNonNull(sourceJson, "sourceJson")); + T decoded = codec.parse(ops, source).getOrThrow(IllegalArgumentException::new); + JsonElement encoded = codec.encodeStart(ops, decoded).getOrThrow(IllegalStateException::new); + return Definition.exactJson(encoded.toString()); + } + + private static Definition exactRegistered( + RegistryAccess access, + ResourceKey> registryKey, + Codec codec, + String resourceKey + ) { + Registry registry = access.lookupOrThrow(registryKey); + Identifier identifier = requireIdentifier(resourceKey); + T value = registry.getValue(identifier); + if (value == null) { + throw new IllegalStateException("Missing generated registry definition " + + registryKey.identifier() + " / " + resourceKey + "."); + } + RegistryOps ops = RegistryOps.create(JsonOps.INSTANCE, access); + JsonElement encoded = codec.encodeStart(ops, value).getOrThrow(IllegalStateException::new); + return Definition.exactJson(encoded.toString()); + } + + private static Identifier requireIdentifier(String resourceKey) { + Identifier identifier = Identifier.tryParse(resourceKey); + if (identifier == null) { + throw new IllegalArgumentException("Invalid registry resource key: " + resourceKey); + } + return identifier; + } + + private static String requireText(String value, String label) { + String required = Objects.requireNonNull(value, label).trim(); + if (required.isEmpty()) { + throw new IllegalArgumentException(label + " must not be blank."); + } + return required; + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsSavedTerrainCapture.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsSavedTerrainCapture.java new file mode 100644 index 000000000..58efebfc4 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsSavedTerrainCapture.java @@ -0,0 +1,266 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import art.arcane.iris.world.history.NativeTerrainReceipt; +import art.arcane.iris.world.history.SavedTerrainChunk; +import art.arcane.iris.world.task.J; +import ca.spottedleaf.concurrentutil.completable.Completable; +import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.common.PlatformHooks; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.storage.SerializableChunkData; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftWorld; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.LockSupport; + +final class NmsSavedTerrainCapture { + private static final long WRITE_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(30L); + private static final int SNAPSHOT_BATCH_SIZE = 64; + + private NmsSavedTerrainCapture() { + } + + static CompletableFuture capture(World world, int chunkX, int chunkZ, int minimumY, int height) { + ServerLevel level = ((CraftWorld) world).getHandle(); + CompletableFuture snapshot = new CompletableFuture<>(); + J.runRegionFuture(world, chunkX, chunkZ, () -> { + try { + NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ); + ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk(); + if (chunk == null) { + snapshot.complete(new CapturedChunk(new ChunkPos(chunkX, chunkZ), CompletableFuture.completedFuture(null))); + return; + } + String status = BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString(); + if (!SavedTerrainChunk.hasTerrain(status)) { + throw new IOException("Native chunk " + chunkX + "," + chunkZ + " has no completed terrain at " + status); + } + snapshot.complete(scheduleSnapshot(level, chunk)); + } catch (Throwable failure) { + snapshot.completeExceptionally(failure); + } + }).whenComplete((ignored, failure) -> { + if (failure != null) { + snapshot.completeExceptionally(failure); + } + }); + return snapshot.thenApplyAsync(captured -> { + try { + long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS; + CompoundTag data = awaitSnapshot(level, captured, deadline); + Checkpoint checkpoint = checkpoint(chunkX, chunkZ, data); + awaitWrite(level, checkpoint.chunk(), deadline); + if (data == null) { + return SavedTerrainChunk.read(world.getWorldFolder().toPath(), chunkX, chunkZ, minimumY, height); + } + verifyCheckpoint(world.getWorldFolder().toPath(), checkpoint); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (DataOutputStream output = new DataOutputStream(bytes)) { + NbtIo.write(data, output); + } + return SavedTerrainChunk.readNbt(bytes.toByteArray(), chunkX, chunkZ, minimumY, height); + } catch (IOException failure) { + throw new CompletionException(failure); + } + }); + } + + static CompletableFuture flush(World world) { + ServerLevel level = ((CraftWorld) world).getHandle(); + List holders = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders(); + return CompletableFuture.runAsync(() -> { + try { + flushSnapshots(world, level, holders); + } catch (IOException failure) { + throw new CompletionException(failure); + } + }); + } + + static void flushSnapshots(World world, ServerLevel level, List holders) throws IOException { + long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS; + List checkpoints = new ArrayList<>(holders.size()); + for (int offset = 0; offset < holders.size(); offset += SNAPSHOT_BATCH_SIZE) { + int end = Math.min(holders.size(), offset + SNAPSHOT_BATCH_SIZE); + List> snapshots = requestSnapshots(world, level, holders.subList(offset, end)); + awaitSnapshots(snapshots, deadline); + for (CompletableFuture future : snapshots) { + CapturedChunk captured = future.join(); + CompoundTag data = awaitSnapshot(level, captured, deadline); + checkpoints.add(checkpoint(captured.chunk().x(), captured.chunk().z(), data)); + } + for (int index = offset; index < end; index++) { + awaitWrite(level, checkpoints.get(index).chunk(), deadline); + } + } + finishCheckpoint(level, world.getWorldFolder().toPath(), checkpoints); + } + + private static List> requestSnapshots( + World world, ServerLevel level, List holders) { + List> snapshots = new ArrayList<>(holders.size()); + for (NewChunkHolder original : holders) { + ChunkPos position = new ChunkPos(original.chunkX, original.chunkZ); + CompletableFuture snapshot = new CompletableFuture<>(); + snapshots.add(snapshot); + J.runRegionFuture(world, position.x(), position.z(), () -> { + try { + NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager + .getChunkHolder(position.x(), position.z()); + ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk(); + snapshot.complete(chunk != null && chunk.isUnsaved() ? scheduleSnapshot(level, chunk) + : new CapturedChunk(position, CompletableFuture.completedFuture(null))); + } catch (Throwable failure) { + snapshot.completeExceptionally(failure); + } + }).whenComplete((ignored, failure) -> { + if (failure != null) { + snapshot.completeExceptionally(failure); + } + }); + } + return snapshots; + } + + private static void awaitSnapshots(List> snapshots, long deadlineNanos) + throws IOException { + try { + CompletableFuture.allOf(snapshots.toArray(CompletableFuture[]::new)) + .get(Math.max(0L, deadlineNanos - System.nanoTime()), TimeUnit.NANOSECONDS); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while copying terrain checkpoint chunks.", failure); + } catch (ExecutionException | TimeoutException failure) { + throw new IOException("Unable to copy terrain checkpoint chunks.", failure); + } + } + + static void finishCheckpoint(ServerLevel level, Path worldFolder, List checkpoints) throws IOException { + long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS; + for (Checkpoint checkpoint : checkpoints) { + awaitWrite(level, checkpoint.chunk(), deadline); + } + MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA); + for (Checkpoint checkpoint : checkpoints) { + verifyCheckpoint(worldFolder, checkpoint); + } + } + + static Checkpoint checkpoint(int chunkX, int chunkZ, CompoundTag data) { + if (data == null) { + return new Checkpoint(new ChunkPos(chunkX, chunkZ), null); + } + byte[] receipt = data.getCompound("ChunkBukkitValues") + .flatMap(values -> values.getByteArray(NativeTerrainReceipt.NBT_KEY)).orElse(null); + long structureActivation = data.getCompound("ChunkBukkitValues").map(values -> values.getLongOr( + NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY, 0)).orElse(0L); + return new Checkpoint(new ChunkPos(chunkX, chunkZ), new Verification( + data.getStringOr("Status", ""), receipt, structureActivation)); + } + + private static void verifyCheckpoint(Path worldFolder, Checkpoint checkpoint) throws IOException { + Verification expected = checkpoint.verification(); + if (expected == null) { + return; + } + SavedTerrainChunk.verifyCheckpoint(worldFolder, checkpoint.chunk().x(), checkpoint.chunk().z(), + expected.status(), expected.receipt(), expected.structureActivation()); + } + + static void awaitWrite(ServerLevel level, ChunkPos chunk, long deadlineNanos) throws IOException { + ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler(); + while (true) { + checkWriteWait(chunk, deadlineNanos); + if (MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(), + MoonriseRegionFileIO.RegionFileType.CHUNK_DATA) == Priority.COMPLETING) { + return; + } + helpSaveTasks(scheduler); + } + } + + static CapturedChunk scheduleSnapshot(ServerLevel level, ChunkAccess chunk) { + SerializableChunkData snapshot = SerializableChunkData.copyOf(level, chunk); + PlatformHooks.get().chunkSyncSave(level, chunk, snapshot); + Completable nativeData = new Completable<>(); + CompletableFuture data = new CompletableFuture<>(); + PrioritisedExecutor.PrioritisedTask task = level.moonrise$getChunkTaskScheduler().saveExecutor + .createTask(() -> serializeSnapshot(snapshot, nativeData, data)); + task.queue(); + ChunkPos position = chunk.getPos(); + MoonriseRegionFileIO.scheduleSave(level, position.x(), position.z(), nativeData, task, + MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, Priority.NORMAL); + return new CapturedChunk(position, data); + } + + static CompoundTag awaitSnapshot(ServerLevel level, CapturedChunk captured, long deadlineNanos) throws IOException { + ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler(); + while (!captured.data().isDone()) { + checkWriteWait(captured.chunk(), deadlineNanos); + helpSaveTasks(scheduler); + } + try { + return captured.data().join(); + } catch (CompletionException failure) { + throw new IOException("Unable to serialize terrain checkpoint chunk " + captured.chunk(), failure.getCause()); + } + } + + private static void serializeSnapshot(SerializableChunkData snapshot, Completable nativeData, + CompletableFuture data) { + try { + CompoundTag serialized = snapshot.write(); + nativeData.complete(serialized); + data.complete(serialized); + } catch (Throwable failure) { + nativeData.completeExceptionally(failure); + data.completeExceptionally(failure); + } + } + + private static void checkWriteWait(ChunkPos chunk, long deadlineNanos) throws IOException { + if (Thread.currentThread().isInterrupted()) { + throw new IOException("Interrupted while saving terrain checkpoint chunk " + chunk); + } + if (System.nanoTime() - deadlineNanos >= 0L) { + throw new IOException("Timed out saving terrain checkpoint chunk " + chunk); + } + } + + private static void helpSaveTasks(ChunkTaskScheduler scheduler) { + boolean helped = scheduler.saveExecutor.executeTask(); + helped |= scheduler.compressionExecutor.executeTask(); + if (!helped) { + LockSupport.parkNanos(1_000_000L); + } + } + + record CapturedChunk(ChunkPos chunk, CompletableFuture data) { + } + + record Checkpoint(ChunkPos chunk, Verification verification) { + } + + record Verification(String status, byte[] receipt, long structureActivation) { + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsWorldLifecycle.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsWorldLifecycle.java new file mode 100644 index 000000000..3a37aeb87 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsWorldLifecycle.java @@ -0,0 +1,403 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import art.arcane.iris.platform.bukkit.nms.ServerShutdownBoundary; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.localization.C; +import art.arcane.iris.platform.agent.Agent; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.agent.builder.ResettableClassFileTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.matcher.ElementMatchers; +import net.bytebuddy.utility.JavaModule; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicBoolean; + +final class NmsWorldLifecycle { + private final AtomicBoolean injected = new AtomicBoolean(); + private volatile ResettableClassFileTransformer levelStorageAccessTransformer; + private volatile ResettableClassFileTransformer serverLevelTransformer; + private volatile ResettableClassFileTransformer pluginClassLoaderTransformer; + private boolean pluginClassLoaderCloseDeferred; + + public boolean injectBukkit() { + synchronized (injected) { + if (injected.get()) { + return true; + } + try { + IrisLogging.info("Injecting Bukkit"); + Agent.requireClassLoaderCloseDeferral(); + Class loaderCloseType = getClass().getClassLoader().getClass() + .getMethod("close").getDeclaringClass(); + PluginClassLoaderInjectionListener loaderListener = new PluginClassLoaderInjectionListener(); + pluginClassLoaderTransformer = new AgentBuilder.Default() + .disableClassFormatChanges() + .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) + .with(AgentBuilder.RedefinitionStrategy.Listener.ErrorEscalating.FAIL_FAST) + .with(loaderListener) + .type(ElementMatchers.is(loaderCloseType)) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> + builder.visit(Advice.to(PluginClassLoaderCloseAdvice.class) + .on(ElementMatchers.named("close") + .and(ElementMatchers.takesArguments(0)) + .and(ElementMatchers.returns(void.class))))) + .installOn(Agent.getInstrumentation()); + loaderListener.requireInstalled(); + levelStorageAccessTransformer = new AgentBuilder.Default() + .disableClassFormatChanges() + .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) + .type(ElementMatchers.is(LevelStorageSource.LevelStorageAccess.class)) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> + builder.visit(Advice.to(LevelStorageAccessAdvice.class).on(ElementMatchers.isConstructor() + .and(ElementMatchers.takesArguments(4)) + .and(ElementMatchers.takesArgument(0, LevelStorageSource.class)) + .and(ElementMatchers.takesArgument(1, String.class)) + .and(ElementMatchers.takesArgument(2, Path.class)) + .and(ElementMatchers.takesArgument(3, ResourceKey.class))))) + .installOn(Agent.getInstrumentation()); + serverLevelTransformer = new AgentBuilder.Default() + .disableClassFormatChanges() + .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) + .type(ElementMatchers.is(ServerLevel.class)) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> + builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor() + .and(ElementMatchers.takesArgument(0, MinecraftServer.class)) + .and(ElementMatchers.takesArgument(5, LevelStem.class))))) + .installOn(Agent.getInstrumentation()); + NmsGenerationHooks.install(); + + injected.set(true); + return true; + } catch (Throwable e) { + IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e); + ResettableClassFileTransformer partialServerLevel = serverLevelTransformer; + ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer; + ResettableClassFileTransformer partialPluginClassLoader = pluginClassLoaderTransformer; + serverLevelTransformer = null; + levelStorageAccessTransformer = null; + pluginClassLoaderTransformer = null; + for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{ + partialServerLevel, + partialStorageAccess, + partialPluginClassLoader + }) { + if (partial == null) { + continue; + } + try { + partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION); + } catch (Throwable cleanupFailure) { + IrisLogging.reportError("Failed to remove partial Bukkit lifecycle injection", cleanupFailure); + } + } + return false; + } + } + } + + public void ensureServerLevelInjection() { + if (!injected.get()) { + throw new IllegalStateException("Iris world lifecycle injection is unavailable. Fix the Java Agent or Code Injection startup failure and restart before creating worlds."); + } + try { + Agent.getInstrumentation().retransformClasses( + LevelStorageSource.LevelStorageAccess.class, + ServerLevel.class + ); + } catch (Throwable e) { + throw new IllegalStateException("Failed to re-apply Bukkit world lifecycle injection. World creation was stopped before loading the world.", e); + } + } + + public void uninjectBukkit() { + synchronized (injected) { + ResettableClassFileTransformer activeServerLevel = serverLevelTransformer; + ResettableClassFileTransformer activeStorageAccess = levelStorageAccessTransformer; + serverLevelTransformer = null; + levelStorageAccessTransformer = null; + injected.set(false); + for (ResettableClassFileTransformer transformer : new ResettableClassFileTransformer[]{ + activeServerLevel, + activeStorageAccess + }) { + if (transformer == null) { + continue; + } + try { + transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION); + } catch (Throwable e) { + IrisLogging.reportError(C.RED + "Failed to remove Bukkit world lifecycle injection", e); + } + } + } + } + + public boolean isServerStopping() { + return ((CraftServer) Bukkit.getServer()).getServer().hasStopped(); + } + + public ServerShutdownBoundary createServerShutdownBoundary() { + MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer(); + return new ServerShutdownBoundary( + () -> server.hasFullyShutdown, + server.getRunningThread() + ); + } + + public void deferPluginClassLoaderClose() { + synchronized (injected) { + if (pluginClassLoaderTransformer == null || pluginClassLoaderCloseDeferred) { + return; + } + Agent.retainClassLoader(getClass().getClassLoader()); + pluginClassLoaderCloseDeferred = true; + } + } + + public void releasePluginClassLoaderClose() { + ResettableClassFileTransformer transformer; + boolean releaseLoader; + synchronized (injected) { + transformer = pluginClassLoaderTransformer; + pluginClassLoaderTransformer = null; + releaseLoader = pluginClassLoaderCloseDeferred; + pluginClassLoaderCloseDeferred = false; + } + try { + if (transformer != null + && !transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)) { + throw new IllegalStateException("Failed to remove Iris plugin class loader lifecycle injection"); + } + } finally { + if (releaseLoader) { + Agent.releaseClassLoader(getClass().getClassLoader()); + } + } + } + + private static final class PluginClassLoaderInjectionListener extends AgentBuilder.Listener.Adapter { + private volatile boolean transformed; + private volatile Throwable failure; + + @Override + public void onTransformation(TypeDescription type, ClassLoader loader, JavaModule module, + boolean loaded, DynamicType dynamicType) { + transformed = true; + } + + @Override + public void onError(String typeName, ClassLoader loader, JavaModule module, + boolean loaded, Throwable throwable) { + failure = throwable; + } + + private void requireInstalled() { + if (!transformed || failure != null) { + throw new IllegalStateException("Iris plugin class loader lifecycle injection failed", failure); + } + } + } + + private static class PluginClassLoaderCloseAdvice { + @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class) + static boolean enter(@Advice.This ClassLoader loader) throws ReflectiveOperationException { + Class installer = Class.forName( + "art.arcane.iris.platform.agent.Installer", true, ClassLoader.getSystemClassLoader()); + return Boolean.TRUE.equals(installer.getMethod("deferClassLoaderClose", ClassLoader.class) + .invoke(null, loader)); + } + } + + private static class LevelStorageAccessAdvice { + @Advice.OnMethodEnter + static void enter( + @Advice.Argument(1) String levelId, + @Advice.Argument(value = 3, readOnly = false) ResourceKey dimensionType + ) { + if (levelId == null || levelId.isBlank()) { + return; + } + + ClassLoader pluginClassLoader; + Class generatorType; + Class stagingType; + try { + Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris"); + if (irisPlugin == null) { + return; + } + pluginClassLoader = irisPlugin.getClass().getClassLoader(); + generatorType = Class.forName("art.arcane.iris.platform.generation.PlatformChunkGenerator", true, pluginClassLoader); + stagingType = Class.forName("art.arcane.iris.world.lifecycle.WorldLifecycleStaging", true, pluginClassLoader); + } catch (Throwable ignored) { + return; + } + + Object generator; + try { + generator = stagingType + .getDeclaredMethod("peekStemGenerator", String.class) + .invoke(null, levelId); + } catch (Throwable e) { + throw new RuntimeException("Iris failed to inspect the staged world generator", + e instanceof InvocationTargetException ex ? ex.getCause() : e); + } + if (generator == null || !generatorType.isInstance(generator)) { + return; + } + + try { + Object target = generatorType.getMethod("getTarget").invoke(generator); + if (target == null) { + throw new IllegalStateException("Iris generator has no engine target."); + } + Object world = target.getClass().getMethod("getWorld").invoke(target); + if (world == null) { + throw new IllegalStateException("Iris generator target has no world identity."); + } + Object rawIdentity = world.getClass().getMethod("identity").invoke(world); + String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim(); + Identifier worldIdentifier = Identifier.parse(worldIdentity); + if (!"iris".equals(worldIdentifier.getNamespace()) + && !"minecraft".equals(worldIdentifier.getNamespace())) { + throw new IllegalStateException( + "Iris generator has an unmanaged world identity: " + worldIdentity); + } + dimensionType = ResourceKey.create(Registries.LEVEL_STEM, worldIdentifier); + } catch (Throwable e) { + throw new RuntimeException("Iris failed to bind the staged world storage identity", + e instanceof InvocationTargetException ex ? ex.getCause() : e); + } + } + } + + private static class ServerLevelAdvice { + @Advice.OnMethodEnter + static void enter( + @Advice.Argument(0) MinecraftServer server, + @Advice.Argument(value = 4, readOnly = false) ResourceKey dimensionKey, + @Advice.Argument(value = 5, readOnly = false) LevelStem levelStem, + @Advice.AllArguments Object[] constructorArguments + ) { + if (dimensionKey == null) + return; + + // This advice is inlined into every ServerLevel construction on the server. Until a + // world is proven Iris-owned, every failure must fail OPEN (keep the vanilla stem): + // Iris being unloaded or half-loaded must never break other plugins' world creation. + String levelId; + ClassLoader pluginClassLoader; + Class generatorType; + Class stagingType; + try { + levelId = dimensionKey.identifier().getPath(); + if (levelId == null || levelId.isBlank()) { + return; + } + + Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris"); + if (irisPlugin == null) { + return; + } + pluginClassLoader = irisPlugin.getClass().getClassLoader(); + generatorType = Class.forName("art.arcane.iris.platform.generation.PlatformChunkGenerator", true, pluginClassLoader); + stagingType = Class.forName("art.arcane.iris.world.lifecycle.WorldLifecycleStaging", true, pluginClassLoader); + } catch (Throwable ignored) { + // Iris absent or half-loaded: fail OPEN for a world we cannot prove ours. + return; + } + + // From here Iris is present and its classes resolve; a failure resolving the + // staged generator must fail LOUD — silently handing a possibly-staged Iris world + // the vanilla stem corrupts generation. + ChunkGenerator gen = null; + try { + ChunkGenerator constructorGenerator = null; + for (Object argument : constructorArguments) { + if (argument instanceof ChunkGenerator candidate) { + constructorGenerator = candidate; + break; + } + } + Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null; + if (generator == null) { + generator = stagingType + .getDeclaredMethod("consumeStemGenerator", String.class) + .invoke(null, levelId); + } + if (generator instanceof ChunkGenerator owned && owned.getClass().getPackageName().startsWith("art.arcane.iris")) { + gen = owned; + } + } catch (Throwable e) { + throw new RuntimeException("Iris failed to resolve the staged world generator", + e instanceof InvocationTargetException ex ? ex.getCause() : e); + } + if (gen == null) { + return; + } + + // Past the ownership gate the world is Iris-owned; silently handing back the + // vanilla stem would corrupt generation, so failures from here rethrow. + try { + Object bindings = Class.forName("art.arcane.iris.platform.bukkit.nms.INMS", true, pluginClassLoader) + .getDeclaredMethod("get") + .invoke(null); + if (bindings == null) { + throw new IllegalStateException("Iris failed to resolve an INMSBinding instance."); + } + + Method stemMethod = null; + for (Method candidate : bindings.getClass().getMethods()) { + if (candidate.getName().equals("createRuntimeLevelStem") && candidate.getParameterCount() == 2) { + stemMethod = candidate; + break; + } + } + if (stemMethod == null) { + throw new IllegalStateException("Iris binding is missing createRuntimeLevelStem."); + } + Object target = generatorType.getMethod("getTarget").invoke(gen); + if (target == null) { + throw new IllegalStateException("Iris generator has no engine target."); + } + Object world = target.getClass().getMethod("getWorld").invoke(target); + if (world == null) { + throw new IllegalStateException("Iris generator target has no world identity."); + } + Object rawIdentity = world.getClass().getMethod("identity").invoke(world); + String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim(); + Identifier worldIdentifier = Identifier.parse(worldIdentity); + if (!"iris".equals(worldIdentifier.getNamespace()) + && !"minecraft".equals(worldIdentifier.getNamespace())) { + throw new IllegalStateException( + "Iris generator has an unmanaged world identity: " + worldIdentity); + } + Object resolvedStem = stemMethod.invoke(bindings, server.registryAccess(), gen); + if (!(resolvedStem instanceof LevelStem runtimeStem)) { + throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + "."); + } + dimensionKey = ResourceKey.create(Registries.DIMENSION, worldIdentifier); + levelStem = runtimeStem; + } catch (Throwable e) { + throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e); + } + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/VanillaStructureBiomes.java b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/VanillaStructureBiomes.java new file mode 100644 index 000000000..c281bc0af --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/VanillaStructureBiomes.java @@ -0,0 +1,80 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureGenerationKeys; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.levelgen.structure.Structure; + +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; + +final class VanillaStructureBiomes { + private VanillaStructureBiomes() { + } + + static Set possibleBiomeKeys(BiomeSource source) { + if (source == null) { + throw new IllegalStateException("Minecraft chunk generator has no biome source"); + } + Set keys = new LinkedHashSet<>(); + Set> possibleBiomes = source instanceof CustomBiomeSource customBiomeSource + ? customBiomeSource.possibleStructureBiomes() + : source.possibleBiomes(); + for (Holder holder : possibleBiomes) { + Optional> key = holder.unwrapKey(); + if (key.isPresent()) { + keys.add(key.get().identifier().toString()); + } + } + if (keys.isEmpty()) { + throw new IllegalStateException("Minecraft biome source exposes no registered possible biomes"); + } + return keys; + } + + static Set structureBiomeKeys(RegistryAccess access, String structureKey) { + if (access == null) { + throw new IllegalStateException("Minecraft registry access is unavailable"); + } + if (structureKey == null || structureKey.isBlank()) { + throw new IllegalArgumentException("Registered structure key must not be blank"); + } + Set keys = new LinkedHashSet<>(); + Registry registry = access.lookupOrThrow(Registries.STRUCTURE); + Structure structure = registry.getValue(Identifier.parse(structureKey)); + if (structure == null) { + throw new IllegalArgumentException("Registered structure does not exist: " + structureKey); + } + boolean hasFilterEntries = false; + for (Holder holder : structure.biomes()) { + hasFilterEntries = true; + Optional> key = holder.unwrapKey(); + if (key.isPresent()) { + keys.add(key.get().identifier().toString()); + } + } + // An empty biome filter is legal datapack content (opt-in structures whose tags only + // reference absent modded biomes); the structure is unreachable, not an error state. + if (keys.isEmpty() && hasFilterEntries) { + throw new IllegalStateException("Registered structure '" + structureKey + + "' has biome filter entries but none resolve to registered biome keys"); + } + return keys; + } + + static Set reachableStructureKeys(ServerLevel level, BiomeSource source) { + if (level == null) { + throw new IllegalStateException("Minecraft server level is unavailable"); + } + Set possible = possibleBiomeKeys(source); + return NativeStructureGenerationKeys.reachable(level, possible); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/IrisChunkGeneratorHeightCacheTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/IrisChunkGeneratorHeightCacheTest.java new file mode 100644 index 000000000..9730421ae --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/IrisChunkGeneratorHeightCacheTest.java @@ -0,0 +1,131 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.platform.bukkit.nms.v26_3_R1.IrisChunkGenerator; +import art.arcane.iris.generation.runtime.IrisComplex; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.GenerationSessionLease; +import art.arcane.iris.world.history.BoundaryColumnGeometry; +import art.arcane.iris.world.history.TerrainBoundarySignature; +import art.arcane.iris.testsupport.PlatformLeakGuard; +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.levelgen.Heightmap; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public final class IrisChunkGeneratorHeightCacheTest { + @Rule + public final PlatformLeakGuard leakGuard = PlatformLeakGuard.clean(); + + @BeforeClass + public static void bootstrap() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + NativeBlockTags.bindHeightmapFixtures(); + } + + @Test + public void repeatedNativeHeightSurvivesResolvedTerrainEvictionAndRuntimeChange() throws Exception { + Fixture fixture = fixture(); + LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32); + assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null)); + when(fixture.complex().resolvedTerrainColumn(-1, -33)) + .thenThrow(new IllegalStateException("Full terrain was evicted")); + + for (int index = 0; index < 100; index++) { + assertEquals(-11, fixture.generator().getBaseHeight( + -1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null)); + } + verify(fixture.complex(), times(1)).resolvedTerrainColumn(-1, -33); + + when(fixture.engine().getCacheID()).thenReturn(2); + doReturn(Optional.of(signature())).when(fixture.complex()).resolvedTerrainColumn(-1, -33); + assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null)); + verify(fixture.complex(), times(2)).resolvedTerrainColumn(-1, -33); + } + + @Test + public void ordinaryHeightRemainsLiveUntilResolvedTransitionTerrainExists() throws Exception { + Fixture fixture = fixture(); + LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32); + when(fixture.complex().resolvedTerrainColumn(0, 0)) + .thenReturn(Optional.empty(), Optional.empty(), Optional.of(signature())); + when(fixture.engine().getHeight(0, 0, false)).thenReturn(20, 25); + + assertEquals(5, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null)); + assertEquals(10, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null)); + assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null)); + assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null)); + verify(fixture.engine(), times(2)).getHeight(0, 0, false); + verify(fixture.complex(), times(3)).resolvedTerrainColumn(0, 0); + } + + @Test + public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception { + Fixture fixture = fixture(); + LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32); + for (int repeat = 0; repeat < 2; repeat++) { + assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null)); + assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null)); + assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null)); + assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null)); + assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, + LevelHeightAccessor.create(-16, 2), null)); + assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, + LevelHeightAccessor.create(-14, 3), null)); + } + verify(fixture.complex(), times(6)).resolvedTerrainColumn(0, 0); + } + + private static Fixture fixture() throws Exception { + Engine engine = mock(Engine.class); + IrisComplex complex = mock(IrisComplex.class); + when(engine.getComplex()).thenReturn(complex); + when(engine.getCacheID()).thenReturn(1); + when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop()); + when(complex.resolvedTerrainColumn(anyInt(), anyInt())).thenReturn(Optional.of(signature())); + IrisChunkGenerator generator = mock(IrisChunkGenerator.class, CALLS_REAL_METHODS); + set(generator, "engine", engine); + set(generator, "terrainHeights", new NativeTerrainHeightCache()); + return new Fixture(generator, engine, complex); + } + + private static void set(IrisChunkGenerator generator, String name, Object value) throws Exception { + Field field = IrisChunkGenerator.class.getDeclaredField(name); + field.setAccessible(true); + field.set(generator, value); + } + + private static TerrainBoundarySignature signature() { + BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-16, List.of( + new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false), + new BoundaryColumnGeometry.Voxel("minecraft:water", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water", false), + new BoundaryColumnGeometry.Voxel("minecraft:oak_leaves[persistent=true]", BoundaryColumnGeometry.Phase.SOLID, "", false), + new BoundaryColumnGeometry.Voxel("minecraft:short_grass", BoundaryColumnGeometry.Phase.SOLID, "", false), + new BoundaryColumnGeometry.Voxel("minecraft:light[level=15]", BoundaryColumnGeometry.Phase.SOLID, "", false))); + return new TerrainBoundarySignature( + new TerrainBoundarySignature.Column(0, 0, 4, 4, OptionalInt.of(1), OptionalInt.empty()), + new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(-16, 5, 1), + new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})), geometry); + } + + private record Fixture(IrisChunkGenerator generator, Engine engine, IrisComplex complex) { + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeGenerationWriteGuardTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeGenerationWriteGuardTest.java new file mode 100644 index 000000000..597c28eb8 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeGenerationWriteGuardTest.java @@ -0,0 +1,125 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.generation.runtime.DimensionStackContext; +import art.arcane.iris.generation.runtime.DimensionStackLayout; +import art.arcane.iris.generation.runtime.IrisComplex; +import art.arcane.iris.generation.runtime.IrisEngine; +import art.arcane.iris.world.history.GenerationActivation; +import art.arcane.iris.world.history.GenerationHistory; +import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.structure.object.IrisStaticObjectLayer; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.Optional; +import java.util.function.Predicate; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +public final class NativeGenerationWriteGuardTest { + @BeforeClass + public static void bootstrap() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void staticObjectsProtectRelativeHeightBeforeStackSampling() { + IrisStaticObjectLayer objects = mock(IrisStaticObjectLayer.class); + DimensionStackContext stack = mock(DimensionStackContext.class); + when(objects.contains(-3, 74, -7)).thenReturn(true); + + Predicate protectedPositions = NativeGenerationWriteGuard.protectedPositions(objects, stack, -64); + + assertTrue(protectedPositions.test(new BlockPos(-3, 10, -7))); + verifyNoInteractions(stack); + assertFalse(NativeGenerationWriteGuard.protectedPositions(objects, null, -64) + .test(new BlockPos(-3, 11, -7))); + } + + @Test + public void emptyStaticLayerSkipsContainsAndCachesEachSignedColumn() { + IrisStaticObjectLayer objects = mock(IrisStaticObjectLayer.class); + DimensionStackContext stack = mock(DimensionStackContext.class); + DimensionStackLayout negative = mock(DimensionStackLayout.class); + DimensionStackLayout positive = mock(DimensionStackLayout.class); + when(objects.isEmpty()).thenReturn(true); + when(stack.sample(-1, -1)).thenReturn(negative); + when(stack.sample(-1, 1)).thenReturn(positive); + when(negative.isHostFeatureProtectedY(64)).thenReturn(true); + when(positive.isHostFeatureProtectedY(65)).thenReturn(true); + + Predicate protectedPositions = NativeGenerationWriteGuard.protectedPositions(objects, stack, -64); + + assertTrue(protectedPositions.test(new BlockPos(-1, 0, -1))); + assertFalse(protectedPositions.test(new BlockPos(-1, 1, -1))); + assertTrue(protectedPositions.test(new BlockPos(-1, 1, 1))); + verify(stack, times(1)).sample(-1, -1); + verify(stack, times(1)).sample(-1, 1); + verify(objects, never()).contains(anyInt(), anyInt(), anyInt()); + } + + @Test + public void preTerrainSavedStampDistinguishesEarlierNativeStarts() { + IrisEngine engine = mock(IrisEngine.class); + GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class); + GenerationHistory history = mock(GenerationHistory.class); + GenerationActivation active = mock(GenerationActivation.class); + when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router)); + when(router.history()).thenReturn(history); + when(history.activeActivation()).thenReturn(active); + when(active.activationId()).thenReturn(4L); + assertTrue(NativeGenerationWriteGuard.isHistoricalStructure(engine, 3)); + assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 0)); + assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 4)); + assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 5)); + } + + @Test + public void partialNeighborsMayFinishButCompletedHistoricalNeighborRejectsPass() { + Engine engine = mock(Engine.class); + IrisComplex complex = mock(IrisComplex.class); + when(engine.getComplex()).thenReturn(complex); + WorldGenLevel region = mock(WorldGenLevel.class); + ChunkAccess partial = mock(ChunkAccess.class); + when(partial.getPersistedStatus()).thenReturn(ChunkStatus.TERRAIN); + when(region.getChunk(anyInt(), anyInt())).thenReturn(partial); + assertTrue(NativeGenerationWriteGuard.allowsDecoration(engine, region, new ChunkPos(0, 0))); + ChunkAccess complete = mock(ChunkAccess.class); + when(complete.getPersistedStatus()).thenReturn(ChunkStatus.FULL); + when(region.getChunk(1, -1)).thenReturn(complete); + assertFalse(NativeGenerationWriteGuard.allowsDecoration(engine, region, new ChunkPos(0, 0))); + } + + @Test + public void historicalNoiseMayFinishFeaturesButNeverReplaysNoiseOrCompletedStage() { + Engine engine = mock(Engine.class); + IrisComplex complex = mock(IrisComplex.class); + when(engine.getComplex()).thenReturn(complex); + ChunkAccess chunk = mock(ChunkAccess.class); + when(chunk.getPos()).thenReturn(new ChunkPos(-2, 3)); + when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.TERRAIN); + assertTrue(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.FEATURES)); + assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.TERRAIN)); + when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FEATURES); + assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.FEATURES)); + when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FULL); + assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.SPAWN)); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureFactoryTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureFactoryTest.java new file mode 100644 index 000000000..99aadec7f --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureFactoryTest.java @@ -0,0 +1,462 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import net.minecraft.world.level.levelgen.densityfunction.SamplerContext; +import java.util.Set; +import art.arcane.iris.structure.jigsaw.IrisJigsawConfiguration; +import art.arcane.iris.structure.placement.IrisStructureTerrain; +import com.mojang.datafixers.util.Either; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.MapCodec; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderOwner; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Vec3i; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.NoiseColumn; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.biome.FixedBiomeSource; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.blending.Blender; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +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.StructurePoolElementType; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure; +import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList; +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 org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class NativeStructureFactoryTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void genericSourceDoesNotRequireJigsawType() { + Structure source = new SwampHutStructure( + new Structure.StructureSettings(HolderSet.empty())); + + Structure configured = NativeStructureFactory.configure( + null, source, null, false, 80); + + assertSame(source, configured); + } + + @Test + public void jigsawOptionsFailClosedForNonJigsawSource() { + Structure source = new SwampHutStructure( + new Structure.StructureSettings(HolderSet.empty())); + + assertThrows(IllegalStateException.class, () -> NativeStructureFactory.configure( + null, source, new IrisJigsawConfiguration(), false, 80)); + } + + @Test + public void sourceMetadataReadsScalarAndAxisSpecificJigsawRanges() { + CompoundTag scalar = new CompoundTag(); + scalar.putInt("max_distance_from_center", 80); + CompoundTag axisSpecific = new CompoundTag(); + CompoundTag distance = new CompoundTag(); + distance.putInt("horizontal", 96); + distance.putInt("vertical", 64); + axisSpecific.put("max_distance_from_center", distance); + + assertEquals(80, NativeStructureFactory.sourceDistance(scalar, "horizontal")); + assertEquals(80, NativeStructureFactory.sourceDistance(scalar, "vertical")); + assertEquals(96, NativeStructureFactory.sourceDistance(axisSpecific, "horizontal")); + assertEquals(64, NativeStructureFactory.sourceDistance(axisSpecific, "vertical")); + } + + @Test + public void sourceMetadataIncludesAdjustBoundingBoxExpansion() { + Structure none = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty())); + Structure beardBox = new SwampHutStructure(new Structure.StructureSettings.Builder(HolderSet.empty()) + .terrainAdapation(TerrainAdjustment.BEARD_BOX) + .build()); + + assertEquals(0, NativeStructureFactory.horizontalReferenceExpansion(none)); + assertEquals(12, NativeStructureFactory.horizontalReferenceExpansion(beardBox)); + } + + @Test + public void templatePoolSpanIncludesWeightedListsFallbacksAndRotations() { + StructurePoolElement narrow = inlineElement(30, 8, 70); + StructurePoolElement wide = inlineElement(96, 8, 12); + ListPoolElement list = new ListPoolElement( + List.of(narrow, wide, EmptyPoolElement.INSTANCE), + StructureTemplatePool.Projection.RIGID); + BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:fallback"); + StructureTemplatePool fallback = new StructureTemplatePool( + fallbackHolder, + List.of(Pair.of(inlineElement(110, 8, 20), 1))); + fallbackHolder.bind(fallback); + BoundPoolHolder startHolder = new BoundPoolHolder("test:start"); + StructureTemplatePool start = new StructureTemplatePool( + fallbackHolder, + List.of(Pair.of(list, 5), Pair.of(EmptyPoolElement.INSTANCE, 1))); + startHolder.bind(start); + + assertEquals(110, NativeStructureTemplatePoolBounds.horizontalSpan(start, null)); + } + + @Test + public void oversizedInlineStartTemplateReportsItsFullSpan() { + BoundPoolHolder holder = new BoundPoolHolder("test:oversized"); + StructureTemplatePool pool = new StructureTemplatePool( + holder, + List.of(Pair.of(inlineElement(160, 8, 9), 1))); + holder.bind(pool); + + assertEquals(160, NativeStructureTemplatePoolBounds.horizontalSpan(pool, null)); + } + + @Test + public void sourceSpanIncludesOnlyAliasesThatCanReplaceTheStartPool() { + BoundPoolHolder emptyHolder = new BoundPoolHolder("test:empty"); + StructureTemplatePool empty = new StructureTemplatePool(emptyHolder, List.of()); + emptyHolder.bind(empty); + BoundPoolHolder startHolder = new BoundPoolHolder("test:start"); + StructureTemplatePool start = new StructureTemplatePool( + emptyHolder, List.of(Pair.of(inlineElement(20, 8, 12), 1))); + startHolder.bind(start); + BoundPoolHolder replacementHolder = new BoundPoolHolder("test:replacement"); + StructureTemplatePool replacement = new StructureTemplatePool( + emptyHolder, List.of(Pair.of(inlineElement(30, 8, 12), 1))); + replacementHolder.bind(replacement); + BoundPoolHolder unrelatedHolder = new BoundPoolHolder("test:unrelated"); + StructureTemplatePool unrelated = new StructureTemplatePool( + emptyHolder, List.of(Pair.of(inlineElement(200, 8, 12), 1))); + unrelatedHolder.bind(unrelated); + ResourceKey startKey = startHolder.unwrapKey().orElseThrow(); + ResourceKey replacementKey = replacementHolder.unwrapKey().orElseThrow(); + ResourceKey unrelatedKey = unrelatedHolder.unwrapKey().orElseThrow(); + ResourceKey childAlias = ResourceKey.create( + Registries.TEMPLATE_POOL, Identifier.parse("test:child_alias")); + List aliases = List.of( + PoolAliasBinding.direct(startKey, replacementKey), + PoolAliasBinding.direct(childAlias, unrelatedKey)); + Map, StructureTemplatePool> pools = Map.of( + replacementKey, replacement, + unrelatedKey, unrelated); + + assertEquals(30, NativeStructureTemplatePoolBounds.sourceHorizontalSpan( + null, start, startKey, aliases, pools::get)); + } + + @Test + public void sourceStartSpanDoesNotIncludeItsFallbackPool() { + BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:large_fallback"); + StructureTemplatePool fallback = new StructureTemplatePool( + fallbackHolder, List.of(Pair.of(inlineElement(180, 8, 10), 1))); + fallbackHolder.bind(fallback); + BoundPoolHolder startHolder = new BoundPoolHolder("test:small_start"); + StructureTemplatePool start = new StructureTemplatePool( + fallbackHolder, List.of(Pair.of(inlineElement(24, 8, 10), 1))); + startHolder.bind(start); + + assertEquals(180, NativeStructureTemplatePoolBounds.horizontalSpan(start, null)); + assertEquals(24, NativeStructureTemplatePoolBounds.sourceHorizontalSpan( + null, start, startHolder.unwrapKey().orElseThrow(), List.of(), ignored -> null)); + } + + @Test + public void customPoolElementUsesItsBoundingBoxCapability() { + BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:custom_fallback"); + StructureTemplatePool fallback = new StructureTemplatePool(fallbackHolder, List.of()); + fallbackHolder.bind(fallback); + BoundPoolHolder poolHolder = new BoundPoolHolder("test:custom"); + StructureTemplatePool pool = new StructureTemplatePool( + fallbackHolder, List.of(Pair.of(new BoundingPoolElement(37, 11), 1))); + poolHolder.bind(pool); + + assertEquals(37, NativeStructureTemplatePoolBounds.horizontalSpan(pool, null)); + } + + @Test + public void forcedGeneratorBypassesBiomeEligibilityWithoutFabricatingTerrain() { + ChunkGenerator delegate = new TestChunkGenerator(); + Holder biome = Holder.direct((Biome) null); + LevelHeightAccessor heightAccessor = LevelHeightAccessor.create(-64, 384); + ForcedStructureChunkGenerator generator = new ForcedStructureChunkGenerator( + delegate, biome); + + assertEquals(63, generator.getSeaLevel()); + assertEquals(64, generator.getBaseHeight( + 0, 0, Heightmap.Types.WORLD_SURFACE_WG, heightAccessor, null)); + NoiseColumn column = generator.getBaseColumn( + 0, 0, heightAccessor, null); + assertSame(Blocks.STONE, column.getBlock(80).getBlock()); + assertSame(Blocks.STONE, column.getBlock(81).getBlock()); + assertNotSame(delegate.getBiomeSource(), generator.getBiomeSource()); + } + + @Test + public void undergroundRelocationMovesGenericPiecesAndReturnsFreshStart() { + Structure source = new SwampHutStructure( + new Structure.StructureSettings(HolderSet.empty())); + SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), 0, 0); + StructureStart start = new StructureStart( + source, + new ChunkPos(0, 0), + 0, + new PiecesContainer(List.of(piece)) + ); + + StructureStart relocated = NativeStructureVerticalPlacer.relocateToMinY( + start, source, -20, LevelHeightAccessor.create(-64, 384)); + + assertNotSame(start, relocated); + assertEquals(-20, relocated.getPieces().getFirst().getBoundingBox().minY()); + assertEquals(-20, relocated.getBoundingBox().minY()); + } + + @Test + public void invalidGeneratedStartIsANormalSkippedCandidate() { + Structure source = new SwampHutStructure( + new Structure.StructureSettings(HolderSet.empty())); + StructureStart valid = new StructureStart( + source, + new ChunkPos(0, 0), + 0, + new PiecesContainer(List.of( + new SwampHutPiece(RandomSource.create(23L), 0, 0))) + ); + + assertFalse(NativeStructureStartInjector.isUsableGeneratedStart( + StructureStart.INVALID_START)); + assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(null)); + assertFalse(NativeStructureStartInjector.isUsableGeneratedStart( + new StructureStart(source, new ChunkPos(0, 0), 0, + new PiecesContainer(List.of())))); + assertTrue(NativeStructureStartInjector.isUsableGeneratedStart(valid)); + } + + @Test + public void unrepresentableGeneratedContentBecomesAnUnpublishedInvalidStart() { + Structure source = new SwampHutStructure( + new Structure.StructureSettings(HolderSet.empty())); + SwampHutPiece boundaryPiece = new SwampHutPiece(RandomSource.create(29L), 0, 0); + boundaryPiece.move(143 - boundaryPiece.getBoundingBox().maxX(), 0, 0); + StructureStart boundary = new StructureStart( + source, + new ChunkPos(0, 0), + 0, + new PiecesContainer(List.of(boundaryPiece)) + ); + SwampHutPiece oversizedPiece = new SwampHutPiece(RandomSource.create(31L), 0, 0); + oversizedPiece.move(144 - oversizedPiece.getBoundingBox().maxX(), 0, 0); + StructureStart oversized = new StructureStart( + source, + new ChunkPos(0, 0), + 0, + new PiecesContainer(List.of(oversizedPiece)) + ); + + StructureStart boundaryResult = NativeStructureReferenceEnvelope.wrapForPublication( + boundary, source, 0, new IrisStructureTerrain(), "test:boundary_content"); + + StructureStart result = NativeStructureReferenceEnvelope.wrapForPublication( + oversized, source, 0, + new IrisStructureTerrain(), + "test:oversized_content"); + + assertTrue(boundaryResult.isValid()); + assertSame(StructureStart.INVALID_START, result); + assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(result)); + assertEquals(1, oversized.getPieces().size()); + } + + private static final class TestChunkGenerator extends ChunkGenerator { + private TestChunkGenerator() { + super(new FixedBiomeSource(Holder.direct((Biome) null))); + } + + @Override + protected MapCodec codec() { + return MapCodec.unit(this); + } + + @Override + public void spawnOriginalMobs(WorldGenRegion worldGenRegion) { + } + + @Override + public int getGenDepth() { + return 384; + } + + @Override + public CompletableFuture buildTerrain( + ChunkAccess centerChunk, Blender blender, RandomState randomState, + StructureManager structureManager, BiomeManager biomeManager, WorldGenRegion region, Set> possibleBiomes) { + return CompletableFuture.completedFuture(centerChunk); + } + + @Override + public int getSeaLevel() { + return 63; + } + + @Override + public int getMinY() { + return -64; + } + + @Override + public int getBaseHeight(int x, int z, Heightmap.Types type, + LevelHeightAccessor heightAccessor, RandomState randomState) { + return 64; + } + + @Override + public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, + RandomState randomState) { + BlockState[] states = new BlockState[heightAccessor.getHeight()]; + for (int index = 0; index < states.length; index++) { + states[index] = Blocks.STONE.defaultBlockState(); + } + return new NoiseColumn(heightAccessor.getMinY(), states); + } + + @Override + public void addDebugScreenInfo(List result, RandomState randomState, + BlockPos feetPos, SamplerContext samplerContext) { + } + } + + private static StructurePoolElement inlineElement(int sizeX, int sizeY, int sizeZ) { + StructureTemplate template = new StructureTemplate(); + CompoundTag tag = new CompoundTag(); + ListTag size = new ListTag(); + size.add(IntTag.valueOf(sizeX)); + size.add(IntTag.valueOf(sizeY)); + size.add(IntTag.valueOf(sizeZ)); + tag.put("size", size); + template.load(BuiltInRegistries.BLOCK, tag); + return new InlinePoolElement(template); + } + + private static final class InlinePoolElement extends SinglePoolElement { + private final StructureTemplate inlineTemplate; + + private InlinePoolElement(StructureTemplate template) { + super(Either.right(template), Holder.direct(new StructureProcessorList(List.of())), + StructureTemplatePool.Projection.RIGID, Optional.empty()); + inlineTemplate = template; + } + + @Override + public BoundingBox getBoundingBox(StructureTemplateManager templateManager, + BlockPos position, Rotation rotation) { + return inlineTemplate.getBoundingBox( + new StructurePlaceSettings().setRotation(rotation), position); + } + } + + private static final class BoundingPoolElement extends StructurePoolElement { + private final int width; + private final int depth; + + private BoundingPoolElement(int width, int depth) { + super(StructureTemplatePool.Projection.RIGID); + this.width = width; + this.depth = depth; + } + + @Override + public Vec3i getSize(StructureTemplateManager templateManager, Rotation rotation) { + return new Vec3i(width, 1, depth); + } + + @Override + public List getShuffledJigsawBlocks( + StructureTemplateManager templateManager, BlockPos position, + Rotation rotation, RandomSource random) { + return List.of(); + } + + @Override + public BoundingBox getBoundingBox(StructureTemplateManager templateManager, + BlockPos position, Rotation rotation) { + return new BoundingBox( + position.getX(), position.getY(), position.getZ(), + position.getX() + width - 1, position.getY(), + position.getZ() + depth - 1); + } + + @Override + public boolean place(StructureTemplateManager templateManager, + WorldGenLevel world, + StructureManager structureManager, ChunkGenerator chunkGenerator, + BlockPos position, BlockPos pivot, Rotation rotation, + BoundingBox area, RandomSource random, + LiquidSettings liquidSettings, boolean keepJigsaws) { + return true; + } + + @Override + public StructurePoolElementType getType() { + return StructurePoolElementType.EMPTY; + } + } + + private static final class BoundPoolHolder extends Holder.Reference { + private BoundPoolHolder(String key) { + super(Type.STAND_ALONE, new HolderOwner<>() { + }, ResourceKey.create(Registries.TEMPLATE_POOL, Identifier.parse(key)), null); + } + + private void bind(StructureTemplatePool pool) { + bindValue(pool); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureGenerationKeysContractTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureGenerationKeysContractTest.java new file mode 100644 index 000000000..e4bc15a75 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureGenerationKeysContractTest.java @@ -0,0 +1,44 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement; +import net.minecraft.SharedConstants; +import net.minecraft.core.Vec3i; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.Optional; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class NativeStructureGenerationKeysContractTest { + @BeforeClass + public static void bootstrapMinecraft() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void frequencyGateUsesMappedAccessorAndRejectsNonPositivePlacements() { + assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.0F))); + assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(-0.1F))); + assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(Float.NaN))); + assertTrue(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.25F))); + } + + private static RandomSpreadStructurePlacement placement(float frequency) { + return new RandomSpreadStructurePlacement( + Vec3i.ZERO, + AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT, + frequency, + 1, + Optional.empty(), + 2, + 1, + RandomSpreadType.LINEAR); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureLocatePersistenceTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureLocatePersistenceTest.java new file mode 100644 index 000000000..5e1444a83 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureLocatePersistenceTest.java @@ -0,0 +1,197 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import net.minecraft.SharedConstants; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.ChunkPos; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class NativeStructureLocatePersistenceTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void storageProbeScansEachCandidateOnlyOnce() { + AtomicInteger reads = new AtomicInteger(); + CompoundTag chunk = new CompoundTag(); + chunk.putInt("DataVersion", SharedConstants.getCurrentVersion().dataVersion().version()); + chunk.putString("Status", "full"); + CompoundTag start = new CompoundTag(); + start.putString("id", "minecraft:village_plains"); + start.putInt("references", 0); + CompoundTag starts = new CompoundTag(); + starts.put("minecraft:village_plains", start); + CompoundTag structures = new CompoundTag(); + structures.put("starts", starts); + chunk.put("structures", structures); + NativeStructureLocatePersistence.ProbeBudget budget = + new NativeStructureLocatePersistence.ProbeBudget( + 512, + (chunkPos, visitor) -> { + reads.incrementAndGet(); + chunk.acceptAsRoot(visitor); + return CompletableFuture.completedFuture(null); + }); + ChunkPos chunkPos = new ChunkPos(7, -11); + + assertTrue(budget.acceptsStored( + null, chunkPos, "minecraft:village_plains", true)); + assertTrue(budget.acceptsStored( + null, chunkPos, "minecraft:village_plains", true)); + assertEquals(1, reads.get()); + assertEquals(1, budget.used()); + } + + @Test + public void legacyStoredChunkIsDatafixedBeforeItsStartIsClassified() { + AtomicBoolean datafixed = new AtomicBoolean(); + CompoundTag legacy = new CompoundTag(); + legacy.putInt("DataVersion", 100); + legacy.putString("Status", "full"); + NativeStructureLocatePersistence.ProbeBudget budget = + new NativeStructureLocatePersistence.ProbeBudget( + 512, + (chunkPos, visitor) -> { + legacy.acceptAsRoot(visitor); + return CompletableFuture.completedFuture(null); + }, + (level, storedChunk) -> { + datafixed.set(true); + CompoundTag fixed = storedChunk.copy(); + fixed.putInt("DataVersion", + SharedConstants.getCurrentVersion().dataVersion().version()); + CompoundTag start = new CompoundTag(); + start.putString("id", "minecraft:village_plains"); + start.putInt("references", 1); + CompoundTag starts = new CompoundTag(); + starts.put("minecraft:village_plains", start); + CompoundTag structures = new CompoundTag(); + structures.put("starts", starts); + fixed.put("structures", structures); + return fixed; + }); + + assertFalse(budget.acceptsStored( + null, new ChunkPos(3, 4), "minecraft:village_plains", true)); + assertTrue(datafixed.get()); + } + + @Test + public void partialDatafixFailureFallsBackToSelectedChunkVerification() { + CompoundTag legacy = new CompoundTag(); + legacy.putInt("DataVersion", 100); + NativeStructureLocatePersistence.ProbeBudget budget = + new NativeStructureLocatePersistence.ProbeBudget( + 512, + (chunkPos, visitor) -> { + legacy.acceptAsRoot(visitor); + return CompletableFuture.completedFuture(null); + }, + (level, storedChunk) -> { + throw new IllegalStateException("broken partial datafix"); + }); + + assertTrue(budget.acceptsStored( + null, new ChunkPos(9, -2), "minecraft:village_plains", true)); + } + + @Test + public void missingStartsContainerFallsBackToSelectedChunkVerification() { + CompoundTag chunk = currentChunk(); + NativeStructureLocatePersistence.ProbeBudget budget = budget(chunk); + + assertTrue(budget.acceptsStored( + null, new ChunkPos(5, 8), "minecraft:village_plains", false)); + assertTrue(budget.acceptsStored( + null, new ChunkPos(5, 8), "minecraft:village_plains", true)); + } + + @Test + public void presentEmptyStartsContainerRejectsTheCandidateWithoutLoading() { + CompoundTag chunk = currentChunk(); + CompoundTag structures = new CompoundTag(); + structures.put("starts", new CompoundTag()); + chunk.put("structures", structures); + NativeStructureLocatePersistence.ProbeBudget budget = budget(chunk); + + assertFalse(budget.acceptsStored( + null, new ChunkPos(-3, 2), "minecraft:village_plains", false)); + } + + @Test + public void collisionTombstoneIsRejectedForNormalAndUnexploredLocate() { + CompoundTag chunk = currentChunk(); + CompoundTag tombstone = new CompoundTag(); + tombstone.putString("id", "INVALID"); + CompoundTag starts = new CompoundTag(); + starts.put("minecraft:village_plains", tombstone); + CompoundTag structures = new CompoundTag(); + structures.put("starts", starts); + chunk.put("structures", structures); + ChunkPos chunkPos = new ChunkPos(12, -9); + + assertFalse(budget(chunk).acceptsStored( + null, chunkPos, "minecraft:village_plains", false)); + assertFalse(budget(chunk).acceptsStored( + null, chunkPos, "minecraft:village_plains", true)); + } + + @Test + public void malformedNonCompoundStartIsRejectedWithoutLoading() { + CompoundTag chunk = currentChunk(); + CompoundTag starts = new CompoundTag(); + starts.putInt("minecraft:village_plains", 1); + CompoundTag structures = new CompoundTag(); + structures.put("starts", starts); + chunk.put("structures", structures); + + assertFalse(budget(chunk).acceptsStored( + null, new ChunkPos(1, 1), "minecraft:village_plains", false)); + } + + @Test + public void referencedStoredStartIsOnlyRejectedForUnexploredLocate() { + CompoundTag chunk = currentChunk(); + CompoundTag start = new CompoundTag(); + start.putString("id", "minecraft:village_plains"); + start.putInt("references", 1); + CompoundTag starts = new CompoundTag(); + starts.put("minecraft:village_plains", start); + CompoundTag structures = new CompoundTag(); + structures.put("starts", starts); + chunk.put("structures", structures); + ChunkPos chunkPos = new ChunkPos(-4, -7); + + assertTrue(budget(chunk).acceptsStored( + null, chunkPos, "minecraft:village_plains", false)); + assertFalse(budget(chunk).acceptsStored( + null, chunkPos, "minecraft:village_plains", true)); + } + + private static CompoundTag currentChunk() { + CompoundTag chunk = new CompoundTag(); + chunk.putInt("DataVersion", SharedConstants.getCurrentVersion().dataVersion().version()); + return chunk; + } + + private static NativeStructureLocatePersistence.ProbeBudget budget(CompoundTag chunk) { + return new NativeStructureLocatePersistence.ProbeBudget( + 512, + (chunkPos, visitor) -> { + chunk.acceptAsRoot(visitor); + return CompletableFuture.completedFuture(null); + }); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureOwnershipFingerprintTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureOwnershipFingerprintTest.java new file mode 100644 index 000000000..ae2be95af --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureOwnershipFingerprintTest.java @@ -0,0 +1,346 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord; +import art.arcane.iris.structure.nativegen.NativeStructureStartPlan; +import art.arcane.iris.structure.nativegen.IrisNativeStructure; +import art.arcane.iris.structure.placement.IrisStructurePlacement; +import art.arcane.iris.structure.placement.IrisStructureTerrain; +import art.arcane.iris.structure.placement.IrisStructureTerrainMode; +import art.arcane.volmlib.util.collection.KList; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Vec3i; +import net.minecraft.nbt.ListTag; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import net.minecraft.world.level.levelgen.RandomSupport; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElementType; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure; +import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class NativeStructureOwnershipFingerprintTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void monumentFingerprintSurvivesSourceYRegenerationWithoutSyntheticPieces() { + long seed = 8372619L; + ChunkPos origin = new ChunkPos(9, -4); + OceanMonumentStructure structure = new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + StructureStart generated = monumentStart(structure, origin, seed); + NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel( + generated, 0, 80, -64, 320); + int alignedLocatorY = NativeStructureOwnershipFingerprint.locatorY(generated); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setHorizontalPadding(24); + StructureStart wrapped = NativeStructureReferenceEnvelope.wrap( + generated, + structure, + 0, + terrain); + String beforeReload = NativeStructureOwnershipFingerprint.fingerprint( + "minecraft:monument", wrapped); + + ListTag serializedPieces = (ListTag) new PiecesContainer(wrapped.getPieces()).save(null); + PiecesContainer loadedPieces = PiecesContainer.load(serializedPieces, null); + assertEquals(1, loadedPieces.pieces().size()); + assertTrue(loadedPieces.pieces().getFirst() + instanceof OceanMonumentPieces.MonumentBuilding); + PiecesContainer regeneratedPieces = OceanMonumentStructure.regeneratePiecesAfterLoad( + origin, seed, loadedPieces); + StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces); + String afterReload = NativeStructureOwnershipFingerprint.fingerprint( + "minecraft:monument", reloaded); + NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture( + "minecraft:monument", + wrapped, + plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()), + NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain)); + + assertEquals(beforeReload, afterReload); + assertTrue(NativeStructureOwnershipFingerprint.matches(ownership, reloaded)); + assertNotEquals(alignedLocatorY, NativeStructureOwnershipFingerprint.locatorY(reloaded)); + assertEquals(alignedLocatorY, ownership.locatorY()); + assertEquals(1, reloaded.getPieces().size()); + assertEquals(1, wrapped.getPieces().size()); + } + + @Test + public void sameKeyAndOriginStillRequiresMatchingContentIdentity() { + long seed = 18273L; + ChunkPos origin = new ChunkPos(2, 3); + OceanMonumentStructure structure = new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + StructureStart expected = monumentStart(structure, origin, seed); + StructureStart moved = monumentStart(structure, origin, seed); + for (StructurePiece piece : moved.getPieces()) { + piece.move(1, 0, 0); + } + NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture( + "minecraft:monument", + expected, + plan(origin, NativeStructureReferenceEnvelope.contentBounds(expected).minY()), + expected.getBoundingBox()); + + assertFalse(NativeStructureOwnershipFingerprint.matches(ownership, moved)); + } + + @Test + public void persistedEnvelopeRepairsReloadedMonumentReferenceOutsideLiveBounds() { + long seed = 772931L; + ChunkPos origin = new ChunkPos(4, -7); + OceanMonumentStructure structure = new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + StructureStart generated = monumentStart(structure, origin, seed); + NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel( + generated, 0, 80, -64, 320); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setHorizontalPadding(24); + StructureStart wrapped = NativeStructureReferenceEnvelope.wrap( + generated, + structure, + 0, + terrain); + NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture( + "minecraft:monument", + wrapped, + plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()), + NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain)); + PiecesContainer regeneratedPieces = OceanMonumentStructure.regeneratePiecesAfterLoad( + origin, seed, new PiecesContainer(wrapped.getPieces())); + StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces); + ChunkPos target = outsideLiveBounds(ownership, reloaded); + + assertNotNull(target); + assertTrue(NativeStructureReferenceRepair.requiresReference( + target, "minecraft:monument", reloaded, ownership)); + assertFalse(NativeStructureReferenceRepair.requiresReference( + target, "minecraft:monument", reloaded, null)); + } + + @Test + public void overlappingManualStartsSixteenChunksApartRemainIndependentlyReferenced() { + OceanMonumentStructure structure = new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + ChunkPos westOrigin = new ChunkPos(0, 0); + ChunkPos eastOrigin = new ChunkPos(16, 0); + StructureStart west = monumentStart(structure, westOrigin, 317L); + StructureStart east = monumentStart(structure, eastOrigin, 941L); + west.getPieces().getFirst().move(112, 0, 0); + east.getPieces().getFirst().move(-96, 0, 0); + NativeStructureOwnershipRecord westOwnership = ownership(westOrigin, west, structure); + NativeStructureOwnershipRecord eastOwnership = ownership(eastOrigin, east, structure); + ChunkPos shared = new ChunkPos(8, 0); + + assertTrue(NativeStructureReferenceEnvelope.contentBounds(west).intersects( + NativeStructureReferenceEnvelope.contentBounds(east))); + assertTrue(NativeStructureReferenceRepair.requiresReference( + shared, "minecraft:monument", west, westOwnership)); + assertTrue(NativeStructureReferenceRepair.requiresReference( + shared, "minecraft:monument", east, eastOwnership)); + } + + @Test + public void denseAdjacentManualStartsDoNotSuppressEachOther() { + OceanMonumentStructure structure = new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + ChunkPos firstOrigin = new ChunkPos(0, 0); + ChunkPos secondOrigin = new ChunkPos(1, 0); + StructureStart first = monumentStart(structure, firstOrigin, 17L); + StructureStart second = monumentStart(structure, secondOrigin, 23L); + NativeStructureOwnershipRecord firstOwnership = ownership(firstOrigin, first, structure); + NativeStructureOwnershipRecord secondOwnership = ownership(secondOrigin, second, structure); + ChunkPos shared = new ChunkPos(0, 0); + + assertTrue(NativeStructureReferenceEnvelope.contentBounds(first).intersects( + NativeStructureReferenceEnvelope.contentBounds(second))); + assertTrue(NativeStructureReferenceRepair.requiresReference( + shared, "minecraft:monument", first, firstOwnership)); + assertTrue(NativeStructureReferenceRepair.requiresReference( + shared, "minecraft:monument", second, secondOwnership)); + } + + @Test + public void customPoolElementIdentityDoesNotDependOnProcessLocalToString() { + OceanMonumentStructure structure = new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + ChunkPos origin = new ChunkPos(3, -2); + StructureStart first = poolStart(structure, origin, new IdentityStringPoolElement()); + StructureStart reloaded = poolStart(structure, origin, new IdentityStringPoolElement()); + + assertEquals( + NativeStructureOwnershipFingerprint.fingerprint("test:custom_pool", first), + NativeStructureOwnershipFingerprint.fingerprint("test:custom_pool", reloaded)); + } + + @Test + public void namedPoolTemplateRemainsPartOfTheStableFingerprint() { + OceanMonumentStructure structure = new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + ChunkPos origin = new ChunkPos(3, -2); + StructurePoolElement firstElement = StructurePoolElement.single("test:first") + .apply(StructureTemplatePool.Projection.RIGID); + StructurePoolElement secondElement = StructurePoolElement.single("test:second") + .apply(StructureTemplatePool.Projection.RIGID); + + assertNotEquals( + NativeStructureOwnershipFingerprint.fingerprint( + "test:named_pool", poolStart(structure, origin, firstElement)), + NativeStructureOwnershipFingerprint.fingerprint( + "test:named_pool", poolStart(structure, origin, secondElement))); + } + + private static StructureStart monumentStart(OceanMonumentStructure structure, + ChunkPos origin, long seed) { + WorldgenRandom random = new WorldgenRandom( + new LegacyRandomSource(RandomSupport.generateUniqueSeed())); + random.setLargeFeatureSeed(seed, origin.x(), origin.z()); + Direction orientation = Direction.Plane.HORIZONTAL.getRandomDirection(random); + OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding( + random, + origin.getMinBlockX() - 29, + origin.getMinBlockZ() - 29, + orientation + ); + return new StructureStart( + structure, + origin, + 0, + new PiecesContainer(List.of(building)) + ); + } + + private static NativeStructureStartPlan plan(ChunkPos origin, int baseY) { + IrisNativeStructure source = new IrisNativeStructure() + .setStructure("minecraft:monument") + .setWeight(1); + IrisStructurePlacement placement = new IrisStructurePlacement() + .setPlacementId("monument-test") + .setNativeStructures(new KList().qadd(source)); + return new NativeStructureStartPlan( + placement, + source, + origin.x(), + origin.z(), + baseY + ); + } + + private static NativeStructureOwnershipRecord ownership( + ChunkPos origin, StructureStart start, OceanMonumentStructure structure) { + return NativeStructureOwnershipFingerprint.capture( + "minecraft:monument", + start, + plan(origin, NativeStructureReferenceEnvelope.contentBounds(start).minY()), + NativeStructureReferenceEnvelope.referenceBounds( + start, structure, new IrisStructureTerrain()) + ); + } + + private static StructureStart poolStart( + OceanMonumentStructure structure, ChunkPos origin, StructurePoolElement element) { + BlockPos position = new BlockPos( + origin.getMiddleBlockX(), 64, origin.getMiddleBlockZ()); + PoolElementStructurePiece piece = new PoolElementStructurePiece( + null, element, position, 0, Rotation.NONE, + new BoundingBox(position), LiquidSettings.APPLY_WATERLOGGING); + return new StructureStart( + structure, origin, 0, new PiecesContainer(List.of(piece))); + } + + private static final class IdentityStringPoolElement extends StructurePoolElement { + private IdentityStringPoolElement() { + super(StructureTemplatePool.Projection.RIGID); + } + + @Override + public Vec3i getSize(StructureTemplateManager templateManager, Rotation rotation) { + return new Vec3i(1, 1, 1); + } + + @Override + public List getShuffledJigsawBlocks( + StructureTemplateManager templateManager, BlockPos position, + Rotation rotation, RandomSource random) { + return List.of(); + } + + @Override + public BoundingBox getBoundingBox(StructureTemplateManager templateManager, + BlockPos position, Rotation rotation) { + return new BoundingBox(position); + } + + @Override + public boolean place(StructureTemplateManager templateManager, + WorldGenLevel world, StructureManager structureManager, + ChunkGenerator chunkGenerator, BlockPos position, BlockPos pivot, + Rotation rotation, BoundingBox area, RandomSource random, + LiquidSettings liquidSettings, boolean keepJigsaws) { + return true; + } + + @Override + public StructurePoolElementType getType() { + return StructurePoolElementType.EMPTY; + } + + @Override + public String toString() { + return "IdentityStringPoolElement@" + + Integer.toHexString(System.identityHashCode(this)); + } + } + + private static ChunkPos outsideLiveBounds(NativeStructureOwnershipRecord ownership, + StructureStart start) { + for (int chunkX = ownership.referenceMinChunkX(); + chunkX <= ownership.referenceMaxChunkX(); chunkX++) { + for (int chunkZ = ownership.referenceMinChunkZ(); + chunkZ <= ownership.referenceMaxChunkZ(); chunkZ++) { + ChunkPos candidate = new ChunkPos(chunkX, chunkZ); + if (!start.getBoundingBox().intersects( + candidate.getMinBlockX(), candidate.getMinBlockZ(), + candidate.getMaxBlockX(), candidate.getMaxBlockZ())) { + return candidate; + } + } + } + return null; + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureOwnershipRecoveryTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureOwnershipRecoveryTest.java new file mode 100644 index 000000000..b692ef480 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureOwnershipRecoveryTest.java @@ -0,0 +1,314 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord; +import art.arcane.iris.structure.nativegen.NativeStructureStartPlan; +import art.arcane.iris.structure.placement.StructurePlacementGrid; +import art.arcane.iris.structure.nativegen.IrisNativeStructure; +import art.arcane.iris.structure.placement.IrisStructurePlacement; +import art.arcane.iris.structure.placement.IrisStructureTerrain; +import art.arcane.iris.structure.placement.IrisStructureTerrainMode; +import art.arcane.volmlib.util.collection.KList; +import net.minecraft.SharedConstants; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderSet; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import net.minecraft.world.level.levelgen.RandomSupport; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class NativeStructureOwnershipRecoveryTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void missingSidecarRecoversExactPolicyEnvelopeAndLocatorAfterReload() { + long seed = 5519231L; + ChunkPos origin = new ChunkPos(7, -5); + OceanMonumentStructure structure = structure(); + StructureStart expected = monumentStart(structure, origin, seed); + NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel( + expected, 0, 80, -64, 320); + int expectedLocatorY = NativeStructureOwnershipFingerprint.locatorY(expected); + NativeStructureStartPlan plan = plan(origin, "recovery-exact", 24); + StructureStart reloaded = new StructureStart( + structure, + origin, + expected.getReferences(), + OceanMonumentStructure.regeneratePiecesAfterLoad( + origin, seed, new PiecesContainer(expected.getPieces())) + ); + + NativeStructureOwnershipRecord recovered = + NativeStructureOwnershipRecovery.proveCandidate( + "minecraft:monument", structure, reloaded, + plan, expected, false); + + assertNotNull(recovered); + assertEquals(StructurePlacementGrid.placementIdentity(plan.placement()), + recovered.placementIdentity()); + assertEquals(expectedLocatorY, recovered.locatorY()); + assertNotEquals(NativeStructureOwnershipFingerprint.locatorY(reloaded), + recovered.locatorY()); + assertEquals(IrisStructureTerrainMode.FORCE_CARVE, + recovered.restoredDecision().terrain().resolvedMode()); + assertEquals(24, recovered.restoredDecision().terrain().getHorizontalPadding()); + assertTrue(recovered.referenceMinChunkX() + < NativeStructureReferenceEnvelope.contentBounds(reloaded).minX() >> 4); + } + + @Test + public void naturalSameKeyStartRemainsUnownedWhenOriginIsAmbiguous() { + long seed = 22817L; + ChunkPos origin = new ChunkPos(-3, 6); + OceanMonumentStructure structure = structure(); + StructureStart natural = monumentStart(structure, origin, seed); + NativeStructureStartPlan plan = plan(origin, "natural-ambiguous", 0); + + NativeStructureOwnershipRecord recovered = + NativeStructureOwnershipRecovery.proveCandidate( + "minecraft:monument", structure, natural, + plan, natural, true); + + assertNull(recovered); + } + + @Test + public void changedPlacementIdentityOrGeometryCannotRecoverOwnership() { + long seed = 991723L; + ChunkPos origin = new ChunkPos(2, 4); + OceanMonumentStructure structure = structure(); + StructureStart expected = monumentStart(structure, origin, seed); + StructureStart changedGeometry = monumentStart(structure, origin, seed); + for (StructurePiece piece : changedGeometry.getPieces()) { + piece.move(1, 0, 0); + } + NativeStructureStartPlan originalIdentity = plan( + origin, "original-identity", 0); + NativeStructureStartPlan changedIdentity = plan( + new ChunkPos(origin.x() + 1, origin.z()), "changed-identity", 0); + + assertNotEquals( + StructurePlacementGrid.placementIdentity(originalIdentity.placement()), + StructurePlacementGrid.placementIdentity(changedIdentity.placement())); + assertNull(NativeStructureOwnershipRecovery.proveCandidate( + "minecraft:monument", structure, expected, + changedIdentity, expected, false)); + assertNull(NativeStructureOwnershipRecovery.proveCandidate( + "minecraft:monument", structure, changedGeometry, + plan(origin, "geometry-check", 0), expected, false)); + } + + @Test + public void recoveredEnvelopeStillRequiresExactFingerprint() { + long seed = 81337L; + ChunkPos origin = new ChunkPos(-8, -9); + OceanMonumentStructure structure = structure(); + StructureStart expected = monumentStart(structure, origin, seed); + NativeStructureOwnershipRecord recovered = + NativeStructureOwnershipRecovery.proveCandidate( + "minecraft:monument", structure, expected, + plan(origin, "fingerprint-check", 16), expected, false); + assertNotNull(recovered); + + StructureStart moved = monumentStart(structure, origin, seed); + for (StructurePiece piece : moved.getPieces()) { + piece.move(0, 0, 1); + } + + assertFalse(NativeStructureOwnershipFingerprint.matches(recovered, moved)); + } + + @Test + public void staleVacuumEnvelopeRefreshesWithoutReplacingOwnershipIdentity() { + String structureKey = "minecraft:monument"; + long seed = 648231L; + ChunkPos origin = new ChunkPos(5, -6); + OceanMonumentStructure structure = structure(); + StructureStart start = monumentStart(structure, origin, seed); + NativeStructureStartPlan plan = plan( + origin, "vacuum-envelope-refresh", IrisStructureTerrainMode.VACUUM, 0); + BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(start); + NativeStructureOwnershipRecord stale = NativeStructureOwnershipFingerprint.capture( + structureKey, start, plan, content); + + NativeStructureOwnershipRecord refreshed = + NativeStructureOwnershipRecovery.refreshReferenceEnvelope( + structureKey, structure, start, stale); + + assertNotNull(refreshed); + assertNotEquals(stale, refreshed); + assertEquals(stale.schema(), refreshed.schema()); + assertEquals(stale.ownershipKey(), refreshed.ownershipKey()); + assertEquals(stale.placementIdentity(), refreshed.placementIdentity()); + assertEquals(stale.baseY(), refreshed.baseY()); + assertEquals(stale.locatorY(), refreshed.locatorY()); + assertEquals(stale.contentFingerprint(), refreshed.contentFingerprint()); + assertEquals(stale.decision(), refreshed.decision()); + assertEquals(IrisStructureTerrainMode.VACUUM, + refreshed.restoredDecision().terrain().resolvedMode()); + BoundingBox expected = NativeStructureReferenceEnvelope.referenceBounds( + start, structure, plan.placement().resolvedTerrain(), structureKey); + assertEquals(expected.minX() >> 4, refreshed.referenceMinChunkX()); + assertEquals(expected.maxX() >> 4, refreshed.referenceMaxChunkX()); + assertEquals(expected.minZ() >> 4, refreshed.referenceMinChunkZ()); + assertEquals(expected.maxZ() >> 4, refreshed.referenceMaxChunkZ()); + assertTrue(hasExpandedCoverage(stale, refreshed)); + assertSame(refreshed, NativeStructureOwnershipRecovery.refreshReferenceEnvelope( + structureKey, structure, start, refreshed)); + } + + @Test + public void staleEnvelopeCannotRefreshAgainstDifferentContent() { + String structureKey = "minecraft:monument"; + long seed = 412987L; + ChunkPos origin = new ChunkPos(-2, 7); + OceanMonumentStructure structure = structure(); + StructureStart expected = monumentStart(structure, origin, seed); + NativeStructureStartPlan plan = plan( + origin, "vacuum-content-check", IrisStructureTerrainMode.VACUUM, 0); + NativeStructureOwnershipRecord stale = NativeStructureOwnershipFingerprint.capture( + structureKey, expected, plan, + NativeStructureReferenceEnvelope.contentBounds(expected)); + StructureStart moved = monumentStart(structure, origin, seed); + for (StructurePiece piece : moved.getPieces()) { + piece.move(1, 0, 0); + } + + assertNull(NativeStructureOwnershipRecovery.refreshReferenceEnvelope( + structureKey, structure, moved, stale)); + } + + @Test + public void currentNonVacuumEnvelopeRemainsThePersistedAuthority() { + String structureKey = "minecraft:monument"; + long seed = 927451L; + ChunkPos origin = new ChunkPos(3, 8); + OceanMonumentStructure structure = structure(); + StructureStart start = monumentStart(structure, origin, seed); + NativeStructureStartPlan plan = plan(origin, "current-force-carve", 24); + BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds( + start, structure, plan.placement().resolvedTerrain(), structureKey); + NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture( + structureKey, start, plan, envelope); + + assertSame(ownership, NativeStructureOwnershipRecovery.refreshReferenceEnvelope( + structureKey, structure, start, ownership)); + } + + @Test + public void clippedVacuumEnvelopeRemainsStableAtTheReferenceLimit() { + String structureKey = "minecraft:monument"; + long seed = 381729L; + ChunkPos origin = new ChunkPos(0, 0); + OceanMonumentStructure structure = structure(); + StructureStart start = monumentStart(structure, origin, seed); + BoundingBox initial = NativeStructureReferenceEnvelope.contentBounds(start); + int maximumReferenceBlockX = ((origin.x() + + NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS) << 4) + 15; + int shiftX = maximumReferenceBlockX - initial.maxX(); + for (StructurePiece piece : start.getPieces()) { + piece.move(shiftX, 0, 0); + } + NativeStructureStartPlan plan = plan( + origin, "clipped-vacuum", IrisStructureTerrainMode.VACUUM, 0); + BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds( + start, structure, plan.placement().resolvedTerrain(), structureKey); + NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture( + structureKey, start, plan, envelope); + + assertEquals(origin.x() + NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS, + ownership.referenceMaxChunkX()); + assertSame(ownership, NativeStructureOwnershipRecovery.refreshReferenceEnvelope( + structureKey, structure, start, ownership)); + } + + private static OceanMonumentStructure structure() { + return new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + } + + private static StructureStart monumentStart(OceanMonumentStructure structure, + ChunkPos origin, long seed) { + WorldgenRandom random = new WorldgenRandom( + new LegacyRandomSource(RandomSupport.generateUniqueSeed())); + random.setLargeFeatureSeed(seed, origin.x(), origin.z()); + Direction orientation = Direction.Plane.HORIZONTAL.getRandomDirection(random); + OceanMonumentPieces.MonumentBuilding building = + new OceanMonumentPieces.MonumentBuilding( + random, + origin.getMinBlockX() - 29, + origin.getMinBlockZ() - 29, + orientation + ); + return new StructureStart( + structure, origin, 0, new PiecesContainer(List.of(building))); + } + + private static NativeStructureStartPlan plan(ChunkPos origin, + String placementId, + int horizontalPadding) { + return plan(origin, placementId, + IrisStructureTerrainMode.FORCE_CARVE, horizontalPadding); + } + + private static NativeStructureStartPlan plan(ChunkPos origin, + String placementId, + IrisStructureTerrainMode terrainMode, + int horizontalPadding) { + IrisNativeStructure source = new IrisNativeStructure() + .setStructure("minecraft:monument") + .setWeight(1); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(terrainMode) + .setHorizontalPadding(horizontalPadding); + IrisStructurePlacement placement = new IrisStructurePlacement() + .setPlacementId(placementId) + .setTerrain(terrain) + .setNativeStructures(new KList().qadd(source)); + return new NativeStructureStartPlan( + placement, + source, + origin.x(), + origin.z(), + NativeStructureReferenceEnvelope.contentBounds( + monumentStart(structure(), origin, 1L)).minY() + ); + } + + private static boolean hasExpandedCoverage( + NativeStructureOwnershipRecord stale, + NativeStructureOwnershipRecord refreshed) { + for (int chunkX = refreshed.referenceMinChunkX(); + chunkX <= refreshed.referenceMaxChunkX(); chunkX++) { + for (int chunkZ = refreshed.referenceMinChunkZ(); + chunkZ <= refreshed.referenceMaxChunkZ(); chunkZ++) { + if (!stale.covers(chunkX, chunkZ)) { + return true; + } + } + } + return false; + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorEncaseTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorEncaseTest.java new file mode 100644 index 000000000..61059a5f1 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorEncaseTest.java @@ -0,0 +1,532 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.generation.terrain.IrisMaterialPalette; +import art.arcane.iris.structure.placement.IrisStructureTerrain; +import art.arcane.iris.structure.placement.IrisStructureTerrainMode; +import art.arcane.iris.structure.placement.IrisStructureYBand; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.dimension.BuiltinDimensionTypes; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +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.templatesystem.BlockIgnoreProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.util.BitSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class NativeStructurePostProcessorEncaseTest { + private static final long TEST_SEED = 1234L; + + @BeforeClass + public static void bootstrapMinecraft() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void encaseFillsAirAndLiquidWithoutOverwritingExistingTerrain() { + StructureStart start = start(TerrainAdjustment.BURY, 64); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + BoundingBox area = new BoundingBox( + bounds.minX() - 1, bounds.minY() - 1, bounds.minZ() - 1, + bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1); + Map blocks = new HashMap<>(); + put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.DIRT.defaultBlockState()); + put(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ(), Blocks.WATER.defaultBlockState()); + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), area, "minecraft:stronghold", start, + new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.ENCASE) + .setHorizontalPadding(1) + .setCeilingPadding(1) + .setFloorPadding(1), + null); + + assertEquals(Blocks.DIRT.defaultBlockState(), + state(blocks, bounds.minX(), bounds.minY(), bounds.minZ())); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ())); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.minX() - 1, bounds.minY() - 1, bounds.minZ() - 1)); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1)); + } + + @Test + public void defaultEncasePaletteSplitsStoneAboveZeroAndDeepslateBelow() { + StructureStart start = start(TerrainAdjustment.BURY, -1); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + Map blocks = new HashMap<>(); + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), bounds, "minecraft:stronghold", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.ENCASE), + null); + + assertEquals(-1, bounds.minY()); + assertEquals(Blocks.DEEPSLATE.defaultBlockState(), + state(blocks, bounds.minX(), -1, bounds.minZ())); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.minX(), 0, bounds.minZ())); + } + + @Test + public void defaultEncaseMaterialFollowsVanillaDimensionTerrain() { + assertEquals(Blocks.NETHERRACK.defaultBlockState(), + NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.NETHER, 64)); + assertEquals(Blocks.END_STONE.defaultBlockState(), + NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.END, 64)); + assertEquals(Blocks.STONE.defaultBlockState(), + NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.OVERWORLD, 64)); + } + + @Test + public void sourceEncaseMaterialFollowsNearbyIrisTerrain() { + Map blocks = new HashMap<>(); + BlockPos fill = new BlockPos(4, 64, 8); + put(blocks, fill.getX() + 3, fill.getY(), fill.getZ(), Blocks.TUFF.defaultBlockState()); + + assertEquals(Blocks.TUFF.defaultBlockState(), + NativeStructureTerrainIntegrator.sourceEncaseBlock(world(blocks), fill)); + } + + @Test + public void sourceMaterialSnapshotIgnoresPreviouslyDecoratedAdjacentChunks() { + Map blocks = new HashMap<>(); + BlockPos fill = new BlockPos(0, 64, 8); + put(blocks, -1, 64, 8, Blocks.OAK_PLANKS.defaultBlockState()); + put(blocks, 3, 64, 8, Blocks.TUFF.defaultBlockState()); + WorldGenLevel world = world(blocks); + NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain = + sourceSnapshot(world, new BoundingBox(0, 64, 0, 15, 64, 15)); + + assertEquals(Blocks.TUFF.defaultBlockState(), + NativeStructureTerrainIntegrator.sourceEncaseBlock( + world, fill, sourceTerrain)); + } + + @Test + public void sourceMaterialSnapshotIsStableAcrossEarlierFillsInTheSameChunk() { + Map blocks = new HashMap<>(); + BlockPos fill = new BlockPos(0, 64, 8); + put(blocks, 3, 64, 8, Blocks.TUFF.defaultBlockState()); + WorldGenLevel world = world(blocks); + NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain = + sourceSnapshot(world, new BoundingBox(0, 64, 0, 15, 64, 15)); + put(blocks, 1, 64, 8, Blocks.OAK_PLANKS.defaultBlockState()); + + assertEquals(Blocks.TUFF.defaultBlockState(), + NativeStructureTerrainIntegrator.sourceEncaseBlock( + world, fill, sourceTerrain)); + } + + @Test + public void sourceMaterialRejectsStructureBlocksAndOresBeforeDimensionFallback() { + Map blocks = new HashMap<>(); + BlockPos fill = new BlockPos(4, -20, 8); + put(blocks, 3, -20, 8, Blocks.OAK_PLANKS.defaultBlockState()); + put(blocks, 5, -20, 8, Blocks.STONE_BRICKS.defaultBlockState()); + put(blocks, 4, -20, 7, Blocks.IRON_ORE.defaultBlockState()); + + assertEquals(Blocks.DEEPSLATE.defaultBlockState(), + NativeStructureTerrainIntegrator.sourceEncaseBlock(world(blocks), fill)); + } + + @Test + public void sourceMaterialSnapshotOnlyAllocatesActualFillLayers() { + StructureStart start = start(TerrainAdjustment.BURY, 64); + BoundingBox area = new BoundingBox(0, -2032, 0, 15, 2030, 15); + NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain = + NativeStructureTerrainIntegrator.captureSourceTerrain( + world(new HashMap<>()), area, + List.of(new NativeStructureTerrainIntegrator.TerrainTarget( + "test:bounded-snapshot", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE)))); + + assertNotNull(sourceTerrain); + assertEquals(23 * 16 * 16, sourceTerrain.sampledCells()); + } + + @Test + public void customDimensionKeyUsesRegisteredVanillaDimensionType() { + ResourceKey customDimension = ResourceKey.create( + Registries.DIMENSION, Identifier.parse("iris:custom_nether")); + + assertEquals(Blocks.NETHERRACK.defaultBlockState(), + NativeStructureTerrainIntegrator.defaultEncaseBlock( + customDimension, BuiltinDimensionTypes.NETHER, null, 64)); + assertEquals(Blocks.END_STONE.defaultBlockState(), + NativeStructureTerrainIntegrator.defaultEncaseBlock( + customDimension, BuiltinDimensionTypes.END, null, 64)); + } + + @Test + public void configuredEncasePaletteReplacesTheDefaultMaterial() { + StructureStart start = start(TerrainAdjustment.BURY, 64); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + Map blocks = new HashMap<>(); + IrisMaterialPalette palette = new IrisMaterialPalette().qclear().qadd("minecraft:tuff"); + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), bounds, "minecraft:stronghold", start, + new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.ENCASE) + .setEncasePalette(palette), + NativeStructurePostProcessorEncaseTest::tuffBlock); + + assertEquals(Blocks.TUFF.defaultBlockState(), + state(blocks, bounds.minX(), bounds.minY(), bounds.minZ())); + } + + @Test + public void omittedTerrainConfigurationRetainsSourceSemantics() { + for (TerrainAdjustment adjustment : TerrainAdjustment.values()) { + IrisStructureTerrain resolved = NativeStructureTerrainIntegrator.resolveNativeTerrain( + start(adjustment, 64), null); + + assertEquals(IrisStructureTerrainMode.SOURCE, resolved.resolvedMode()); + } + } + + @Test + public void sourceBuryUsesTheVanillaSixByTwelveInfluenceEnvelope() { + StructureStart start = start(TerrainAdjustment.BURY, 64); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + int groundY = bounds.minY(); + BoundingBox area = new BoundingBox( + bounds.minX() - 7, groundY - 13, bounds.minZ(), + bounds.maxX(), groundY + 13, bounds.maxZ()); + Map blocks = new HashMap<>(); + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), area, "test:bury", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE), null); + + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.minX() - 5, groundY, bounds.minZ())); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.minX(), groundY + 11, bounds.minZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.minX() - 6, groundY, bounds.minZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.minX(), groundY + 12, bounds.minZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.minX() - 7, groundY, bounds.minZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.minX(), groundY + 13, bounds.minZ())); + } + + @Test + public void sourceEncapsulateWrapsTheFullPieceBounds() { + StructureStart start = start(TerrainAdjustment.ENCAPSULATE, 64); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + BoundingBox area = new BoundingBox( + bounds.minX() - 13, bounds.minY(), bounds.minZ(), + bounds.maxX(), bounds.maxY() + 13, bounds.maxZ()); + Map blocks = new HashMap<>(); + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), area, "test:encapsulate", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE), null); + + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.minX() - 11, bounds.minY(), bounds.minZ())); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.minX(), bounds.maxY() + 11, bounds.minZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.minX() - 12, bounds.minY(), bounds.minZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.minX(), bounds.maxY() + 12, bounds.minZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.minX() - 13, bounds.minY(), bounds.minZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.minX(), bounds.maxY() + 13, bounds.minZ())); + } + + @Test + public void sourceTerrainDensityOnlyUsesRigidPoolPieces() { + PoolElementStructurePiece terrainMatching = poolPiece(StructureTemplatePool.Projection.TERRAIN_MATCHING); + PoolElementStructurePiece rigid = poolPiece(StructureTemplatePool.Projection.RIGID); + + assertFalse(NativeStructureTerrainIntegrator.isSourceRigidPiece(terrainMatching)); + assertTrue(NativeStructureTerrainIntegrator.isSourceRigidPiece(rigid)); + } + + @Test + public void sourceJunctionTerrainUsesTheStrictLowerTwelveBlockEnvelope() { + assertTrue(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -1, 0)); + assertTrue(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -11, 0)); + assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, 0, 0)); + assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -12, 0)); + assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(12, -1, 0)); + } + + @Test + public void explicitTerrainConfigurationWinsOverAutoEncase() { + IrisStructureTerrain configured = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.PRESERVE); + + assertSame(configured, NativeStructureTerrainIntegrator.resolveNativeTerrain( + start(TerrainAdjustment.BURY, 64), configured)); + } + + @Test + public void legacyTemplateAirIsClearedAfterEveryTerrainFillPath() { + IrisStructureTerrain source = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.SOURCE); + IrisStructureTerrain preserve = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.PRESERVE); + IrisStructureTerrain encase = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.ENCASE); + + assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir( + start(TerrainAdjustment.BURY, 64), source)); + assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir( + start(TerrainAdjustment.BEARD_BOX, 64), source)); + assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir( + start(TerrainAdjustment.NONE, 64), encase)); + assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir( + start(TerrainAdjustment.NONE, 64), source)); + assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir( + start(TerrainAdjustment.BURY, 64), preserve)); + } + + @Test + public void encaseReservesTheNeighborChunkEnvelope() { + StructureStart generated = start(TerrainAdjustment.BURY, 64); + BoundingBox sourceBounds = generated.getBoundingBox(); + BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.ENCASE) + .setHorizontalPadding(4); + + StructureStart wrapped = NativeStructureReferenceEnvelope.wrap( + generated, + generated.getStructure(), + 0, + terrain); + BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds( + wrapped, wrapped.getStructure(), terrain); + + assertEquals(sourceBounds, wrapped.getBoundingBox()); + assertEquals(content.minX() - 4, referenceBounds.minX()); + assertEquals(content.maxX() + 4, referenceBounds.maxX()); + assertEquals(content.minZ() - 4, referenceBounds.minZ()); + assertEquals(content.maxZ() + 4, referenceBounds.maxZ()); + assertEquals(generated.getPieces().size(), wrapped.getPieces().size()); + } + + @Test + public void referenceEnvelopeRejectsChunkCoordinateOverflow() { + StructureStart generated = start(TerrainAdjustment.NONE, 64); + StructureStart extreme = new StructureStart( + generated.getStructure(), new ChunkPos(Integer.MAX_VALUE, 0), 0, + new PiecesContainer(generated.getPieces())); + + assertThrows(IllegalStateException.class, () -> + NativeStructureReferenceEnvelope.referenceBounds( + extreme, extreme.getStructure(), new IrisStructureTerrain())); + } + + @Test + public void explicitTerrainEnvelopeDoesNotInheritSourceAdjustment() { + StructureStart generated = start(TerrainAdjustment.BURY, 64); + BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.ENCASE) + .setHorizontalPadding(112); + + StructureStart wrapped = NativeStructureReferenceEnvelope.wrap( + generated, generated.getStructure(), 0, terrain); + BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds( + wrapped, wrapped.getStructure(), terrain); + + assertEquals(content.minX() - 112, referenceBounds.minX()); + assertEquals(content.maxX() + 112, referenceBounds.maxX()); + } + + @Test + public void yBandRelocatesTheStartMidpointDeterministically() { + IrisStructureYBand band = new IrisStructureYBand().setMin(-120).setMax(-20); + StructureStart first = start(TerrainAdjustment.BURY, 64); + StructureStart second = start(TerrainAdjustment.BURY, 64); + + NativeStructureVerticalPlacer.applyVerticalShift( + first, -64, -256, 320, true, false, band, (x, z) -> 40); + NativeStructureVerticalPlacer.applyVerticalShift( + second, -64, -256, 320, true, false, band, (x, z) -> 40); + + BoundingBox bounds = first.getBoundingBox(); + assertEquals(bounds.minY(), second.getBoundingBox().minY()); + assertTrue(bounds.minY() >= -120); + assertTrue(bounds.maxY() <= -20); + + int repeated = NativeStructureVerticalPlacer.applyVerticalShift( + first, -64, -256, 320, true, false, band, (x, z) -> 40); + assertEquals(0, repeated); + } + + @Test + public void yBandClampsWhenTheBandCannotContainTheStructure() { + IrisStructureYBand band = new IrisStructureYBand().setMin(-50).setMax(-45); + StructureStart start = start(TerrainAdjustment.BURY, 64); + + NativeStructureVerticalPlacer.applyVerticalShift( + start, 0, -256, 320, true, false, band, (x, z) -> 40); + + BoundingBox bounds = start.getBoundingBox(); + int midpoint = bounds.minY() + (bounds.maxY() - bounds.minY()) / 2; + assertTrue(midpoint >= -50); + assertTrue(midpoint <= -45); + } + + @Test + public void yBandReplacesBothTheBlindShiftAndBurial() { + IrisStructureYBand band = new IrisStructureYBand().setMin(-64).setMax(-64); + StructureStart start = start(TerrainAdjustment.BURY, 64); + BoundingBox bounds = start.getBoundingBox(); + int height = bounds.maxY() - bounds.minY(); + + NativeStructureVerticalPlacer.applyVerticalShift( + start, -200, -256, 320, true, false, band, (x, z) -> 40); + + assertEquals(-64 - height / 2, start.getBoundingBox().minY()); + } + + @Test + public void preserveSourceYWinsOverTheConfiguredBand() { + IrisStructureYBand band = new IrisStructureYBand().setMin(-120).setMax(-20); + StructureStart start = start(TerrainAdjustment.BURY, 64); + int minY = start.getBoundingBox().minY(); + + int offset = NativeStructureVerticalPlacer.applyVerticalShift( + start, 0, -256, 320, true, true, band, (x, z) -> 40); + + assertEquals(0, offset); + assertEquals(minY, start.getBoundingBox().minY()); + } + + private static BlockState tuffBlock(IrisMaterialPalette palette, RNG rng, int x, int y, int z) { + assertNotNull(palette); + assertNotNull(rng); + return Blocks.TUFF.defaultBlockState(); + } + + private static StructureStart start(TerrainAdjustment adjustment, int minY) { + Structure structure = new DesertPyramidStructure(new Structure.StructureSettings( + HolderSet.empty(), Map.of(), GenerationStep.Decoration.STRONGHOLDS, adjustment)); + StructurePiece piece = new DesertPyramidPiece(RandomSource.create(7L), 0, 0); + piece.move(0, minY - piece.getBoundingBox().minY(), 0); + return new StructureStart( + structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece))); + } + + private static PoolElementStructurePiece poolPiece(StructureTemplatePool.Projection projection) { + BlockPos position = new BlockPos(0, 64, 0); + StructurePoolElement element = StructurePoolElement.single( + "minecraft:empty", + Holder.direct(new StructureProcessorList(List.of( + BlockIgnoreProcessor.STRUCTURE_AND_AIR))), + LiquidSettings.APPLY_WATERLOGGING + ).apply(projection); + return new PoolElementStructurePiece( + null, + element, + position, + 0, + Rotation.NONE, + new BoundingBox(position), + LiquidSettings.APPLY_WATERLOGGING); + } + + private static WorldGenLevel world(Map blocks) { + InvocationHandler handler = (proxy, method, arguments) -> { + String methodName = method.getName(); + if (methodName.equals("getBlockState")) { + BlockPos position = (BlockPos) arguments[0]; + return state(blocks, position.getX(), position.getY(), position.getZ()); + } + if (methodName.equals("setBlock")) { + BlockPos position = (BlockPos) arguments[0]; + BlockState blockState = (BlockState) arguments[1]; + put(blocks, position.getX(), position.getY(), position.getZ(), blockState); + return true; + } + if (methodName.equals("getSeed")) { + return TEST_SEED; + } + if (methodName.equals("getLevel")) { + return null; + } + if (methodName.equals("hashCode")) { + return System.identityHashCode(proxy); + } + if (methodName.equals("equals")) { + return proxy == arguments[0]; + } + if (methodName.equals("toString")) { + return "encase-test-world"; + } + throw new UnsupportedOperationException(method.toString()); + }; + return (WorldGenLevel) Proxy.newProxyInstance( + WorldGenLevel.class.getClassLoader(), new Class[]{WorldGenLevel.class}, handler); + } + + private static NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceSnapshot( + WorldGenLevel world, BoundingBox area) { + BitSet requiredLayers = new BitSet(area.getYSpan()); + requiredLayers.set(0, area.getYSpan()); + return NativeStructureTerrainIntegrator.SourceTerrainSnapshot.capture( + world, area, requiredLayers); + } + + private static void put(Map blocks, + int x, int y, int z, BlockState state) { + blocks.put(new BlockPos(x, y, z), state); + } + + private static BlockState state(Map blocks, int x, int y, int z) { + return blocks.getOrDefault(new BlockPos(x, y, z), Blocks.AIR.defaultBlockState()); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorMonumentTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorMonumentTest.java new file mode 100644 index 000000000..51aef687f --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorMonumentTest.java @@ -0,0 +1,202 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.structure.placement.IrisStructureTerrain; +import art.arcane.iris.structure.placement.IrisStructureTerrainMode; +import net.minecraft.SharedConstants; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderSet; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class NativeStructurePostProcessorMonumentTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void vanillaSeaLevelKeepsTheVanillaMonumentHeight() { + StructureStart start = monumentStart(1337L); + + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:monument", 0, 63, -64, 320, false, false, null, (x, z) -> 0); + + assertEquals(0, offset); + assertEquals(39, start.getBoundingBox().minY()); + assertEquals(61, start.getBoundingBox().maxY()); + } + + @Test + public void shiftedSeaLevelMovesTheShellAndEveryRoomTogether() { + StructureStart start = monumentStart(1337L); + OceanMonumentPieces.MonumentBuilding building = monumentBuilding(start); + List children = NativeStructureVerticalPlacer.monumentChildPieces(building); + int[] childMinY = minimumYs(children); + BoundingBox cachedBounds = start.getBoundingBox(); + + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0); + + assertEquals(-13, offset); + assertSame(cachedBounds, start.getBoundingBox()); + assertEquals(26, building.getBoundingBox().minY()); + assertEquals(48, building.getBoundingBox().maxY()); + assertEquals(26, start.getBoundingBox().minY()); + assertEquals(48, start.getBoundingBox().maxY()); + assertTrue(children.size() > 20); + for (int i = 0; i < children.size(); i++) { + assertEquals(childMinY[i] - 13, children.get(i).getBoundingBox().minY()); + } + + int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0); + assertEquals(0, repeatedOffset); + } + + @Test + public void configuredOffsetIsRelativeToTheActualSeaLevel() { + StructureStart start = monumentStart(1337L); + + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:monument", 3, 50, -256, 512, false, false, null, (x, z) -> 0); + + assertEquals(-10, offset); + assertEquals(29, start.getBoundingBox().minY()); + assertEquals(51, start.getBoundingBox().maxY()); + } + + @Test + public void referenceEnvelopeDoesNotAddMonumentPieces() { + StructureStart start = monumentStart(1337L); + StructureStart wrapped = NativeStructureReferenceEnvelope.wrap( + start, + start.getStructure(), + 0, + new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setHorizontalPadding(4)); + + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + wrapped, "minecraft:monument", 0, 63, -64, 320, + false, false, null, (x, z) -> 0); + + assertEquals(0, offset); + assertEquals(1, wrapped.getPieces().size()); + } + + @Test + public void vanillaReloadRegenerationIsRealignedBeforePlacement() { + long seed = 1337L; + ChunkPos chunkPos = new ChunkPos(0, 0); + StructureStart initial = monumentStart(seed); + NativeStructureVerticalPlacer.applyVerticalPlacement( + initial, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0); + + PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad( + chunkPos, seed, new PiecesContainer(initial.getPieces())); + StructureStart reloaded = new StructureStart( + monumentStructure(), chunkPos, 0, regenerated); + + assertEquals(39, reloaded.getBoundingBox().minY()); + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + reloaded, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0); + assertEquals(-13, offset); + assertEquals(26, reloaded.getBoundingBox().minY()); + assertEquals(48, reloaded.getBoundingBox().maxY()); + } + + @Test + public void neighboringChunkPlacementRealignsReloadedMonumentOnce() throws Exception { + long seed = 1337L; + ChunkPos chunkPos = new ChunkPos(0, 0); + StructureStart initial = monumentStart(seed); + NativeStructureVerticalPlacer.applyVerticalPlacement( + initial, "minecraft:monument", 0, 50, -256, 512, + false, false, null, (x, z) -> 0); + PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad( + chunkPos, seed, new PiecesContainer(initial.getPieces())); + StructureStart reloaded = new StructureStart( + monumentStructure(), chunkPos, 0, regenerated); + ExecutorService executor = Executors.newFixedThreadPool(8); + CountDownLatch release = new CountDownLatch(1); + List> futures = new ArrayList<>(); + try { + for (int task = 0; task < 16; task++) { + futures.add(executor.submit(() -> { + release.await(); + NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment( + reloaded, "minecraft:monument", 0, 50, -256, 512); + return null; + })); + } + release.countDown(); + for (Future future : futures) { + future.get(); + } + } finally { + executor.shutdownNow(); + } + + assertEquals(26, reloaded.getBoundingBox().minY()); + assertEquals(48, reloaded.getBoundingBox().maxY()); + } + + @Test + public void impossibleSeaLevelAlignmentFailsInsteadOfClippingTheMonument() { + StructureStart start = monumentStart(1337L); + try { + NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:monument", 0, -50, -64, 320, false, false, null, (x, z) -> 0); + } catch (IllegalStateException error) { + assertTrue(error.getMessage().contains("cannot align")); + return; + } + throw new AssertionError("Expected an out-of-bounds monument alignment to fail"); + } + + private static StructureStart monumentStart(long seed) { + ChunkPos chunkPos = new ChunkPos(0, 0); + OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding( + RandomSource.create(seed), -29, -29, Direction.NORTH); + return new StructureStart( + monumentStructure(), chunkPos, 0, new PiecesContainer(List.of(building))); + } + + private static OceanMonumentStructure monumentStructure() { + return new OceanMonumentStructure(new Structure.StructureSettings(HolderSet.empty())); + } + + private static OceanMonumentPieces.MonumentBuilding monumentBuilding(StructureStart start) { + return (OceanMonumentPieces.MonumentBuilding) start.getPieces().get(0); + } + + private static int[] minimumYs(List pieces) { + int[] ys = new int[pieces.size()]; + for (int i = 0; i < pieces.size(); i++) { + ys[i] = pieces.get(i).getBoundingBox().minY(); + } + return ys; + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorScatteredFeatureTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorScatteredFeatureTest.java new file mode 100644 index 000000000..9e941f0a5 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorScatteredFeatureTest.java @@ -0,0 +1,198 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import net.minecraft.SharedConstants; +import net.minecraft.core.HolderSet; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure; +import net.minecraft.world.level.levelgen.structure.structures.JungleTemplePiece; +import net.minecraft.world.level.levelgen.structure.structures.JungleTempleStructure; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class NativeStructurePostProcessorScatteredFeatureTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void desertPyramidUsesTheLowestFootprintSurfaceAndLocksVanillaReanchor() { + TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(11L), 0, 0); + StructureStart start = desertStart(piece); + BoundingBox cachedBounds = start.getBoundingBox(); + BoundingBox footprint = piece.getBoundingBox(); + + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, + (x, z) -> x == footprint.maxX() && z == footprint.maxZ() ? 64 : 92); + + assertEquals(1, offset); + assertSame(cachedBounds, start.getBoundingBox()); + assertEquals(65, piece.getBoundingBox().minY()); + assertEquals(65, start.getBoundingBox().minY()); + assertTrue(piece.attemptVanillaAlignment(-2)); + assertEquals(65, piece.getBoundingBox().minY()); + } + + @Test + public void jungleTempleUsesAverageFootprintSurfaceAndHonorsConfiguredShift() { + TestJungleTemplePiece piece = new TestJungleTemplePiece(RandomSource.create(19L), 32, -16); + StructureStart start = jungleStart(piece); + BoundingBox footprint = piece.getBoundingBox(); + + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:jungle_pyramid", 4, 63, -64, 320, false, false, null, + (x, z) -> x == footprint.minX() && z == footprint.minZ() ? 88 : 70); + + assertEquals(11, offset); + assertEquals(75, piece.getBoundingBox().minY()); + assertTrue(piece.attemptVanillaAlignment()); + assertEquals(75, piece.getBoundingBox().minY()); + } + + @Test + public void repeatedAlignmentIsIdempotent() { + TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(23L), 0, 0); + StructureStart start = desertStart(piece); + + int initialOffset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80); + int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80); + + assertEquals(17, initialOffset); + assertEquals(0, repeatedOffset); + assertEquals(81, piece.getBoundingBox().minY()); + } + + @Test + public void scatteredAlignmentStaysInsideWorldBounds() { + TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(29L), 0, 0); + StructureStart start = desertStart(piece); + + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 318); + + assertEquals(241, offset); + assertEquals(305, piece.getBoundingBox().minY()); + assertEquals(319, start.getBoundingBox().maxY()); + } + + @Test + public void scatteredStructuresDoNotOptIntoFullFootprintSurfaceFitting() { + TestDesertPyramidPiece desert = new TestDesertPyramidPiece(RandomSource.create(31L), 0, 0); + TestJungleTemplePiece jungle = new TestJungleTemplePiece(RandomSource.create(37L), 0, 0); + SwampHutPiece swamp = new SwampHutPiece(RandomSource.create(41L), 0, 0); + StructureStart desertStart = desertStart(desert); + StructureStart jungleStart = jungleStart(jungle); + StructureStart swampStart = swampStart(swamp); + + assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(desertStart)); + assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(jungleStart)); + assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(swampStart)); + + AtomicInteger terrainQueries = new AtomicInteger(); + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + swampStart, "minecraft:swamp_hut", 0, 63, -64, 320, false, false, null, + (x, z) -> terrainQueries.incrementAndGet()); + assertEquals(0, offset); + assertEquals(0, terrainQueries.get()); + assertEquals(64, swamp.getBoundingBox().minY()); + } + + @Test + public void scatteredHeightFieldMatchesTheRuntimeContract() { + Field field = NativeStructureReflection.resolveScatteredHeightPositionField(); + + assertEquals(ScatteredFeaturePiece.class, field.getDeclaringClass()); + assertEquals(int.class, field.getType()); + assertTrue(Modifier.isProtected(field.getModifiers())); + assertFalse(Modifier.isFinal(field.getModifiers())); + assertFalse(Modifier.isStatic(field.getModifiers())); + assertTrue(field.trySetAccessible()); + } + + @Test + public void datapackOverridesOfSpecialVanillaIdsUseGenericPlacement() { + StructureStart pyramidOverride = swampStart( + new SwampHutPiece(RandomSource.create(43L), 0, 0)); + int pyramidMinY = pyramidOverride.getBoundingBox().minY(); + int pyramidOffset = NativeStructureVerticalPlacer.applyVerticalPlacement( + pyramidOverride, "minecraft:desert_pyramid", 5, + 63, -64, 320, false, false, null, (x, z) -> 200); + + assertEquals(5, pyramidOffset); + assertEquals(pyramidMinY + 5, pyramidOverride.getBoundingBox().minY()); + + StructureStart monumentOverride = swampStart( + new SwampHutPiece(RandomSource.create(47L), 0, 0)); + int monumentMinY = monumentOverride.getBoundingBox().minY(); + NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment( + monumentOverride, "minecraft:monument", 0, 40, -64, 320); + + assertEquals(monumentMinY, monumentOverride.getBoundingBox().minY()); + } + + private static StructureStart desertStart(StructurePiece piece) { + Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(HolderSet.empty())); + return start(structure, piece); + } + + private static StructureStart jungleStart(StructurePiece piece) { + Structure structure = new JungleTempleStructure(new Structure.StructureSettings(HolderSet.empty())); + return start(structure, piece); + } + + private static StructureStart swampStart(StructurePiece piece) { + Structure structure = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty())); + return start(structure, piece); + } + + private static StructureStart start(Structure structure, StructurePiece piece) { + return new StructureStart( + structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece))); + } + + private static final class TestDesertPyramidPiece extends DesertPyramidPiece { + private TestDesertPyramidPiece(RandomSource random, int west, int north) { + super(random, west, north); + } + + private boolean attemptVanillaAlignment(int offset) { + return updateHeightPositionToLowestGroundHeight((LevelAccessor) null, offset); + } + } + + private static final class TestJungleTemplePiece extends JungleTemplePiece { + private TestJungleTemplePiece(RandomSource random, int west, int north) { + super(random, west, north); + } + + private boolean attemptVanillaAlignment() { + return updateAverageGroundHeight((LevelAccessor) null, null, 0); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java new file mode 100644 index 000000000..a32dd1b4c --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java @@ -0,0 +1,2900 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.generation.mantle.StructureCarvingFootprint; +import art.arcane.iris.structure.placement.IrisStructureCarveShape; +import art.arcane.iris.structure.placement.IrisStructureTerrain; +import art.arcane.iris.structure.placement.IrisStructureTerrainMode; +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.IdMapper; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelAccessor; +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.Rotation; +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.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction; +import net.minecraft.world.level.levelgen.structure.pools.LegacySinglePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.templatesystem.AlwaysTrueTest; +import net.minecraft.world.level.levelgen.structure.templatesystem.BlockStateMatchTest; +import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.ProcessorRule; +import net.minecraft.world.level.levelgen.structure.templatesystem.RuleProcessor; +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.OceanMonumentStructure; +import net.minecraft.world.level.levelgen.structure.structures.ShipwreckStructure; +import net.minecraft.world.level.levelgen.structure.structures.StrongholdStructure; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class NativeStructurePostProcessorSurfaceTerrainTest { + private static final int SLAB_DEPTH = 512; + private static final int SLAB_MIN_Y = 64; + private static final int SLAB_PADDING = 14; + private static final int SLAB_WIDTH = 16; + private static final long TEST_SEED = 8675309L; + + @BeforeClass + public static void bootstrapMinecraft() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void flattenCutsAndFillsSymmetricallyAcrossItsConfiguredBlend() { + NativeStructureSurfaceFitter.FlattenAnchor anchor = new NativeStructureSurfaceFitter.FlattenAnchor( + new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 80, 2), 24, 48); + List anchors = List.of(anchor); + assertEquals(80, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 128).targetY()); + assertEquals(80, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 32).targetY()); + assertEquals(152, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 200).targetY()); + assertEquals(48, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 0).targetY()); + int previousCut = 80; + for (int x = 1; x <= 24; x++) { + int cut = NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, x, 0, 128).targetY(); + int fill = NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, x, 0, 32).targetY(); + assertEquals(160, cut + fill); + assertTrue(cut >= previousCut); + assertTrue(cut - previousCut <= 3); + previousCut = cut; + } + assertEquals(128, previousCut); + assertEquals(0, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 24, 0, 128).range()); + } + + @Test + public void flattenBlendsNeighboringFoundationsWithoutOrderDependentRidges() { + NativeStructureSurfaceFitter.FlattenAnchor left = new NativeStructureSurfaceFitter.FlattenAnchor( + new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 80, 2), 24, 64); + NativeStructureSurfaceFitter.FlattenAnchor right = new NativeStructureSurfaceFitter.FlattenAnchor( + new NativeStructureSurfaceFitter.SurfaceAnchor(8, 8, 0, 0, 80, 2), 24, 64); + int previous = 80; + for (int x = 0; x <= 8; x++) { + NativeStructureSurfaceFitter.FlattenResolution forward = NativeStructureSurfaceFitter.resolveFlattenSurface( + List.of(left, right), x, 0, 104); + assertEquals(forward, NativeStructureSurfaceFitter.resolveFlattenSurface( + List.of(right, left), x, 0, 104)); + assertTrue(forward.targetY() <= 82); + assertTrue(Math.abs(forward.targetY() - previous) <= 1); + previous = forward.targetY(); + } + } + + @Test + public void flattenBlendsAnElevatedRoadJunctionWithNearbyFoundations() { + List anchors = new ArrayList<>(); + for (int x : List.of(-3, 3)) { + for (int z = -1; z <= 1; z++) { + anchors.add(new NativeStructureSurfaceFitter.FlattenAnchor( + new NativeStructureSurfaceFitter.SurfaceAnchor(x, x, z, z, 64, 2), 24, 64)); + } + } + anchors.add(new NativeStructureSurfaceFitter.FlattenAnchor( + new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 105, 1), 24, 64)); + for (int x = -1; x <= 1; x++) { + for (int z = -1; z <= 1; z++) { + NativeStructureSurfaceFitter.FlattenResolution grade = + NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, x, z, 96); + assertTrue(grade.targetY() >= 64 && grade.targetY() <= 72); + assertEquals(grade, NativeStructureSurfaceFitter.resolveFlattenSurface( + anchors.reversed(), x, z, 96)); + if (x < 1) { + assertTrue(Math.abs(grade.targetY() - NativeStructureSurfaceFitter.resolveFlattenSurface( + anchors, x + 1, z, 96).targetY()) <= 1); + } + if (z < 1) { + assertTrue(Math.abs(grade.targetY() - NativeStructureSurfaceFitter.resolveFlattenSurface( + anchors, x, z + 1, 96).targetY()) <= 1); + } + } + } + } + + @Test + public void flattenPreservesAnExactRigidFloorBesideAnElevatedJunction() { + NativeStructureSurfaceFitter.FlattenAnchor rigid = new NativeStructureSurfaceFitter.FlattenAnchor( + new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 64, 2), 24, 64); + NativeStructureSurfaceFitter.FlattenAnchor junction = new NativeStructureSurfaceFitter.FlattenAnchor( + new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 105, 1), 24, 64); + assertEquals(64, NativeStructureSurfaceFitter.resolveFlattenSurface( + List.of(rigid, junction), 0, 0, 96).targetY()); + assertEquals(64, NativeStructureSurfaceFitter.resolveFlattenSurface( + List.of(junction, rigid), 0, 0, 96).targetY()); + } + + @Test + public void flattenRetainsIsolatedJunctionSupportWithinItsConfiguredBounds() { + for (int radius : List.of(0, 24)) { + NativeStructureSurfaceFitter.FlattenAnchor junction = new NativeStructureSurfaceFitter.FlattenAnchor( + new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 105, 1), radius, 8); + List anchors = List.of(junction); + assertEquals(104, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 96).targetY()); + assertEquals(8, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 96).range()); + int outside = Math.max(1, radius); + assertEquals(96, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, outside, 0, 96).targetY()); + assertEquals(0, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, outside, 0, 96).range()); + } + } + + @Test + public void flattenLevelsNonJigsawFoundationsAndRemovesDetachedRoofs() { + StructureStart start = desertStart(); + int groundY = start.getBoundingBox().minY() - 1; + BoundingBox area = new BoundingBox(0, 0, 0, 3, 127, 0); + Map blocks = new HashMap<>(); + for (int x = 0; x <= 3; x++) { + int sourceY = x < 2 ? groundY - 32 : groundY + 32; + put(blocks, x, 12, 0, Blocks.STONE.defaultBlockState()); + put(blocks, x, sourceY - 1, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, x, sourceY, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + } + assertTrue(NativeStructureSurfaceFitter.requiresFlattenTerrain( + surfaceTarget(start, IrisStructureTerrainMode.FLATTEN))); + + NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), + (x, z) -> x < 2 ? groundY - 32 : groundY + 32); + + for (int x = 0; x <= 3; x++) { + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, x, groundY, 0)); + for (int y = groundY + 1; y <= groundY + 32; y++) { + assertTrue("Roof at " + x + "," + y, state(blocks, x, y, 0).isAir()); + } + for (int y = 12; y < groundY; y++) { + assertTrue("Missing foundation at " + x + "," + y, state(blocks, x, y, 0).isSolid()); + } + } + } + + @Test + public void flattenRestoresAnUnchangedFoundationSurfaceOverADeepAirGap() { + StructureStart start = desertStart(); + int groundY = start.getBoundingBox().minY() - 1; + BoundingBox area = new BoundingBox(0, 0, 0, 0, 127, 0); + Map blocks = new HashMap<>(); + put(blocks, 0, groundY - 19, 0, Blocks.DIRT.defaultBlockState()); + + NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), (x, z) -> groundY); + + for (int y = groundY - 19; y <= groundY; y++) { + assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, y, 0)); + } + assertTrue(state(blocks, 0, groundY + 1, 0).isAir()); + } + + @Test + public void flattenLeavesBuriedAndSubmergedInstancesUntouched() { + StructureStart start = desertStart(); + int groundY = start.getBoundingBox().minY() - 1; + BoundingBox area = new BoundingBox(0, 0, 0, 0, 127, 0); + Map buried = flatTerrain(area, 100); + Map originalBuried = new HashMap<>(buried); + Map submerged = flatTerrain(area, groundY - 8); + put(submerged, 0, groundY - 7, 0, Blocks.WATER.defaultBlockState()); + Map originalSubmerged = new HashMap<>(submerged); + + NativeStructureSurfaceFitter.prepareSurfaceStructures(world(buried), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), (x, z) -> 100); + NativeStructureSurfaceFitter.prepareSurfaceStructures(world(submerged), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), (x, z) -> groundY - 8); + + assertEquals(originalBuried, buried); + assertEquals(originalSubmerged, submerged); + } + + @Test + public void flattenExemptsUndergroundAndSubmergedStructureTypes() { + Structure.StructureSettings surface = new Structure.StructureSettings( + HolderSet.empty(), Map.of(), GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.NONE); + Structure.StructureSettings underground = new Structure.StructureSettings( + HolderSet.empty(), Map.of(), GenerationStep.Decoration.UNDERGROUND_STRUCTURES, TerrainAdjustment.NONE); + List exempt = List.of(new StrongholdStructure(surface), new OceanMonumentStructure(surface), + new ShipwreckStructure(surface, false), new DesertPyramidStructure(underground)); + for (Structure structure : exempt) { + StructureStart start = new StructureStart(structure, new ChunkPos(0, 0), 0, + new PiecesContainer(desertStart().getPieces())); + assertFalse(NativeStructureSurfaceFitter.requiresFlattenTerrain( + surfaceTarget(start, IrisStructureTerrainMode.FLATTEN))); + } + assertFalse(NativeStructureSurfaceFitter.requiresFlattenTerrain( + surfaceTarget(desertStart(TerrainAdjustment.BURY), IrisStructureTerrainMode.FLATTEN))); + assertFalse(NativeStructureSurfaceFitter.requiresFlattenTerrain( + surfaceTarget(desertStart(TerrainAdjustment.ENCAPSULATE), IrisStructureTerrainMode.FLATTEN))); + } + + @Test + public void flattenFoundationReadsAndWritesStayInsideConfiguredVerticalRange() throws Exception { + PoolElementStructurePiece piece = rigidTemplatePiece(new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 128, 0, 0, 136, 0), 0, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(piece)); + NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN); + target.terrain().setFlattenRange(32); + BoundingBox area = new BoundingBox(0, 0, 0, 0, 255, 0); + Map blocks = new HashMap<>(); + put(blocks, 0, 94, 0, Blocks.GOLD_BLOCK.defaultBlockState()); + WorldGenLevel delegate = world(blocks); + WorldGenLevel bounded = (WorldGenLevel) Proxy.newProxyInstance(WorldGenLevel.class.getClassLoader(), + new Class[]{WorldGenLevel.class}, (proxy, method, arguments) -> { + if (method.getName().equals("getBlockState") || method.getName().equals("setBlock")) { + BlockPos position = (BlockPos) arguments[0]; + assertTrue(position.toString(), position.getY() >= 95 && position.getY() <= 159); + } + return method.invoke(delegate, arguments); + }); + + NativeStructureSurfaceFitter.prepareSurfaceStructures(bounded, area, List.of(target), (x, z) -> 127); + + assertEquals(Blocks.GOLD_BLOCK.defaultBlockState(), state(blocks, 0, 94, 0)); + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 95, 0)); + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 127, 0)); + assertTrue(state(blocks, 0, 128, 0).isAir()); + } + + @Test + public void flattenReferencesIncludeTheMaximumConfiguredFeather() throws Exception { + PoolElementStructurePiece piece = rigidTemplatePiece(new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 64, 0, 0, 70, 0), 0, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(piece)); + IrisStructureTerrain terrain = new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FLATTEN) + .setHorizontalPadding(128).setFlattenRange(128); + + BoundingBox references = NativeStructureReferenceEnvelope.referenceBounds(start, start.getStructure(), terrain); + + assertEquals(-128, references.minX()); + assertEquals(128, references.maxX()); + assertEquals(-128, references.minZ()); + assertEquals(128, references.maxZ()); + NativeStructureSurfaceFitter.FlattenAnchor anchor = new NativeStructureSurfaceFitter.FlattenAnchor( + new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 64, 2), 128, 128); + assertTrue(NativeStructureSurfaceFitter.resolveFlattenSurface(List.of(anchor), 127, 0, 100).range() > 0); + assertEquals(0, NativeStructureSurfaceFitter.resolveFlattenSurface(List.of(anchor), 128, 0, 100).range()); + } + + @Test + public void flattenPreservesNativeVillageSupportBridges() throws Exception { + PoolElementStructurePiece lower = rigidTemplatePiece(new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(0, 62, 0, 0, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece(new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 64, 0, 0, 70, 0), 0, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(lower, upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0); + Map blocks = supportTerrain(0, 0); + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport(world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), written); + assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, 63, 0)); + } + + @Test + public void flattenDoesNotBridgeACompletelyBuriedVillage() throws Exception { + PoolElementStructurePiece lower = rigidTemplatePiece(new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(0, 62, 0, 0, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece(new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 64, 0, 0, 70, 0), 0, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(lower, upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 0, 90, 0); + Map blocks = supportTerrain(0, 0); + put(blocks, 0, 80, 0, Blocks.STONE.defaultBlockState()); + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport(world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertTrue(written.isEmpty()); + assertTrue(state(blocks, 0, 63, 0).isAir()); + } + + @Test + public void flattenKeepsPyramidPlacementAboveItsPreparedGround() { + FlattenDesertPiece piece = new FlattenDesertPiece(); + StructureStart start = new StructureStart(desertStart().getStructure(), new ChunkPos(0, 0), 0, + new PiecesContainer(List.of(piece))); + BoundingBox footprint = piece.getBoundingBox(); + NativeStructureVerticalPlacer.applyVerticalPlacement(start, "minecraft:desert_pyramid", 0, + 63, 0, 256, false, false, null, (x, z) -> x == footprint.minX() ? 80 : 112); + BoundingBox area = new BoundingBox(footprint.minX(), 0, footprint.minZ(), + footprint.maxX(), 160, footprint.maxZ()); + Map blocks = new HashMap<>(); + for (int x = footprint.minX(); x <= footprint.maxX(); x++) { + for (int z = footprint.minZ(); z <= footprint.maxZ(); z++) { + int surfaceY = x == footprint.minX() ? 80 : 112; + put(blocks, x, surfaceY - 1, z, Blocks.DIRT.defaultBlockState()); + put(blocks, x, surfaceY, z, Blocks.GRASS_BLOCK.defaultBlockState()); + } + } + + NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), + (x, z) -> x == footprint.minX() ? 80 : 112); + + assertEquals(81, piece.getBoundingBox().minY()); + assertTrue(piece.attemptVanillaAlignment()); + assertEquals(81, piece.getBoundingBox().minY()); + assertTrue(state(blocks, footprint.maxX(), 80, footprint.maxZ()).isSolid()); + assertTrue(state(blocks, footprint.maxX(), 81, footprint.maxZ()).isAir()); + } + + @Test + public void flattenLetsTerrainMatchingPathsFollowThePreparedVillageGrade() throws Exception { + StructureTemplate foundation = template(List.of(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece left = rigidTemplatePiece(new InlineSinglePoolElement(foundation), + new BoundingBox(0, 65, 0, 0, 75, 0), 0, Rotation.NONE); + PoolElementStructurePiece right = rigidTemplatePiece(new InlineSinglePoolElement(foundation), + new BoundingBox(8, 65, 0, 8, 75, 0), 0, Rotation.NONE); + PoolElementStructurePiece path = rigidTemplatePiece(new InlineSinglePoolElement( + template(List.of(block(0, 0, 0, Blocks.DIRT_PATH.defaultBlockState()))), + List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING), + new BoundingBox(4, 65, 0, 4, 75, 0), 0, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(left, path, right)); + NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN); + target.terrain().setHorizontalPadding(24); + BoundingBox area = new BoundingBox(0, 0, 0, 8, 128, 0); + Map blocks = flatTerrain(area, 96); + for (int x : List.of(0, 8)) { + blocks.remove(new BlockPos(x, 95, 0)); + blocks.remove(new BlockPos(x, 96, 0)); + put(blocks, x, 63, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, x, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + } + WorldGenLevel world = world(blocks); + + NativeStructureSurfaceFitter.prepareSurfaceStructures(world, area, List.of(target), + (x, z) -> x == 0 || x == 8 ? 64 : 96); + NativeStructureTemplateOccupancy.OccupancyResult occupancy = NativeStructureTemplateOccupancy.resolve( + world, path, BlockPos.ZERO, area, + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager, + area::isInside, cells -> {}); + Map pathGround = + NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells()); + + assertTrue(state(blocks, 4, 96, 0).isAir()); + assertEquals(1, pathGround.size()); + int pathY = pathGround.values().iterator().next().y(); + assertTrue("Path Y " + pathY, pathY >= 64 && pathY <= 67); + } + + @Test + public void flattenPublishesCutAndFillHeightsBeforeNativePathProjectionAtFeatures() throws Exception { + ProtoChunk chunk = new ProtoChunk(new ChunkPos(0, 0), UpgradeData.EMPTY, + LevelHeightAccessor.create(0, 256), containerFactory(), null); + chunk.setPersistedStatus(ChunkStatus.TERRAIN); + for (int x = 0; x <= 8; x++) { + int originalY = x < 4 ? 96 : 32; + write(chunk, x, originalY - 1, 0, Blocks.DIRT.defaultBlockState()); + write(chunk, x, originalY, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + } + WorldgenTerrainHeightmaps.primeTerrain(chunk, + (x, z) -> x == 15 ? 81 : x < 4 ? 97 : 33, + (x, z) -> x == 15 ? 61 : x < 4 ? 97 : 33); + StructureTemplate foundation = template(List.of(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece left = rigidTemplatePiece(new InlineSinglePoolElement(foundation), + new BoundingBox(0, 65, 0, 0, 75, 0), 0, Rotation.NONE); + PoolElementStructurePiece right = rigidTemplatePiece(new InlineSinglePoolElement(foundation), + new BoundingBox(8, 65, 0, 8, 75, 0), 0, Rotation.NONE); + PoolElementStructurePiece path = rigidTemplatePiece(new InlineSinglePoolElement( + template(List.of(block(0, 0, 0, Blocks.DIRT_PATH.defaultBlockState()), + block(8, 0, 0, Blocks.DIRT_PATH.defaultBlockState()))), + List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING), + new BoundingBox(0, 65, 0, 8, 75, 0), 0, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(left, path, right)); + NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN); + target.terrain().setHorizontalPadding(24); + BoundingBox area = new BoundingBox(0, 0, 0, 8, 127, 0); + WorldGenLevel world = world(chunk); + + NativeStructureSurfaceFitter.SurfaceTerrainPlan plan = NativeStructureSurfaceFitter.prepareSurfaceStructures( + world, area, List.of(target), (x, z) -> x < 4 ? 96 : 32); + + assertEquals(96, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 0, 0)); + assertEquals(32, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 0)); + plan.primeHeightmaps(chunk); + chunk.setBlockState(new BlockPos(0, 85, 0), Blocks.OAK_PLANKS.defaultBlockState(), 2); + NativeStructureTemplateOccupancy.OccupancyResult occupancy = NativeStructureTemplateOccupancy.resolve( + world, path, BlockPos.ZERO, area, + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager, + area::isInside, cells -> {}); + Map pathGround = + NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells()); + + assertEquals(2, pathGround.size()); + for (NativeStructureTemplateOccupancy.LowestCell cell : pathGround.values()) { + assertEquals(64, cell.y()); + chunk.setBlockState(new BlockPos(cell.x(), cell.y(), cell.z()), cell.occupancy().state(), 2); + assertTrue(chunk.getBlockState(new BlockPos(cell.x(), 63, cell.z())).isSolid()); + assertTrue(chunk.getBlockState(new BlockPos(cell.x(), 65, cell.z())).isAir()); + assertTrue(chunk.getBlockState(new BlockPos(cell.x(), 66, cell.z())).isAir()); + assertEquals(64, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, cell.x(), cell.z())); + assertEquals(64, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, cell.x(), cell.z())); + } + assertEquals(80, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 15, 0)); + assertEquals(60, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, 15, 0)); + } + + @Test + public void flattenSupportsProjectedRoadsAndTheirSurroundingGradeAcrossRoofCavities() throws Exception { + ProtoChunk chunk = new ProtoChunk(new ChunkPos(0, 0), UpgradeData.EMPTY, + LevelHeightAccessor.create(0, 256), containerFactory(), null); + chunk.setPersistedStatus(ChunkStatus.TERRAIN); + for (int x = 0; x <= 8; x++) { + int originalY = x == 0 || x == 8 ? 64 : 96; + write(chunk, x, 20, 0, Blocks.STONE.defaultBlockState()); + write(chunk, x, originalY - 1, 0, Blocks.DIRT.defaultBlockState()); + write(chunk, x, originalY, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + } + write(chunk, 6, 50, 0, Blocks.WATER.defaultBlockState()); + WorldgenTerrainHeightmaps.primeTerrain(chunk, + (x, z) -> x == 0 || x == 8 ? 65 : 97, + (x, z) -> x == 0 || x == 8 ? 65 : 97); + StructureTemplate foundation = template(List.of(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece left = rigidTemplatePiece(new InlineSinglePoolElement(foundation), + new BoundingBox(0, 65, 0, 0, 75, 0), 0, Rotation.NONE); + PoolElementStructurePiece right = rigidTemplatePiece(new InlineSinglePoolElement(foundation), + new BoundingBox(8, 65, 0, 8, 75, 0), 0, Rotation.NONE); + PoolElementStructurePiece path = rigidTemplatePiece(new InlineSinglePoolElement( + template(List.of(block(0, 0, 0, Blocks.DIRT_PATH.defaultBlockState()), + block(1, 0, 0, Blocks.AIR.defaultBlockState()), + block(2, 0, 0, Blocks.DIRT_PATH.defaultBlockState()))), + List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING), + new BoundingBox(4, 65, 0, 6, 75, 0), 0, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(left, path, right)); + NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN); + target.terrain().setHorizontalPadding(24).setFlattenRange(32); + BoundingBox area = new BoundingBox(0, 0, 0, 8, 127, 0); + WorldGenLevel world = world(chunk); + + NativeStructureSurfaceFitter.SurfaceTerrainPlan plan = NativeStructureSurfaceFitter.prepareSurfaceStructures( + world, area, List.of(target), (x, z) -> x == 0 || x == 8 ? 64 : 96); + plan.primeHeightmaps(chunk); + NativeStructureTemplateOccupancy.OccupancyResult occupancy = NativeStructureTemplateOccupancy.resolve( + world, path, BlockPos.ZERO, area, + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager, + area::isInside, cells -> {}); + Map pathGround = + NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells()); + + assertEquals(2, pathGround.size()); + for (NativeStructureTemplateOccupancy.LowestCell cell : pathGround.values()) { + write(chunk, cell.x(), cell.y(), cell.z(), cell.occupancy().state()); + assertTrue(cell.y() > 64 && cell.y() < 72); + assertTrue(chunk.getBlockState(new BlockPos(cell.x(), cell.y() - 1, cell.z())).isSolid()); + assertTrue(chunk.getBlockState(new BlockPos(cell.x(), cell.y() + 1, cell.z())).isAir()); + } + int roadY = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 4, 0); + for (int y = roadY - 32; y < roadY; y++) { + assertTrue("Missing road support at " + y, chunk.getBlockState(new BlockPos(4, y, 0)).isSolid()); + } + assertTrue(chunk.getBlockState(new BlockPos(4, roadY - 33, 0)).isAir()); + int adjacentY = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 5, 0); + for (int y = adjacentY - 32; y < adjacentY; y++) { + assertTrue("Missing ground between road cells at " + y, + chunk.getBlockState(new BlockPos(5, y, 0)).isSolid()); + } + assertEquals(Blocks.WATER.defaultBlockState(), chunk.getBlockState(new BlockPos(6, 50, 0))); + assertTrue(chunk.getBlockState(new BlockPos(6, 49, 0)).isAir()); + } + + @Test + public void flattenBuildsAContinuousVillageGradeBetweenSparseFoundations() throws Exception { + StructureTemplate foundation = template(List.of(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + List pieces = new ArrayList<>(); + for (int x : List.of(0, 8)) { + for (int z : List.of(0, 8)) { + pieces.add(rigidTemplatePiece(new InlineSinglePoolElement(foundation), + new BoundingBox(x, 65, z, x, 75, z), 0, Rotation.NONE)); + } + } + StructureStart start = rigidSurfaceStart(pieces); + NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN); + target.terrain().setHorizontalPadding(24).setFlattenRange(64); + BoundingBox area = new BoundingBox(0, 0, 0, 8, 127, 8); + Map blocks = new HashMap<>(); + for (int x = 0; x <= 8; x++) { + for (int z = 0; z <= 8; z++) { + int originalY = (x == 0 || x == 8) && (z == 0 || z == 8) ? 64 : 96; + put(blocks, x, 20, z, Blocks.STONE.defaultBlockState()); + put(blocks, x, originalY - 1, z, Blocks.DIRT.defaultBlockState()); + put(blocks, x, originalY, z, Blocks.GRASS_BLOCK.defaultBlockState()); + } + } + NativeStructureSurfaceFitter.SurfaceTerrainPlan plan = NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, List.of(target), + (x, z) -> (x == 0 || x == 8) && (z == 0 || z == 8) ? 64 : 96); + + for (int x = 0; x <= 8; x++) { + for (int z = 0; z <= 8; z++) { + int groundY = plan.flattenedHeights().get(NativeStructureTemplateOccupancy.columnKey(x, z)) - 1; + assertTrue("Village grade at " + x + "," + z + ": " + groundY, groundY >= 64 && groundY <= 70); + for (int y = 20; y <= groundY; y++) { + assertTrue("Disconnected village ground at " + x + "," + y + "," + z, + state(blocks, x, y, z).isSolid()); + } + for (int y = groundY + 1; y <= 96; y++) { + assertTrue("Roof remains above village at " + x + "," + y + "," + z, + state(blocks, x, y, z).isAir()); + } + } + } + } + + @Test + public void sourceBeardAdjustmentsPrepareOnlyAtTheSurfaceStructuresStep() { + assertTrue(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( + TerrainAdjustment.BEARD_THIN, GenerationStep.Decoration.SURFACE_STRUCTURES)); + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( + TerrainAdjustment.BEARD_BOX, GenerationStep.Decoration.FLUID_SPRINGS)); + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( + TerrainAdjustment.BEARD_THIN, GenerationStep.Decoration.UNDERGROUND_STRUCTURES)); + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( + TerrainAdjustment.BEARD_BOX, GenerationStep.Decoration.UNDERGROUND_DECORATION)); + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( + TerrainAdjustment.BEARD_THIN, GenerationStep.Decoration.STRONGHOLDS)); + } + + @Test + public void nonBeardAdjustmentsDoNotPrepareSurfaceTerrain() { + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( + TerrainAdjustment.BURY, GenerationStep.Decoration.SURFACE_STRUCTURES)); + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( + TerrainAdjustment.ENCAPSULATE, GenerationStep.Decoration.SURFACE_STRUCTURES)); + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( + TerrainAdjustment.NONE, GenerationStep.Decoration.FLUID_SPRINGS)); + } + + @Test + public void shallowUndergroundBeardBoxDoesNotMutateTheTopSurface() throws Exception { + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(0, 1, 0, Blocks.DEEPSLATE_BRICKS.defaultBlockState())))), + new BoundingBox(0, 67, 0, 0, 74, 0), 1, Rotation.NONE); + Structure structure = new DesertPyramidStructure( + new Structure.StructureSettings( + HolderSet.empty(), Map.of(), + GenerationStep.Decoration.UNDERGROUND_DECORATION, + TerrainAdjustment.BEARD_BOX)); + StructureStart start = new StructureStart( + structure, new ChunkPos(0, 0), 0, + new PiecesContainer(List.of(piece))); + BoundingBox area = new BoundingBox(0, 48, 0, 0, 80, 0); + Map blocks = flatTerrain(area, 64); + Map before = new HashMap<>(blocks); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, List.of(surfaceTarget(start)), + (x, z) -> 64); + + assertEquals(before, blocks); + } + + @Test + public void explicitTerrainOverrideDisablesSourceBeardFitting() { + StructureStart start = desertStart(TerrainAdjustment.BEARD_BOX); + + assertTrue(NativeStructureSurfaceFitter.requiresSurfaceTerrain( + new NativeStructureTerrainIntegrator.TerrainTarget( + "test:beard_box", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE)))); + assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain( + new NativeStructureTerrainIntegrator.TerrainTarget( + "test:beard_box", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.PRESERVE)))); + } + + @Test + public void explicitVacuumUsesIndependentTerrainFittingWithoutAuthoredAdaptation() { + StructureStart none = desertStart(TerrainAdjustment.NONE); + StructureStart box = desertStart(TerrainAdjustment.BEARD_BOX); + NativeStructureTerrainIntegrator.TerrainTarget sourceNone = + new NativeStructureTerrainIntegrator.TerrainTarget( + "test:none", none, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE)); + NativeStructureTerrainIntegrator.TerrainTarget vacuumNone = + new NativeStructureTerrainIntegrator.TerrainTarget( + "test:none", none, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.VACUUM)); + NativeStructureTerrainIntegrator.TerrainTarget vacuumBox = + new NativeStructureTerrainIntegrator.TerrainTarget( + "test:box", box, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.VACUUM)); + + assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(sourceNone)); + assertTrue(NativeStructureSurfaceFitter.requiresSurfaceTerrain(vacuumNone)); + assertTrue(NativeStructureSurfaceFitter.requiresSurfaceTerrain(vacuumBox)); + assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir( + none, vacuumNone.terrain())); + } + + @Test + public void sourceSurfaceAnchorMeetsOneBlockBelowTheAuthoredGroundPlane() { + BoundingBox bounds = new BoundingBox(0, 64, 0, 4, 90, 4); + NativeStructureSurfaceFitter.SurfaceAnchor anchor = + NativeStructureSurfaceFitter.surfaceAnchor(bounds, 68, 2); + + assertEquals(67, anchor.meetY()); + assertEquals(0, anchor.minX()); + assertEquals(4, anchor.maxX()); + assertEquals(0, anchor.minZ()); + assertEquals(4, anchor.maxZ()); + } + + @Test + public void rigidFootprintsBoundTerrainAcrossGapsLargerThanTheBeardKernel() { + NativeStructureSurfaceFitter.SurfaceAnchor rigid = anchor(72, 2); + NativeStructureSurfaceFitter.SurfaceAnchor junction = anchor(72, 1); + + assertEquals(46, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(rigid), 2, 2, 40)); + assertEquals(46, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(junction), 2, 2, 40)); + } + + @Test + public void beardSkirtBoundsAndGradesUpwardAndDownwardSymmetrically() { + int originalY = 64; + NativeStructureSurfaceFitter.SurfaceAnchor high = anchor(96, 2); + NativeStructureSurfaceFitter.SurfaceAnchor low = anchor(32, 2); + int[] raised = new int[13]; + int[] lowered = new int[13]; + for (int outset = 0; outset <= 12; outset++) { + int x = 4 + outset; + raised[outset] = NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(high), x, 2, originalY); + lowered[outset] = NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(low), x, 2, originalY); + } + + assertArrayEquals( + new int[]{70, 69, 68, 67, 67, 66, 66, 65, 65, 64, 64, 64, 64}, + raised); + assertArrayEquals( + new int[]{58, 59, 60, 61, 61, 62, 62, 63, 63, 64, 64, 64, 64}, + lowered); + for (int outset = 0; outset <= 12; outset++) { + assertEquals(raised[outset] - originalY, originalY - lowered[outset]); + } + } + + @Test + public void projectedCenterAndRigidChildrenUseProcessedFoundationCells() throws Exception { + Structure structure = new DesertPyramidStructure( + new Structure.StructureSettings( + HolderSet.empty(), Map.of(), + GenerationStep.Decoration.SURFACE_STRUCTURES, + TerrainAdjustment.BEARD_BOX)); + PoolElementStructurePiece center = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(1, 1, 1, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 71, 0, 3, 80, 3), 1, Rotation.NONE); + PoolElementStructurePiece child = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(1, 7, 1, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(10, 65, 0, 13, 76, 3), 7, Rotation.NONE); + StructureStart start = new StructureStart( + structure, new ChunkPos(0, 0), 0, + new PiecesContainer(List.of(center, child))); + BoundingBox area = new BoundingBox(0, 60, 0, 15, 90, 15); + Map blocks = new HashMap<>(); + for (int x = area.minX(); x <= area.maxX(); x++) { + for (int z = area.minZ(); z <= area.maxZ(); z++) { + put(blocks, x, 64, z, Blocks.DIRT.defaultBlockState()); + put(blocks, x, 65, z, Blocks.GRASS_BLOCK.defaultBlockState()); + } + } + + List targets = List.of( + new NativeStructureTerrainIntegrator.TerrainTarget( + "nova_structures:tavern_oak", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE))); + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, targets, + (x, z) -> 65); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 1, 71, 1)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 11, 71, 1)); + assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 11, 70, 1)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 2, 65, 2)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 12, 65, 2)); + } + + @Test + public void sparseRigidFootprintsIgnoreAirAndSolidsAboveTheGroundPlane() throws Exception { + StructureTemplate sparseTemplate = template(List.of( + block(0, 1, 0, Blocks.COBBLESTONE.defaultBlockState()), + block(15, 1, 0, Blocks.AIR.defaultBlockState()), + block(30, 5, 0, Blocks.STONE.defaultBlockState()))); + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineSinglePoolElement(sparseTemplate), + new BoundingBox(0, 68, 0, 30, 76, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart( + List.of(piece), TerrainAdjustment.BEARD_THIN); + BoundingBox area = new BoundingBox(0, 52, 0, 30, 80, 0); + Map blocks = flatTerrain(area, 64); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, List.of(surfaceTarget(start)), + (x, z) -> 64); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 68, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 15, 64, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 30, 64, 0)); + } + + @Test + public void extremeRigidFoundationGapDoesNotCreateATerrainPillar() throws Exception { + StructureTemplate template = template(List.of( + block(0, 1, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineSinglePoolElement(template), + new BoundingBox(0, 90, 0, 0, 96, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart( + List.of(piece), TerrainAdjustment.BEARD_THIN); + BoundingBox area = new BoundingBox(0, 30, 0, 0, 100, 0); + Map blocks = flatTerrain(area, 40); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, List.of(surfaceTarget(start)), + (x, z) -> 40); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 46, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 47, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 89, 0)); + } + + @Test + public void terrainMatchingFootprintsFollowProcessedLowestSolidColumns() throws Exception { + StructureTemplate pathTemplate = template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState()), + block(0, 2, 0, Blocks.DIRT.defaultBlockState()), + block(15, 0, 0, Blocks.AIR.defaultBlockState()), + block(30, 0, 0, Blocks.DIRT.defaultBlockState()), + block(45, 0, 0, Blocks.DANDELION.defaultBlockState()))); + InlineSinglePoolElement element = new InlineSinglePoolElement( + pathTemplate, List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING); + PoolElementStructurePiece piece = rigidTemplatePiece( + element, new BoundingBox(0, 68, 0, 45, 74, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart( + List.of(piece), TerrainAdjustment.BEARD_THIN); + BoundingBox area = new BoundingBox(0, 52, 0, 45, 76, 0); + Map blocks = flatTerrain(area, 64); + blocks.remove(new BlockPos(0, 63, 0)); + blocks.remove(new BlockPos(0, 64, 0)); + put(blocks, 0, 59, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 60, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + put(blocks, 30, 71, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 30, 72, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, List.of(surfaceTarget(start)), + (x, z) -> x == 0 ? 60 : x == 30 ? 72 : 64); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 61, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 30, 71, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 30, 72, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 15, 64, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 15, 65, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 45, 64, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 45, 65, 0)); + } + + @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(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 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 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 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 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(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 blocks = flatTerrain(area, 60); + + NativeStructureSurfaceFitter.SurfaceTerrainPlan 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 blocks = flatTerrain(area, 60); + + NativeStructureSurfaceFitter.SurfaceTerrainPlan 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 wideBlocks = flatTerrain(wideArea, 60); + Map splitBlocks = flatTerrain(wideArea, 60); + + NativeStructureSurfaceFitter.SurfaceTerrainPlan widePlan = + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(wideBlocks), wideArea, List.of(target), (x, z) -> 60); + NativeStructureSurfaceFitter.SurfaceTerrainPlan westPlan = + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(splitBlocks), westArea, List.of(target), (x, z) -> 60); + NativeStructureSurfaceFitter.SurfaceTerrainPlan 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 blocks = flatTerrain(area, 60); + Map 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 blocks = flatTerrain(area, 60); + Map 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 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(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 blocks = flatTerrain(area, 60); + Map 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 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 blocks = flatTerrain(area, 60); + Map 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.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 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 blocks = flatTerrain(area, 50); + Map 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 blocks = flatTerrain(area, 60); + Map 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 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 wideBlocks = flatTerrain(wideArea, 60); + Map splitBlocks = flatTerrain(wideArea, 60); + Map reverseSplitBlocks = flatTerrain(wideArea, 60); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(wideBlocks), wideArea, List.of(target), (x, z) -> 60); + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(splitBlocks), westArea, List.of(target), (x, z) -> 60); + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(splitBlocks), eastArea, List.of(target), (x, z) -> 60); + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(reverseSplitBlocks), eastArea, List.of(target), (x, z) -> 60); + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(reverseSplitBlocks), westArea, List.of(target), (x, z) -> 60); + + assertEquals(wideBlocks, splitBlocks); + assertEquals(wideBlocks, reverseSplitBlocks); + } + + @Test + public void surfaceAnchorCapsInsideAndIsUnchangedAtRadius() { + NativeStructureSurfaceFitter.SurfaceAnchor anchor = anchor(72, 2); + + assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(anchor), 2, 2, 64)); + assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(anchor), 16, 2, 64)); + assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(anchor), 17, 2, 64)); + } + + @Test + public void surfaceAnchorRaisesAndLowersThroughTheTaper() { + NativeStructureSurfaceFitter.SurfaceAnchor raised = anchor(68, 2); + NativeStructureSurfaceFitter.SurfaceAnchor lowered = anchor(64, 2); + + assertEquals(65, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(raised), 10, 2, 64)); + assertEquals(67, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(lowered), 10, 2, 68)); + } + + @Test + public void containingRigidFloorsHaveDeterministicPriority() { + NativeStructureSurfaceFitter.SurfaceAnchor rigid = anchor(70, 2); + NativeStructureSurfaceFitter.SurfaceAnchor junction = anchor(74, 1); + + assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(rigid, junction), 2, 2, 64)); + assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(junction, rigid), 2, 2, 64)); + + NativeStructureSurfaceFitter.SurfaceAnchor weakTie = anchor(58, 1); + NativeStructureSurfaceFitter.SurfaceAnchor strongTie = anchor(70, 2); + assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(weakTie, strongTie), 2, 2, 64)); + assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(strongTie, weakTie), 2, 2, 64)); + } + + @Test + public void postClearSupportUsesUpperOccupancyOverLowerLegacyAir() throws Exception { + StructureTemplate lowerTemplate = template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState()), + block(1, 0, 0, Blocks.AIR.defaultBlockState()))); + StructureTemplate upperTemplate = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()), + block(1, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(lowerTemplate), + new BoundingBox(0, 62, 0, 1, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineLegacyPoolElement(upperTemplate), + new BoundingBox(0, 64, 0, 1, 70, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(lower, upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 1, 72, 0); + Map blocks = new HashMap<>(); + for (int x = 0; x <= 1; x++) { + put(blocks, x, 61, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, x, 62, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + } + put(blocks, 1, 63, 0, Blocks.DIRT.defaultBlockState()); + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), written); + assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, 63, 0)); + assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 1, 63, 0)); + } + + @Test + public void rotatedLowestAuthoredVoidFluidAirAndJigsawColumnsRemainOpen() throws Exception { + List upperBlocks = new ArrayList<>(); + upperBlocks.add(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())); + upperBlocks.add(block(1, 0, 0, Blocks.AIR.defaultBlockState())); + upperBlocks.add(block(2, 0, 0, Blocks.STRUCTURE_VOID.defaultBlockState())); + upperBlocks.add(block(3, 0, 0, Blocks.WATER.defaultBlockState())); + upperBlocks.add(block(4, 0, 0, Blocks.JIGSAW.defaultBlockState())); + for (int x = 1; x <= 4; x++) { + upperBlocks.add(block(x, 1, 0, Blocks.STONE.defaultBlockState())); + } + StructureTemplate upperTemplate = template(upperBlocks); + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(10, 62, 10, 10, 65, 14), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineSinglePoolElement(upperTemplate), + new BoundingBox(10, 64, 10, 10, 70, 14), 1, + Rotation.CLOCKWISE_90); + StructureStart start = rigidSurfaceStart(List.of(lower, upper)); + BoundingBox area = new BoundingBox(10, 58, 10, 10, 72, 14); + Map blocks = new HashMap<>(); + for (int z = 10; z <= 14; z++) { + put(blocks, 10, 61, z, Blocks.DIRT.defaultBlockState()); + put(blocks, 10, 62, z, Blocks.GRASS_BLOCK.defaultBlockState()); + } + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + StructurePlaceSettings settings = new StructurePlaceSettings() + .setRotation(Rotation.CLOCKWISE_90); + BlockPos origin = upper.getPosition(); + BlockPos supportedBase = origin.offset( + StructureTemplate.calculateRelativePosition(settings, BlockPos.ZERO)); + assertEquals(Set.of(supportedBase.below().asLong()), written); + assertEquals(Blocks.DIRT.defaultBlockState(), state( + blocks, supportedBase.getX(), supportedBase.getY() - 1, supportedBase.getZ())); + for (int localX = 1; localX <= 4; localX++) { + BlockPos vetoedBase = origin.offset(StructureTemplate.calculateRelativePosition( + settings, new BlockPos(localX, 0, 0))); + assertEquals(Blocks.AIR.defaultBlockState(), state( + blocks, vetoedBase.getX(), vetoedBase.getY() - 1, vetoedBase.getZ())); + } + } + + @Test + public void listElementsUseDeclaredOverlayOrderForLowestAuthoredCells() throws Exception { + InlineLegacyPoolElement legacy = new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()), + block(1, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))); + InlineSinglePoolElement single = new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState()), + block(1, 0, 0, Blocks.STONE.defaultBlockState())))); + ListPoolElement list = new ListPoolElement( + List.of(legacy, single), StructureTemplatePool.Projection.RIGID); + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(0, 62, 0, 1, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + list, new BoundingBox(0, 64, 0, 1, 70, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(lower, upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 1, 72, 0); + Map blocks = new HashMap<>(); + for (int x = 0; x <= 1; x++) { + put(blocks, x, 61, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, x, 62, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + } + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(1, 63, 0)), written); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 63, 0)); + assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 1, 63, 0)); + } + + @Test + public void surfaceSupportIsChunkClippedAndStableAcrossSplitAreas() throws Exception { + StructureTemplate upperTemplate = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()), + block(1, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(15, 62, 0, 16, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineSinglePoolElement(upperTemplate), + new BoundingBox(15, 64, 0, 16, 70, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(lower, upper)); + BoundingBox wideArea = new BoundingBox(0, 58, 0, 31, 72, 15); + BoundingBox westArea = new BoundingBox(0, 58, 0, 15, 72, 15); + BoundingBox eastArea = new BoundingBox(16, 58, 0, 31, 72, 15); + Map wideBlocks = supportTerrain(15, 16); + Map splitBlocks = supportTerrain(15, 16); + + Set wide = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(wideBlocks), wideArea, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + Set split = new HashSet<>( + NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(splitBlocks), westArea, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager)); + assertEquals(Blocks.AIR.defaultBlockState(), state(splitBlocks, 16, 63, 0)); + split.addAll(NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(splitBlocks), eastArea, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager)); + + assertEquals(Set.of( + BlockPos.asLong(15, 63, 0), BlockPos.asLong(16, 63, 0)), wide); + assertEquals(wide, split); + assertEquals(wideBlocks, splitBlocks); + } + + @Test + public void supportNeverPairsRigidAnchorsAcrossStarts() throws Exception { + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(0, 62, 0, 0, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 64, 0, 0, 70, 0), 1, Rotation.NONE); + StructureStart lowerStart = rigidSurfaceStart(List.of(lower)); + StructureStart upperStart = rigidSurfaceStart(List.of(upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0); + Map blocks = supportTerrain(0, 0); + + assertTrue(NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(lowerStart)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager).isEmpty()); + assertTrue(NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(upperStart)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager).isEmpty()); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 63, 0)); + } + + @Test + public void vacuumSupportsUnauthoredTerrainModesButLongMeetGapsRemainOpen() throws Exception { + StructureTemplate upperTemplate = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(0, 62, 0, 0, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineSinglePoolElement(upperTemplate), + new BoundingBox(0, 64, 0, 0, 70, 0), 1, Rotation.NONE); + StructureStart vacuumStart = rigidSurfaceStart( + List.of(lower, upper), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0); + Map vacuumBlocks = supportTerrain(0, 0); + + Set vacuumWritten = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(vacuumBlocks), area, + List.of(surfaceTarget(vacuumStart, IrisStructureTerrainMode.VACUUM)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), vacuumWritten); + assertTrue(NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(supportTerrain(0, 0)), area, + List.of(surfaceTarget(vacuumStart, IrisStructureTerrainMode.SOURCE)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager).isEmpty()); + + PoolElementStructurePiece distantUpper = rigidTemplatePiece( + new InlineSinglePoolElement(upperTemplate), + new BoundingBox(0, 65, 0, 0, 71, 0), 1, Rotation.NONE); + StructureStart distantStart = rigidSurfaceStart( + List.of(lower, distantUpper), TerrainAdjustment.NONE); + assertTrue(NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(supportTerrain(0, 0)), area, + List.of(surfaceTarget(distantStart, IrisStructureTerrainMode.VACUUM)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager).isEmpty()); + } + + @Test + public void threeRigidPlanesCannotBuildAnUpwardSupportChain() throws Exception { + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(0, 62, 0, 0, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece middle = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 64, 0, 0, 69, 0), 0, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.STONE.defaultBlockState())))), + new BoundingBox(0, 65, 0, 0, 70, 0), 0, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(lower, middle, upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0); + Map blocks = supportTerrain(0, 0); + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), written); + assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, 63, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 64, 0)); + } + + @Test + public void reversingTargetsCannotCreateCrossStartSupportChains() throws Exception { + PoolElementStructurePiece ground = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(0, 62, 0, 0, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece middle = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 64, 0, 0, 69, 0), 0, Rotation.NONE); + PoolElementStructurePiece raisedGround = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(0, 64, 0, 0, 69, 0), 0, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.STONE.defaultBlockState())))), + new BoundingBox(0, 65, 0, 0, 70, 0), 0, Rotation.NONE); + StructureStart lowerStart = rigidSurfaceStart(List.of(ground, middle)); + StructureStart upperStart = rigidSurfaceStart(List.of(raisedGround, upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0); + Map forwardBlocks = supportTerrain(0, 0); + Map reverseBlocks = supportTerrain(0, 0); + NativeStructureTerrainIntegrator.TerrainTarget lowerTarget = surfaceTarget(lowerStart); + NativeStructureTerrainIntegrator.TerrainTarget upperTarget = surfaceTarget(upperStart); + + Set forward = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(forwardBlocks), area, List.of(lowerTarget, upperTarget), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + Set reverse = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(reverseBlocks), area, List.of(upperTarget, lowerTarget), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), forward); + assertEquals(forward, reverse); + assertEquals(forwardBlocks, reverseBlocks); + assertEquals(Blocks.AIR.defaultBlockState(), state(forwardBlocks, 0, 64, 0)); + } + + @Test + public void preserveDuplicateCannotSuppressVacuumSupport() throws Exception { + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(0, 62, 0, 0, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 64, 0, 0, 70, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart( + List.of(lower, upper), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0); + Map blocks = supportTerrain(0, 0); + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of( + surfaceTarget(start, IrisStructureTerrainMode.PRESERVE), + surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), written); + } + + @Test + public void processorCreatedAirAndFluidVetoSurfaceSupport() throws Exception { + StructureTemplate upperTemplate = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()), + block(1, 0, 0, Blocks.STONE.defaultBlockState()))); + InlineSinglePoolElement upperElement = new InlineSinglePoolElement( + upperTemplate, List.of( + replaceBlockProcessor( + Blocks.COBBLESTONE.defaultBlockState(), + Blocks.AIR.defaultBlockState()), + replaceBlockProcessor( + Blocks.STONE.defaultBlockState(), + Blocks.WATER.defaultBlockState()))); + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(0, 62, 0, 1, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + upperElement, new BoundingBox(0, 64, 0, 1, 70, 0), + 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(lower, upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 1, 72, 0); + Map blocks = supportTerrain(0, 1); + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertTrue(written.isEmpty()); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 63, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 1, 63, 0)); + } + + @Test + public void solidJigsawFinalStateCreatesSurfaceSupport() throws Exception { + CompoundTag jigsawData = new CompoundTag(); + jigsawData.putString("final_state", "minecraft:cobblestone"); + StructureTemplate upperTemplate = template(List.of( + new StructureTemplate.StructureBlockInfo( + BlockPos.ZERO, Blocks.JIGSAW.defaultBlockState(), jigsawData))); + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(0, 62, 0, 0, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineSinglePoolElement(upperTemplate), + new BoundingBox(0, 64, 0, 0, 70, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(lower, upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0); + Map blocks = supportTerrain(0, 0); + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), written); + assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, 63, 0)); + } + + @Test + public void laterLegacyAirDoesNotOverlayEarlierListSolid() throws Exception { + InlineSinglePoolElement solid = new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))); + InlineLegacyPoolElement legacyAir = new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))); + ListPoolElement list = new ListPoolElement( + List.of(solid, legacyAir), StructureTemplatePool.Projection.RIGID); + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(0, 62, 0, 0, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + list, new BoundingBox(0, 64, 0, 0, 70, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(lower, upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0); + Map blocks = supportTerrain(0, 0); + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), written); + } + + @Test + public void stackedRigidPiecesPreserveTheLowerProcessedFoundation() throws Exception { + Structure structure = new DesertPyramidStructure( + new Structure.StructureSettings( + HolderSet.empty(), Map.of(), + GenerationStep.Decoration.SURFACE_STRUCTURES, + TerrainAdjustment.BEARD_BOX)); + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(2, 1, 2, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 62, 0, 4, 65, 4), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(2, 1, 2, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 66, 0, 4, 78, 4), 1, Rotation.NONE); + StructureStart start = new StructureStart( + structure, new ChunkPos(0, 0), 0, + new PiecesContainer(List.of(upper, lower))); + BoundingBox area = new BoundingBox(0, 58, 0, 4, 78, 4); + Map blocks = new HashMap<>(); + for (int x = area.minX(); x <= area.maxX(); x++) { + for (int z = area.minZ(); z <= area.maxZ(); z++) { + put(blocks, x, 59, z, Blocks.DIRT.defaultBlockState()); + put(blocks, x, 60, z, Blocks.GRASS_BLOCK.defaultBlockState()); + } + } + + WorldGenLevel world = world(blocks); + List targets = + List.of(surfaceTarget(start)); + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world, area, targets, (x, z) -> 60); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 2, 62, 2)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 2, 65, 2)); + } + + @Test + public void containingFootprintOverridesAnAdjacentPiecesFalloff() { + NativeStructureSurfaceFitter.SurfaceAnchor local = + new NativeStructureSurfaceFitter.SurfaceAnchor(0, 4, 0, 4, 66, 2); + NativeStructureSurfaceFitter.SurfaceAnchor adjacent = + new NativeStructureSurfaceFitter.SurfaceAnchor(5, 9, 0, 4, 70, 2); + + assertEquals(69, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(adjacent), 4, 2, 64)); + assertEquals(66, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(local, adjacent), 4, 2, 64)); + assertEquals(66, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(adjacent, local), 4, 2, 64)); + } + + @Test + public void opposingFalloffsBlendWithoutAnAbruptMidpointSeam() { + NativeStructureSurfaceFitter.SurfaceAnchor high = + new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 70, 2); + NativeStructureSurfaceFitter.SurfaceAnchor low = + new NativeStructureSurfaceFitter.SurfaceAnchor(12, 12, 0, 0, 58, 2); + int previous = NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(high, low), 0, 0, 64); + + for (int x = 1; x <= 12; x++) { + int forward = NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(high, low), x, 0, 64); + int reversed = NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(low, high), x, 0, 64); + assertEquals(forward, reversed); + assertTrue(Math.abs(forward - previous) <= 6); + previous = forward; + } + assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(high, low), 6, 0, 64)); + } + + @Test + public void surfaceColumnsMutateTerrainAndRemoveUnsupportedDecoration() { + Map lowered = new HashMap<>(); + put(lowered, 0, 61, 0, Blocks.STONE.defaultBlockState()); + put(lowered, 0, 62, 0, Blocks.DIRT.defaultBlockState()); + put(lowered, 0, 63, 0, Blocks.DIRT.defaultBlockState()); + put(lowered, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + put(lowered, 0, 65, 0, Blocks.DANDELION.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(lowered), new BlockPos.MutableBlockPos(), + 0, 0, 64, 62, -64, 319); + + assertEquals(Blocks.STONE.defaultBlockState(), state(lowered, 0, 61, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(lowered, 0, 62, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(lowered, 0, 63, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(lowered, 0, 64, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(lowered, 0, 65, 0)); + + Map raised = new HashMap<>(); + put(raised, 0, 63, 0, Blocks.DIRT.defaultBlockState()); + put(raised, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + put(raised, 0, 65, 0, Blocks.DANDELION.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(raised), new BlockPos.MutableBlockPos(), + 0, 0, 64, 68, -64, 319); + + assertEquals(Blocks.DIRT.defaultBlockState(), state(raised, 0, 65, 0)); + assertEquals(Blocks.DIRT.defaultBlockState(), state(raised, 0, 66, 0)); + assertEquals(Blocks.DIRT.defaultBlockState(), state(raised, 0, 67, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(raised, 0, 68, 0)); + } + + @Test + public void rigidSurfaceBaseClosesAOneBlockSubsurfaceGap() { + Map blocks = new HashMap<>(); + put(blocks, 0, 62, 0, Blocks.STONE.defaultBlockState()); + put(blocks, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, 64, 64, -64, 319, true); + + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 63, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 64, 0)); + } + + @Test + public void rigidSurfaceBaseReconstructsAMissingMeetLayer() { + Map blocks = new HashMap<>(); + put(blocks, 0, 62, 0, Blocks.STONE.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, 64, 64, -64, 319, true); + + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 63, 0)); + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 64, 0)); + } + + @Test + public void rigidSurfaceBaseReconstructsAMissingMeetLayerAtTheWorldFloor() { + Map blocks = new HashMap<>(); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, -64, -64, -64, 319, true); + + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, -64, 0)); + } + + @Test + public void raisedSurfaceTerrainDoesNotSliceTreeBlocks() { + Map blocks = new HashMap<>(); + BlockState log = Blocks.OAK_LOG.defaultBlockState(); + BlockState leaves = Blocks.OAK_LEAVES.defaultBlockState(); + put(blocks, 0, 63, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + put(blocks, 0, 66, 0, log); + put(blocks, 0, 68, 0, leaves); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, 64, 68, -64, 319); + + assertEquals(log, state(blocks, 0, 66, 0)); + assertEquals(leaves, state(blocks, 0, 68, 0)); + } + + @Test + public void clearedIntersectingTreeCellsBecomeRaisedTerrainSupport() { + ProtoChunk chunk = new ProtoChunk( + new ChunkPos(0, 0), UpgradeData.EMPTY, + LevelHeightAccessor.create(-64, 384), containerFactory(), null); + write(chunk, 0, 63, 0, Blocks.DIRT.defaultBlockState()); + write(chunk, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + write(chunk, 0, 66, 0, Blocks.OAK_LOG.defaultBlockState()); + write(chunk, 0, 68, 0, Blocks.OAK_LEAVES.defaultBlockState()); + WorldGenLevel world = world(chunk); + BoundingBox area = new BoundingBox(0, 63, 0, 0, 68, 0); + + NativeStructureVegetationClearer.clearIntersectingVegetation( + world, chunk, area, List.of(desertStart())); + NativeStructureSurfaceFitter.applySurfaceColumn( + world, new BlockPos.MutableBlockPos(), + 0, 0, 64, 68, -64, 319); + + assertEquals(Blocks.DIRT.defaultBlockState(), chunk.getBlockState(new BlockPos(0, 65, 0))); + assertEquals(Blocks.DIRT.defaultBlockState(), chunk.getBlockState(new BlockPos(0, 66, 0))); + assertEquals(Blocks.DIRT.defaultBlockState(), chunk.getBlockState(new BlockPos(0, 67, 0))); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), chunk.getBlockState(new BlockPos(0, 68, 0))); + } + + @Test + public void loweredFluidColumnsRemainFluidFilled() { + Map blocks = new HashMap<>(); + put(blocks, 0, 62, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 63, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 64, 0, Blocks.GRAVEL.defaultBlockState()); + put(blocks, 0, 65, 0, Blocks.WATER.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, 64, 62, -64, 319); + + assertEquals(Blocks.WATER.defaultBlockState(), state(blocks, 0, 63, 0)); + assertEquals(Blocks.WATER.defaultBlockState(), state(blocks, 0, 64, 0)); + assertEquals(Blocks.WATER.defaultBlockState(), state(blocks, 0, 65, 0)); + assertEquals(Blocks.GRAVEL.defaultBlockState(), state(blocks, 0, 62, 0)); + } + + @Test + public void preserveSourceYSkipsBurialAndKeepsTheVanillaStartY() { + StructureStart start = desertStart(); + int minY = start.getBoundingBox().minY(); + + int offset = NativeStructureVerticalPlacer.applyVerticalShift( + start, 0, -64, 320, true, true, null, (x, z) -> 40); + + assertEquals(0, offset); + assertEquals(minY, start.getBoundingBox().minY()); + } + + @Test + public void preserveSourceYStillAppliesAnExplicitShift() { + StructureStart start = desertStart(); + int minY = start.getBoundingBox().minY(); + + int offset = NativeStructureVerticalPlacer.applyVerticalShift( + start, -8, -64, 320, true, true, null, (x, z) -> 40); + + assertEquals(-8, offset); + assertEquals(minY - 8, start.getBoundingBox().minY()); + } + + @Test + public void burialStillSinksTheStructureBelowTheLowestTerrainColumn() { + StructureStart start = desertStart(); + int minY = start.getBoundingBox().minY(); + int maxY = start.getBoundingBox().maxY(); + int expected = 40 - 1 - maxY; + + int offset = NativeStructureVerticalPlacer.applyVerticalShift( + start, 0, -64, 320, true, false, null, (x, z) -> 40); + + assertEquals(expected, offset); + assertEquals(minY + expected, start.getBoundingBox().minY()); + } + + @Test + public void unfittableBurialClampsToTheWorldFloorInsteadOfAborting() { + StructureStart start = desertStart(); + int minY = start.getBoundingBox().minY(); + int worldMinY = minY - 4; + + int offset = NativeStructureVerticalPlacer.applyVerticalShift( + start, 0, worldMinY, 320, true, false, null, (x, z) -> worldMinY); + + assertEquals(-4, offset); + assertEquals(worldMinY, start.getBoundingBox().minY()); + } + + @Test + public void nativeVacuumLeavesPieceBlocksForSurfaceFitting() { + StructureStart start = desertStart(); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + Map blocks = new HashMap<>(); + put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.STONE.defaultBlockState()); + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), bounds, "minecraft:desert_pyramid", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.VACUUM), null); + + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.minX(), bounds.minY(), bounds.minZ())); + } + + @Test + public void nativeForceCarveHonorsConfiguredPadding() { + StructureStart start = desertStart(); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + BoundingBox area = new BoundingBox( + bounds.minX() - 1, bounds.minY(), bounds.minZ() - 1, + bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1); + Map blocks = new HashMap<>(); + put(blocks, bounds.maxX() + 1, bounds.minY(), bounds.maxZ(), + Blocks.STONE.defaultBlockState()); + put(blocks, bounds.maxX(), bounds.maxY() + 1, bounds.maxZ(), + Blocks.STONE.defaultBlockState()); + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), area, "minecraft:desert_pyramid", start, + new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setHorizontalPadding(1) + .setCeilingPadding(1), null); + + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.maxX() + 1, bounds.minY(), bounds.maxZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.maxX(), bounds.maxY() + 1, bounds.maxZ())); + } + + @Test + public void nativeForceCarveUsesPieceUnionInsteadOfCombinedBounds() { + Structure structure = new DesertPyramidStructure( + new Structure.StructureSettings(HolderSet.empty())); + DesertPyramidPiece first = new DesertPyramidPiece(RandomSource.create(7L), 0, 0); + DesertPyramidPiece second = new DesertPyramidPiece(RandomSource.create(8L), 0, 0); + second.move(64, 0, 0); + StructureStart start = new StructureStart( + structure, new ChunkPos(0, 0), 0, + new PiecesContainer(List.of(first, second))); + BoundingBox firstBounds = first.getBoundingBox(); + BoundingBox secondBounds = second.getBoundingBox(); + int gapX = (firstBounds.maxX() + secondBounds.minX()) / 2; + int y = firstBounds.minY(); + int z = firstBounds.minZ(); + BoundingBox area = new BoundingBox( + firstBounds.minX(), y, z, + secondBounds.maxX(), y, z); + Map blocks = new HashMap<>(); + put(blocks, firstBounds.minX(), y, z, Blocks.STONE.defaultBlockState()); + put(blocks, gapX, y, z, Blocks.STONE.defaultBlockState()); + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), area, "minecraft:ancient_city", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FORCE_CARVE), null); + + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, firstBounds.minX(), y, z)); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, gapX, y, z)); + } + + @Test + public void templateBackedColumnsUseTheTemplateAirComplement() throws Exception { + StructureTemplate template = template(List.of( + new StructureTemplate.StructureBlockInfo( + new BlockPos(0, 0, 0), Blocks.AIR.defaultBlockState(), null), + new StructureTemplate.StructureBlockInfo( + new BlockPos(0, 1, 0), Blocks.AIR.defaultBlockState(), null), + new StructureTemplate.StructureBlockInfo( + new BlockPos(0, 2, 0), Blocks.DEEPSLATE_BRICKS.defaultBlockState(), null), + new StructureTemplate.StructureBlockInfo( + new BlockPos(1, 0, 0), Blocks.STRUCTURE_VOID.defaultBlockState(), null), + new StructureTemplate.StructureBlockInfo( + new BlockPos(1, 1, 0), Blocks.DEEPSLATE.defaultBlockState(), null), + new StructureTemplate.StructureBlockInfo( + new BlockPos(1, 2, 0), Blocks.DEEPSLATE.defaultBlockState(), null))); + Map columns = new HashMap<>(); + + assertTrue(NativeStructureTerrainIntegrator.emitTemplateColumns( + List.of(template), new BlockPos(0, 0, 0), Rotation.NONE, + new BoundingBox(0, 0, 0, 1, 2, 0), + (x, z, minY, maxY) -> columns.put((long) x << 32 | z & 0xffffffffL, + new int[]{minY, maxY}))); + + assertEquals(2, columns.size()); + assertArrayEquals(new int[]{2, 2}, columns.get(0L)); + assertArrayEquals(new int[]{1, 2}, columns.get(1L << 32)); + } + + @Test + public void fullyVoidTemplateColumnsAreNotCarveSources() throws Exception { + StructureTemplate template = template(List.of( + new StructureTemplate.StructureBlockInfo( + new BlockPos(0, 0, 0), Blocks.AIR.defaultBlockState(), null), + new StructureTemplate.StructureBlockInfo( + new BlockPos(0, 1, 0), Blocks.STRUCTURE_VOID.defaultBlockState(), null))); + Map columns = new HashMap<>(); + + assertTrue(NativeStructureTerrainIntegrator.emitTemplateColumns( + List.of(template), new BlockPos(0, 0, 0), Rotation.NONE, + new BoundingBox(0, 0, 0, 0, 1, 0), + (x, z, minY, maxY) -> columns.put((long) x << 32 | z & 0xffffffffL, + new int[]{minY, maxY}))); + + assertTrue(columns.isEmpty()); + } + + @Test + public void nonTemplatePiecesFallBackToTheirBoundingBoxColumns() { + StructureStart start = desertStart(); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + + StructureCarvingFootprint footprint = NativeStructureTerrainIntegrator.carveFootprint( + start, 4, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(bounds.minX() - 4, footprint.minX()); + assertEquals(bounds.maxX() + 4, footprint.maxX()); + assertEquals(bounds.minZ() - 4, footprint.minZ()); + assertEquals(bounds.maxZ() + 4, footprint.maxZ()); + assertEquals(0L, footprint.distanceSquaredAt(bounds.minX(), bounds.minZ())); + assertEquals(32L, footprint.distanceSquaredAt(bounds.minX() - 4, bounds.minZ() - 4)); + assertEquals(bounds.minY(), footprint.sourceMinYAt(bounds.minX(), bounds.minZ())); + assertEquals(bounds.maxY(), footprint.sourceMaxYAt(bounds.minX(), bounds.minZ())); + } + + @Test + public void carveFootprintIsComputedOncePerStartAndPadding() { + StructureStart start = desertStart(); + + StructureCarvingFootprint first = NativeStructureTerrainIntegrator.carveFootprint( + start, 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + StructureCarvingFootprint repeated = NativeStructureTerrainIntegrator.carveFootprint( + start, 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + StructureCarvingFootprint widened = NativeStructureTerrainIntegrator.carveFootprint( + start, 7, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + StructureCarvingFootprint other = NativeStructureTerrainIntegrator.carveFootprint( + desertStart(), 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertSame(first, repeated); + assertNotSame(first, widened); + assertNotSame(first, other); + } + + @Test + public void concurrentCarveRequestsShareOneFootprintBuild() throws Exception { + StructureStart start = desertStart(); + int requestCount = 16; + ExecutorService executor = Executors.newFixedThreadPool(requestCount); + CountDownLatch ready = new CountDownLatch(requestCount); + CountDownLatch release = new CountDownLatch(1); + List> futures = new ArrayList<>(); + try { + for (int request = 0; request < requestCount; request++) { + futures.add(executor.submit(() -> { + ready.countDown(); + release.await(); + return NativeStructureTerrainIntegrator.carveFootprint( + start, 11, + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + })); + } + assertTrue(ready.await(5, TimeUnit.SECONDS)); + release.countDown(); + StructureCarvingFootprint expected = futures.getFirst().get(); + for (Future future : futures) { + assertSame(expected, future.get()); + } + } finally { + executor.shutdownNow(); + } + } + + @Test + public void carveFootprintCacheStaysInsideItsCellBudget() { + for (int index = 0; index < 40; index++) { + NativeStructureTerrainIntegrator.carveFootprint( + desertStart(), 128, + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + } + + assertTrue(NativeStructureTerrainIntegrator.cachedCarveFootprintCells() + <= NativeStructureTerrainIntegrator.maximumCachedCarveFootprintCells()); + } + + @Test + public void organicCarveNeverCutsBelowTheColumnSupportingFloor() { + StructureStart start = desertStart(); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + NativeStructureTerrainIntegrator.OrganicCarve carve = organicCarve(start, 6); + BoundingBox area = new BoundingBox( + bounds.minX() - 6, bounds.minY() - 4, bounds.minZ() - 6, + bounds.maxX() + 6, bounds.maxY() + 12, bounds.maxZ() + 6); + Map blocks = fill(area); + + NativeStructureTerrainIntegrator.carveOrganicColumns(world(blocks), area, carve); + + int centerX = bounds.minX() + bounds.getXSpan() / 2; + int centerZ = bounds.minZ() + bounds.getZSpan() / 2; + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, centerX, bounds.minY(), centerZ)); + for (int x = area.minX(); x <= area.maxX(); x++) { + for (int z = area.minZ(); z <= area.maxZ(); z++) { + for (int y = area.minY(); y < bounds.minY(); y++) { + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, x, y, z)); + } + } + } + } + + @Test + public void lobedCarveStaysInsideTheUniformCarveAndRemovesLess() { + StructureStart start = desertStart(); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + BoundingBox area = new BoundingBox( + bounds.minX() - 10, bounds.minY(), bounds.minZ() - 10, + bounds.maxX() + 10, bounds.maxY() + 12, bounds.maxZ() + 10); + Map uniformBlocks = fill(area); + Map lobedBlocks = fill(area); + + NativeStructureTerrainIntegrator.carveOrganicColumns( + world(uniformBlocks), area, organicCarve(start, 10, 0D)); + NativeStructureTerrainIntegrator.carveOrganicColumns( + world(lobedBlocks), area, organicCarve(start, 10, 0.85D)); + + int uniform = 0; + int lobed = 0; + for (int x = area.minX(); x <= area.maxX(); x++) { + for (int z = area.minZ(); z <= area.maxZ(); z++) { + for (int y = area.minY(); y <= area.maxY(); y++) { + boolean uniformAir = state(uniformBlocks, x, y, z).isAir(); + boolean lobedAir = state(lobedBlocks, x, y, z).isAir(); + assertTrue("lobe carved outside the uniform padding at " + + x + "," + y + "," + z, uniformAir || !lobedAir); + uniform += uniformAir ? 1 : 0; + lobed += lobedAir ? 1 : 0; + } + } + } + + assertTrue(lobed > 0); + assertTrue("uniform " + uniform + " lobed " + lobed, lobed < uniform); + } + + @Test + public void lobedCarveDepthWandersAlongAStraightFootprintEdge() { + int[] uniform = slabEdgeCarveDepths(0D); + int[] lobed = slabEdgeCarveDepths(0.85D); + + assertTrue("uniform depths wander " + span(uniform), span(uniform) <= 2); + assertTrue("lobed depths wander " + span(lobed), span(lobed) >= 6); + for (int depth : lobed) { + assertTrue(depth >= 0 && depth <= SLAB_PADDING); + } + } + + @Test + public void organicCarveIsIdenticalAcrossNeighboringChunkContexts() { + StructureStart start = desertStart(); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + BoundingBox wide = new BoundingBox( + bounds.minX() - 6, bounds.minY(), bounds.minZ() - 6, + bounds.maxX() + 6, bounds.maxY() + 12, bounds.maxZ() + 6); + BoundingBox narrow = new BoundingBox( + bounds.maxX() - 3, bounds.minY(), bounds.maxZ() - 3, + bounds.maxX() + 6, bounds.maxY() + 12, bounds.maxZ() + 6); + Map wideBlocks = fill(wide); + Map narrowBlocks = fill(narrow); + + // Each chunk context rebuilds its own noise channels from the shared start identity. + NativeStructureTerrainIntegrator.carveOrganicColumns( + world(wideBlocks), wide, organicCarve(start, 6)); + NativeStructureTerrainIntegrator.carveOrganicColumns( + world(narrowBlocks), narrow, organicCarve(start, 6)); + + int carved = 0; + for (int x = narrow.minX(); x <= narrow.maxX(); x++) { + for (int z = narrow.minZ(); z <= narrow.maxZ(); z++) { + for (int y = narrow.minY(); y <= narrow.maxY(); y++) { + BlockState expected = state(wideBlocks, x, y, z); + assertEquals(expected, state(narrowBlocks, x, y, z)); + if (expected.isAir()) { + carved++; + } + } + } + } + assertTrue(carved > 0); + } + + @Test + public void erodedForceCarveShrinkwrapsWithoutUnderminingTheFloor() { + StructureStart start = desertStart(); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + BoundingBox area = new BoundingBox( + bounds.minX() - 6, bounds.minY() - 2, bounds.minZ() - 6, + bounds.maxX() + 6, bounds.maxY() + 12, bounds.maxZ() + 6); + Map blocks = fill(area); + int centerX = bounds.minX() + bounds.getXSpan() / 2; + int centerZ = bounds.minZ() + bounds.getZSpan() / 2; + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), area, "minecraft:ancient_city", start, + new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setShape(IrisStructureCarveShape.ERODED) + .setHorizontalPadding(6) + .setCeilingPadding(8) + .setFloorPadding(0) + .setErosionStrength(1D) + .setErosionFrequency(0.05D), null); + + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, centerX, bounds.minY(), centerZ)); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, centerX, bounds.minY() - 1, centerZ)); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, area.minX(), area.maxY(), area.minZ())); + } + + @Test + public void sparseStiltGridIsDeterministicAndPreflightsGround() { + assertTrue(NativeStructureFoundationBuilder.isStiltColumn(0, 0, 4)); + assertTrue(NativeStructureFoundationBuilder.isStiltColumn(-4, 8, 4)); + assertFalse(NativeStructureFoundationBuilder.isStiltColumn(1, 0, 4)); + assertTrue(NativeStructureFoundationBuilder.isStiltColumn(1, 1, 1)); + + Map blocks = new HashMap<>(); + put(blocks, 0, 7, 0, Blocks.DEEPSLATE.defaultBlockState()); + put(blocks, 0, 8, 0, Blocks.SCULK_VEIN.defaultBlockState()); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + + assertEquals(7, NativeStructureFoundationBuilder.findStiltAnchorY( + world(blocks), 0, 0, 10, 2, -64, -64, position)); + assertEquals(Integer.MIN_VALUE, NativeStructureFoundationBuilder.findStiltAnchorY( + world(blocks), 0, 0, 10, 1, -64, -64, position)); + assertEquals(Integer.MIN_VALUE, NativeStructureFoundationBuilder.findStiltAnchorY( + world(new HashMap<>()), 0, 0, 10, 64, -64, -64, position)); + } + + @Test + public void nativeTerrainEnvelopePersistsHorizontalReferenceCoverage() { + StructureStart generated = desertStart(); + BoundingBox content = generated.getBoundingBox(); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setHorizontalPadding(24); + + StructureStart wrapped = NativeStructureReferenceEnvelope.wrap( + generated, + generated.getStructure(), + 0, + terrain); + BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds( + wrapped, wrapped.getStructure(), terrain); + + assertEquals(content, NativeStructureReferenceEnvelope.contentBounds(wrapped)); + assertEquals(content, wrapped.getBoundingBox()); + assertEquals(content.minX() - 24, referenceBounds.minX()); + assertEquals(content.minZ() - 24, referenceBounds.minZ()); + assertEquals(content.maxX() + 24, referenceBounds.maxX()); + assertEquals(content.maxZ() + 24, referenceBounds.maxZ()); + assertEquals(generated.getPieces().size(), wrapped.getPieces().size()); + } + + @Test + public void nativeVacuumReservesItsFixedSurfaceFalloff() { + StructureStart generated = desertStart(TerrainAdjustment.NONE); + BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.VACUUM) + .setHorizontalPadding(64); + + BoundingBox references = NativeStructureReferenceEnvelope.referenceBounds( + generated, generated.getStructure(), terrain); + + assertEquals(content.minX() - NativeStructureSurfaceFitter.surfaceTerrainRadius(), + references.minX()); + assertEquals(content.maxX() + NativeStructureSurfaceFitter.surfaceTerrainRadius(), + references.maxX()); + assertEquals(content.minZ() - NativeStructureSurfaceFitter.surfaceTerrainRadius(), + references.minZ()); + assertEquals(content.maxZ() + NativeStructureSurfaceFitter.surfaceTerrainRadius(), + references.maxZ()); + } + + @Test + public void nativeTerrainEnvelopeClipsOptionalCoverageWithoutDroppingContent() { + StructureStart generated = desertStart(); + ChunkPos origin = generated.getChunkPos(); + BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setHorizontalPadding(124); + + BoundingBox references = NativeStructureReferenceEnvelope.referenceBounds( + generated, generated.getStructure(), terrain, "test:clipped_terrain"); + StructureStart published = NativeStructureReferenceEnvelope.wrapForPublication( + generated, generated.getStructure(), 0, terrain, "test:clipped_terrain"); + + assertEquals(Math.max(content.minX() - 124, (origin.x() - 8) << 4), references.minX()); + assertEquals(Math.min(content.maxX() + 124, ((origin.x() + 8) << 4) + 15), references.maxX()); + assertEquals(Math.max(content.minZ() - 124, (origin.z() - 8) << 4), references.minZ()); + assertEquals(Math.min(content.maxZ() + 124, ((origin.z() + 8) << 4) + 15), references.maxZ()); + assertTrue(references.isInside(content.getCenter())); + assertTrue(published.isValid()); + assertEquals(1, generated.getPieces().size()); + } + + @Test + public void singlePoolTemplateFieldMatchesTheRuntimeContract() { + Field field = NativeStructureReflection.resolveSinglePoolTemplateField(); + + assertEquals(SinglePoolElement.class, field.getDeclaringClass()); + assertEquals(Either.class, field.getType()); + assertTrue(Modifier.isProtected(field.getModifiers())); + assertTrue(Modifier.isFinal(field.getModifiers())); + assertFalse(Modifier.isStatic(field.getModifiers())); + assertTrue(field.trySetAccessible()); + } + + @Test + public void runtimeTemplatesAndLegacyAirUseTheExactContract() { + StructureTemplate runtimeTemplate = new StructureTemplate(); + + assertEquals(runtimeTemplate, NativeStructureReflection.resolveTemplateReference( + Either.right(runtimeTemplate), null)); + assertFalse(NativeStructureTerrainIntegrator.shouldClearLegacyAir(79, 80, false)); + assertTrue(NativeStructureTerrainIntegrator.shouldClearLegacyAir(80, 80, false)); + assertTrue(NativeStructureTerrainIntegrator.shouldClearLegacyAir(96, 80, false)); + assertFalse(NativeStructureTerrainIntegrator.shouldClearLegacyAir(96, 80, true)); + } + + @Test + public void rotatedTemplateAirClearsOnlyInsideTheChunkAndAboveTheFloor() throws Exception { + StructureTemplate.StructureBlockInfo clear = new StructureTemplate.StructureBlockInfo( + new BlockPos(1, 0, 0), Blocks.AIR.defaultBlockState(), null); + StructureTemplate.StructureBlockInfo belowFloor = new StructureTemplate.StructureBlockInfo( + new BlockPos(0, -1, 0), Blocks.AIR.defaultBlockState(), null); + StructureTemplate.StructureBlockInfo outsideChunk = new StructureTemplate.StructureBlockInfo( + new BlockPos(3, 0, 0), Blocks.AIR.defaultBlockState(), null); + StructureTemplate template = template(List.of(clear, belowFloor, outsideChunk)); + BlockPos origin = new BlockPos(10, 80, 10); + StructurePlaceSettings settings = new StructurePlaceSettings().setRotation(Rotation.CLOCKWISE_90); + BlockPos clearPosition = origin.offset(StructureTemplate.calculateRelativePosition(settings, clear.pos())); + BlockPos belowFloorPosition = origin.offset( + StructureTemplate.calculateRelativePosition(settings, belowFloor.pos())); + BlockPos outsidePosition = origin.offset( + StructureTemplate.calculateRelativePosition(settings, outsideChunk.pos())); + BoundingBox area = new BoundingBox( + Math.min(clearPosition.getX(), belowFloorPosition.getX()), + Math.min(clearPosition.getY(), belowFloorPosition.getY()), + Math.min(clearPosition.getZ(), belowFloorPosition.getZ()), + Math.max(clearPosition.getX(), belowFloorPosition.getX()), + Math.max(clearPosition.getY(), belowFloorPosition.getY()), + Math.max(clearPosition.getZ(), belowFloorPosition.getZ())); + settings.setBoundingBox(area); + assertFalse(area.isInside(outsidePosition)); + Map blocks = new HashMap<>(); + blocks.put(clearPosition, Blocks.DIRT.defaultBlockState()); + blocks.put(belowFloorPosition, Blocks.DIRT.defaultBlockState()); + blocks.put(outsidePosition, Blocks.DIRT.defaultBlockState()); + + NativeStructureTerrainIntegrator.clearTemplateAir( + world(blocks), template, origin, 80, settings); + + assertEquals(Blocks.AIR.defaultBlockState(), blocks.get(clearPosition)); + assertEquals(Blocks.DIRT.defaultBlockState(), blocks.get(belowFloorPosition)); + assertEquals(Blocks.DIRT.defaultBlockState(), blocks.get(outsidePosition)); + } + + @Test + public void unrelatedPieceBoundsAreRejectedBeforeTemplateScanning() { + BoundingBox area = new BoundingBox(0, -64, 0, 15, 319, 15); + + assertTrue(NativeStructureTerrainIntegrator.intersects( + new BoundingBox(15, 60, 15, 30, 90, 30), area)); + assertFalse(NativeStructureTerrainIntegrator.intersects( + new BoundingBox(16, 60, 16, 30, 90, 30), area)); + assertFalse(NativeStructureTerrainIntegrator.intersects( + new BoundingBox(0, 320, 0, 15, 350, 15), area)); + } + + @Test + public void templateAirDoesNotEraseTreesInsideVillagePieces() throws Exception { + BlockPos origin = new BlockPos(0, 80, 0); + StructureTemplate template = template(List.of( + new StructureTemplate.StructureBlockInfo(BlockPos.ZERO, Blocks.AIR.defaultBlockState(), null))); + StructurePlaceSettings settings = new StructurePlaceSettings().setRotation(Rotation.NONE); + Map blocks = new HashMap<>(); + BlockState log = Blocks.OAK_LOG.defaultBlockState(); + blocks.put(origin, log); + + NativeStructureTerrainIntegrator.clearTemplateAir( + world(blocks), template, origin, 80, settings); + + assertEquals(log, blocks.get(origin)); + } + + private static NativeStructureSurfaceFitter.SurfaceAnchor anchor(int meetY, int strength) { + return new NativeStructureSurfaceFitter.SurfaceAnchor(0, 4, 0, 4, meetY, strength); + } + + private static PoolElementStructurePiece rigidPiece(BoundingBox bounds, int groundLevelDelta) { + StructurePoolElement element = StructurePoolElement.single( + "minecraft:empty").apply(StructureTemplatePool.Projection.RIGID); + return new PoolElementStructurePiece( + null, element, + new BlockPos(bounds.minX(), bounds.minY(), bounds.minZ()), + groundLevelDelta, Rotation.NONE, bounds, + LiquidSettings.APPLY_WATERLOGGING); + } + + private static PoolElementStructurePiece rigidTemplatePiece( + StructurePoolElement element, BoundingBox bounds, + int groundLevelDelta, Rotation rotation) { + return new PoolElementStructurePiece( + null, element, + new BlockPos(bounds.minX(), bounds.minY(), bounds.minZ()), + groundLevelDelta, rotation, bounds, + LiquidSettings.APPLY_WATERLOGGING); + } + + private static StructureStart rigidSurfaceStart(List pieces) { + return rigidSurfaceStart(pieces, TerrainAdjustment.BEARD_BOX); + } + + private static StructureStart rigidSurfaceStart( + List pieces, TerrainAdjustment adjustment) { + Structure structure = new DesertPyramidStructure( + new Structure.StructureSettings( + HolderSet.empty(), Map.of(), + GenerationStep.Decoration.SURFACE_STRUCTURES, + adjustment)); + List structurePieces = new ArrayList<>(pieces); + return new StructureStart( + structure, new ChunkPos(0, 0), 0, + new PiecesContainer(structurePieces)); + } + + private static NativeStructureTerrainIntegrator.TerrainTarget surfaceTarget( + StructureStart start) { + return surfaceTarget(start, IrisStructureTerrainMode.SOURCE); + } + + private static NativeStructureTerrainIntegrator.TerrainTarget surfaceTarget( + StructureStart start, IrisStructureTerrainMode mode) { + return new NativeStructureTerrainIntegrator.TerrainTarget( + "nova_structures:tavern_oak", start, + new IrisStructureTerrain().setMode(mode)); + } + + private static StructureTemplate.StructureBlockInfo block( + int x, int y, int z, BlockState state) { + return new StructureTemplate.StructureBlockInfo( + new BlockPos(x, y, z), state, null); + } + + private static Map supportTerrain(int minimumX, int maximumX) { + Map blocks = new HashMap<>(); + for (int x = minimumX; x <= maximumX; x++) { + put(blocks, x, 61, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, x, 62, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + } + return blocks; + } + + private static Map flatTerrain(BoundingBox area, int surfaceY) { + Map blocks = new HashMap<>(); + for (int x = area.minX(); x <= area.maxX(); x++) { + for (int z = area.minZ(); z <= area.maxZ(); z++) { + put(blocks, x, surfaceY - 1, z, Blocks.DIRT.defaultBlockState()); + put(blocks, x, surfaceY, z, Blocks.GRASS_BLOCK.defaultBlockState()); + } + } + return blocks; + } + + private static NativeStructureTerrainIntegrator.OrganicCarve organicCarve( + StructureStart start, int horizontalPadding) { + return organicCarve(start, horizontalPadding, 0.85D); + } + + private static NativeStructureTerrainIntegrator.OrganicCarve organicCarve( + StructureStart start, int horizontalPadding, double lobeStrength) { + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setShape(IrisStructureCarveShape.ERODED) + .setHorizontalPadding(horizontalPadding) + .setCeilingPadding(8) + .setFloorPadding(0) + .setErosionStrength(1D) + .setErosionFrequency(0.05D) + .setLobeStrength(lobeStrength); + return NativeStructureTerrainIntegrator.organicCarve( + NativeStructureTerrainIntegrator.carveFootprint(start, horizontalPadding, + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager), + terrain, IrisStructureCarveShape.ERODED, TEST_SEED); + } + + /** + * Outward carve depth for every column along the straight +X edge of a slab footprint, measured + * inside the source vertical span so the boundary is decided purely by the horizontal threshold. + * The slab spans several lobe wavelengths so a lobed boundary is distinguishable from a uniform one. + */ + private static int[] slabEdgeCarveDepths(double lobeStrength) { + StructureCarvingFootprint footprint = StructureCarvingFootprint.fromColumns(sink -> { + for (int x = 0; x < SLAB_WIDTH; x++) { + for (int z = 0; z < SLAB_DEPTH; z++) { + sink.column(x, z, SLAB_MIN_Y, SLAB_MIN_Y + 8); + } + } + return true; + }, SLAB_PADDING, 1_000_000); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setShape(IrisStructureCarveShape.ERODED) + .setHorizontalPadding(SLAB_PADDING) + .setCeilingPadding(8) + .setFloorPadding(0) + .setErosionStrength(1D) + .setErosionFrequency(0.05D) + .setLobeStrength(lobeStrength); + int transectY = SLAB_MIN_Y + 4; + BoundingBox area = new BoundingBox( + SLAB_WIDTH, transectY, 0, + SLAB_WIDTH - 1 + SLAB_PADDING, transectY, SLAB_DEPTH - 1); + Map blocks = fill(area); + + NativeStructureTerrainIntegrator.carveOrganicColumns(world(blocks), area, + NativeStructureTerrainIntegrator.organicCarve( + footprint, terrain, IrisStructureCarveShape.ERODED, TEST_SEED)); + + int[] depths = new int[SLAB_DEPTH]; + for (int z = 0; z < SLAB_DEPTH; z++) { + int depth = 0; + while (depth < SLAB_PADDING + && state(blocks, SLAB_WIDTH + depth, transectY, z).isAir()) { + depth++; + } + depths[z] = depth; + } + return depths; + } + + private static int span(int[] values) { + int lowest = Integer.MAX_VALUE; + int highest = Integer.MIN_VALUE; + for (int value : values) { + lowest = Math.min(lowest, value); + highest = Math.max(highest, value); + } + return highest - lowest; + } + + private static StructureTemplateManager forbiddenTemplateManager() { + throw new AssertionError("Bounding-box carve columns must not resolve templates"); + } + + private static Map fill(BoundingBox area) { + Map blocks = new HashMap<>(); + for (int x = area.minX(); x <= area.maxX(); x++) { + for (int z = area.minZ(); z <= area.maxZ(); z++) { + for (int y = area.minY(); y <= area.maxY(); y++) { + put(blocks, x, y, z, Blocks.STONE.defaultBlockState()); + } + } + } + return blocks; + } + + private static StructureTemplate template( + List blocks) throws Exception { + Constructor constructor = + StructureTemplate.Palette.class.getDeclaredConstructor(List.class); + assertTrue(constructor.trySetAccessible()); + StructureTemplate.Palette palette = constructor.newInstance(blocks); + StructureTemplate template = new StructureTemplate(); + template.palettes.add(palette); + return template; + } + + private static final class FlattenDesertPiece extends DesertPyramidPiece { + private FlattenDesertPiece() { + super(RandomSource.create(29L), 0, 0); + } + + private boolean attemptVanillaAlignment() { + return updateHeightPositionToLowestGroundHeight((LevelAccessor) null, -2); + } + } + + private static final class InlineSinglePoolElement extends SinglePoolElement { + private InlineSinglePoolElement(StructureTemplate template) { + this(template, List.of()); + } + + private InlineSinglePoolElement( + StructureTemplate template, List processors) { + this(template, processors, StructureTemplatePool.Projection.RIGID); + } + + private InlineSinglePoolElement( + StructureTemplate template, List processors, + StructureTemplatePool.Projection projection) { + super(Either.right(template), + Holder.direct(new StructureProcessorList(processors)), + projection, + Optional.empty()); + } + } + + private static final class InlineLegacyPoolElement extends LegacySinglePoolElement { + private InlineLegacyPoolElement(StructureTemplate template) { + super(Either.right(template), + Holder.direct(new StructureProcessorList(List.of())), + StructureTemplatePool.Projection.RIGID, + Optional.empty()); + } + } + + private static StructureProcessor replaceBlockProcessor(BlockState source, BlockState replacement) { + // Vanilla RuleProcessor instead of a custom StructureProcessor subtype: StructureProcessor + // is an abstract class on 26.1.2 and an interface on 26.2, so a direct subtype cannot + // compile against both dev bundles. BlockStateMatchTest matches the exact source state, + // matching the previous custom processor's identity-equality semantics. + return new RuleProcessor(List.of(new ProcessorRule( + new BlockStateMatchTest(source), AlwaysTrueTest.INSTANCE, replacement))); + } + + private static StructureStart desertStart() { + return desertStart(TerrainAdjustment.NONE); + } + + private static StructureStart desertStart(TerrainAdjustment adjustment) { + Structure structure = new DesertPyramidStructure( + new Structure.StructureSettings( + HolderSet.empty(), Map.of(), + GenerationStep.Decoration.SURFACE_STRUCTURES, adjustment)); + DesertPyramidPiece piece = new DesertPyramidPiece(RandomSource.create(7L), 0, 0); + return new StructureStart( + structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece))); + } + + private static WorldGenLevel world(Map blocks) { + InvocationHandler handler = (proxy, method, arguments) -> { + String methodName = method.getName(); + if (methodName.equals("getBlockState")) { + BlockPos position = (BlockPos) arguments[0]; + return state(blocks, position.getX(), position.getY(), position.getZ()); + } + if (methodName.equals("setBlock")) { + BlockPos position = (BlockPos) arguments[0]; + BlockState blockState = (BlockState) arguments[1]; + put(blocks, position.getX(), position.getY(), position.getZ(), blockState); + return true; + } + if (methodName.equals("getSeed")) { + return TEST_SEED; + } + if (methodName.equals("getHeight")) { + int x = (int) arguments[1]; + int z = (int) arguments[2]; + int highest = Integer.MIN_VALUE; + for (Map.Entry entry : blocks.entrySet()) { + BlockPos position = entry.getKey(); + if (position.getX() == x && position.getZ() == z + && !entry.getValue().isAir()) { + highest = Math.max(highest, position.getY()); + } + } + return highest == Integer.MIN_VALUE ? 0 : highest + 1; + } + 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 "surface-test-world"; + } + throw new UnsupportedOperationException(method.toString()); + }; + return (WorldGenLevel) Proxy.newProxyInstance( + WorldGenLevel.class.getClassLoader(), new Class[]{WorldGenLevel.class}, handler); + } + + 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("getHeight") && arguments != null && arguments.length == 3) { + return chunk.getHeight((Heightmap.Types) arguments[0], (int) arguments[1], (int) arguments[2]) + 1; + } + 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 "surface-test-chunk-world"; + } + throw new UnsupportedOperationException(method.toString()); + }; + return (WorldGenLevel) Proxy.newProxyInstance( + WorldGenLevel.class.getClassLoader(), new Class[]{WorldGenLevel.class}, handler); + } + + 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 PalettedContainerFactory containerFactory() { + Strategy blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY); + BlockState air = Blocks.AIR.defaultBlockState(); + IdMapper> biomeIds = new IdMapper<>(); + Holder defaultBiome = Holder.direct((Biome) null); + biomeIds.add(defaultBiome); + Strategy> biomes = Strategy.createForBiomes(biomeIds); + Codec> 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)); + } + + private static void put(Map blocks, + int x, int y, int z, BlockState state) { + blocks.put(new BlockPos(x, y, z), state); + } + + private static BlockState state(Map blocks, int x, int y, int z) { + return blocks.getOrDefault(new BlockPos(x, y, z), Blocks.AIR.defaultBlockState()); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java new file mode 100644 index 000000000..6f04a83fc --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java @@ -0,0 +1,354 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import com.mojang.serialization.Codec; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.IdMapper; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.chunk.PalettedContainerFactory; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.Strategy; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class NativeStructurePostProcessorVegetationTest { + private static final long TEST_SEED = 1234L; + private static final int MIN_Y = -64; + private static final int MAX_Y = 319; + private static final int GROUND_Y = 64; + private static final BoundingBox AREA = new BoundingBox(0, MIN_Y, 0, 15, MAX_Y, 15); + + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void canopyAboveTheStructureRoofSurvivesVegetationClearing() { + ProtoChunk chunk = chunk(); + StructureStart start = desertStart(0, 0, GROUND_Y + 15); + BoundingBox bounds = start.getBoundingBox(); + for (int y = bounds.maxY() - 1; y <= GROUND_Y + 40; y++) { + fillLayer(chunk, y, leaves()); + } + write(chunk, 4, bounds.minY() - 1, 4, log()); + + clearVegetation(chunk, start); + + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + assertEquals(leaves(), chunk.getBlockState(new BlockPos(x, GROUND_Y + 40, z))); + assertEquals(leaves(), chunk.getBlockState(new BlockPos(x, bounds.maxY() + 1, z))); + assertEquals(air(), chunk.getBlockState(new BlockPos(x, bounds.maxY(), z))); + assertEquals(air(), chunk.getBlockState(new BlockPos(x, bounds.maxY() - 1, z))); + } + } + assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 1, 4))); + } + + @Test + public void buriedStructuresPreserveTheForestAboveThem() { + ProtoChunk chunk = chunk(); + StructureStart start = desertStart(0, 0, -20); + write(chunk, 8, GROUND_Y + 1, 8, log()); + write(chunk, 8, GROUND_Y + 2, 8, log()); + write(chunk, 8, GROUND_Y + 3, 8, log()); + write(chunk, 8, GROUND_Y + 4, 8, leaves()); + + clearVegetation(chunk, start); + + assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 1, 8))); + assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 2, 8))); + assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 3, 8))); + assertEquals(leaves(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 4, 8))); + } + + @Test + public void columnsOutsideThePieceFootprintKeepEveryTreeBlock() { + ProtoChunk chunk = chunk(); + StructureStart start = desertStart(8, 8, GROUND_Y + 15); + BoundingBox bounds = start.getBoundingBox(); + int inside = bounds.minY() + 4; + write(chunk, 0, inside, 0, log()); + write(chunk, 0, GROUND_Y + 40, 0, leaves()); + write(chunk, 12, inside, 12, log()); + write(chunk, 12, GROUND_Y + 40, 12, leaves()); + + clearVegetation(chunk, start); + + assertEquals(log(), chunk.getBlockState(new BlockPos(0, inside, 0))); + assertEquals(leaves(), chunk.getBlockState(new BlockPos(0, GROUND_Y + 40, 0))); + assertEquals(air(), chunk.getBlockState(new BlockPos(12, inside, 12))); + assertEquals(leaves(), chunk.getBlockState(new BlockPos(12, GROUND_Y + 40, 12))); + } + + @Test + public void clearingStopsAtThePieceEnvelopeBoundaries() { + ProtoChunk chunk = chunk(); + StructureStart start = desertStart(0, 0, GROUND_Y + 15); + BoundingBox bounds = start.getBoundingBox(); + write(chunk, 4, bounds.maxY() + 1, 4, log()); + write(chunk, 4, bounds.maxY(), 4, log()); + write(chunk, 4, bounds.minY() - 1, 4, log()); + write(chunk, 4, bounds.minY() - 2, 4, log()); + + clearVegetation(chunk, start); + + assertEquals(log(), chunk.getBlockState(new BlockPos(4, bounds.maxY() + 1, 4))); + assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.maxY(), 4))); + assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 1, 4))); + assertEquals(log(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 2, 4))); + } + + @Test + public void overlappingPieceEnvelopesMergeIntoOneClearedRange() { + ProtoChunk chunk = chunk(); + StructureStart low = desertStart(0, 0, GROUND_Y); + StructureStart high = desertStart(0, 0, GROUND_Y + 40); + int between = GROUND_Y + 20; + write(chunk, 4, between, 4, log()); + write(chunk, 4, high.getBoundingBox().maxY() + 1, 4, log()); + write(chunk, 4, low.getBoundingBox().minY() - 2, 4, log()); + + NativeStructureVegetationClearer.clearIntersectingVegetation( + world(chunk), chunk, AREA, List.of(low, high)); + + assertEquals(air(), chunk.getBlockState(new BlockPos(4, between, 4))); + assertEquals(log(), chunk.getBlockState( + new BlockPos(4, high.getBoundingBox().maxY() + 1, 4))); + assertEquals(log(), chunk.getBlockState( + new BlockPos(4, low.getBoundingBox().minY() - 2, 4))); + } + + @Test + public void validStartsReachIntersectionProcessing() { + assertTrue(NativeStructureVegetationClearer.shouldProcessStart( + desertStart(0, 0, GROUND_Y + 15))); + assertFalse(NativeStructureVegetationClearer.shouldProcessStart( + StructureStart.INVALID_START)); + } + + @Test + public void allUndergroundGenerationStepsShareOneClassification() { + assertTrue(NativeStructureVegetationClearer.isUndergroundStep( + GenerationStep.Decoration.UNDERGROUND_STRUCTURES)); + assertTrue(NativeStructureVegetationClearer.isUndergroundStep( + GenerationStep.Decoration.UNDERGROUND_DECORATION)); + assertTrue(NativeStructureVegetationClearer.isUndergroundStep( + GenerationStep.Decoration.STRONGHOLDS)); + assertFalse(NativeStructureVegetationClearer.isUndergroundStep( + GenerationStep.Decoration.SURFACE_STRUCTURES)); + } + + @Test + public void undergroundStructuresUseTheLowestTerrainColumn() { + BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0); + int offset = NativeStructureVerticalPlacer.resolveBuriedOffset( + bounds, 0, -64, 320, (x, z) -> x == 1 ? 76 : 100); + assertEquals(-5, offset); + } + + @Test + public void undergroundBurialClampsToTheWorldFloorInsteadOfFailing() { + BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0); + assertEquals(-2, NativeStructureVerticalPlacer.resolveBuriedOffset( + bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100)); + } + + @Test + public void loweringSurfaceRemovesCactusStacksAndTheirFlowers() { + ProtoChunk chunk = chunk(); + for (int y = 80; y <= 102; y++) { + write(chunk, 4, y, 4, Blocks.SAND.defaultBlockState()); + } + for (int y = 103; y <= 105; y++) { + write(chunk, 4, y, 4, Blocks.CACTUS.defaultBlockState()); + } + write(chunk, 4, 106, 4, Blocks.CACTUS_FLOWER.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(chunk), new BlockPos.MutableBlockPos(), 4, 4, 102, 93, MIN_Y, MAX_Y); + + assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 93, 4))); + for (int y = 94; y <= 106; y++) { + assertTrue("Unsupported decoration at " + y, chunk.getBlockState(new BlockPos(4, y, 4)).isAir()); + } + } + + @Test + public void loweringSurfacePreservesCactusOnUnchangedColumns() { + ProtoChunk chunk = chunk(); + for (int x : List.of(4, 5)) { + for (int y = 60; y <= 64; y++) { + write(chunk, x, y, 4, Blocks.SAND.defaultBlockState()); + } + write(chunk, x, 65, 4, Blocks.CACTUS.defaultBlockState()); + write(chunk, x, 66, 4, Blocks.CACTUS_FLOWER.defaultBlockState()); + } + WorldGenLevel world = world(chunk); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world, new BlockPos.MutableBlockPos(), 4, 4, 64, 60, MIN_Y, MAX_Y); + NativeStructureSurfaceFitter.applySurfaceColumn( + world, new BlockPos.MutableBlockPos(), 5, 4, 64, 64, MIN_Y, MAX_Y); + + assertTrue(chunk.getBlockState(new BlockPos(4, 65, 4)).isAir()); + assertTrue(chunk.getBlockState(new BlockPos(4, 66, 4)).isAir()); + assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 64, 4))); + assertEquals(Blocks.CACTUS.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 65, 4))); + assertEquals(Blocks.CACTUS_FLOWER.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 66, 4))); + } + + @Test + public void loweringSurfaceKeepsTreeAndSolidObjectBoundaries() { + for (BlockState boundary : List.of(Blocks.OAK_LOG.defaultBlockState(), Blocks.OAK_PLANKS.defaultBlockState())) { + ProtoChunk chunk = chunk(); + write(chunk, 4, 63, 4, Blocks.SAND.defaultBlockState()); + write(chunk, 4, 64, 4, Blocks.SAND.defaultBlockState()); + write(chunk, 4, 65, 4, boundary); + write(chunk, 4, 66, 4, Blocks.CACTUS.defaultBlockState()); + write(chunk, 4, 67, 4, Blocks.CACTUS_FLOWER.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(chunk), new BlockPos.MutableBlockPos(), 4, 4, 64, 60, MIN_Y, MAX_Y); + + assertEquals(boundary, chunk.getBlockState(new BlockPos(4, 65, 4))); + assertEquals(Blocks.CACTUS.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 66, 4))); + assertEquals(Blocks.CACTUS_FLOWER.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 67, 4))); + } + } + + 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 blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY); + BlockState air = Blocks.AIR.defaultBlockState(); + IdMapper> biomeIds = new IdMapper<>(); + Holder defaultBiome = Holder.direct((Biome) null); + biomeIds.add(defaultBiome); + Strategy> biomes = Strategy.createForBiomes(biomeIds); + Codec> 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)); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancyTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancyTest.java new file mode 100644 index 000000000..1b0e9e0a9 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancyTest.java @@ -0,0 +1,236 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import com.mojang.datafixers.util.Either; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.Bootstrap; +import net.minecraft.tags.TagKey; +import net.minecraft.util.valueproviders.ConstantInt; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.templatesystem.BlackstoneReplaceProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.BlockAgeProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.CappedProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.GravityProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.JigsawReplacementProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.LavaSubmergedBlockProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.NopProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.ProtectedBlockProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.RuleProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class NativeStructureTemplateOccupancyTest { + private static final BoundingBox PROCESSING_AREA = new BoundingBox(0, 56, 0, 15, 80, 0); + private static final int TEMPLATE_WIDTH = 32; + private static final int TERRAIN_HEIGHT = 64; + + @BeforeClass + public static void bootstrapMinecraft() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void straddlingGravityPieceHeightReadsHonorTheVanillaClipContract() throws Exception { + ColumnRecorder recorder = new ColumnRecorder(); + + resolve(straddlingPiece( + List.of(new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0))), recorder); + + // 26.2 vanilla clips processor evaluation to the placement bounding box; 26.1.2 vanilla + // runs processors across the whole piece. Iris mirrors the pinned version's semantics. + assertEquals(expectedClippedColumns(), recorder.heightColumns); + } + + @Test + public void cappedStraddlingPieceRunsProcessorsAcrossTheWholePiece() throws Exception { + ColumnRecorder recorder = new ColumnRecorder(); + + resolve(straddlingPiece(List.of( + new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0), + new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(0)))), recorder); + + assertEquals(columns(0, TEMPLATE_WIDTH - 1), recorder.heightColumns); + } + + @Test + public void cappedProcessorIsTheOnlyProcessorThatDisablesTheProcessingAreaClip() throws Exception { + Method contract = clipContractMethod(); + Assume.assumeTrue("evaluatesEntirePieceState only exists on 26.2+", contract != null); + + for (StructureProcessor processor : List.of( + BlockIgnoreProcessor.STRUCTURE_BLOCK, + JigsawReplacementProcessor.INSTANCE, + LavaSubmergedBlockProcessor.INSTANCE, + NopProcessor.INSTANCE, + BlackstoneReplaceProcessor.INSTANCE, + new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0), + new RuleProcessor(List.of()), + new BlockAgeProcessor(0.5F), + protectedBlockProcessor())) { + assertFalse(processor.getClass().getName(), + (Boolean) contract.invoke(processor)); + } + assertTrue((Boolean) contract.invoke( + new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(4)))); + } + + @Test + public void placementSettingsCarryTheProcessingAreaAsTheirClipBox() throws Exception { + PoolElementStructurePiece piece = straddlingPiece(List.of()); + + StructurePlaceSettings settings = NativeStructureReflection.resolvePlacementSettings( + (SinglePoolElement) piece.getElement(), piece, PROCESSING_AREA); + + assertEquals(PROCESSING_AREA, settings.getBoundingBox()); + } + + private static void resolve(PoolElementStructurePiece piece, ColumnRecorder recorder) { + NativeStructureTemplateOccupancy.resolve( + world(recorder), piece, piece.getPosition(), PROCESSING_AREA, + NativeStructureTemplateOccupancyTest::forbiddenTemplateManager, + PROCESSING_AREA::isInside, amount -> { + }); + } + + private static PoolElementStructurePiece straddlingPiece( + List processors) throws Exception { + BoundingBox bounds = new BoundingBox(0, 70, 0, TEMPLATE_WIDTH - 1, 76, 0); + return new PoolElementStructurePiece( + null, new InlineSinglePoolElement(template(), processors), + new BlockPos(bounds.minX(), bounds.minY(), bounds.minZ()), + 1, Rotation.NONE, bounds, LiquidSettings.APPLY_WATERLOGGING); + } + + private static Set columns(int minimumX, int maximumX) { + Set columns = new TreeSet<>(); + for (int x = minimumX; x <= maximumX; x++) { + columns.add(x); + } + return columns; + } + + private static Method clipContractMethod() { + try { + return StructureProcessor.class.getMethod("evaluatesEntirePieceState"); + } catch (NoSuchMethodException absent) { + return null; + } + } + + private static Set expectedClippedColumns() { + return clipContractMethod() != null + ? columns(PROCESSING_AREA.minX(), PROCESSING_AREA.maxX()) + : columns(0, TEMPLATE_WIDTH - 1); + } + + private static StructureProcessor protectedBlockProcessor() throws Exception { + // Constructed reflectively: the constructor takes HolderSet on 26.2 and TagKey on 26.1.2. + Constructor constructor = ProtectedBlockProcessor.class.getConstructors()[0]; + Class parameter = constructor.getParameterTypes()[0]; + Object argument = parameter == HolderSet.class + ? HolderSet.empty() + : TagKey.create(Registries.BLOCK, Identifier.fromNamespaceAndPath("minecraft", "air")); + return (StructureProcessor) constructor.newInstance(argument); + } + + private static StructureTemplateManager forbiddenTemplateManager() { + throw new AssertionError("Inline templates must not resolve a template manager"); + } + + private static StructureTemplate template() throws Exception { + List blocks = new ArrayList<>(); + for (int x = 0; x < TEMPLATE_WIDTH; x++) { + blocks.add(new StructureTemplate.StructureBlockInfo( + new BlockPos(x, 0, 0), Blocks.COBBLESTONE.defaultBlockState(), null)); + } + Constructor constructor = + StructureTemplate.Palette.class.getDeclaredConstructor(List.class); + assertTrue(constructor.trySetAccessible()); + StructureTemplate template = new StructureTemplate(); + template.palettes.add(constructor.newInstance(blocks)); + return template; + } + + private static WorldGenLevel world(ColumnRecorder recorder) { + InvocationHandler handler = (proxy, method, arguments) -> { + String methodName = method.getName(); + if (methodName.equals("getHeight") && arguments.length == 3) { + recorder.heightColumns.add((Integer) arguments[1]); + return TERRAIN_HEIGHT; + } + if (methodName.equals("getBlockState")) { + recorder.stateColumns.add(((BlockPos) arguments[0]).getX()); + return Blocks.STONE.defaultBlockState(); + } + 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 "occupancy-test-world"; + } + throw new UnsupportedOperationException(method.toString()); + }; + return (WorldGenLevel) Proxy.newProxyInstance( + WorldGenLevel.class.getClassLoader(), + new Class[]{WorldGenLevel.class}, handler); + } + + private static final class ColumnRecorder { + private final Set heightColumns = new TreeSet<>(); + private final Set stateColumns = new TreeSet<>(); + } + + private static final class InlineSinglePoolElement extends SinglePoolElement { + private InlineSinglePoolElement( + StructureTemplate template, List processors) { + super(Either.right(template), + Holder.direct(new StructureProcessorList(processors)), + StructureTemplatePool.Projection.RIGID, + Optional.empty()); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureVolumeIndexTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureVolumeIndexTest.java new file mode 100644 index 000000000..23c933745 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureVolumeIndexTest.java @@ -0,0 +1,561 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.generation.runtime.IrisEngine; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.structure.nativegen.NativeStructureVolume; +import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter; +import art.arcane.volmlib.util.collection.KList; +import net.minecraft.SharedConstants; +import net.minecraft.core.HolderSet; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Proxy; +import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.LockSupport; +import java.util.function.IntConsumer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class NativeStructureVolumeIndexTest { + private static final String STRUCTURE_KEY = "minecraft:swamp_hut"; + + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void assembledPiecesBecomeWorldSpacePieceVolumes() { + StructureStart start = swampHut(0, 0, 4, 6); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + + KList volumes = NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, start); + + assertEquals(1, volumes.size()); + NativeStructureVolume volume = volumes.getFirst(); + assertEquals(STRUCTURE_KEY, volume.structure()); + assertEquals(bounds.minX(), volume.minX()); + assertEquals(bounds.minY(), volume.minY()); + assertEquals(bounds.minZ(), volume.minZ()); + assertEquals(bounds.maxX(), volume.maxX()); + assertEquals(bounds.maxY(), volume.maxY()); + assertEquals(bounds.maxZ(), volume.maxZ()); + } + + @Test + public void invalidStartsContributeNoVolumes() { + assertNull(NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START)); + assertNull(NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, null)); + } + + @Test + public void coldAndCachedQueriesResolveIdenticalVolumes() { + CountingResolver resolver = new CountingResolver(0, 0); + NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver); + + KList cold = index.resolve(null, 0, 0, 15, 15); + int coldResolutions = resolver.resolutions(); + KList cached = index.resolve(null, 0, 0, 15, 15); + + assertTrue(coldResolutions > 0); + assertFalse(cold.isEmpty()); + assertEquals(cold, cached); + assertEquals(coldResolutions, resolver.resolutions()); + } + + @Test + public void runtimeBucketsStayIsolatedAndRetirementEvictsOnlyTheirEntries() { + AtomicInteger runtimeId = new AtomicInteger(1); + Engine engine = runtimeEngine(runtimeId); + CountingResolver resolver = new CountingResolver(0, 0); + NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver); + + index.resolve(engine, 0, 0, 15, 15); + assertEquals(289, resolver.resolutions()); + index.resolve(engine, 0, 0, 15, 15); + assertEquals(289, resolver.resolutions()); + + runtimeId.set(2); + index.resolve(engine, 0, 0, 15, 15); + assertEquals(578, resolver.resolutions()); + index.resolve(engine, 0, 0, 15, 15); + assertEquals(578, resolver.resolutions()); + + index.evictRuntime(1); + runtimeId.set(1); + index.resolve(engine, 0, 0, 15, 15); + assertEquals(867, resolver.resolutions()); + } + + @Test + public void pinnedRuntimeReusesCachedOriginsWithoutOpeningCoordinateScopes() throws Exception { + ScopedEngine scoped = scopedEngine(true, 1); + CountingResolver resolver = new CountingResolver(0, 0); + NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver); + + KList first = index.resolve(scoped.engine(), 0, 0, 15, 15); + assertEquals(289, scoped.scopes().get()); + index.resolve(scoped.engine(), 16, 0, 31, 15); + assertEquals(306, resolver.resolutions()); + assertEquals(306, scoped.scopes().get()); + assertEquals(first, index.resolve(scoped.engine(), 0, 0, 15, 15)); + assertEquals(306, scoped.scopes().get()); + } + + @Test + public void absentOrMismatchedRuntimeOwnershipKeepsCoordinateRouting() throws Exception { + ScopedEngine detached = scopedEngine(true, 1); + when(detached.engine().getGenerationHistoryRuntimeRouter()).thenReturn(Optional.empty()); + for (ScopedEngine scoped : List.of(scopedEngine(false, 1), scopedEngine(true, 0), + scopedEngine(true, 2), detached)) { + NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0)); + index.resolve(scoped.engine(), 0, 0, 15, 15); + index.resolve(scoped.engine(), 16, 0, 31, 15); + assertEquals(578, scoped.scopes().get()); + } + } + + @Test + public void retiredPinnedOriginsRebuildThroughCoordinateScopes() throws Exception { + ScopedEngine scoped = scopedEngine(true, 1); + CountingResolver resolver = new CountingResolver(0, 0); + NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver); + + KList first = index.resolve(scoped.engine(), 0, 0, 15, 15); + index.evictRuntime(1); + assertEquals(first, index.resolve(scoped.engine(), 0, 0, 15, 15)); + assertEquals(578, resolver.resolutions()); + assertEquals(578, scoped.scopes().get()); + } + + @Test + public void invalidatedIndexReceivesRuntimeRetirementAndDetachesOnUninstall() throws Exception { + IrisEngine engine = mock(IrisEngine.class); + Set listeners = new CopyOnWriteArraySet<>(); + doAnswer(invocation -> { + listeners.add(invocation.getArgument(0)); + return null; + }).when(engine).addGenerationRuntimeRetirementListener(any()); + doAnswer(invocation -> { + listeners.remove(invocation.getArgument(0)); + return null; + }).when(engine).removeGenerationRuntimeRetirementListener(any()); + NativeStructureVolumeIndex.Context context = new NativeStructureVolumeIndex.Context( + null, null, null, null, () -> null, () -> null, () -> null); + NativeStructureVolumeIndex.install(engine, context); + try { + assertEquals(1, listeners.size()); + IntConsumer original = listeners.iterator().next(); + NativeStructureVolumeIndex.invalidate(engine); + assertEquals(1, listeners.size()); + assertFalse(listeners.contains(original)); + + NativeStructureVolumeIndex.volumes(engine, 0, 0, 15, 15); + NativeStructureVolumeIndex current = installedIndexes().get(engine); + assertEquals(289, cache(current, "originCache").size()); + assertEquals(1, cache(current, "queryCache").size()); + for (IntConsumer listener : listeners) { + listener.accept(0); + } + assertTrue(cache(current, "originCache").isEmpty()); + assertTrue(cache(current, "queryCache").isEmpty()); + } finally { + NativeStructureVolumeIndex.uninstall(engine); + } + assertTrue(listeners.isEmpty()); + } + + @Test + public void retiringOriginBuildCannotRepublishWhileQueryEvictionWaits() throws Exception { + BlockingResolver resolver = new BlockingResolver(); + NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver); + ExecutorService executor = Executors.newFixedThreadPool(2); + Map origins = cache(index, "originCache"); + Map queries = cache(index, "queryCache"); + try { + Future> builder = executor.submit(() -> index.originVolumes(null, 0, 0)); + assertTrue(resolver.awaitFirstEntry()); + index.originVolumes(null, 1, 0); + assertEquals(1, origins.size()); + Future retirement; + synchronized (queries) { + retirement = executor.submit(() -> index.evictRuntime(0)); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + boolean cleared = false; + while (System.nanoTime() < deadline) { + synchronized (origins) { + cleared = origins.isEmpty(); + } + if (cleared) { + break; + } + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10L)); + } + assertTrue("Origin eviction did not complete before the query cache lock", cleared); + resolver.release(); + assertTrue(builder.get(5, TimeUnit.SECONDS).isEmpty()); + synchronized (origins) { + assertTrue("The retired in-flight origin was cached again", origins.isEmpty()); + } + } + retirement.get(5, TimeUnit.SECONDS); + index.originVolumes(null, 0, 0); + assertEquals(3, resolver.resolutions()); + } finally { + resolver.release(); + executor.shutdownNow(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } + + @Test + public void independentIndexesResolveIdenticalVolumesFromTheSameSeed() { + NativeStructureVolumeIndex first = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0)); + NativeStructureVolumeIndex second = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0)); + + assertEquals(first.resolve(null, 0, 0, 15, 15), second.resolve(null, 0, 0, 15, 15)); + assertEquals(first.resolve(null, -32, -32, 47, 47), second.resolve(null, -32, -32, 47, 47)); + } + + @Test + public void volumesOutsideTheQueryRectAreExcluded() { + NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0)); + BoundingBox bounds = swampHut(0, 0, 4, 6).getPieces().getFirst().getBoundingBox(); + + KList hit = index.resolve(null, bounds.minX(), bounds.minZ(), bounds.maxX(), bounds.maxZ()); + KList miss = index.resolve(null, + bounds.maxX() + 1, bounds.minZ(), bounds.maxX() + 8, bounds.maxZ()); + + assertEquals(1, hit.size()); + assertTrue(miss.isEmpty()); + } + + @Test + public void startsWithinTheOriginReachContributeAndBeyondItDoNot() { + int reach = NativeStructureVolumeIndex.originReachChunks(); + NativeStructureVolumeIndex reachable = NativeStructureVolumeIndex.forTesting(new CountingResolver(reach, 0)); + NativeStructureVolumeIndex unreachable = NativeStructureVolumeIndex.forTesting(new CountingResolver(reach + 1, 0)); + + assertFalse(reachable.resolve(null, 0, 0, 15, 15).isEmpty()); + assertTrue(unreachable.resolve(null, 0, 0, 15, 15).isEmpty()); + } + + @Test + public void overlappingColdQueriesCoordinateBeforeResolvingOrigins() throws Exception { + BlockingResolver resolver = new BlockingResolver(); + NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver); + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + Future> first = executor.submit(() -> index.resolve(null, 0, 0, 15, 15)); + assertTrue(resolver.awaitFirstEntry()); + Future> adjacent = executor.submit(() -> index.resolve(null, 16, 0, 31, 15)); + + assertFalse(resolver.awaitSecondEntry(250)); + resolver.release(); + assertTrue(first.get(5, TimeUnit.SECONDS).isEmpty()); + assertTrue(adjacent.get(5, TimeUnit.SECONDS).isEmpty()); + assertEquals(306, resolver.resolutions()); + } finally { + resolver.release(); + executor.shutdownNow(); + } + } + + @Test + public void contendedNativeWindowsAllowQueuedTerrainWorkToRun() throws Exception { + assertQueuedTerrainCanComplete(false); + } + + @Test + public void interruptedNativeWindowWaitStillAcquiresAndReleasesOnce() throws Exception { + assertQueuedTerrainCanComplete(true); + } + + private static void assertQueuedTerrainCanComplete(boolean interruptWaiter) throws Exception { + CountDownLatch ownerEntered = new CountDownLatch(1); + CompletableFuture terrain = new CompletableFuture<>(); + AtomicBoolean firstOrigin = new AtomicBoolean(true); + AtomicBoolean interruptedAfterQuery = new AtomicBoolean(); + NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> { + if (firstOrigin.compareAndSet(true, false)) { + ownerEntered.countDown(); + terrain.join(); + } + return new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ, + -256, 0, -256, 256, 100, 256)); + }); + ExecutorService owner = Executors.newSingleThreadExecutor(); + ForkJoinPool workers = new ForkJoinPool(2); + AtomicReference firstWaiter = new AtomicReference<>(); + AtomicReference secondWaiter = new AtomicReference<>(); + try { + Future> original = owner.submit( + () -> index.resolve(null, 0, 0, 15, 15)); + assertTrue(ownerEntered.await(5, TimeUnit.SECONDS)); + Future> adjacent = workers.submit(() -> { + firstWaiter.set(Thread.currentThread()); + KList result = index.resolve(null, 16, 0, 31, 15); + interruptedAfterQuery.set(Thread.currentThread().isInterrupted()); + return result; + }); + Future> next = workers.submit(() -> { + secondWaiter.set(Thread.currentThread()); + return index.resolve(null, 32, 0, 47, 15); + }); + awaitOriginWindowWaiter(firstWaiter); + awaitOriginWindowWaiter(secondWaiter); + if (interruptWaiter) { + firstWaiter.get().interrupt(); + } + + Future terrainTask = workers.submit(() -> terrain.complete(null)); + terrainTask.get(3, TimeUnit.SECONDS); + assertEquals(289, original.get(3, TimeUnit.SECONDS).size()); + KList adjacentVolumes = adjacent.get(3, TimeUnit.SECONDS); + assertEquals(289, adjacentVolumes.size()); + assertEquals(adjacentVolumes, index.resolve(null, 16, 0, 31, 15)); + assertEquals(289, next.get(3, TimeUnit.SECONDS).size()); + assertEquals(interruptWaiter, interruptedAfterQuery.get()); + } finally { + terrain.complete(null); + owner.shutdownNow(); + workers.shutdownNow(); + assertTrue(owner.awaitTermination(5, TimeUnit.SECONDS)); + assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS)); + } + } + + private static void awaitOriginWindowWaiter(AtomicReference reference) { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + while (System.nanoTime() < deadline) { + Thread thread = reference.get(); + if (thread != null && thread.getState() == Thread.State.WAITING) { + for (StackTraceElement frame : thread.getStackTrace()) { + if (frame.getClassName().equals(NativeStructureVolumeIndex.class.getName()) + && frame.getMethodName().equals("lockOriginWindow")) { + return; + } + } + } + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1)); + } + throw new AssertionError("Native query did not wait for the occupied origin window"); + } + + @Test + public void distantColdQueriesCanResolveOriginsConcurrently() throws Exception { + BlockingResolver resolver = new BlockingResolver(); + NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver); + int distantChunkX = findDisjointWindow(0, 0); + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + Future> first = executor.submit(() -> index.resolve(null, 0, 0, 15, 15)); + assertTrue(resolver.awaitFirstEntry()); + Future> distant = executor.submit( + () -> index.resolve(null, distantChunkX << 4, 0, (distantChunkX << 4) + 15, 15)); + + assertTrue(resolver.awaitSecondEntry(5_000)); + resolver.release(); + assertTrue(first.get(5, TimeUnit.SECONDS).isEmpty()); + assertTrue(distant.get(5, TimeUnit.SECONDS).isEmpty()); + } finally { + resolver.release(); + executor.shutdownNow(); + } + } + + @Test + public void coordinatedResolutionPreservesCanonicalOriginOrder() { + NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new OrderedResolver()); + + KList volumes = index.resolve(null, 0, 0, 15, 15); + + assertEquals(289, volumes.size()); + assertEquals("-8:-8", volumes.getFirst().structure()); + assertEquals("0:0", volumes.get(144).structure()); + assertEquals("8:8", volumes.getLast().structure()); + } + + private static ScopedEngine scopedEngine(boolean scoped, int ownerRuntimeId) throws Exception { + IrisEngine engine = mock(IrisEngine.class); + GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class); + when(engine.hasGenerationRuntimeScope()).thenReturn(scoped); + when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router)); + when(engine.getCacheID()).thenReturn(1); + if (ownerRuntimeId != 0) { + IrisEngine.GenerationRuntimeBinding binding = mock(IrisEngine.GenerationRuntimeBinding.class); + when(binding.runtimeId()).thenReturn(ownerRuntimeId); + when(router.currentRuntimeOwnership()).thenReturn(Optional.of( + new GenerationHistoryRuntimeRouter.RuntimeOwnership(1L, binding))); + } + AtomicInteger scopes = new AtomicInteger(); + when(engine.openGenerationHistoryCoordinateScope(anyInt(), anyInt())).thenAnswer(invocation -> { + scopes.incrementAndGet(); + return null; + }); + return new ScopedEngine(engine, scopes); + } + + @SuppressWarnings("unchecked") + private static Map installedIndexes() throws Exception { + Field indexes = NativeStructureVolumeIndex.class.getDeclaredField("INDEXES"); + indexes.setAccessible(true); + return (Map) indexes.get(null); + } + + private static Map cache(NativeStructureVolumeIndex index, String name) throws Exception { + Field field = NativeStructureVolumeIndex.class.getDeclaredField(name); + field.setAccessible(true); + return (Map) field.get(index); + } + + private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) { + Structure source = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty())); + SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), x, z); + return new StructureStart(source, new ChunkPos(chunkX, chunkZ), 0, new PiecesContainer(List.of(piece))); + } + + private static Engine runtimeEngine(AtomicInteger runtimeId) { + return (Engine) Proxy.newProxyInstance( + Engine.class.getClassLoader(), + new Class[]{Engine.class}, + (proxy, method, arguments) -> { + if (method.getName().equals("getCacheID")) { + return runtimeId.get(); + } + if (method.getName().equals("hashCode")) { + return System.identityHashCode(proxy); + } + if (method.getName().equals("equals")) { + return proxy == arguments[0]; + } + if (method.getName().equals("toString")) { + return "runtime-engine-" + runtimeId.get(); + } + throw new UnsupportedOperationException(method.getName()); + } + ); + } + + private static int findDisjointWindow(int chunkX, int chunkZ) { + long sourceMask = NativeStructureVolumeIndex.originWindowStripeMask(chunkX, chunkZ); + for (int candidate = 64; candidate < 16_384; candidate += 17) { + if ((sourceMask & NativeStructureVolumeIndex.originWindowStripeMask(candidate, chunkZ)) == 0L) { + return candidate; + } + } + throw new IllegalStateException("Unable to find a disjoint native structure window stripe"); + } + + private static final class CountingResolver implements NativeStructureVolumeIndex.OriginResolver { + private final AtomicInteger resolutions = new AtomicInteger(); + private final int originChunkX; + private final int originChunkZ; + + private CountingResolver(int originChunkX, int originChunkZ) { + this.originChunkX = originChunkX; + this.originChunkZ = originChunkZ; + } + + private int resolutions() { + return resolutions.get(); + } + + @Override + public KList volumesAt(Engine engine, int chunkX, int chunkZ) { + resolutions.incrementAndGet(); + if (chunkX != originChunkX || chunkZ != originChunkZ) { + return NativeStructureVolume.NONE; + } + return NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6)); + } + } + + private static final class BlockingResolver implements NativeStructureVolumeIndex.OriginResolver { + private final AtomicInteger resolutions = new AtomicInteger(); + private final CountDownLatch firstEntry = new CountDownLatch(1); + private final CountDownLatch secondEntry = new CountDownLatch(1); + private final CountDownLatch release = new CountDownLatch(1); + + private int resolutions() { + return resolutions.get(); + } + + private boolean awaitFirstEntry() throws InterruptedException { + return firstEntry.await(5, TimeUnit.SECONDS); + } + + private boolean awaitSecondEntry(long timeoutMillis) throws InterruptedException { + return secondEntry.await(timeoutMillis, TimeUnit.MILLISECONDS); + } + + private void release() { + release.countDown(); + } + + @Override + public KList volumesAt(Engine engine, int chunkX, int chunkZ) { + int resolution = resolutions.incrementAndGet(); + if (resolution == 1) { + firstEntry.countDown(); + try { + if (!release.await(5, TimeUnit.SECONDS)) { + throw new IllegalStateException("Timed out waiting to release native structure resolution"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while coordinating native structure resolution", e); + } + } else if (resolution == 2) { + secondEntry.countDown(); + } + return NativeStructureVolume.NONE; + } + } + + private static final class OrderedResolver implements NativeStructureVolumeIndex.OriginResolver { + @Override + public KList volumesAt(Engine engine, int chunkX, int chunkZ) { + return new KList<>(new NativeStructureVolume( + chunkX + ":" + chunkZ, + 0, 0, 0, + 15, 255, 15)); + } + } + private record ScopedEngine(IrisEngine engine, AtomicInteger scopes) { + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeTerrainHeightCacheTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeTerrainHeightCacheTest.java new file mode 100644 index 000000000..8eeef2d6c --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeTerrainHeightCacheTest.java @@ -0,0 +1,129 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.levelgen.Heightmap; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.OptionalInt; +import java.util.function.IntSupplier; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public final class NativeTerrainHeightCacheTest { + @BeforeClass + public static void bootstrap() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void coordinatesRuntimePredicatesAndBoundsHaveSeparateEntries() { + NativeTerrainHeightCache cache = new NativeTerrainHeightCache(); + AtomicInteger calls = new AtomicInteger(); + NativeTerrainHeightCache.Query[] queries = { + query(1, -1, 1), + query(1, 1, -1), + query(2, -1, 1), + new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.OCEAN_FLOOR, -256, 768), + new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.WORLD_SURFACE, -64, 768), + new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.WORLD_SURFACE, -256, 256) + }; + for (int index = 0; index < queries.length; index++) { + assertEquals(index + 1, cached(cache, queries[index], calls::incrementAndGet)); + } + for (int index = 0; index < queries.length; index++) { + assertEquals(index + 1, cached(cache, queries[index], calls::incrementAndGet)); + } + assertEquals(queries.length, calls.get()); + cache.evictRuntime(1); + assertEquals(3, cached(cache, queries[2], calls::incrementAndGet)); + assertEquals(7, cached(cache, queries[0], calls::incrementAndGet)); + } + + @Test + public void allIntegerResultsAreCacheable() { + NativeTerrainHeightCache cache = new NativeTerrainHeightCache(); + NativeTerrainHeightCache.Query query = query(1, 0, 0); + assertEquals(Integer.MIN_VALUE, cached(cache, query, () -> Integer.MIN_VALUE)); + assertEquals(Integer.MIN_VALUE, cached(cache, query, () -> 0)); + } + + @Test + public void failedResolutionCanBeRetried() { + NativeTerrainHeightCache cache = new NativeTerrainHeightCache(); + NativeTerrainHeightCache.Query query = query(1, 0, 0); + assertThrows(IllegalStateException.class, () -> cached(cache, query, () -> { + throw new IllegalStateException("terrain unavailable"); + })); + assertEquals(42, cached(cache, query, () -> 42)); + } + + @Test + public void coldResolutionDoesNotHoldCacheMonitor() throws Exception { + NativeTerrainHeightCache cache = new NativeTerrainHeightCache(); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + Future slow = executor.submit(() -> cached(cache, query(1, 0, 0), () -> { + entered.countDown(); + try { + assertTrue(release.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException interruption) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(interruption); + } + return 9; + })); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + Future independent = executor.submit(() -> cached(cache, query(1, 1, 0), () -> 17)); + assertEquals(Integer.valueOf(17), independent.get(1, TimeUnit.SECONDS)); + release.countDown(); + assertEquals(Integer.valueOf(9), slow.get(5, TimeUnit.SECONDS)); + } finally { + release.countDown(); + executor.shutdownNow(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } + + @Test + public void boundedCacheKeepsRecentlyReadCoordinates() { + NativeTerrainHeightCache cache = new NativeTerrainHeightCache(); + for (int x = 0; x < 65_536; x++) { + cached(cache, query(1, x, 0), () -> 8); + } + assertEquals(8, cached(cache, query(1, 0, 0), () -> 99)); + cached(cache, query(1, 65_536, 0), () -> 9); + assertEquals(8, cached(cache, query(1, 0, 0), () -> 99)); + assertEquals(99, cached(cache, query(1, 1, 0), () -> 99)); + } + + @Test + public void unavailableResolvedHeightIsNotCached() { + NativeTerrainHeightCache cache = new NativeTerrainHeightCache(); + NativeTerrainHeightCache.Query query = query(1, 0, 0); + assertTrue(cache.resolvedHeight(query, OptionalInt::empty).isEmpty()); + assertEquals(42, cache.resolvedHeight(query, () -> OptionalInt.of(42)).orElseThrow()); + assertEquals(42, cache.resolvedHeight(query, OptionalInt::empty).orElseThrow()); + } + + private static int cached(NativeTerrainHeightCache cache, NativeTerrainHeightCache.Query query, + IntSupplier resolver) { + return cache.resolvedHeight(query, () -> OptionalInt.of(resolver.getAsInt())).orElseThrow(); + } + + private static NativeTerrainHeightCache.Query query(int runtimeId, int x, int z) { + return new NativeTerrainHeightCache.Query(runtimeId, x, z, Heightmap.Types.WORLD_SURFACE, -256, 768); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeTransitionColumnTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeTransitionColumnTest.java new file mode 100644 index 000000000..f19117842 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeTransitionColumnTest.java @@ -0,0 +1,106 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import art.arcane.iris.world.history.BoundaryColumnGeometry; +import art.arcane.iris.world.history.TerrainBoundarySignature; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.testsupport.PlatformLeakGuard; +import net.minecraft.SharedConstants; +import net.minecraft.core.Direction; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.NoiseColumn; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.RotatedPillarBlock; +import net.minecraft.world.level.block.LiquidBlock; +import net.minecraft.world.level.levelgen.Heightmap; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.MockedStatic; + +import java.util.List; +import java.util.OptionalInt; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +public final class NativeTransitionColumnTest { + @Rule + public final PlatformLeakGuard leakGuard = PlatformLeakGuard.clean(); + + @BeforeClass + public static void bootstrap() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + NativeBlockTags.bindHeightmapFixtures(); + } + + @Test + public void customKeysUseCarrierForNativeColumnsAndHeightmaps() { + String key = "itemsadder:rocks/ruby_ore"; + PlatformBlockState custom = mock(PlatformBlockState.class); + PlatformBlockState carrier = mock(PlatformBlockState.class); + when(custom.placementBaseState()).thenReturn(carrier); + when(carrier.key()).thenReturn("minecraft:oak_log[axis=z]"); + IrisPlatform platform = mock(IrisPlatform.class); + PlatformRegistries registries = mock(PlatformRegistries.class); + when(platform.registries()).thenReturn(registries); + when(registries.blockOrNull(key)).thenReturn(custom); + LevelHeightAccessor height = LevelHeightAccessor.create(-16, 32); + TerrainBoundarySignature signature = signature(key); + + try (MockedStatic platforms = mockStatic(IrisPlatforms.class)) { + platforms.when(IrisPlatforms::isBound).thenReturn(true); + platforms.when(IrisPlatforms::get).thenReturn(platform); + NoiseColumn column = NativeTransitionColumn.column(signature, height); + + assertEquals(Direction.Axis.Z, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS)); + assertEquals(-14, NativeTransitionColumn.height(signature, Heightmap.Types.OCEAN_FLOOR_WG, height)); + } + } + + @Test + public void preservesNativePropertiesCavesAndFluids() { + NoiseColumn column = NativeTransitionColumn.column(signature("minecraft:oak_log[axis=x]"), + LevelHeightAccessor.create(-16, 32)); + assertEquals(Direction.Axis.X, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS)); + assertTrue(column.getBlock(-14).is(Blocks.CAVE_AIR)); + assertEquals(Integer.valueOf(3), column.getBlock(-13).getValue(LiquidBlock.LEVEL)); + assertTrue(column.getBlock(0).isAir()); + } + + @Test + public void usesNativeHeightmapPredicatesForWaterAndOpenSpace() { + TerrainBoundarySignature signature = signature("minecraft:oak_log[axis=x]"); + LevelHeightAccessor height = LevelHeightAccessor.create(-16, 32); + assertEquals(-12, NativeTransitionColumn.height(signature, Heightmap.Types.WORLD_SURFACE_WG, height)); + assertEquals(-14, NativeTransitionColumn.height(signature, Heightmap.Types.OCEAN_FLOOR_WG, height)); + } + + @Test + public void rejectsUnknownSavedStatesInsteadOfReplacingTerrain() { + assertThrows(IllegalArgumentException.class, () -> NativeTransitionColumn.column( + signature("missing:removed_block"), LevelHeightAccessor.create(-16, 32))); + } + + private static TerrainBoundarySignature signature(String solidState) { + BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-16, List.of( + new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false), + new BoundaryColumnGeometry.Voxel(solidState, BoundaryColumnGeometry.Phase.SOLID, "", false), + new BoundaryColumnGeometry.Voxel("minecraft:cave_air", BoundaryColumnGeometry.Phase.AIR, "", false), + new BoundaryColumnGeometry.Voxel("minecraft:water[level=3]", BoundaryColumnGeometry.Phase.FLUID, + "minecraft:water[level=3]", false))); + return new TerrainBoundarySignature( + new TerrainBoundarySignature.Column(0, 0, 3, 1, OptionalInt.of(3), OptionalInt.empty()), + new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(-16, 4, 1), + new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})), + geometry); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/v26_3_R1/NativeBlockTags.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/v26_3_R1/NativeBlockTags.java new file mode 100644 index 000000000..441028018 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/v26_3_R1/NativeBlockTags.java @@ -0,0 +1,28 @@ +package art.arcane.iris.nativegen.v26_3_R1; + +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.TagLoader; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; + +import java.util.List; +import java.util.Map; + +public final class NativeBlockTags { + private NativeBlockTags() { + } + + public static void bindHeightmapFixtures() { + List> solid = List.of(Blocks.STONE.builtInRegistryHolder(), + Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder()); + List> canopy = List.of(Blocks.STONE.builtInRegistryHolder(), + Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder(), + Blocks.OAK_LEAVES.builtInRegistryHolder(), Blocks.SPRUCE_LEAVES.builtInRegistryHolder()); + BuiltInRegistries.BLOCK.prepareTagReload(new TagLoader.LoadResult<>(Registries.BLOCK, Map.of( + BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP, canopy, + BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP_NO_LEAVES, solid))).apply(); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/CustomBiomeCodecTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/CustomBiomeCodecTest.java new file mode 100644 index 000000000..5ee25b4ce --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/CustomBiomeCodecTest.java @@ -0,0 +1,57 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import art.arcane.iris.generation.biome.IrisBiomeCustom; +import art.arcane.iris.generation.biome.IrisBiomeCustomSpawn; +import art.arcane.iris.generation.biome.IrisBiomeCustomSpawnType; +import art.arcane.iris.platform.bukkit.nms.datapack.v263.DataFixerV263; +import art.arcane.volmlib.util.collection.KList; +import com.google.gson.JsonParser; +import com.mojang.serialization.JsonOps; +import net.minecraft.SharedConstants; +import net.minecraft.core.HolderLookup; +import net.minecraft.data.registries.VanillaRegistries; +import net.minecraft.resources.RegistryOps; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.attribute.EnvironmentAttributes; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.MobSpawnSettings; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class CustomBiomeCodecTest { + private static HolderLookup.Provider registries; + + @BeforeClass + public static void bootstrap() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + registries = VanillaRegistries.createWorldLookup(); + } + + @Test + public void nativeCodecAcceptsCustomColorsAndSpawnCounts() { + IrisBiomeCustom custom = new IrisBiomeCustom(); + custom.setId("codec_test"); + custom.setWaterColor("#102030"); + custom.setFogColor("#405060"); + custom.setSpawnRarity(2); + IrisBiomeCustomSpawn spawn = new IrisBiomeCustomSpawn(); + spawn.setType("minecraft:zombie"); + spawn.setGroup(IrisBiomeCustomSpawnType.MONSTER); + spawn.setMinCount(2); + spawn.setMaxCount(4); + spawn.setWeight(7); + custom.setSpawns(new KList<>(spawn)); + Biome biome = Biome.DIRECT_CODEC.parse(RegistryOps.create(JsonOps.INSTANCE, registries), + JsonParser.parseString(custom.generateJson(new DataFixerV263()))).getOrThrow(); + MobSpawnSettings settings = biome.getAttributes().applyModifier(EnvironmentAttributes.NATURAL_MOB_SPAWNS, + EnvironmentAttributes.NATURAL_MOB_SPAWNS.defaultValue()); + MobSpawnSettings.SpawnerData decoded = settings.getMobsToSpawn(MobCategory.MONSTER).unwrap().getFirst().value(); + assertEquals(2, decoded.count().minInclusive()); + assertEquals(4, decoded.count().maxInclusive()); + assertEquals(0x102030, biome.getWaterColor() & 0xFFFFFF); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/CustomBiomeSourceStructureContractTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/CustomBiomeSourceStructureContractTest.java new file mode 100644 index 000000000..41ac5d5a7 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/CustomBiomeSourceStructureContractTest.java @@ -0,0 +1,64 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.terrain.IrisDimension; +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Proxy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +public class CustomBiomeSourceStructureContractTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void stackedCustomBiomesUseTheirOwningDimensionNamespace() { + IrisDimension host = new IrisDimension(); + host.setLoadKey("Host"); + IrisDimension upper = new IrisDimension(); + upper.setLoadKey("Layers/Upper"); + + assertEquals("layers:upper/aurora", CustomBiomeSource.customBiomeKey(upper, "Aurora")); + assertNotEquals( + CustomBiomeSource.customBiomeKey(host, "Aurora"), + CustomBiomeSource.customBiomeKey(upper, "Aurora") + ); + } + + @Test + public void temporaryNaturalBiomeAnswersAreNotMemoized() { + Engine temporary = engineAnsweringNaturalFallback(true, 12, -8); + Engine stable = engineAnsweringNaturalFallback(false, 12, -8); + assertFalse(CustomBiomeSource.isBiomeCacheable(temporary, 12, -8)); + assertTrue(CustomBiomeSource.isBiomeCacheable(stable, 12, -8)); + } + + private static Engine engineAnsweringNaturalFallback( + boolean naturalFallback, + int expectedX, + int expectedZ + ) { + return (Engine) Proxy.newProxyInstance( + Engine.class.getClassLoader(), + new Class[]{Engine.class}, + (proxy, method, arguments) -> { + if (!method.getName().equals("answersFromNaturalTerrain")) { + throw new UnsupportedOperationException(method.getName()); + } + assertEquals(Integer.valueOf(expectedX), arguments[0]); + assertEquals(Integer.valueOf(expectedZ), arguments[1]); + return naturalFallback; + } + ); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/IrisChunkGeneratorFailureContractTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/IrisChunkGeneratorFailureContractTest.java new file mode 100644 index 000000000..605357d92 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/IrisChunkGeneratorFailureContractTest.java @@ -0,0 +1,97 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import org.junit.Test; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.LockSupport; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class IrisChunkGeneratorFailureContractTest { + @Test + public void releasedNoiseAdmissionLetsSynchronousDependentWaitForQueuedExclusive() throws Exception { + Semaphore admission = new Semaphore(1, true); + admission.acquire(); + CountDownLatch exclusiveEntered = new CountDownLatch(1); + CountDownLatch releaseExclusive = new CountDownLatch(1); + AtomicBoolean dependentFinished = new AtomicBoolean(false); + ExecutorService executor = Executors.newSingleThreadExecutor(); + boolean stageReleased = false; + + try { + Future exclusive = executor.submit(() -> { + boolean acquired = false; + try { + admission.acquire(); + acquired = true; + exclusiveEntered.countDown(); + assertTrue(releaseExclusive.await(2, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } finally { + if (acquired) { + admission.release(); + } + } + }); + awaitQueueLength(admission, 1); + CompletableFuture outward = new CompletableFuture<>(); + outward.thenRun(() -> { + try { + assertTrue(exclusiveEntered.await(2, TimeUnit.SECONDS)); + dependentFinished.set(true); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + }); + + admission.release(); + stageReleased = true; + assertTrue(outward.complete(null)); + assertTrue(dependentFinished.get()); + releaseExclusive.countDown(); + exclusive.get(2, TimeUnit.SECONDS); + assertEquals(1, admission.availablePermits()); + } finally { + if (!stageReleased) { + admission.release(); + } + releaseExclusive.countDown(); + executor.shutdownNow(); + } + } + + @Test + public void transformedDelegateCancellationIsDetectedThroughItsCompletionFailure() { + CompletableFuture delegate = new CompletableFuture<>(); + CompletableFuture transformed = delegate.thenApply(value -> value); + + assertTrue(delegate.cancel(false)); + assertFalse(transformed.isCancelled()); + CompletionException failure = assertThrows(CompletionException.class, transformed::join); + assertTrue(failure.getCause() instanceof CancellationException); + } + + private static void awaitQueueLength(Semaphore semaphore, int expected) { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2); + while (semaphore.getQueueLength() < expected && System.nanoTime() < deadline) { + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1)); + } + assertTrue("Expected at least " + expected + " queued semaphore threads", + semaphore.getQueueLength() >= expected); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/IrisChunkGeneratorMonumentLocateContractTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/IrisChunkGeneratorMonumentLocateContractTest.java new file mode 100644 index 000000000..50200394d --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/IrisChunkGeneratorMonumentLocateContractTest.java @@ -0,0 +1,58 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureLocateResults; +import com.mojang.datafixers.util.Pair; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.world.level.levelgen.structure.Structure; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +public class IrisChunkGeneratorMonumentLocateContractTest { + @Test + public void mixedLocateSelectsNearestProviderAndPrefersNativeOnTie() { + BlockPos origin = BlockPos.ZERO; + Pair> irisNear = Pair.of(new BlockPos(4, 70, 0), null); + Pair> nativeFar = Pair.of(new BlockPos(8, 70, 0), null); + Pair> nativeNear = Pair.of(new BlockPos(2, 70, 0), null); + Pair> nativeTie = Pair.of(new BlockPos(0, 70, 4), null); + + assertSame(irisNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeFar)); + assertSame(nativeNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeNear)); + assertSame(nativeTie, NativeStructureLocateResults.nearest(origin, irisNear, nativeTie)); + } + + @Test + public void mixedUnexploredLocateReferencesOnlyTheSelectedProvider() { + BlockPos origin = BlockPos.ZERO; + Pair> irisNear = Pair.of(new BlockPos(4, 70, 0), null); + Pair> nativeFar = Pair.of(new BlockPos(8, 70, 0), null); + AtomicInteger irisReferences = new AtomicInteger(); + AtomicInteger nativeReferences = new AtomicInteger(); + + Pair> irisSelected = + NativeStructureLocateResults.selectAndReference( + origin, + irisNear, () -> irisReferences.incrementAndGet(), + nativeFar, () -> nativeReferences.incrementAndGet()); + + assertSame(irisNear, irisSelected); + assertEquals(1, irisReferences.get()); + assertEquals(0, nativeReferences.get()); + + Pair> nativeNear = Pair.of(new BlockPos(2, 70, 0), null); + Pair> nativeSelected = + NativeStructureLocateResults.selectAndReference( + origin, + irisNear, () -> irisReferences.incrementAndGet(), + nativeNear, () -> nativeReferences.incrementAndGet()); + + assertSame(nativeNear, nativeSelected); + assertEquals(1, irisReferences.get()); + assertEquals(1, nativeReferences.get()); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/IrisChunkGeneratorSpawnAdmissionTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/IrisChunkGeneratorSpawnAdmissionTest.java new file mode 100644 index 000000000..c114acef6 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/IrisChunkGeneratorSpawnAdmissionTest.java @@ -0,0 +1,148 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import net.minecraft.server.level.ServerLevel; +import art.arcane.iris.generation.runtime.IrisEngine; +import art.arcane.iris.generation.runtime.GenerationSessionManager; +import art.arcane.iris.generation.runtime.GenerationTransitionGate; +import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter; +import art.arcane.iris.world.history.NativeBiomeSpawnSelection; +import art.arcane.iris.world.history.SavedBiomeRuntime; +import art.arcane.iris.generation.context.IrisContext; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.random.WeightedList; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.MobSpawnSettings; +import net.minecraft.world.level.chunk.ChunkGenerator; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class IrisChunkGeneratorSpawnAdmissionTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void tickSpawnQueryLeavesOwnerThreadAvailableForBoundaryCheckpoint() throws Exception { + Fixture fixture = new Fixture(); + ExecutorService owner = Executors.newSingleThreadExecutor(); + ExecutorService reloader = Executors.newSingleThreadExecutor(); + AtomicInteger snapshots = new AtomicInteger(); + long previousSession = fixture.sessions.currentSessionId(); + try { + assertSame(fixture.expected, owner.submit(fixture::query).get(2L, TimeUnit.SECONDS)); + assertEquals(previousSession, fixture.readSession.get()); + CompletableFuture cutover = CompletableFuture.runAsync(() -> { + try (GenerationTransitionGate.Transition ignored = fixture.sessions.transitionGate().beginTransition(2_000L)) { + fixture.sessions.sealAndAwait("Studio generation cutover", 2_000L); + Future query = owner.submit(() -> { + assertTrue(fixture.query().isEmpty()); + assertNull(IrisContext.get()); + }); + owner.submit(snapshots::incrementAndGet).get(2L, TimeUnit.SECONDS); + query.get(2L, TimeUnit.SECONDS); + assertEquals(1, fixture.savedReads.get()); + assertEquals(1, fixture.routes.get()); + fixture.sessions.activateNextSession(); + } catch (Exception failure) { + throw new IllegalStateException(failure); + } + }, reloader); + + cutover.get(3L, TimeUnit.SECONDS); + assertEquals(1, snapshots.get()); + assertEquals(0, fixture.sessions.activeLeases()); + assertSame(fixture.expected, owner.submit(fixture::query).get(2L, TimeUnit.SECONDS)); + assertEquals(fixture.sessions.currentSessionId(), fixture.readSession.get()); + assertTrue(fixture.readSession.get() > previousSession); + assertEquals(2, fixture.savedReads.get()); + assertEquals(2, fixture.routes.get()); + assertEquals(0, fixture.sessions.activeLeases()); + } finally { + owner.shutdownNow(); + reloader.shutdownNow(); + assertTrue(owner.awaitTermination(2L, TimeUnit.SECONDS)); + assertTrue(reloader.awaitTermination(2L, TimeUnit.SECONDS)); + } + } + + private static void setField(IrisChunkGenerator generator, String name, Object value) throws Exception { + Field field = IrisChunkGenerator.class.getDeclaredField(name); + field.setAccessible(true); + field.set(generator, value); + } + + private static final class Fixture { + private final GenerationSessionManager sessions = new GenerationSessionManager(true); + private final IrisEngine engine = mock(IrisEngine.class); + private final IrisChunkGenerator generator = mock(IrisChunkGenerator.class, CALLS_REAL_METHODS); + private final Holder biome = Holder.direct(mock(Biome.class)); + private final StructureManager structures = mock(StructureManager.class); + private final WeightedList expected = mock(WeightedList.class); + private final ServerLevel level = mock(ServerLevel.class); + private final AtomicInteger savedReads = new AtomicInteger(); + private final AtomicInteger routes = new AtomicInteger(); + private final AtomicLong readSession = new AtomicLong(); + + private Fixture() throws Exception { + GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class); + SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class); + ChunkGenerator delegate = mock(ChunkGenerator.class); + CustomBiomeSource source = mock(CustomBiomeSource.class); + when(level.getBiome(new BlockPos(0, 64, 0))).thenReturn(biome); + setField(generator, "runtimeLevel", level); + when(engine.getGenerationSessions()).thenReturn(sessions); + when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router)); + when(router.biomes()).thenReturn(saved); + when(saved.nativeSpawnSelection(0, 64, 0, "")).thenAnswer(invocation -> { + assertAdmitted(); + savedReads.incrementAndGet(); + return new NativeBiomeSpawnSelection(NativeBiomeSpawnSelection.Mode.RETAINED, "minecraft:plains"); + }); + when(engine.openGenerationHistoryCoordinateScope(0, 0)).thenAnswer(invocation -> { + assertAdmitted(); + routes.incrementAndGet(); + return null; + }); + when(delegate.getMobsAt(level, structures, MobCategory.MONSTER, new BlockPos(0, 64, 0))) + .thenReturn(expected); + setField(generator, "engine", engine); + setField(generator, "delegate", delegate); + setField(generator, "customBiomeSource", source); + } + + private WeightedList query() { + return generator.getMobsAt(level, structures, MobCategory.MONSTER, new BlockPos(0, 64, 0)); + } + + private void assertAdmitted() { + assertEquals(1, sessions.activeLeases()); + assertSame(engine, IrisContext.get().getEngine()); + readSession.set(IrisContext.get().getGenerationSessionId()); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NMSBindingDatapackStructureScopeTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NMSBindingDatapackStructureScopeTest.java new file mode 100644 index 000000000..ab0c0fe21 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NMSBindingDatapackStructureScopeTest.java @@ -0,0 +1,437 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement; +import art.arcane.iris.pack.datapack.DatapackIngestService; +import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex; +import art.arcane.iris.structure.nativegen.IrisImportedStructureControl; +import art.arcane.iris.structure.placement.IrisStructureSetFrequencyOverride; +import art.arcane.volmlib.util.collection.KList; +import com.mojang.datafixers.util.Either; +import net.minecraft.SharedConstants; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderOwner; +import net.minecraft.core.Vec3i; +import net.minecraft.core.component.DataComponentMap; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.Bootstrap; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; +import com.mojang.serialization.MapCodec; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import org.junit.Test; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +public class NMSBindingDatapackStructureScopeTest { + private static final String SOURCE = "https://example.test/managed.zip"; + + @BeforeClass + public static void bootstrapMinecraft() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void managedSetContainingVanillaStructureIsAbsentOutsideDeclaringDimension() { + Holder vanillaStructure = structureHolder("minecraft:pillager_outpost"); + Holder managedSet = structureSetHolder( + "managed:illager_barracks", vanillaStructure); + DatapackStructureScopeIndex index = index( + List.of(), + List.of("managed:illager_barracks")); + + DatapackStructureStateFilter.Selection vanilla = DatapackStructureStateFilter.filter( + List.of(managedSet), index, Set.of()); + DatapackStructureStateFilter.Selection declaring = DatapackStructureStateFilter.filter( + List.of(managedSet), index, index.declaredSources(List.of(SOURCE))); + + assertEquals(0, vanilla.structureSets().size()); + assertEquals(1, vanilla.excludedManagedSets()); + assertEquals(1, declaring.structureSets().size()); + assertSame(managedSet, declaring.structureSets().getFirst()); + } + + @Test + public void unmanagedSetRetainsOnlyDefinitionsAllowedInTheWorld() { + Holder vanillaStructure = structureHolder("minecraft:village_plains"); + Holder managedStructure = structureHolder("managed:tavern"); + Holder vanillaSet = structureSetHolder( + "minecraft:villages", vanillaStructure, managedStructure); + DatapackStructureScopeIndex index = index( + List.of("managed:tavern"), + List.of()); + + DatapackStructureStateFilter.Selection vanilla = DatapackStructureStateFilter.filter( + List.of(vanillaSet), index, Set.of()); + DatapackStructureStateFilter.Selection declaring = DatapackStructureStateFilter.filter( + List.of(vanillaSet), index, index.declaredSources(List.of(SOURCE))); + + assertEquals(1, vanilla.structureSets().size()); + assertEquals(1, vanilla.structureSets().getFirst().value().structures().size()); + assertSame(vanillaStructure, + vanilla.structureSets().getFirst().value().structures().getFirst().structure()); + assertSame(vanillaSet.value().placement(), + vanilla.structureSets().getFirst().value().placement()); + assertSame(vanillaSet, declaring.structureSets().getFirst()); + } + + @Test + public void setWithNoAllowedDefinitionsIsRemoved() { + Holder unmanagedSet = structureSetHolder( + "minecraft:custom", structureHolder("managed:only")); + DatapackStructureScopeIndex index = index(List.of("managed:only"), List.of()); + + DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter( + List.of(unmanagedSet), index, Set.of()); + + assertEquals(0, selection.structureSets().size()); + } + + @Test + public void spigotDirectHolderRecoversItsRegistryKeyFromSharedEntries() { + ResourceKey key = ResourceKey.create( + Registries.STRUCTURE_SET, + Identifier.parse("managed:illager_barracks")); + List entries = List.of( + new StructureSet.StructureSelectionEntry( + structureHolder("minecraft:pillager_outpost"), 1)); + Holder registered = new KeyedHolder<>(key, new StructureSet( + entries, + new RandomSpreadStructurePlacement( + 34, 8, RandomSpreadType.LINEAR, 10387312))); + Holder spigotDirect = Holder.direct(new StructureSet( + entries, + new RandomSpreadStructurePlacement( + 34, 8, RandomSpreadType.LINEAR, 14357620))); + DatapackStructureScopeIndex scopeIndex = index( + List.of(), List.of("managed:illager_barracks")); + DatapackStructureStateFilter.StructureSetKeyIndex keyIndex = + DatapackStructureStateFilter.keyIndex(List.of(registered)); + + DatapackStructureStateFilter.Selection excluded = + DatapackStructureStateFilter.filter( + List.of(spigotDirect), + scopeIndex, + Set.of(), + new IrisImportedStructureControl(), + keyIndex); + DatapackStructureStateFilter.Selection retained = + DatapackStructureStateFilter.filter( + List.of(spigotDirect), + scopeIndex, + scopeIndex.declaredSources(List.of(SOURCE)), + new IrisImportedStructureControl(), + keyIndex); + + assertEquals(0, excluded.structureSets().size()); + assertEquals(1, excluded.excludedManagedSets()); + assertSame(spigotDirect, retained.structureSets().getFirst()); + assertEquals(1, retained.retainedManagedSets()); + } + + @Test + public void structureScopeDoesNotLinkPaperOnlyPlacementClasses() throws IOException { + InputStream classResource = NMSBindingDatapackStructureScopeTest.class + .getResourceAsStream("DatapackStructureStateFilter.class"); + assertNotNull(classResource); + try (InputStream input = classResource) { + String classFile = new String(input.readAllBytes(), StandardCharsets.ISO_8859_1); + assertFalse(classFile.contains("KeyedRandomSpreadStructurePlacement")); + } + } + + @Test + public void exactFrequencyOverridesScaleOnlyTheNamedNativeStructureSet() { + Holder complexes = structureSetHolder( + "minecraft:nether_complexes", + new RandomSpreadStructurePlacement(27, 4, RandomSpreadType.LINEAR, 30084232), + structureHolder("minecraft:fortress")); + Holder fossils = structureSetHolder( + "minecraft:nether_fossils", + new RandomSpreadStructurePlacement(2, 1, RandomSpreadType.LINEAR, 14357921), + structureHolder("minecraft:nether_fossil")); + KList overrides = new KList<>(); + overrides.add(new IrisStructureSetFrequencyOverride() + .setStructureSet("minecraft:nether_complexes") + .setMultiplier(1.1D)); + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setFrequencyOverrides(overrides); + + DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter( + List.of(complexes, fossils), index(List.of(), List.of()), Set.of(), control); + + RandomSpreadStructurePlacement scaledComplexes = + (RandomSpreadStructurePlacement) selection.structureSets().get(0).value().placement(); + RandomSpreadStructurePlacement unchangedFossils = + (RandomSpreadStructurePlacement) selection.structureSets().get(1).value().placement(); + assertEquals(26, scaledComplexes.spacing()); + assertEquals(4, scaledComplexes.separation()); + assertEquals(2, unchangedFossils.spacing()); + assertSame(fossils, selection.structureSets().get(1)); + assertEquals(27, ((RandomSpreadStructurePlacement) complexes.value().placement()).spacing()); + } + + @Test + public void frequencyOnlyPassLeavesUnrelatedCustomPlacementUntouched() { + Holder exclusionTarget = structureSetHolder( + "example:target", structureHolder("example:target")); + Holder custom = structureSetHolder( + "example:custom", + new UnsupportedPlacement(Optional.of( + new AbstractSpreadingStructurePlacement.ExclusionZone(exclusionTarget, 1))), + structureHolder("example:custom")); + KList overrides = new KList<>(); + overrides.add(new IrisStructureSetFrequencyOverride() + .setStructureSet("minecraft:nether_complexes") + .setMultiplier(1.1D)); + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setFrequencyOverrides(overrides); + + DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter( + List.of(custom), index(List.of(), List.of()), Set.of(), control); + + assertSame(custom, selection.structureSets().getFirst()); + } + + @Test + public void frequencyOnlyPassRebindsDependentExclusionTarget() { + Holder target = structureSetHolder( + "example:target", + new RandomSpreadStructurePlacement(27, 4, RandomSpreadType.LINEAR, 30084232), + structureHolder("example:target")); + RandomSpreadStructurePlacement dependentPlacement = new RandomSpreadStructurePlacement( + Vec3i.ZERO, + AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT, + 1.0F, + 4567, + Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(target, 1)), + 32, + 8, + RandomSpreadType.LINEAR); + Holder dependent = structureSetHolder( + "example:dependent", + dependentPlacement, + structureHolder("example:dependent")); + KList overrides = new KList<>(); + overrides.add(new IrisStructureSetFrequencyOverride() + .setStructureSet("example:target") + .setMultiplier(1.1D)); + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setFrequencyOverrides(overrides); + + DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter( + List.of(dependent, target), index(List.of(), List.of()), Set.of(), control); + + Holder scaledDependent = selection.structureSets().get(0); + Holder scaledTarget = selection.structureSets().get(1); + assertNotSame(dependent, scaledDependent); + assertNotSame(target, scaledTarget); + assertSame(scaledTarget, DatapackStructureStateFilter.exclusionZone( + scaledDependent.value().placement()).orElseThrow().otherSet()); + assertEquals(26, ((RandomSpreadStructurePlacement) + scaledTarget.value().placement()).spacing()); + } + + @Test + public void frequencyOnlyPassRebindsAffectedExclusionCycle() { + MutableStructureSetHolder first = new MutableStructureSetHolder("example:first"); + MutableStructureSetHolder second = new MutableStructureSetHolder("example:second"); + first.bind(structureSet( + new RandomSpreadStructurePlacement( + Vec3i.ZERO, + AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT, + 1.0F, + 101, + Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(second, 1)), + 32, + 8, + RandomSpreadType.LINEAR), + "example:first")); + second.bind(structureSet( + new RandomSpreadStructurePlacement( + Vec3i.ZERO, + AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT, + 1.0F, + 102, + Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(first, 1)), + 27, + 4, + RandomSpreadType.LINEAR), + "example:second")); + KList overrides = new KList<>(); + overrides.add(new IrisStructureSetFrequencyOverride() + .setStructureSet("example:second") + .setMultiplier(1.1D)); + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setFrequencyOverrides(overrides); + + DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter( + List.of(first, second), index(List.of(), List.of()), Set.of(), control); + + Holder scaledFirst = selection.structureSets().get(0); + Holder scaledSecond = selection.structureSets().get(1); + assertSame(scaledSecond, DatapackStructureStateFilter.exclusionZone( + scaledFirst.value().placement()).orElseThrow().otherSet()); + assertSame(scaledFirst, DatapackStructureStateFilter.exclusionZone( + scaledSecond.value().placement()).orElseThrow().otherSet()); + assertEquals(26, ((RandomSpreadStructurePlacement) + scaledSecond.value().placement()).spacing()); + } + + @Test + public void randomSpreadSubtypeUsesTheCanonicalPlacementContract() { + Holder custom = structureSetHolder( + "example:custom", + new CustomRandomSpreadPlacement(), + structureHolder("example:custom")); + KList overrides = new KList<>(); + overrides.add(new IrisStructureSetFrequencyOverride() + .setStructureSet("example:custom") + .setMultiplier(1.1D)); + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setFrequencyOverrides(overrides); + + DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter( + List.of(custom), index(List.of(), List.of()), Set.of(), control); + StructurePlacement placement = selection.structureSets().getFirst().value().placement(); + + assertEquals(RandomSpreadStructurePlacement.class, placement.getClass()); + assertEquals(26, ((RandomSpreadStructurePlacement) placement).spacing()); + } + + @Test + public void excludedManagedSetCannotSuppressAnAllowedSetThroughExclusionZone() { + Holder managedSet = structureSetHolder( + "managed:blocked", + structureHolder("managed:blocked")); + RandomSpreadStructurePlacement originalPlacement = new RandomSpreadStructurePlacement( + Vec3i.ZERO, + AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT, + 1.0F, + 4567, + Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(managedSet, 1)), + 32, + 8, + RandomSpreadType.LINEAR); + Holder vanillaSet = structureSetHolder( + "minecraft:allowed", + originalPlacement, + structureHolder("minecraft:village_plains")); + DatapackStructureScopeIndex index = index( + List.of("managed:blocked"), + List.of("managed:blocked")); + + DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter( + List.of(vanillaSet, managedSet), index, Set.of()); + + assertEquals(1, selection.structureSets().size()); + StructurePlacement scopedPlacement = selection.structureSets().getFirst().value().placement(); + assertEquals(0, DatapackStructureStateFilter.exclusionZone(scopedPlacement).stream().count()); + } + + private static DatapackStructureScopeIndex index( + List structureKeys, + List structureSetKeys + ) { + return DatapackStructureScopeIndex.create(List.of( + new DatapackIngestService.StructureScopeResources( + SOURCE, + structureKeys, + structureSetKeys))); + } + + private static Holder structureHolder(String key) { + return new KeyedHolder<>(ResourceKey.create(Registries.STRUCTURE, Identifier.parse(key)), null); + } + + private static Holder structureSetHolder( + String key, + Holder... structures + ) { + return structureSetHolder( + key, + new RandomSpreadStructurePlacement(32, 8, RandomSpreadType.LINEAR, 12345), + structures); + } + + private static Holder structureSetHolder( + String key, + StructurePlacement placement, + Holder... structures + ) { + List entries = Stream.of(structures) + .map(structure -> new StructureSet.StructureSelectionEntry(structure, 1)) + .toList(); + StructureSet value = new StructureSet(entries, placement); + return new KeyedHolder<>( + ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)), + value); + } + + private static StructureSet structureSet(StructurePlacement placement, String structureKey) { + return new StructureSet( + List.of(new StructureSet.StructureSelectionEntry( + structureHolder(structureKey), 1)), + placement); + } + + private static final class KeyedHolder extends Holder.Reference { + private KeyedHolder(ResourceKey key, T value) { + super(Type.STAND_ALONE, new HolderOwner<>() {}, key, null); + bindValue(value); + } + } + + private static final class UnsupportedPlacement extends AbstractSpreadingStructurePlacement { + private UnsupportedPlacement(Optional exclusionZone) { + super(Vec3i.ZERO, FrequencyReductionMethod.DEFAULT, 1F, 1, exclusionZone); + } + + @Override + protected boolean isPlacementChunk(ChunkGeneratorStructureState state, int x, int z) { + return false; + } + + @Override + public MapCodec codec() { + return null; + } + } + + private static final class MutableStructureSetHolder extends Holder.Reference { + private MutableStructureSetHolder(String key) { + super(Type.STAND_ALONE, new HolderOwner<>() { + }, ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)), null); + } + + private void bind(StructureSet structureSet) { + bindValue(structureSet); + } + } + + private static final class CustomRandomSpreadPlacement extends RandomSpreadStructurePlacement { + private CustomRandomSpreadPlacement() { + super(27, 4, RandomSpreadType.LINEAR, 30084232); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructurePoiUpdatesTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructurePoiUpdatesTest.java new file mode 100644 index 000000000..0232d6b7a --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructurePoiUpdatesTest.java @@ -0,0 +1,250 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import ca.spottedleaf.moonrise.common.util.TickThread; +import com.mojang.serialization.Codec; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.IdMapper; +import net.minecraft.core.SectionPos; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import net.minecraft.world.entity.ai.village.poi.PoiSection; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.entity.ai.village.poi.PoiTypes; +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.ImposterProtoChunk; +import net.minecraft.world.level.chunk.LevelChunk; +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 org.bukkit.NamespacedKey; +import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry; +import org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class NativeStructurePoiUpdatesTest { + private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections"); + + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void generationRecordsIntentBeforeSuppressingNativePoiCallbacks() { + Fixture fixture = new Fixture(); + BlockPos position = new BlockPos(-31, 5, 49); + doAnswer(call -> { + assertNotNull(fixture.proto.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY)); + assertEquals(Block.UPDATE_CLIENTS | 4096, (int) call.getArgument(2)); + fixture.proto.setBlockState(position, call.getArgument(1), 0); + return true; + }).when(fixture.world).setBlock(any(), any(), anyInt(), anyInt()); + + assertTrue(fixture.updates.setBlock(fixture.world, position, + Blocks.COMPOSTER.defaultBlockState(), Block.UPDATE_CLIENTS, 17)); + assertArrayEquals(new long[]{16L}, fixture.marker()); + } + + @Test + public void denseWritesStoreOneSectionBitAndSurvivePersistentDataRoundTrip() throws Exception { + Fixture fixture = new Fixture(); + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + fixture.write(new BlockPos(-32 + x, 5, 48 + z), Blocks.COMPOSTER.defaultBlockState()); + } + } + assertArrayEquals(new long[]{16L}, fixture.marker()); + DirtyCraftPersistentDataContainer restored = new DirtyCraftPersistentDataContainer(new CraftPersistentDataTypeRegistry()); + restored.putAll(fixture.proto.persistentDataContainer.toTagCompound()); + fixture.loaded.persistentDataContainer = restored; + + onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded)); + + assertEquals(256L, fixture.records().getRecords(type -> true, PoiManager.Occupancy.ANY).count()); + assertFalse(restored.has(DIRTY_SECTIONS)); + } + + @Test + public void validSectionsReconcileFinalStatesAndKeepMatchingClaimedTickets() throws Exception { + Fixture fixture = new Fixture(); + BlockPos retained = new BlockPos(-31, 5, 49); + BlockPos replaced = new BlockPos(-30, 5, 49); + BlockPos removed = new BlockPos(-29, 5, 49); + Holder farmer = PoiTypes.forState(Blocks.COMPOSTER.defaultBlockState()).orElseThrow(); + PoiSection existing = new PoiSection.Packed(true, List.of( + new PoiRecord.Packed(retained, farmer, 0), + new PoiRecord.Packed(replaced, farmer, 1), + new PoiRecord.Packed(removed, farmer, 1))).unpack(() -> {}); + fixture.pois.put(SectionPos.of(retained).asLong(), existing); + PoiRecord retainedRecord = existing.getRecords(type -> true, PoiManager.Occupancy.ANY) + .filter(record -> record.getPos().equals(retained)).findFirst().orElseThrow(); + for (BlockPos position : List.of(retained, replaced, removed)) { + fixture.proto.setBlockState(position, Blocks.COMPOSTER.defaultBlockState(), 0); + } + fixture.write(replaced, Blocks.BARREL.defaultBlockState()); + fixture.write(removed, Blocks.AIR.defaultBlockState()); + + onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded)); + + assertEquals(PoiTypes.forState(Blocks.BARREL.defaultBlockState()), existing.getType(replaced)); + assertTrue(existing.getType(removed).isEmpty()); + assertSame(retainedRecord, existing.getRecords(type -> true, PoiManager.Occupancy.ANY) + .filter(record -> record.getPos().equals(retained)).findFirst().orElseThrow()); + assertEquals(0, existing.getFreeTickets(retained)); + onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded)); + assertEquals(0, existing.getFreeTickets(retained)); + } + + @Test + public void failedReconciliationRetainsIntentForRetry() throws Exception { + Fixture fixture = new Fixture(); + fixture.write(new BlockPos(-31, 5, 49), Blocks.COMPOSTER.defaultBlockState()); + IllegalStateException failure = new IllegalStateException("POI storage failure"); + doThrow(failure).when(fixture.manager).add(any(), any()); + + onOwner(() -> assertSame(failure, assertThrows(IllegalStateException.class, + () -> NativeStructurePoiUpdates.reconcile(fixture.loaded)))); + + assertArrayEquals(new long[]{16L}, fixture.marker()); + fixture.allowPoiAdds(); + onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded)); + assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS)); + } + + @Test + public void explicitPoiSuppressionAndReadOnlyFullChunksDoNotCreateIntent() { + Fixture fixture = new Fixture(); + BlockPos position = new BlockPos(-31, 5, 49); + fixture.updates.setBlock(fixture.world, position, Blocks.COMPOSTER.defaultBlockState(), + 4096, 17); + assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS)); + ImposterProtoChunk full = mock(ImposterProtoChunk.class); + when(fixture.world.getChunk(any(BlockPos.class))).thenReturn(full); + fixture.updates.setBlock(fixture.world, position, Blocks.BARREL.defaultBlockState(), 2, 17); + assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS)); + } + + @Test + public void invalidSectionBitsRetainTheStoredRecord() throws Exception { + Fixture fixture = new Fixture(); + fixture.proto.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, new long[]{1L << 24}); + onOwner(() -> assertThrows(IllegalArgumentException.class, + () -> NativeStructurePoiUpdates.reconcile(fixture.loaded))); + assertArrayEquals(new long[]{1L << 24}, fixture.marker()); + } + + private static void onOwner(Runnable action) throws Exception { + FutureTask task = new FutureTask<>(action, null); + new TickThread(task, "Native POI test").start(); + task.get(5L, TimeUnit.SECONDS); + } + + private static PalettedContainerFactory containerFactory() { + Strategy blocks = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY); + BlockState air = Blocks.AIR.defaultBlockState(); + IdMapper> biomeIds = new IdMapper<>(); + Holder biome = Holder.direct((Biome) null); + biomeIds.add(biome); + Strategy> biomes = Strategy.createForBiomes(biomeIds); + Codec> biomeCodec = Codec.STRING.xmap(name -> biome, holder -> "iris-test-biome"); + return new PalettedContainerFactory(blocks, air, PalettedContainer.codecRW(BlockState.CODEC, blocks, air), + biomes, biome, PalettedContainer.codecRO(biomeCodec, biomes, biome), + PalettedContainer.codecRW(biomeCodec, biomes, biome)); + } + + private static final class Fixture { + private final NativeStructurePoiUpdates updates = new NativeStructurePoiUpdates(4096); + private final ProtoChunk proto = new ProtoChunk(new ChunkPos(-2, 3), UpgradeData.EMPTY, + LevelHeightAccessor.create(-64, 384), containerFactory(), null); + private final LevelChunk loaded = mock(LevelChunk.class); + private final WorldGenLevel world = mock(WorldGenLevel.class); + private final PoiManager manager = mock(PoiManager.class); + private final Map pois = new HashMap<>(); + + private Fixture() { + ServerLevel level = mock(ServerLevel.class); + when(level.getPoiManager()).thenReturn(manager); + loaded.persistentDataContainer = proto.persistentDataContainer; + when(loaded.getLevel()).thenReturn(level); + when(loaded.getPos()).thenReturn(proto.getPos()); + when(loaded.getSectionsCount()).thenReturn(proto.getSectionsCount()); + when(loaded.getSectionYFromSectionIndex(anyInt())).thenAnswer(call -> proto.getSectionYFromSectionIndex(call.getArgument(0))); + when(loaded.getSection(anyInt())).thenAnswer(call -> proto.getSection(call.getArgument(0))); + when(loaded.getBlockState(any())).thenAnswer(call -> proto.getBlockState(call.getArgument(0))); + when(world.getChunk(any(BlockPos.class))).thenReturn(proto); + when(world.setBlock(any(), any(), anyInt(), anyInt())).thenAnswer(call -> { + proto.setBlockState(call.getArgument(0), call.getArgument(1), call.getArgument(2)); + return true; + }); + when(manager.getOrLoad(anyLong())).thenAnswer(call -> Optional.ofNullable(pois.get((long) call.getArgument(0)))); + when(manager.getType(any())).thenAnswer(call -> { + BlockPos position = call.getArgument(0); + PoiSection section = pois.get(SectionPos.of(position).asLong()); + return section == null ? Optional.empty() : section.getType(position); + }); + doAnswer(call -> { + BlockPos position = call.getArgument(0); + pois.get(SectionPos.of(position).asLong()).remove(position); + return null; + }).when(manager).remove(any()); + allowPoiAdds(); + } + + private void allowPoiAdds() { + doAnswer(call -> { + BlockPos position = call.getArgument(0); + return pois.computeIfAbsent(SectionPos.of(position).asLong(), ignored -> new PoiSection(() -> {})) + .add(position, call.getArgument(1)); + }).when(manager).add(any(), any()); + } + + private void write(BlockPos position, BlockState state) { + updates.setBlock(world, position, state, Block.UPDATE_CLIENTS, Block.UPDATE_LIMIT); + } + + private long[] marker() { + return proto.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY); + } + + private PoiSection records() { + return pois.get(SectionPos.of(new BlockPos(-31, 5, 49)).asLong()); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructureWorldgenAccessTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructureWorldgenAccessTest.java new file mode 100644 index 000000000..f6d20f7ce --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructureWorldgenAccessTest.java @@ -0,0 +1,464 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import art.arcane.iris.structure.placement.IrisStructureTerrain; +import art.arcane.iris.structure.placement.IrisStructureTerrainMode; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructurePostProcessor; +import art.arcane.iris.nativegen.v26_3_R1.NativeStructureTerrainIntegrator; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure; +import net.minecraft.world.level.levelgen.structure.templatesystem.GravityProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.ticks.LevelTickAccess; +import net.minecraft.world.ticks.ScheduledTick; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class NativeStructureWorldgenAccessTest { + private static final int GENERATION_CENTER_X = 60; + private static final int GENERATION_CENTER_Z = 15; + private static final int SURFACE_FIRST_FREE_Y = 80; + private static final int FLOOR_FIRST_FREE_Y = 70; + + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void ordinaryWorldFlattenPreparationHandlesReferencedVillagePiecesBeyondEightChunks() { + int farX = (GENERATION_CENTER_X - 9) << 4; + int farZ = (GENERATION_CENTER_Z + 4) << 4; + BoundingBox bounds = new BoundingBox(farX, 64, farZ, farX + 4, 90, farZ + 4); + PoolElementStructurePiece piece = new PoolElementStructurePiece( + null, StructurePoolElement.single("minecraft:empty").apply(StructureTemplatePool.Projection.RIGID), + new BlockPos(farX, 64, farZ), 1, Rotation.NONE, bounds, LiquidSettings.APPLY_WATERLOGGING); + Structure structure = new DesertPyramidStructure(new Structure.StructureSettings( + HolderSet.empty(), Map.of(), GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.BEARD_BOX)); + StructureStart start = new StructureStart(structure, generationCenter(), 0, new PiecesContainer(List.of(piece))); + List targets = List.of( + new NativeStructureTerrainIntegrator.TerrainTarget("minecraft:village_plains", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FLATTEN))); + BoundingBox area = new BoundingBox(generationCenter().getMinBlockX(), -64, generationCenter().getMinBlockZ(), + generationCenter().getMaxBlockX(), 319, generationCenter().getMaxBlockZ()); + RecordingDelegate unbounded = new RecordingDelegate(); + unbounded.rejectDistantTerrainReads = true; + + assertThrows(IllegalStateException.class, () -> NativeStructurePostProcessor.prepareTerrain( + unbounded.world(), area, targets, (palette, rng, x, y, z) -> Blocks.STONE.defaultBlockState())); + + RecordingDelegate recording = new RecordingDelegate(); + recording.rejectDistantTerrainReads = true; + NativeStructureWorldgenAccess access = access(recording); + NativeStructurePostProcessor.prepareTerrain( + access, area, targets, (palette, rng, x, y, z) -> Blocks.STONE.defaultBlockState()); + + assertEquals(0, recording.heightReads); + assertEquals(0, recording.chunkReads); + assertEquals(0, recording.terrainReads); + assertEquals(0, recording.mutations); + assertEquals(91, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, + generationCenter().getMinBlockX(), generationCenter().getMinBlockZ())); + assertEquals(1, recording.heightReads); + } + + @Test + public void projectedTemplatePrefiltersUseRemoteTerrainAndRetainLocalHeightmaps() { + RecordingDelegate recording = new RecordingDelegate(); + recording.rejectDistantTerrainReads = true; + NativeStructureWorldgenAccess access = access(recording); + GravityProcessor gravity = new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, -1); + BlockPos remote = new BlockPos((GENERATION_CENTER_X - 9) << 4, 64, (GENERATION_CENTER_Z + 4) << 4); + StructureTemplate.StructureBlockInfo raw = new StructureTemplate.StructureBlockInfo( + new BlockPos(0, 4, 0), Blocks.DIRT_PATH.defaultBlockState(), null); + StructurePlaceSettings settings = new StructurePlaceSettings().addProcessor(gravity); + + StructureTemplate.StructureBlockInfo projected = StructureTemplate.processBlockInfos( + access, remote, BlockPos.ZERO, settings, List.of(raw)).getFirst(); + + assertEquals(remote.atY(SURFACE_FIRST_FREE_Y + 3), projected.pos()); + assertEquals(0, recording.heightReads); + BlockPos local = generationCenter().getMiddleBlockPosition(64); + StructureTemplate.StructureBlockInfo localBlock = new StructureTemplate.StructureBlockInfo( + BlockPos.ZERO, Blocks.DIRT_PATH.defaultBlockState(), null); + StructureTemplate.StructureBlockInfo localProjection = StructureTemplate.processBlockInfos( + access, local, BlockPos.ZERO, settings, List.of(localBlock)).getFirst(); + assertEquals(local.atY(90), localProjection.pos()); + assertEquals(1, recording.heightReads); + assertEquals(0, recording.chunkReads); + } + + @Test + public void distanceTwoTerrainReadsNeverReachTheDelegate() { + RecordingDelegate recording = new RecordingDelegate(); + NativeStructureWorldgenAccess access = access(recording); + ChunkPos generationCenter = generationCenter(); + int x = generationCenter.getMiddleBlockX(); + int z = (generationCenter.z() + 2) << 4; + + assertSame(Blocks.STONE, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y - 1, z)).getBlock()); + assertSame(Blocks.WATER, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)).getBlock()); + assertSame(Blocks.AIR, access.getBlockState(new BlockPos(x, SURFACE_FIRST_FREE_Y, z)).getBlock()); + assertSame(Blocks.WATER.defaultBlockState().getFluidState(), + access.getFluidState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z))); + assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z)); + assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z)); + assertNull(access.getBlockStateIfLoaded(new BlockPos(x, FLOOR_FIRST_FREE_Y, z))); + assertNull(access.getChunk(generationCenter.x(), generationCenter.z() + 2, ChunkStatus.FEATURES, false)); + assertNull(access.getChunkIfLoadedImmediately( + generationCenter.x(), generationCenter.z() + 2)); + List streamed = access.getBlockStates(new AABB( + x, FLOOR_FIRST_FREE_Y, z, x, FLOOR_FIRST_FREE_Y, z)).toList(); + + assertEquals(1, streamed.size()); + assertSame(Blocks.WATER, streamed.getFirst().getBlock()); + assertEquals(0, recording.terrainReads); + assertEquals(0, recording.heightReads); + assertEquals(0, recording.chunkReads); + } + + @Test + public void distanceTwoMutationsAndSideEffectsNeverReachTheDelegate() { + RecordingDelegate recording = new RecordingDelegate(); + NativeStructureWorldgenAccess access = access(recording); + ChunkPos generationCenter = generationCenter(); + BlockPos position = new BlockPos( + generationCenter.getMiddleBlockX(), + FLOOR_FIRST_FREE_Y, + (generationCenter.z() + 2) << 4); + + assertFalse(access.ensureCanWrite(position)); + assertFalse(access.setBlock(position, Blocks.DIRT.defaultBlockState(), 2)); + assertFalse(access.removeBlock(position, false)); + assertFalse(access.destroyBlock(position, false)); + access.updateNeighborsAt(position, Blocks.DIRT); + access.neighborShapeChanged( + Direction.NORTH, position, position.north(), + Blocks.DIRT.defaultBlockState(), 2, 512); + access.levelEvent(null, 2001, position, 0); + access.gameEvent(null, Vec3.atCenterOf(position), null); + access.addParticle(null, position.getX(), position.getY(), position.getZ(), 0, 0, 0); + access.getBlockTicks().schedule(new ScheduledTick<>( + Blocks.DIRT, position, 1L, 0L)); + + assertEquals(0, recording.mutations); + assertEquals(0, recording.events); + assertEquals(0, recording.blockTicks.count()); + } + + @Test + public void radiusOneReadsWritesAndTicksRemainUnchanged() { + RecordingDelegate recording = new RecordingDelegate(); + NativeStructureWorldgenAccess access = access(recording); + ChunkPos generationCenter = generationCenter(); + BlockPos position = new BlockPos( + (generationCenter.x() + 1) << 4, + FLOOR_FIRST_FREE_Y, + (generationCenter.z() - 1) << 4); + + assertTrue(access.isInsideGenerationRegion( + generationCenter.x() + 1, generationCenter.z() - 1)); + assertSame(Blocks.DIRT, access.getBlockState(position).getBlock()); + assertEquals(91, access.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ())); + assertTrue(access.ensureCanWrite(position)); + assertTrue(access.setBlock(position, Blocks.STONE.defaultBlockState(), 2)); + assertTrue(access.removeBlock(position, false)); + assertTrue(access.destroyBlock(position, false)); + access.getBlockTicks().schedule(new ScheduledTick<>( + Blocks.DIRT, position, 1L, 0L)); + + assertEquals(1, recording.terrainReads); + assertEquals(1, recording.heightReads); + assertEquals(4, recording.mutations); + assertEquals(1, recording.blockTicks.count()); + } + + @Test + public void virtualizedHeightsHideInRegionTerrainFromTheDelegate() { + RecordingDelegate recording = new RecordingDelegate(); + ChunkPos generationCenter = generationCenter(); + NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create( + recording.world(), generationCenter, + (x, z) -> SURFACE_FIRST_FREE_Y, + (x, z) -> FLOOR_FIRST_FREE_Y, + true, + position -> false); + int x = generationCenter.getMiddleBlockX(); + int z = generationCenter.getMiddleBlockZ(); + + assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z)); + assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z)); + assertEquals(0, recording.heightReads); + } + + @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, + false, + 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(); + NativeStructureWorldgenAccess access = access(recording); + ChunkPos generationCenter = generationCenter(); + + assertNull(access.getChunk(generationCenter.getWorldPosition())); + + assertEquals(1, recording.statuslessChunkReads); + assertEquals(0, recording.statusChunkReads); + } + + @Test + public void entityQueriesRejectDisjointAreasAndClampOverlaps() { + RecordingDelegate recording = new RecordingDelegate(); + NativeStructureWorldgenAccess access = access(recording); + ChunkPos generationCenter = generationCenter(); + int safeMaxX = generationCenter.getMaxBlockX() + 17; + int safeMinZ = generationCenter.getMinBlockZ() - 16; + int safeMaxZ = generationCenter.getMaxBlockZ() + 17; + AABB disjoint = new AABB( + safeMaxX, -64, safeMinZ, + safeMaxX + 16, 320, safeMaxZ); + AABB overlap = new AABB( + safeMaxX - 8, -128, safeMinZ - 8, + safeMaxX + 16, 400, safeMaxZ + 8); + EntityTypeTest type = EntityTypeTest.forClass(Entity.class); + + assertTrue(access.getEntities((Entity) null, disjoint, entity -> true).isEmpty()); + assertTrue(access.getEntities(type, disjoint, entity -> true).isEmpty()); + access.getEntities((Entity) null, overlap, entity -> true); + access.getEntities(type, overlap, entity -> true); + + assertEquals(2, recording.entityAreas.size()); + for (AABB delegatedArea : recording.entityAreas) { + assertEquals(safeMaxX - 8, delegatedArea.minX, 0D); + assertEquals(-64, delegatedArea.minY, 0D); + assertEquals(safeMinZ, delegatedArea.minZ, 0D); + assertEquals(safeMaxX, delegatedArea.maxX, 0D); + assertEquals(320, delegatedArea.maxY, 0D); + assertEquals(safeMaxZ, delegatedArea.maxZ, 0D); + } + } + + private static NativeStructureWorldgenAccess access(RecordingDelegate recording) { + return NativeStructureWorldgenAccess.create( + recording.world(), generationCenter(), + (x, z) -> SURFACE_FIRST_FREE_Y, + (x, z) -> FLOOR_FIRST_FREE_Y, + false, + position -> false); + } + + private static ChunkPos generationCenter() { + return new ChunkPos(GENERATION_CENTER_X, GENERATION_CENTER_Z); + } + + private static final class RecordingDelegate implements InvocationHandler { + private final Holder biome; + private final BiomeManager biomeManager; + private final RecordingTicks blockTicks; + private final RecordingTicks fluidTicks; + private final List entityAreas; + private int terrainReads; + private int heightReads; + private int chunkReads; + private int statuslessChunkReads; + private int statusChunkReads; + private int mutations; + private int events; + private boolean rejectDistantTerrainReads; + + private RecordingDelegate() { + this.biome = Holder.direct((Biome) null); + this.biomeManager = new BiomeManager((x, y, z) -> biome, 13L); + this.blockTicks = new RecordingTicks<>(); + this.fluidTicks = new RecordingTicks<>(); + this.entityAreas = new ArrayList<>(); + } + + private WorldGenLevel world() { + return (WorldGenLevel) Proxy.newProxyInstance( + WorldGenLevel.class.getClassLoader(), + new Class[]{WorldGenLevel.class}, this); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] arguments) { + String name = method.getName(); + if (name.equals("getSeaLevel")) { + return 63; + } + if (name.equals("getBiome")) { + return biome; + } + if (name.equals("getBiomeManager")) { + return biomeManager; + } + if (name.equals("getBlockTicks")) { + return blockTicks; + } + if (name.equals("getFluidTicks")) { + return fluidTicks; + } + if (name.equals("getMinY")) { + return -64; + } + if (name.equals("getHeight") && arguments == null) { + return 384; + } + if (name.equals("getHeight")) { + if (rejectDistantTerrainReads + && (Math.abs(((int) arguments[1] >> 4) - GENERATION_CENTER_X) > 1 + || Math.abs(((int) arguments[2] >> 4) - GENERATION_CENTER_Z) > 1)) { + throw new IllegalStateException("Requested chunk unavailable during world generation"); + } + heightReads++; + return 91; + } + if (name.equals("getBlockState")) { + terrainReads++; + return Blocks.DIRT.defaultBlockState(); + } + if (name.equals("getFluidState")) { + terrainReads++; + return Blocks.WATER.defaultBlockState().getFluidState(); + } + if (name.equals("getChunk")) { + chunkReads++; + if (arguments.length == 2) { + statuslessChunkReads++; + } else { + statusChunkReads++; + } + return null; + } + if (name.equals("getChunkIfLoadedImmediately")) { + chunkReads++; + return null; + } + if (name.equals("getEntities")) { + entityAreas.add((AABB) arguments[1]); + return List.of(); + } + if (name.equals("ensureCanWrite") || name.equals("setBlock") + || name.equals("removeBlock") || name.equals("destroyBlock")) { + mutations++; + return true; + } + if (name.equals("updateNeighborsAt") || name.equals("neighborShapeChanged")) { + mutations++; + return null; + } + if (name.equals("levelEvent") || name.equals("gameEvent") || name.equals("addParticle")) { + events++; + return null; + } + if (name.equals("hashCode")) { + return System.identityHashCode(proxy); + } + if (name.equals("equals")) { + return proxy == arguments[0]; + } + if (name.equals("toString")) { + return "native-structure-worldgen-test-delegate"; + } + throw new UnsupportedOperationException(method.toString()); + } + } + + private static final class RecordingTicks implements LevelTickAccess { + private int count; + + @Override + public void schedule(ScheduledTick tick) { + count++; + } + + @Override + public boolean hasScheduledTick(BlockPos position, T type) { + return false; + } + + @Override + public int count() { + return count; + } + + @Override + public boolean willTickThisTick(BlockPos position, T type) { + return false; + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsChunkIOFlushTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsChunkIOFlushTest.java new file mode 100644 index 000000000..5b967837c --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsChunkIOFlushTest.java @@ -0,0 +1,100 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController; +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.level.ServerLevel; +import org.bukkit.craftbukkit.CraftWorld; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class NmsChunkIOFlushTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void nativeDrainCanWaitOffOwnerUntilChunkPoiAndEntityIoFinish() throws Exception { + Fixture fixture = new Fixture(); + AtomicBoolean chunkPending = new AtomicBoolean(true); + AtomicBoolean poiPending = new AtomicBoolean(true); + AtomicBoolean entityPending = new AtomicBoolean(true); + CountDownLatch chunkDrain = new CountDownLatch(1); + CountDownLatch poiDrain = new CountDownLatch(1); + CountDownLatch entityDrain = new CountDownLatch(1); + when(fixture.chunks.hasTasks()).thenAnswer(invocation -> { + chunkDrain.countDown(); + return chunkPending.get(); + }); + when(fixture.pois.hasTasks()).thenAnswer(invocation -> { + poiDrain.countDown(); + return poiPending.get(); + }); + when(fixture.entities.hasTasks()).thenAnswer(invocation -> { + entityDrain.countDown(); + return entityPending.get(); + }); + ExecutorService io = Executors.newSingleThreadExecutor(); + try { + Future drain = io.submit(() -> fixture.binding.flushChunkIO(fixture.world)); + assertTrue(chunkDrain.await(5L, TimeUnit.SECONDS)); + assertFalse(drain.isDone()); + chunkPending.set(false); + assertTrue(poiDrain.await(5L, TimeUnit.SECONDS)); + assertFalse(drain.isDone()); + poiPending.set(false); + assertTrue(entityDrain.await(5L, TimeUnit.SECONDS)); + assertFalse(drain.isDone()); + entityPending.set(false); + drain.get(5L, TimeUnit.SECONDS); + } finally { + chunkPending.set(false); + poiPending.set(false); + entityPending.set(false); + io.shutdownNow(); + assertTrue(io.awaitTermination(5L, TimeUnit.SECONDS)); + } + } + + @Test + public void nativeDrainFailurePropagatesToItsCompletionReceipt() { + Fixture fixture = new Fixture(); + IllegalStateException failure = new IllegalStateException("native IO failed"); + when(fixture.chunks.hasTasks()).thenThrow(failure); + assertSame(failure, assertThrows(IllegalStateException.class, + () -> fixture.binding.flushChunkIO(fixture.world))); + } + + private static final class Fixture { + private final NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS); + private final CraftWorld world = mock(CraftWorld.class); + private final ServerLevel level = mock(ServerLevel.class); + private final RegionDataController chunks = mock(RegionDataController.class); + private final RegionDataController pois = mock(RegionDataController.class); + private final RegionDataController entities = mock(RegionDataController.class); + + private Fixture() { + when(world.getHandle()).thenReturn(level); + when(level.moonrise$getChunkDataController()).thenReturn(chunks); + when(level.moonrise$getPoiChunkDataController()).thenReturn(pois); + when(level.moonrise$getEntityChunkDataController()).thenReturn(entities); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsChunkTaskPollingTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsChunkTaskPollingTest.java new file mode 100644 index 000000000..8e4475314 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsChunkTaskPollingTest.java @@ -0,0 +1,95 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import art.arcane.volmlib.util.scheduling.FoliaScheduler; +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.craftbukkit.CraftWorld; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.MockedStatic; + +import java.util.concurrent.CompletableFuture; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +public class NmsChunkTaskPollingTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void serverOwnerPumpsOnlyTheTargetWorldQueue() { + Server bukkitServer = mock(Server.class); + CraftWorld world = mock(CraftWorld.class); + ServerLevel level = mock(ServerLevel.class); + MinecraftServer server = mock(MinecraftServer.class); + ServerChunkCache chunks = mock(ServerChunkCache.class); + NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS); + CompletableFuture completed = new CompletableFuture<>(); + when(world.getHandle()).thenReturn(level); + when(level.getServer()).thenReturn(server); + when(server.getRunningThread()).thenReturn(Thread.currentThread()); + when(level.getChunkSource()).thenReturn(chunks); + when(chunks.pollTask()).thenAnswer(invocation -> completed.complete(null)).thenReturn(false); + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic folia = mockStatic(FoliaScheduler.class)) { + bukkit.when(Bukkit::getServer).thenReturn(bukkitServer); + folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(false); + assertTrue(binding.pollChunkTask(world)); + assertTrue(completed.isDone()); + assertFalse(binding.pollChunkTask(world)); + } + verify(chunks, times(2)).pollTask(); + verify(server, never()).executeAllRecentInternalTasks(); + } + + @Test + public void nonOwnerIsRejectedBeforeAccessingTheChunkQueue() { + Server bukkitServer = mock(Server.class); + CraftWorld world = mock(CraftWorld.class); + ServerLevel level = mock(ServerLevel.class); + MinecraftServer server = mock(MinecraftServer.class); + NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS); + when(world.getHandle()).thenReturn(level); + when(level.getServer()).thenReturn(server); + when(server.getRunningThread()).thenReturn(new Thread("Other server owner")); + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic folia = mockStatic(FoliaScheduler.class)) { + bukkit.when(Bukkit::getServer).thenReturn(bukkitServer); + folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(false); + assertThrows(IllegalStateException.class, () -> binding.pollChunkTask(world)); + } + verify(level, never()).getChunkSource(); + } + + @Test + public void foliaNeverUsesTheWorldWideTaskPump() { + Server bukkitServer = mock(Server.class); + CraftWorld world = mock(CraftWorld.class); + NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS); + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic folia = mockStatic(FoliaScheduler.class)) { + bukkit.when(Bukkit::getServer).thenReturn(bukkitServer); + folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(true); + assertFalse(binding.pollChunkTask(world)); + } + verifyNoInteractions(world); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsEntitySpawnDifficultyTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsEntitySpawnDifficultyTest.java new file mode 100644 index 000000000..4a4ec9255 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsEntitySpawnDifficultyTest.java @@ -0,0 +1,103 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import net.minecraft.SharedConstants; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.Bootstrap; +import org.bukkit.Difficulty; +import org.bukkit.Location; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class NmsEntitySpawnDifficultyTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void peacefulRejectsEveryNativeForbiddenTypeBeforeBukkitCreatesIt() { + CraftWorld world = mock(CraftWorld.class); + when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL); + NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS); + Location location = new Location(world, 0.5, 80, 0.5); + int rejected = 0; + + for (EntityType type : EntityType.values()) { + if (type.getEntityClass() == null) { + continue; + } + if (BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse(type.getKey().toString())).isAllowedInPeaceful()) { + continue; + } + assertNull(type.name(), binding.spawnEntity(location, type, CreatureSpawnEvent.SpawnReason.NATURAL)); + rejected++; + } + + assertTrue(rejected > 20); + verify(world, never()).spawn(any(Location.class), any(Class.class), isNull(), any(CreatureSpawnEvent.SpawnReason.class)); + } + + @Test + public void peacefulPreservesPassiveAndNativeMonsterCategoryExceptions() { + CraftWorld world = mock(CraftWorld.class); + when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL); + NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS); + Location location = new Location(world, 0.5, 80, 0.5); + Entity created = mock(Entity.class); + doReturn(created).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL)); + + for (EntityType type : List.of(EntityType.COW, EntityType.VILLAGER, EntityType.WOLF, EntityType.PIGLIN, EntityType.SHULKER)) { + assertSame(type.name(), created, binding.spawnEntity(location, type, CreatureSpawnEvent.SpawnReason.NATURAL)); + } + } + + @Test + public void nonPeacefulDifficultiesPreserveHostileSpawns() { + CraftWorld world = mock(CraftWorld.class); + NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS); + Location location = new Location(world, 0.5, 80, 0.5); + Entity created = mock(Entity.class); + doReturn(created).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL)); + + for (Difficulty difficulty : List.of(Difficulty.EASY, Difficulty.NORMAL, Difficulty.HARD)) { + when(world.getDifficulty()).thenReturn(difficulty); + assertSame(created, binding.spawnEntity(location, EntityType.ZOMBIE, CreatureSpawnEvent.SpawnReason.NATURAL)); + } + } + + @Test + public void unrelatedBukkitSpawnFailuresStillPropagate() { + CraftWorld world = mock(CraftWorld.class); + when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL); + NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS); + Location location = new Location(world, 0.5, 80, 0.5); + IllegalStateException failure = new IllegalStateException("Spawn rejected by platform"); + doThrow(failure).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL)); + + assertSame(failure, assertThrows(IllegalStateException.class, + () -> binding.spawnEntity(location, EntityType.COW, CreatureSpawnEvent.SpawnReason.NATURAL))); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsSavedTerrainCheckpointTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsSavedTerrainCheckpointTest.java new file mode 100644 index 000000000..2a670dc58 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsSavedTerrainCheckpointTest.java @@ -0,0 +1,114 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import art.arcane.iris.world.history.SavedTerrainChunk; +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.MockedStatic; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; + +public class NmsSavedTerrainCheckpointTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void checkpointWaitsForDirtyAndUnloadedChunkWritesBeforeFlushAndVerification() throws Exception { + ServerLevel level = mock(ServerLevel.class); + Path root = Path.of("checkpoint-world"); + byte[] receipt = new byte[]{1, 2, 3}; + NmsSavedTerrainCapture.Checkpoint saved = new NmsSavedTerrainCapture.Checkpoint(new ChunkPos(4, -5), + new NmsSavedTerrainCapture.Verification("minecraft:full", receipt, 7L)); + NmsSavedTerrainCapture.Checkpoint unloaded = NmsSavedTerrainCapture.checkpoint(-8, 9, null); + Set completed = new HashSet<>(); + AtomicBoolean flushed = new AtomicBoolean(); + try (MockedStatic io = mockStatic(MoonriseRegionFileIO.class); + MockedStatic terrain = mockStatic(SavedTerrainChunk.class)) { + io.when(() -> MoonriseRegionFileIO.getPriority(level, 4, -5, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA)) + .thenAnswer(invocation -> { + completed.add(saved.chunk()); + return Priority.COMPLETING; + }); + io.when(() -> MoonriseRegionFileIO.getPriority(level, -8, 9, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA)) + .thenAnswer(invocation -> { + completed.add(unloaded.chunk()); + return Priority.COMPLETING; + }); + io.when(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA)) + .thenAnswer(invocation -> { + assertEquals(Set.of(saved.chunk(), unloaded.chunk()), completed); + flushed.set(true); + return null; + }); + terrain.when(() -> SavedTerrainChunk.verifyCheckpoint(root, 4, -5, "minecraft:full", receipt, 7L)) + .thenAnswer(invocation -> { + assertTrue(flushed.get()); + return null; + }); + + NmsSavedTerrainCapture.finishCheckpoint(level, root, List.of(saved, unloaded)); + + terrain.verify(() -> SavedTerrainChunk.verifyCheckpoint(root, 4, -5, "minecraft:full", receipt, 7L)); + io.verify(() -> MoonriseRegionFileIO.flush(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), never()); + io.verify(() -> MoonriseRegionFileIO.getPriority(level, 100, 100, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), never()); + terrain.verifyNoMoreInteractions(); + } + } + + @Test + public void storageFlushFailureCannotCompleteTheCheckpoint() throws Exception { + ServerLevel level = mock(ServerLevel.class); + IOException failure = new IOException("storage flush failed"); + try (MockedStatic io = mockStatic(MoonriseRegionFileIO.class); + MockedStatic terrain = mockStatic(SavedTerrainChunk.class)) { + io.when(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA)) + .thenThrow(failure); + + assertSame(failure, assertThrows(IOException.class, + () -> NmsSavedTerrainCapture.finishCheckpoint(level, Path.of("checkpoint-world"), List.of()))); + terrain.verifyNoInteractions(); + } + } + + @Test + public void persistedReceiptMismatchCannotCompleteTheCheckpoint() throws Exception { + ServerLevel level = mock(ServerLevel.class); + Path root = Path.of("checkpoint-world"); + byte[] receipt = new byte[]{4, 5}; + IOException failure = new IOException("saved terrain receipt mismatch"); + NmsSavedTerrainCapture.Checkpoint saved = new NmsSavedTerrainCapture.Checkpoint(new ChunkPos(1, 2), + new NmsSavedTerrainCapture.Verification("minecraft:noise", receipt, 3L)); + try (MockedStatic io = mockStatic(MoonriseRegionFileIO.class); + MockedStatic terrain = mockStatic(SavedTerrainChunk.class)) { + io.when(() -> MoonriseRegionFileIO.getPriority(level, 1, 2, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA)) + .thenReturn(Priority.COMPLETING); + terrain.when(() -> SavedTerrainChunk.verifyCheckpoint(root, 1, 2, "minecraft:noise", receipt, 3L)) + .thenThrow(failure); + + assertSame(failure, assertThrows(IOException.class, + () -> NmsSavedTerrainCapture.finishCheckpoint(level, root, List.of(saved)))); + io.verify(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA)); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsSavedTerrainSnapshotTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsSavedTerrainSnapshotTest.java new file mode 100644 index 000000000..815575ad0 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsSavedTerrainSnapshotTest.java @@ -0,0 +1,223 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import art.arcane.iris.world.task.J; +import ca.spottedleaf.concurrentutil.completable.Completable; +import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.executor.thread.BalancedPrioritisedThreadPool; +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.common.PlatformHooks; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import net.minecraft.SharedConstants; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.storage.SerializableChunkData; +import org.bukkit.World; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.MockedStatic; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class NmsSavedTerrainSnapshotTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void ownerReservesSaveOrderBeforeOffOwnerSerializationAndLaterAutosave() throws Exception { + try (SaveFixture fixture = new SaveFixture(); + ExecutorService checkpoint = Executors.newSingleThreadExecutor(); + MockedStatic snapshots = mockStatic(SerializableChunkData.class); + MockedStatic hooks = mockStatic(PlatformHooks.class); + MockedStatic io = mockStatic(MoonriseRegionFileIO.class)) { + Thread owner = Thread.currentThread(); + ChunkAccess chunk = mock(ChunkAccess.class); + SerializableChunkData snapshot = mock(SerializableChunkData.class); + PlatformHooks platform = mock(PlatformHooks.class); + CompoundTag capturedData = new CompoundTag(); + CompoundTag autosave = new CompoundTag(); + List submissions = new ArrayList<>(); + when(chunk.getPos()).thenReturn(new ChunkPos(4, -9)); + snapshots.when(() -> SerializableChunkData.copyOf(fixture.level, chunk)).thenAnswer(invocation -> { + assertSame(owner, Thread.currentThread()); + return snapshot; + }); + hooks.when(PlatformHooks::get).thenReturn(platform); + doAnswer(invocation -> { + assertSame(owner, Thread.currentThread()); + return null; + }).when(platform).chunkSyncSave(fixture.level, chunk, snapshot); + when(snapshot.write()).thenAnswer(invocation -> { + assertNotSame(owner, Thread.currentThread()); + assertEquals(List.of("checkpoint", "autosave"), submissions); + return capturedData; + }); + io.when(() -> MoonriseRegionFileIO.scheduleSave(eq(fixture.level), eq(4), eq(-9), + any(Completable.class), any(PrioritisedExecutor.PrioritisedTask.class), + eq(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), eq(Priority.NORMAL))) + .thenAnswer(invocation -> { + assertSame(owner, Thread.currentThread()); + submissions.add("checkpoint"); + return null; + }); + io.when(() -> MoonriseRegionFileIO.scheduleSave(fixture.level, 4, -9, autosave, + MoonriseRegionFileIO.RegionFileType.CHUNK_DATA)).thenAnswer(invocation -> { + submissions.add("autosave"); + return null; + }); + + NmsSavedTerrainCapture.CapturedChunk captured = NmsSavedTerrainCapture.scheduleSnapshot(fixture.level, chunk); + + assertEquals(List.of("checkpoint"), submissions); + assertFalse(captured.data().isDone()); + verify(snapshot, never()).write(); + verify(platform).chunkSyncSave(fixture.level, chunk, snapshot); + MoonriseRegionFileIO.scheduleSave(fixture.level, 4, -9, autosave, + MoonriseRegionFileIO.RegionFileType.CHUNK_DATA); + Future result = checkpoint.submit(() -> NmsSavedTerrainCapture.awaitSnapshot( + fixture.level, captured, System.nanoTime() + TimeUnit.SECONDS.toNanos(3))); + + assertSame(capturedData, result.get(5, TimeUnit.SECONDS)); + verify(snapshot).write(); + } + } + + @Test + public void failedOffOwnerSerializationCannotSucceedAsACheckpoint() throws Exception { + try (SaveFixture fixture = new SaveFixture(); + ExecutorService checkpoint = Executors.newSingleThreadExecutor(); + MockedStatic snapshots = mockStatic(SerializableChunkData.class); + MockedStatic hooks = mockStatic(PlatformHooks.class); + MockedStatic io = mockStatic(MoonriseRegionFileIO.class)) { + ChunkAccess chunk = mock(ChunkAccess.class); + SerializableChunkData snapshot = mock(SerializableChunkData.class); + IllegalStateException failure = new IllegalStateException("serialization failed"); + when(chunk.getPos()).thenReturn(new ChunkPos(4, -9)); + snapshots.when(() -> SerializableChunkData.copyOf(fixture.level, chunk)).thenReturn(snapshot); + hooks.when(PlatformHooks::get).thenReturn(mock(PlatformHooks.class)); + when(snapshot.write()).thenThrow(failure); + NmsSavedTerrainCapture.CapturedChunk captured = NmsSavedTerrainCapture.scheduleSnapshot(fixture.level, chunk); + + Future result = checkpoint.submit(() -> assertThrows(IOException.class, + () -> NmsSavedTerrainCapture.awaitSnapshot(fixture.level, captured, + System.nanoTime() + TimeUnit.SECONDS.toNanos(3)))); + + assertSame(failure, result.get(5, TimeUnit.SECONDS).getCause()); + assertTrue(captured.data().isCompletedExceptionally()); + } + } + + @Test + public void checkpointBoundsOwnerRequestsUntilPriorNativeWritesFinish() throws Exception { + try (SaveFixture fixture = new SaveFixture(); + ExecutorService owner = Executors.newSingleThreadExecutor(); + ExecutorService checkpoint = Executors.newSingleThreadExecutor()) { + World world = mock(World.class); + when(world.getWorldFolder()).thenReturn(new File("checkpoint-world")); + List holders = new ArrayList<>(129); + for (int index = 0; index < 129; index++) { + NewChunkHolder holder = mock(NewChunkHolder.class); + setField(holder, "chunkX", index); + holders.add(holder); + } + AtomicInteger requested = new AtomicInteger(); + AtomicBoolean writesBlocked = new AtomicBoolean(true); + CountDownLatch firstBatchWaiting = new CountDownLatch(1); + Future result = checkpoint.submit(() -> { + try (MockedStatic scheduling = mockStatic(J.class); + MockedStatic io = mockStatic(MoonriseRegionFileIO.class)) { + scheduling.when(() -> J.runRegionFuture(eq(world), anyInt(), anyInt(), any(Runnable.class))) + .thenAnswer(invocation -> { + requested.incrementAndGet(); + Runnable action = invocation.getArgument(3); + return CompletableFuture.runAsync(action, owner); + }); + io.when(() -> MoonriseRegionFileIO.getPriority(eq(fixture.level), anyInt(), anyInt(), + eq(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))).thenAnswer(invocation -> { + firstBatchWaiting.countDown(); + return writesBlocked.get() ? Priority.NORMAL : Priority.COMPLETING; + }); + NmsSavedTerrainCapture.flushSnapshots(world, fixture.level, holders); + io.verify(() -> MoonriseRegionFileIO.getPriority(fixture.level, 128, 0, + MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), times(2)); + return null; + } + }); + try { + assertTrue(firstBatchWaiting.await(5, TimeUnit.SECONDS)); + assertEquals(64, requested.get()); + assertTrue(owner.submit(() -> true).get(5, TimeUnit.SECONDS)); + assertFalse(result.isDone()); + writesBlocked.set(false); + result.get(5, TimeUnit.SECONDS); + assertEquals(129, requested.get()); + } finally { + writesBlocked.set(false); + } + } + } + + private static void setField(Object instance, String name, Object value) throws Exception { + Field field = instance.getClass().getField(name); + field.setAccessible(true); + field.set(instance, value); + } + + private static final class SaveFixture implements AutoCloseable { + private final BalancedPrioritisedThreadPool pool = new BalancedPrioritisedThreadPool( + TimeUnit.MILLISECONDS.toNanos(1), Executors.defaultThreadFactory()); + private final ServerLevel level = mock(ServerLevel.class); + + private SaveFixture() throws Exception { + BalancedPrioritisedThreadPool.OrderedStreamGroup group = pool.createOrderedStreamGroup(); + ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class); + setField(scheduler, "saveExecutor", group.createExecutor()); + setField(scheduler, "compressionExecutor", group.createExecutor()); + setField(scheduler, "chunkHolderManager", mock(ChunkHolderManager.class)); + when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler); + } + + @Override + public void close() throws Exception { + pool.halt(true); + assertTrue(pool.joinInterruptable(5_000)); + } + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsSavedTerrainWriteBarrierTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsSavedTerrainWriteBarrierTest.java new file mode 100644 index 000000000..d391d328a --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsSavedTerrainWriteBarrierTest.java @@ -0,0 +1,135 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import ca.spottedleaf.concurrentutil.executor.thread.BalancedPrioritisedThreadPool; +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.MockedStatic; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +public class NmsSavedTerrainWriteBarrierTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void checkpointHelpsSaveAndCompressionWhileNativeGenerationOccupiesTheWorker() throws Exception { + BalancedPrioritisedThreadPool pool = new BalancedPrioritisedThreadPool( + TimeUnit.MILLISECONDS.toNanos(1), Executors.defaultThreadFactory()); + BalancedPrioritisedThreadPool.OrderedStreamGroup group = pool.createOrderedStreamGroup(); + BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue generation = group.createExecutor(); + BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue saves = group.createExecutor(); + BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue compression = group.createExecutor(); + ServerLevel level = level(saves, compression); + ChunkPos chunk = new ChunkPos(4, -9); + CountDownLatch generationEntered = new CountDownLatch(1); + CountDownLatch releaseGeneration = new CountDownLatch(1); + AtomicBoolean pendingWrite = new AtomicBoolean(true); + AtomicInteger serialized = new AtomicInteger(); + AtomicInteger compressed = new AtomicInteger(); + AtomicReference saveThread = new AtomicReference<>(); + AtomicReference compressionThread = new AtomicReference<>(); + try (MockedStatic io = mockStatic(MoonriseRegionFileIO.class)) { + io.when(() -> MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(), + MoonriseRegionFileIO.RegionFileType.CHUNK_DATA)) + .thenAnswer(invocation -> pendingWrite.get() ? Priority.NORMAL : Priority.COMPLETING); + generation.queueTask(() -> { + generationEntered.countDown(); + try { + assertTrue(releaseGeneration.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException interruption) { + Thread.currentThread().interrupt(); + throw new AssertionError(interruption); + } + }); + pool.adjustThreadCount(1); + assertTrue(generationEntered.await(5, TimeUnit.SECONDS)); + saves.queueTask(() -> { + saveThread.set(Thread.currentThread()); + serialized.incrementAndGet(); + compression.queueTask(() -> { + compressionThread.set(Thread.currentThread()); + compressed.incrementAndGet(); + pendingWrite.set(false); + }); + }); + + NmsSavedTerrainCapture.awaitWrite(level, chunk, System.nanoTime() + TimeUnit.SECONDS.toNanos(3)); + + assertEquals(1L, releaseGeneration.getCount()); + assertEquals(1, serialized.get()); + assertEquals(1, compressed.get()); + assertFalse(pendingWrite.get()); + assertSame(Thread.currentThread(), saveThread.get()); + assertSame(Thread.currentThread(), compressionThread.get()); + } finally { + releaseGeneration.countDown(); + pool.halt(true); + assertTrue(pool.joinInterruptable(5_000)); + } + } + + @Test + public void waitingForNativeWriteHonorsTheSharedDeadline() throws Exception { + ServerLevel level = mock(ServerLevel.class); + ChunkPos chunk = new ChunkPos(2, 3); + IOException failure = assertThrows(IOException.class, + () -> NmsSavedTerrainCapture.awaitWrite(level, chunk, System.nanoTime() - 1L)); + assertTrue(failure.getMessage().contains("Timed out")); + } + + @Test + public void interruptedNativeWriteWaitPreservesInterruption() throws Exception { + ServerLevel level = mock(ServerLevel.class); + ChunkPos chunk = new ChunkPos(2, 3); + Thread.currentThread().interrupt(); + try { + IOException failure = assertThrows(IOException.class, + () -> NmsSavedTerrainCapture.awaitWrite(level, chunk, + System.nanoTime() + TimeUnit.SECONDS.toNanos(3))); + assertTrue(failure.getMessage().contains("Interrupted")); + assertTrue(Thread.currentThread().isInterrupted()); + } finally { + Thread.interrupted(); + } + } + + private static ServerLevel level(BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue saves, + BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue compression) throws Exception { + ServerLevel level = mock(ServerLevel.class); + ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class); + Field savesField = ChunkTaskScheduler.class.getField("saveExecutor"); + Field compressionField = ChunkTaskScheduler.class.getField("compressionExecutor"); + savesField.setAccessible(true); + compressionField.setAccessible(true); + savesField.set(scheduler, saves); + compressionField.set(scheduler, compression); + when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler); + return level; + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/WorldgenTerrainHeightmapsTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/WorldgenTerrainHeightmapsTest.java new file mode 100644 index 000000000..ef3b1dfc8 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/WorldgenTerrainHeightmapsTest.java @@ -0,0 +1,345 @@ +package art.arcane.iris.platform.bukkit.nms.v26_3_R1; + +import art.arcane.iris.nativegen.v26_3_R1.NativeBlockTags; +import art.arcane.iris.nativegen.v26_3_R1.WorldgenTerrainHeightmaps; +import com.mojang.serialization.Codec; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.IdMapper; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Block; +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.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.structures.IglooPieces; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.IntBinaryOperator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class WorldgenTerrainHeightmapsTest { + private static final int MIN_Y = -64; + private static final int HEIGHT = 384; + private static final int LAND_TERRAIN_TOP = 70; + private static final int LAND_CANOPY_TOP = 80; + private static final int OCEAN_FLOOR_TOP = 60; + private static final int OCEAN_FLUID_TOP = 64; + private static final int LAND_X = 3; + private static final int LAND_Z = 5; + private static final int OCEAN_X = 8; + private static final int OCEAN_Z = 8; + private static final int TERRAIN_SLAB_DEPTH = 4; + + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + NativeBlockTags.bindHeightmapFixtures(); + } + + @Test + public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() { + ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0)); + + IrisChunkGenerator.primeAuthoringHeightmaps(chunk); + + assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z)); + assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z)); + assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, LAND_X, LAND_Z)); + assertEquals(LAND_TERRAIN_TOP + 6, + chunk.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, LAND_X, LAND_Z)); + assertEquals(OCEAN_FLUID_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, OCEAN_X, OCEAN_Z)); + assertEquals(OCEAN_FLOOR_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, OCEAN_X, OCEAN_Z)); + } + + @Test + public void primeTerrainWritesEngineSurfaceAndOceanFloorHeights() { + ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0)); + + assertFalse(chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG)); + assertFalse(chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG)); + + WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY()); + + assertTrue(chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG)); + assertTrue(chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG)); + assertEquals(LAND_TERRAIN_TOP, chunk.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z)); + assertEquals(LAND_TERRAIN_TOP, chunk.getHeight( + Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z)); + assertEquals(OCEAN_FLUID_TOP, chunk.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, OCEAN_X, OCEAN_Z)); + assertEquals(OCEAN_FLOOR_TOP, chunk.getHeight( + Heightmap.Types.OCEAN_FLOOR_WG, OCEAN_X, OCEAN_Z)); + } + + @Test + public void finalHeightmapPrimingDoesNotClobberTheWorldgenHeightmaps() { + ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0)); + + WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY()); + Heightmap.primeHeightmaps(chunk, ChunkStatus.FINAL_HEIGHTMAPS); + + assertEquals(LAND_TERRAIN_TOP, chunk.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z)); + assertEquals(LAND_TERRAIN_TOP, chunk.getHeight( + Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z)); + assertEquals(LAND_CANOPY_TOP, chunk.getHeight( + Heightmap.Types.WORLD_SURFACE, LAND_X, LAND_Z)); + } + + @Test + public void unprimedWorldgenHeightmapResolvesTheIrisCanopyInstead() { + ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0)); + + assertEquals(LAND_CANOPY_TOP, chunk.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z)); + } + + @Test + public void primeTerrainOverwritesAnAlreadyLazyPrimedCanopyHeight() { + ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0)); + + assertEquals(LAND_CANOPY_TOP, chunk.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z)); + WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY()); + + assertEquals(LAND_TERRAIN_TOP, chunk.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z)); + } + + @Test + public void structurePlacementPrimesTheWestNeighbourAnIglooEntranceReads() { + Map chunks = new HashMap<>(); + chunks.put(ChunkPos.pack(0, 0), terrainChunk(new ChunkPos(0, 0))); + chunks.put(ChunkPos.pack(-1, 0), terrainChunk(new ChunkPos(-1, 0))); + WorldGenLevel world = world(chunks); + StructureStart start = startInOrigin(); + + int canopySnap = IglooPieces.GENERATION_HEIGHT + iglooSnapOffset(world); + WorldgenTerrainHeightmaps.primeStructurePlacement( + world, new ChunkPos(0, 0), List.of(start), surfaceFirstFreeY(), floorFirstFreeY()); + int terrainSnap = IglooPieces.GENERATION_HEIGHT + iglooSnapOffset(world); + + assertEquals(LAND_TERRAIN_TOP + 1, world.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, -2, 5)); + assertEquals(LAND_TERRAIN_TOP, terrainSnap); + assertEquals(LAND_CANOPY_TOP, canopySnap); + assertEquals(LAND_CANOPY_TOP - LAND_TERRAIN_TOP, canopySnap - terrainSnap); + } + + @Test + public void structurePlacementPrimesEveryChunkTheStartFootprintTouches() { + Map chunks = new HashMap<>(); + for (int chunkX = -1; chunkX <= 1; chunkX++) { + for (int chunkZ = -1; chunkZ <= 1; chunkZ++) { + chunks.put(ChunkPos.pack(chunkX, chunkZ), terrainChunk(new ChunkPos(chunkX, chunkZ))); + } + } + WorldGenLevel world = world(chunks); + + WorldgenTerrainHeightmaps.primeStructurePlacement( + world, new ChunkPos(0, 0), List.of(startInOrigin()), + surfaceFirstFreeY(), floorFirstFreeY()); + + for (ChunkAccess chunk : chunks.values()) { + assertTrue(chunk.getPos().toString(), + chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG)); + assertTrue(chunk.getPos().toString(), + chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG)); + assertEquals(LAND_TERRAIN_TOP, chunk.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z)); + } + } + + @Test + public void structurePlacementDoesNotPrimeALoadedDistanceTwoChunk() { + Map chunks = new HashMap<>(); + ProtoChunk origin = terrainChunk(new ChunkPos(0, 0)); + ProtoChunk neighbour = terrainChunk(new ChunkPos(1, 0)); + ProtoChunk distanceTwo = terrainChunk(new ChunkPos(2, 0)); + chunks.put(ChunkPos.pack(0, 0), origin); + chunks.put(ChunkPos.pack(1, 0), neighbour); + chunks.put(ChunkPos.pack(2, 0), distanceTwo); + WorldGenLevel world = world(chunks); + StructureStart shifted = startInOrigin(); + shifted.getPieces().forEach(piece -> piece.move(16, 0, 0)); + + WorldgenTerrainHeightmaps.primeStructurePlacement( + world, new ChunkPos(0, 0), List.of(shifted), + surfaceFirstFreeY(), floorFirstFreeY()); + + assertTrue(origin.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG)); + assertTrue(neighbour.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG)); + assertFalse(distanceTwo.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG)); + } + + @Test + public void structurePlacementSkipsChunksOutsideTheGenerationRegion() { + Map chunks = new HashMap<>(); + ProtoChunk origin = terrainChunk(new ChunkPos(0, 0)); + chunks.put(ChunkPos.pack(0, 0), origin); + WorldGenLevel world = world(chunks); + + WorldgenTerrainHeightmaps.primeStructurePlacement( + world, new ChunkPos(0, 0), List.of(startInOrigin()), + surfaceFirstFreeY(), floorFirstFreeY()); + + assertTrue(origin.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG)); + assertEquals(LAND_TERRAIN_TOP, origin.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z)); + } + + private static int iglooSnapOffset(WorldGenLevel world) { + return world.getHeight(Heightmap.Types.WORLD_SURFACE_WG, -2, 5) + - IglooPieces.GENERATION_HEIGHT - 1; + } + + private static StructureStart startInOrigin() { + Structure structure = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty())); + SwampHutPiece piece = new SwampHutPiece(RandomSource.create(13L), 0, 0); + return new StructureStart( + structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece))); + } + + private static IntBinaryOperator surfaceFirstFreeY() { + return (x, z) -> isOcean(x, z) ? OCEAN_FLUID_TOP + 1 : LAND_TERRAIN_TOP + 1; + } + + private static IntBinaryOperator floorFirstFreeY() { + return (x, z) -> isOcean(x, z) ? OCEAN_FLOOR_TOP + 1 : LAND_TERRAIN_TOP + 1; + } + + private static boolean isOcean(int x, int z) { + return Math.floorMod(x, 16) == OCEAN_X && Math.floorMod(z, 16) == OCEAN_Z; + } + + private static ProtoChunk terrainChunk(ChunkPos pos) { + ProtoChunk chunk = new ProtoChunk( + pos, UpgradeData.EMPTY, LevelHeightAccessor.create(MIN_Y, HEIGHT), + containerFactory(), null); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + boolean ocean = isOcean(pos.getMinBlockX() + x, pos.getMinBlockZ() + z); + int stoneTop = ocean ? OCEAN_FLOOR_TOP : LAND_TERRAIN_TOP; + for (int y = stoneTop - TERRAIN_SLAB_DEPTH; y <= stoneTop; y++) { + write(chunk, x, y, z, Blocks.STONE.defaultBlockState()); + } + if (ocean) { + for (int y = OCEAN_FLOOR_TOP + 1; y <= OCEAN_FLUID_TOP; y++) { + write(chunk, x, y, z, Blocks.WATER.defaultBlockState()); + } + continue; + } + for (int y = LAND_TERRAIN_TOP + 1; y <= LAND_TERRAIN_TOP + 6; y++) { + write(chunk, x, y, z, Blocks.SPRUCE_LOG.defaultBlockState()); + } + for (int y = LAND_TERRAIN_TOP + 7; y <= LAND_CANOPY_TOP; y++) { + write(chunk, x, y, z, Blocks.SPRUCE_LEAVES.defaultBlockState()); + } + } + } + return chunk; + } + + 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 PalettedContainerFactory containerFactory() { + Strategy blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY); + BlockState air = Blocks.AIR.defaultBlockState(); + IdMapper> biomeIds = new IdMapper<>(); + Holder defaultBiome = Holder.direct((Biome) null); + biomeIds.add(defaultBiome); + Strategy> biomes = Strategy.createForBiomes(biomeIds); + Codec> 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)); + } + + private static WorldGenLevel world(Map chunks) { + InvocationHandler handler = (proxy, method, arguments) -> { + String methodName = method.getName(); + if (methodName.equals("hasChunk")) { + return chunks.containsKey(ChunkPos.pack((int) arguments[0], (int) arguments[1])); + } + if (methodName.equals("getChunk") && arguments.length == 2) { + ChunkAccess chunk = chunks.get(ChunkPos.pack((int) arguments[0], (int) arguments[1])); + if (chunk == null) { + throw new AssertionError("Heightmap priming requested an unavailable chunk " + + arguments[0] + "," + arguments[1]); + } + return chunk; + } + if (methodName.equals("getHeight") && arguments.length == 3) { + Heightmap.Types type = (Heightmap.Types) arguments[0]; + int x = (int) arguments[1]; + int z = (int) arguments[2]; + ChunkAccess chunk = chunks.get(ChunkPos.pack(x >> 4, z >> 4)); + if (chunk == null) { + throw new AssertionError("Heightmap read requested an unavailable chunk at " + + x + "," + z); + } + return chunk.getHeight(type, x & 15, z & 15) + 1; + } + if (methodName.equals("getBlockState")) { + BlockPos position = (BlockPos) arguments[0]; + ChunkAccess chunk = chunks.get(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4)); + if (chunk == null) { + throw new AssertionError("Block read requested an unavailable chunk at " + position); + } + return chunk.getBlockState(position); + } + if (methodName.equals("hashCode")) { + return System.identityHashCode(proxy); + } + if (methodName.equals("equals")) { + return proxy == arguments[0]; + } + if (methodName.equals("toString")) { + return "worldgen-heightmap-test-world"; + } + throw new UnsupportedOperationException(method.toString()); + }; + return (WorldGenLevel) Proxy.newProxyInstance( + WorldGenLevel.class.getClassLoader(), new Class[]{WorldGenLevel.class}, handler); + } +} diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/structure/VillageImporterListPoolElementRuntimeContractTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/structure/VillageImporterListPoolElementRuntimeContractTest.java new file mode 100644 index 000000000..4e4f08681 --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/structure/VillageImporterListPoolElementRuntimeContractTest.java @@ -0,0 +1,294 @@ +/* + * 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 . + */ + +package art.arcane.iris.structure; + +import art.arcane.iris.structure.authoring.StructureCapability; +import art.arcane.iris.structure.authoring.StructureLoss; +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class VillageImporterListPoolElementRuntimeContractTest { + @BeforeClass + public static void bootstrapMinecraft() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void pillagerOutpostTowerCompositeProducesOnePhysicalTowerChoice() throws Exception { + StructurePoolElement tower = StructurePoolElement.legacy("minecraft:pillager_outpost/watchtower") + .apply(StructureTemplatePool.Projection.RIGID); + StructurePoolElement overgrown = StructurePoolElement.legacy("minecraft:pillager_outpost/watchtower_overgrown") + .apply(StructureTemplatePool.Projection.RIGID); + ListPoolElement towers = new ListPoolElement( + List.of(tower, overgrown), + StructureTemplatePool.Projection.RIGID + ); + + VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(towers); + Map entry = VillageImporter.piecePoolEntry( + "qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower", + 1 + ); + StructureLoss loss = VillageImporter.listElementFallbackLoss( + resolution, + "minecraft:pillager_outpost/towers" + ); + Map basePlatePiece = VillageImporter.pieceJson( + "qa/pillager_outpost/piece/minecraft/pillager_outpost/base_plate", + List.of(Map.of("name", "minecraft:bottom")), + 0 + ); + Map watchtowerPiece = VillageImporter.pieceJson( + "qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower", + List.of(), + 1155 + ); + VillageImporter.PoolMemberNormalization connectorScaffold = VillageImporter.normalizePoolMember( + "minecraft:pillager_outpost/features", + "qa/pillager_outpost/pool/minecraft/pillager_outpost/features", + false, + 2, + "minecraft:empty", + "minecraft:pillager_outpost/feature_plate", + "qa/pillager_outpost/piece/minecraft/pillager_outpost/feature_plate", + 1, + 0, + List.of(Map.of("name", "minecraft:bottom"))); + + assertSame(tower, resolution.physicalElement()); + assertEquals("minecraft:pillager_outpost/watchtower", resolution.templateLocation()); + assertEquals(1, resolution.omittedElements()); + assertEquals("qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower", entry.get("piece")); + assertEquals(1, entry.get("weight")); + assertEquals(StructureCapability.LIST_ELEMENTS, loss.capability()); + assertEquals("list_pool_overlays_not_imported", loss.code()); + assertEquals(false, basePlatePiece.get("collidable")); + assertFalse(watchtowerPiece.containsKey("collidable")); + assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, connectorScaffold.disposition()); + assertTrue(connectorScaffold.losses().isEmpty()); + } + + @Test + public void actualWaystoneLikeAllAirMemberNormalizesToEmpty() throws Exception { + StructurePoolElement waystone = StructurePoolElement + .legacy("dungeons_and_taverns:waystones/waystone_scaffold") + .apply(StructureTemplatePool.Projection.RIGID); + VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(waystone); + VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember( + "dungeons_and_taverns:waystones", + "qa/pool/dungeons_and_taverns/waystones", + false, + 1, + "dungeons_and_taverns:waystones", + resolution.templateLocation(), + "qa/piece/dungeons_and_taverns/waystones/waystone_scaffold", + 2, + 0, + List.of()); + + assertSame(waystone, resolution.physicalElement()); + assertEquals("dungeons_and_taverns:waystones/waystone_scaffold", resolution.templateLocation()); + assertEquals(VillageImporter.PoolMemberDisposition.EMPTY, normalization.disposition()); + assertEquals(true, normalization.poolEntry().get("empty")); + assertEquals(2, normalization.poolEntry().get("weight")); + assertEquals("connectorless_all_air_member_normalized_empty", + normalization.losses().getFirst().code()); + } + + @Test + public void actualNonAirOrphanMemberIsOmitted() throws Exception { + StructurePoolElement orphan = StructurePoolElement + .legacy("test:orphan_house") + .apply(StructureTemplatePool.Projection.RIGID); + VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(orphan); + VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember( + "test:orphan_pool", + "qa/pool/test/orphan_pool", + false, + 1, + null, + resolution.templateLocation(), + "qa/piece/test/orphan_house", + 4, + 37, + List.of()); + + assertSame(orphan, resolution.physicalElement()); + assertEquals("test:orphan_house", resolution.templateLocation()); + assertEquals(VillageImporter.PoolMemberDisposition.OMITTED, normalization.disposition()); + assertTrue(normalization.poolEntry().isEmpty()); + assertEquals("connectorless_non_air_member_omitted", + normalization.losses().getFirst().code()); + assertTrue(normalization.losses().getFirst().detail().contains("37 non-air block(s)")); + assertTrue(normalization.losses().getFirst().detail().contains("no source fallback")); + assertTrue(normalization.losses().getFirst().detail().contains("selection weights")); + assertTrue(normalization.losses().getFirst().detail().contains("RNG consumption")); + } + + @Test + public void actualConnectorlessStartMemberRemainsPhysical() throws Exception { + StructurePoolElement start = StructurePoolElement + .legacy("test:air_start") + .apply(StructureTemplatePool.Projection.RIGID); + VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(start); + VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember( + "test:start", + "qa/pool/test/start", + true, + 2, + "test:fallback", + resolution.templateLocation(), + "qa/piece/test/air_start", + 6, + 0, + List.of()); + + assertSame(start, resolution.physicalElement()); + assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, normalization.disposition()); + assertEquals("qa/piece/test/air_start", normalization.poolEntry().get("piece")); + assertTrue(normalization.losses().isEmpty()); + } + + @Test + public void actualMixedPoolAllAirMemberIsOmittedWithoutBecomingEmpty() throws Exception { + StructurePoolElement inert = StructurePoolElement + .legacy("test:mixed_air_inert") + .apply(StructureTemplatePool.Projection.RIGID); + VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(inert); + VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember( + "test:mixed", + "qa/pool/test/mixed", + false, + 4, + "test:mixed", + resolution.templateLocation(), + "qa/piece/test/mixed_air_inert", + 9, + 0, + List.of()); + + assertSame(inert, resolution.physicalElement()); + assertEquals(VillageImporter.PoolMemberDisposition.OMITTED, normalization.disposition()); + assertTrue(normalization.poolEntry().isEmpty()); + assertEquals("connectorless_all_air_mixed_member_omitted", + normalization.losses().getFirst().code()); + assertTrue(normalization.losses().getFirst().detail().contains("selection weights")); + assertTrue(normalization.losses().getFirst().detail().contains("RNG consumption")); + } + + @Test + public void actualMixedDistinctFallbackRetainsAllConnectorlessMembers() throws Exception { + StructurePoolElement allAirElement = StructurePoolElement + .legacy("test:mixed_all_air_primary") + .apply(StructureTemplatePool.Projection.RIGID); + StructurePoolElement nonAirElement = StructurePoolElement + .legacy("test:mixed_non_air_primary") + .apply(StructureTemplatePool.Projection.RIGID); + VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(allAirElement); + VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(nonAirElement); + VillageImporter.PoolMemberNormalization allAir = VillageImporter.normalizePoolMember( + "test:mixed", + "qa/pool/test/mixed", + false, + 4, + "test:fallback", + allAirResolution.templateLocation(), + "qa/piece/test/mixed_all_air_primary", + 9, + 0, + List.of()); + VillageImporter.PoolMemberNormalization nonAir = VillageImporter.normalizePoolMember( + "test:mixed", + "qa/pool/test/mixed", + false, + 4, + "test:fallback", + nonAirResolution.templateLocation(), + "qa/piece/test/mixed_non_air_primary", + 12, + 41, + List.of()); + + assertSame(allAirElement, allAirResolution.physicalElement()); + assertSame(nonAirElement, nonAirResolution.physicalElement()); + assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, allAir.disposition()); + assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, nonAir.disposition()); + assertEquals("qa/piece/test/mixed_all_air_primary", allAir.poolEntry().get("piece")); + assertEquals("qa/piece/test/mixed_non_air_primary", nonAir.poolEntry().get("piece")); + assertTrue(allAir.losses().isEmpty()); + assertTrue(nonAir.losses().isEmpty()); + } + + @Test + public void actualSingletonDistinctFallbackRetainsAllAirAndNonAirMembers() throws Exception { + StructurePoolElement allAirElement = StructurePoolElement + .legacy("test:all_air_primary") + .apply(StructureTemplatePool.Projection.RIGID); + StructurePoolElement nonAirElement = StructurePoolElement + .legacy("test:non_air_primary") + .apply(StructureTemplatePool.Projection.RIGID); + VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(allAirElement); + VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(nonAirElement); + VillageImporter.PoolMemberNormalization allAir = VillageImporter.normalizePoolMember( + "test:primary", + "qa/pool/test/primary", + false, + 1, + "test:fallback", + allAirResolution.templateLocation(), + "qa/piece/test/all_air_primary", + 5, + 0, + List.of()); + VillageImporter.PoolMemberNormalization nonAir = VillageImporter.normalizePoolMember( + "test:primary", + "qa/pool/test/primary", + false, + 1, + "test:fallback", + nonAirResolution.templateLocation(), + "qa/piece/test/non_air_primary", + 7, + 29, + List.of()); + + assertSame(allAirElement, allAirResolution.physicalElement()); + assertSame(nonAirElement, nonAirResolution.physicalElement()); + assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, allAir.disposition()); + assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, nonAir.disposition()); + assertEquals("qa/piece/test/all_air_primary", allAir.poolEntry().get("piece")); + assertEquals("qa/piece/test/non_air_primary", nonAir.poolEntry().get("piece")); + assertTrue(allAir.losses().isEmpty()); + assertTrue(nonAir.losses().isEmpty()); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/IrisBootstrap.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/IrisBootstrap.java index be35b826f..72581c5d3 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/IrisBootstrap.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/IrisBootstrap.java @@ -8,6 +8,9 @@ import art.arcane.iris.world.lifecycle.MissingWorldStorageLog; import art.arcane.iris.world.lifecycle.WorldReplacementBootstrap; import art.arcane.iris.world.lifecycle.WorldReplacementBootstrapMarker; import art.arcane.iris.pack.DefaultPackBootstrapProvisioner; +import art.arcane.iris.pack.DefaultPackBootstrapProvisioner.BootstrapRequest; +import art.arcane.iris.platform.bukkit.nms.datapack.DataVersion; +import io.papermc.paper.ServerBuildInfo; import art.arcane.iris.pack.DefaultPackBootstrapProvisioner.ProvisionResult; import art.arcane.iris.platform.bootstrap.SlimJar; import io.papermc.paper.plugin.bootstrap.BootstrapContext; @@ -184,11 +187,12 @@ public final class IrisBootstrap implements PluginBootstrap { private static ProvisionResult provision(BootstrapContext context, BukkitStartupPaths startupPaths) { try { - return DefaultPackBootstrapProvisioner.provision( + return DefaultPackBootstrapProvisioner.provision(new BootstrapRequest( context.getDataDirectory(), message -> context.getLogger().info(message), - startupPaths - ); + startupPaths, + DataVersion.forMinecraftVersion(ServerBuildInfo.buildInfo().minecraftVersionId()) + )); } catch (IOException e) { throw new IllegalStateException("Unable to provision the Iris startup datapack", e); } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedForcedDatapack.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedForcedDatapack.java index 9d5c3487c..c43fdb63f 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedForcedDatapack.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedForcedDatapack.java @@ -343,11 +343,11 @@ public final class ModdedForcedDatapack { throw new IllegalStateException("SHA-256 is unavailable", missing); } digest.update((HASH_SALT + '|' + ModdedEngineBootstrap.loader().platformName() - + '|' + DataVersion.getLatest().getPackFormat() + '\n').getBytes(StandardCharsets.UTF_8)); + + '|' + DataVersion.V26_2.getPackFormat() + '\n').getBytes(StandardCharsets.UTF_8)); String inputFingerprint = IrisDatapackCompiler.computeInputFingerprint( compilationPackRoots(), List.of(), - DataVersion.getLatest().get(), + DataVersion.V26_2.get(), false ); digest.update(inputFingerprint.getBytes(StandardCharsets.UTF_8)); @@ -356,7 +356,7 @@ public final class ModdedForcedDatapack { private static void writeStagedPack(Path stagingDirectory) throws IOException { Map> seenBiomes = new LinkedHashMap<>(); - IDataFixer fixer = DataVersion.getLatest().get(); + IDataFixer fixer = DataVersion.V26_2.get(); int packCount = 0; KList presetIds = new KList<>(); @@ -706,8 +706,8 @@ public final class ModdedForcedDatapack { + "}\n", StandardCharsets.UTF_8); } - private static void writePackMeta(Path packDirectory) throws IOException { - int packFormat = DataVersion.getLatest().getPackFormat(); + static void writePackMeta(Path packDirectory) throws IOException { + int packFormat = DataVersion.V26_2.getPackFormat(); String json = "{\n" + " \"pack\": {\n" + " \"description\": \"Iris world generation biomes and dimension types for installed packs.\",\n" diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckDimensionContract.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckDimensionContract.java index 0342df0a0..66ee00edf 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckDimensionContract.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckDimensionContract.java @@ -55,7 +55,7 @@ final class WorldCheckDimensionContract { } static DimensionContract expectedDimensionContract(IrisDimension dimension) { - JSONObject json = new JSONObject(dimension.getDimensionType().toJson(DataVersion.getLatest().get())); + JSONObject json = new JSONObject(dimension.getDimensionType().toJson(DataVersion.V26_2.get())); return new DimensionContract( json.getInt("min_y"), json.getInt("height"), diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDatapackCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDatapackCommands.java index db97aea31..ee7dd9f9e 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDatapackCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDatapackCommands.java @@ -166,7 +166,7 @@ public final class ModdedDatapackCommands { IrisDimension dimension = engine.getDimension(); String json; try { - json = dimension.getDimensionType().toJson(DataVersion.getLatest().get()); + json = dimension.getDimensionType().toJson(DataVersion.V26_2.get()); } catch (Throwable e) { ModdedIrisLog.error("Iris dimension type generation failed for {}", dimension.getLoadKey(), e); IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_DIMENSION_TYPE_GENERATION_FAILED, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("value2", String.valueOf(e.getMessage())))); @@ -188,7 +188,7 @@ public final class ModdedDatapackCommands { } File mcmeta = new File(worldDatapacksFolder(server), WORLD_PACK_NAME + "/pack.mcmeta"); - int packFormat = DataVersion.getLatest().getPackFormat(); + int packFormat = DataVersion.V26_2.getPackFormat(); String meta = "{\n" + " \"pack\": {\n" + " \"description\": \"Iris dimension types derived from the installed Iris packs.\",\n" diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionTypeParityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionTypeParityTest.java index 825ce7f2c..cd891a307 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionTypeParityTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionTypeParityTest.java @@ -77,7 +77,7 @@ public class ModdedDimensionTypeParityTest { IrisDimension end = dimension("the_end", IrisEnvironment.THE_END, 0, 256, 256, new IrisDimensionTypeOptions()); IrisDimension custom = dimension("custom_contract", IrisEnvironment.CUSTOM, -128, 384, 384, customOptions); List dimensions = List.of(overworld, nether, end, custom); - IDataFixer fixer = DataVersion.getLatest().get(); + IDataFixer fixer = DataVersion.V26_2.get(); Path packDirectory = Files.createTempDirectory("iris-dimension-contracts"); KList roots = new KList<>(); roots.add(packDirectory.toFile()); diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedForcedDatapackTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedForcedDatapackTest.java index 266a5151c..1d69c36a3 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedForcedDatapackTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedForcedDatapackTest.java @@ -19,6 +19,7 @@ package art.arcane.iris.modded; import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.json.JSONObject; import art.arcane.iris.platform.bukkit.nms.datapack.v1217.DataFixerV1217; import art.arcane.iris.world.history.GenerationEpochContractFactory; import art.arcane.iris.world.history.GenerationRegistryContract; @@ -49,6 +50,21 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public class ModdedForcedDatapackTest { + @Test + public void metadataRetains262FormatWhenBukkitAlsoSupports263() throws IOException { + Path packDirectory = Files.createTempDirectory("iris-modded-pack-format"); + try { + ModdedForcedDatapack.writePackMeta(packDirectory); + JSONObject pack = new JSONObject(Files.readString(packDirectory.resolve("pack.mcmeta"))) + .getJSONObject("pack"); + assertEquals(107, pack.getInt("pack_format")); + assertEquals(107, pack.getInt("min_format")); + assertEquals(107, pack.getInt("max_format")); + } finally { + deleteTree(packDirectory); + } + } + @Test public void retainedDimensionSourceRendersBeforeNativeRegistryExists() throws Exception { DataFixerV1217 fixer = new DataFixerV1217(); diff --git a/build.gradle b/build.gradle index 334517085..14d52a77c 100644 --- a/build.gradle +++ b/build.gradle @@ -74,8 +74,8 @@ project(':core') { version = rootProject.version } String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse('26.2') -// The Bukkit jar runs Paper 26.1.2 through 26.2 (compile-low pins); the loader jars stay 26.2-only. -String bukkitMinecraftRange = providers.gradleProperty('bukkitMinecraftRange').getOrElse('26.1.2-26.2') +// The Bukkit jar retains 26.1.2 and 26.2 alongside 26.3; loader jars stay 26.2-only. +String bukkitMinecraftRange = providers.gradleProperty('bukkitMinecraftRange').getOrElse('26.1.2-26.3') String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse('0.19.3') String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse('26.2-65.1.1') String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse('26.2.0.59') @@ -141,6 +141,7 @@ String neoForgeConsumerPath = "${consumerLocation}/neoforge-mod-consumers/dropin def nmsBindings = [ v26_2_R1: '26.1.2.build.74-stable', + v26_3_R1: '26.3.build.25-alpha', ] Class nmsTypeClass = Class.forName('NMSBinding$Type') nmsBindings.each { key, value -> @@ -154,7 +155,9 @@ nmsBindings.each { key, value -> extensions.extraProperties.set('nms', nmsConfig) plugins.apply(NMSBinding) - sourceSets.main.java.srcDir(rootProject.file('adapters/minecraft-common/src/main/java')) + if (key == 'v26_2_R1') { + sourceSets.main.java.srcDir(rootProject.file('adapters/minecraft-common/src/main/java')) + } dependencies { compileOnly(project(':core')) @@ -619,6 +622,7 @@ List requiredBukkitArtifactEntries = [ 'art/arcane/iris/IrisBootstrap.class', 'art/arcane/iris/platform/bukkit/nms/INMS.class', 'art/arcane/iris/platform/bukkit/nms/v26_2_R1/NMSBinding.class', + 'art/arcane/iris/platform/bukkit/nms/v26_3_R1/NMSBinding.class', 'art/arcane/iris/platform/bukkit/nms/v1X/NMSBinding1X.class', 'art/arcane/iris/platform/generation/PlatformChunkGenerator.class', 'art/arcane/iris/world/lifecycle/WorldLifecycleStaging.class', @@ -856,9 +860,34 @@ pluginPackaging { prunePrefixes = ['art/arcane/volmlib/'] keepPrefixes = ['art/arcane/volmlib/util/matter/slices/'] shrinkRules = [file('packaging/shrink.pro')] + shrinkDontwarn = [ + 'java.lang.invoke.MethodHandle', + 'java.lang.invoke.VarHandle', + 'net.minecraft.server.MinecraftServer', + 'net.minecraft.server.level.ServerLevel', + 'net.minecraft.world.attribute.EnvironmentAttributes', + 'net.minecraft.world.level.NaturalSpawner', + 'net.minecraft.world.level.StructureManager', + 'net.minecraft.world.level.biome.BiomeManager', + 'net.minecraft.world.level.biome.BiomeResolver', + 'net.minecraft.world.level.biome.BiomeSource', + 'net.minecraft.world.level.biome.MobSpawnSettings', + 'net.minecraft.world.level.biome.MobSpawnSettings$SpawnerData', + 'net.minecraft.world.level.chunk.ChunkAccess', + 'net.minecraft.world.level.chunk.ChunkGenerator', + 'net.minecraft.world.level.chunk.status.ChunkStatus', + 'net.minecraft.world.level.levelgen.RandomState', + 'net.minecraft.world.level.levelgen.feature.Feature', + 'net.minecraft.world.level.levelgen.structure.Structure', + 'net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement', + 'net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement', + 'org.bukkit.craftbukkit.inventory.CraftItemStack', + ] shrinkLibraries.from(provider { project(':core').configurations.compileClasspath }) shrinkLibraries.from(provider { project(':adapters:bukkit:plugin').configurations.compileClasspath }) - shrinkLibraries.from(provider { project(':adapters:bukkit:nms:v26_2_R1').configurations.compileClasspath }) + nmsBindings.keySet().each { binding -> + shrinkLibraries.from(provider { project(":adapters:bukkit:nms:${binding}").configurations.compileClasspath }) + } shrinkRelocations = runtimeDownloadRelocations } } diff --git a/buildSrc/src/main/java/art/arcane/iris/buildtools/GenerationRevisionScope.java b/buildSrc/src/main/java/art/arcane/iris/buildtools/GenerationRevisionScope.java index 2f314bc3b..452d5d760 100644 --- a/buildSrc/src/main/java/art/arcane/iris/buildtools/GenerationRevisionScope.java +++ b/buildSrc/src/main/java/art/arcane/iris/buildtools/GenerationRevisionScope.java @@ -12,6 +12,7 @@ public final class GenerationRevisionScope { "spi/src/main/java", "adapters/bukkit/plugin/src/main/java", "adapters/bukkit/nms/v26_2_R1/src/main/java", + "adapters/bukkit/nms/v26_3_R1/src/main/java", "adapters/minecraft-common/src/main/java", "adapters/modded-common/src/main/java", "adapters/client-common/src/main/java", @@ -31,6 +32,7 @@ public final class GenerationRevisionScope { "core/src/main/java/art/arcane/iris/world/safeguard/ValueWithDiagnostics.java", "core/src/main/java/art/arcane/iris/diagnostics/splash/IrisSplashRenderer.java", "adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_2_R1/NmsWorldLifecycle.java", + "adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsWorldLifecycle.java", "adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandDeveloper.java", "adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandPregen.java", "adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandIris.java", diff --git a/core/src/main/java/art/arcane/iris/pack/DefaultPackBootstrapProvisioner.java b/core/src/main/java/art/arcane/iris/pack/DefaultPackBootstrapProvisioner.java index 0208ae2f6..1baf001c5 100644 --- a/core/src/main/java/art/arcane/iris/pack/DefaultPackBootstrapProvisioner.java +++ b/core/src/main/java/art/arcane/iris/pack/DefaultPackBootstrapProvisioner.java @@ -62,15 +62,10 @@ public final class DefaultPackBootstrapProvisioner { return DEFAULT_PACKS; } - public static ProvisionResult provision(Path dataDirectory, Consumer feedback) throws IOException { - return provision(dataDirectory, feedback, BukkitStartupPaths.resolveCurrent()); - } - - public static ProvisionResult provision( - Path dataDirectory, - Consumer feedback, - BukkitStartupPaths startupPaths - ) throws IOException { + public static ProvisionResult provision(BootstrapRequest request) throws IOException { + Path dataDirectory = request.dataDirectory(); + Consumer feedback = request.feedback(); + BukkitStartupPaths startupPaths = request.startupPaths(); Objects.requireNonNull(dataDirectory, "dataDirectory"); Objects.requireNonNull(feedback, "feedback"); BukkitStartupPaths requiredStartupPaths = Objects.requireNonNull(startupPaths, "startupPaths"); @@ -94,7 +89,8 @@ public final class DefaultPackBootstrapProvisioner { 3, Duration.ofMillis(250), MAX_ARCHIVE_BYTES, - levelRoot + levelRoot, + request.dataVersion() ); return provision(dataDirectory, feedback, options); } @@ -135,7 +131,7 @@ public final class DefaultPackBootstrapProvisioner { if (!Integer.toString(MARKER_SCHEMA).equals(marker.getProperty("schema"))) { return false; } - IDataFixer fixer = DataVersion.getLatest().get(); + IDataFixer fixer = DataVersion.getRuntime().get(); if (fixer == null || !IrisDatapackCompiler.compilerIdentity(fixer) .equals(marker.getProperty("compilerIdentity"))) { @@ -248,9 +244,9 @@ public final class DefaultPackBootstrapProvisioner { } List packRoots = IrisDatapackCompiler.collectPackRoots(normalizedData, options.levelRoot()); - IDataFixer fixer = DataVersion.getLatest().get(); + IDataFixer fixer = options.dataVersion().get(); if (fixer == null) { - throw new IOException("Latest Iris datapack fixer is unavailable during bootstrap"); + throw new IOException("Selected Iris datapack fixer is unavailable during bootstrap"); } String compilerIdentity = IrisDatapackCompiler.compilerIdentity(fixer); String aggregateFingerprint = packRootsFingerprint(packRoots); @@ -915,6 +911,16 @@ public final class DefaultPackBootstrapProvisioner { } } + public record BootstrapRequest(Path dataDirectory, Consumer feedback, + BukkitStartupPaths startupPaths, DataVersion dataVersion) { + public BootstrapRequest { + Objects.requireNonNull(dataDirectory); + Objects.requireNonNull(feedback); + Objects.requireNonNull(startupPaths); + Objects.requireNonNull(dataVersion); + } + } + record ProvisionOptions( List packs, List bindings, @@ -925,7 +931,8 @@ public final class DefaultPackBootstrapProvisioner { int attempts, Duration retryDelay, long maxArchiveBytes, - Path levelRoot + Path levelRoot, + DataVersion dataVersion ) { ProvisionOptions { packs = List.copyOf(Objects.requireNonNull(packs, "packs")); @@ -936,6 +943,10 @@ public final class DefaultPackBootstrapProvisioner { Objects.requireNonNull(requestTimeout, "requestTimeout"); Objects.requireNonNull(retryDelay, "retryDelay"); Objects.requireNonNull(levelRoot, "levelRoot"); + Objects.requireNonNull(dataVersion, "dataVersion"); + if (dataVersion == DataVersion.UNSUPPORTED) { + throw new IllegalArgumentException("Unsupported bootstrap datapack version"); + } if (attempts < 1 || maxArchiveBytes < 1L) { throw new IllegalArgumentException("Invalid bootstrap provisioning options"); } diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/INMSBinding.java b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/INMSBinding.java index 3fbd43cc4..8d04a4277 100644 --- a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/INMSBinding.java +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/INMSBinding.java @@ -321,7 +321,7 @@ public interface INMSBinding { if (!supportsIrisWorldGeneration()) { throw new IllegalStateException("Iris world '" + c.name() + "' cannot be created with limited NMS binding " + getClass().getSimpleName() - + "; set general.disableNMS=false and use a supported Minecraft server runtime (26.1.2 or 26.2)"); + + "; set general.disableNMS=false and use a supported Minecraft server runtime (26.1.2, 26.2 or 26.3)"); } if (missingDimensionTypes(generator.getTarget().getDimension().getDimensionTypeKey())) { throw new IllegalStateException("Missing dimension types to create world"); diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/NmsBindingSelector.java b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/NmsBindingSelector.java index e5508d4d3..e18e7eafa 100644 --- a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/NmsBindingSelector.java +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/NmsBindingSelector.java @@ -1,7 +1,7 @@ package art.arcane.iris.platform.bukkit.nms; final class NmsBindingSelector { - private static final String SUPPORTED_VERSIONS = "26.1.2 or 26.2"; + private static final String SUPPORTED_VERSIONS = "26.1.2, 26.2 or 26.3"; private static final String SUPPORTED_TAG = "v26_2_R1"; private NmsBindingSelector() { @@ -11,6 +11,9 @@ final class NmsBindingSelector { if (version == null) { throw new IllegalStateException("Iris requires an exact Minecraft version before selecting NMS"); } + if (version.isSameRelease(26, 3, 0)) { + return "v26_3_R1"; + } if (!version.isSameRelease(26, 2, 0) && !version.isSameRelease(26, 1, 2)) { throw new IllegalStateException("Iris requires Minecraft " + SUPPORTED_VERSIONS + ". Detected server version: " + version.value()); diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/datapack/DataVersion.java b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/datapack/DataVersion.java index 7f3a8c5ca..bcfc985f7 100644 --- a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/datapack/DataVersion.java +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/datapack/DataVersion.java @@ -1,6 +1,7 @@ package art.arcane.iris.platform.bukkit.nms.datapack; import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.platform.bukkit.nms.INMS; import art.arcane.iris.platform.bukkit.nms.datapack.v1217.DataFixerV1217; import art.arcane.volmlib.util.collection.KMap; @@ -8,13 +9,15 @@ import lombok.AccessLevel; import lombok.Getter; import java.util.function.Supplier; +import art.arcane.iris.platform.bukkit.nms.datapack.v263.DataFixerV263; //https://minecraft.wiki/w/Pack_format @Getter public enum DataVersion { UNSUPPORTED("0.0.0", 0, () -> null), V26_1_2("26.1.2", 101, DataFixerV1217::new), - V26_2("26.2", 107, DataFixerV1217::new); + V26_2("26.2", 107, DataFixerV1217::new), + V26_3("26.3", 121, DataFixerV263::new); private static final KMap cache = new KMap<>(); @Getter(AccessLevel.NONE) private final Supplier constructor; @@ -49,6 +52,29 @@ public enum DataVersion { return fixer; } + public static DataVersion forMinecraftVersion(String version) { + if (version == null) { + return UNSUPPORTED; + } + return switch (version) { + case "26.1.2" -> V26_1_2; + case "26.2", "26.2.0" -> V26_2; + case "26.3", "26.3.0" -> V26_3; + default -> UNSUPPORTED; + }; + } + + public static DataVersion getRuntime() { + if (!IrisPlatforms.isBound() || IrisPlatforms.get().minecraftVersion() == null) { + return getLatest(); + } + DataVersion version = forMinecraftVersion(IrisPlatforms.get().minecraftVersion()); + if (version == UNSUPPORTED) { + throw new IllegalStateException("Unsupported Minecraft datapack version: " + IrisPlatforms.get().minecraftVersion()); + } + return version; + } + public static DataVersion getLatest() { return values()[values().length - 1]; } diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/datapack/v263/DataFixerV263.java b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/datapack/v263/DataFixerV263.java new file mode 100644 index 000000000..77512dda0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/datapack/v263/DataFixerV263.java @@ -0,0 +1,54 @@ +package art.arcane.iris.platform.bukkit.nms.datapack.v263; + +import art.arcane.iris.generation.biome.IrisBiomeCustom; +import art.arcane.iris.platform.bukkit.nms.datapack.v1217.DataFixerV1217; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.util.Locale; + +public final class DataFixerV263 extends DataFixerV1217 { + @Override + public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) { + JSONObject fixed = super.fixCustomBiome(biome, json); + JSONObject attributes = fixed.getJSONObject("attributes"); + JSONObject effects = fixed.getJSONObject("effects"); + for (String key : new String[]{"water_color", "foliage_color", "dry_foliage_color", "grass_color"}) { + color(effects, key); + } + for (String key : new String[]{"minecraft:visual/sky_color", "minecraft:visual/fog_color", "minecraft:visual/water_fog_color"}) { + color(attributes, key); + } + JSONObject spawners = fixed.getJSONObject("spawners"); + for (String category : spawners.keySet()) { + JSONArray entries = spawners.getJSONArray(category); + for (int index = 0; index < entries.length(); index++) { + JSONObject spawn = entries.getJSONObject(index); + int minimum = spawn.getInt("minCount"); + int maximum = spawn.getInt("maxCount"); + spawn.remove("minCount"); + spawn.remove("maxCount"); + spawn.put("count", minimum == maximum ? minimum : new JSONObject() + .put("type", "minecraft:uniform") + .put("min_inclusive", minimum) + .put("max_inclusive", maximum)); + } + } + attributes.put("minecraft:gameplay/natural_mob_spawns", new JSONObject() + .put("spawns_by_category", spawners) + .put("spawn_costs", fixed.getJSONObject("spawn_costs"))); + Object probability = fixed.remove("creature_spawn_probability"); + if (probability != null) { + attributes.put("minecraft:gameplay/creature_world_gen_spawn_probability", probability); + } + fixed.remove("spawners"); + fixed.remove("spawn_costs"); + return fixed; + } + + private static void color(JSONObject object, String key) { + if (object.opt(key) instanceof Number value) { + object.put(key, String.format(Locale.ROOT, "#%06x", value.intValue() & 0xFFFFFF)); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/v1X/NMSBinding1X.java b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/v1X/NMSBinding1X.java index 8280eb9d4..4ca6ab3e5 100644 --- a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/v1X/NMSBinding1X.java +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/v1X/NMSBinding1X.java @@ -317,6 +317,6 @@ public class NMSBinding1X implements INMSBinding { private IllegalStateException unsupportedStructureHook(String operation) { return new IllegalStateException("Iris cannot " + operation + " with limited NMS binding " + getClass().getSimpleName() - + "; set general.disableNMS=false and use a supported Minecraft server runtime (26.1.2 or 26.2)"); + + "; set general.disableNMS=false and use a supported Minecraft server runtime (26.1.2, 26.2 or 26.3)"); } } diff --git a/core/src/main/java/art/arcane/iris/structure/export/VanillaBlockState.java b/core/src/main/java/art/arcane/iris/structure/export/VanillaBlockState.java index 28da3ff49..321eb1f67 100644 --- a/core/src/main/java/art/arcane/iris/structure/export/VanillaBlockState.java +++ b/core/src/main/java/art/arcane/iris/structure/export/VanillaBlockState.java @@ -1,6 +1,7 @@ package art.arcane.iris.structure.export; import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport; import java.util.Collections; import java.util.LinkedHashMap; @@ -73,15 +74,15 @@ record VanillaBlockState(String name, Map properties) { return builder.append(']').toString(); } - CompoundTag toNbt() { + CompoundTag toNbt(MCABlockStateCodecSupport.Format format) { CompoundTag state = new CompoundTag(); - state.putString("Name", name); + state.putString(format.nameKey(), name); if (!properties.isEmpty()) { CompoundTag propertyTag = new CompoundTag(); for (Map.Entry property : properties.entrySet()) { propertyTag.putString(property.getKey(), property.getValue()); } - state.put("Properties", propertyTag); + state.put(format.propertiesKey(), propertyTag); } return state; } diff --git a/core/src/main/java/art/arcane/iris/structure/export/VanillaJigsawExportCompiler.java b/core/src/main/java/art/arcane/iris/structure/export/VanillaJigsawExportCompiler.java index ac79463ec..aa80f5bff 100644 --- a/core/src/main/java/art/arcane/iris/structure/export/VanillaJigsawExportCompiler.java +++ b/core/src/main/java/art/arcane/iris/structure/export/VanillaJigsawExportCompiler.java @@ -211,7 +211,7 @@ final class VanillaJigsawExportCompiler { addError(diagnostics, VanillaJigsawExportDiagnostic.Code.INVALID_MAX_DEPTH, structureKey(structure), - "Minecraft 26.2 jigsaw size must be within 0..20; Iris export requires 1..20."); + "Minecraft jigsaw size must be within 0..20; Iris export requires 1..20."); } long horizontalDistance = (long) structure.getMaxSizeChunks() * 16L; if (horizontalDistance < 1L || horizontalDistance > MAX_HORIZONTAL_DISTANCE) { @@ -219,7 +219,7 @@ final class VanillaJigsawExportCompiler { VanillaJigsawExportDiagnostic.Code.INVALID_MAX_DISTANCE, structureKey(structure), "Iris maxSizeChunks maps to " + horizontalDistance - + " blocks, outside Minecraft 26.2's 1..128 horizontal limit."); + + " blocks, outside Minecraft's 1..128 horizontal limit."); } int terrainPadding = settings.terrainAdaptation() == VanillaJigsawExportSettings.TerrainAdaptation.NONE ? 0 : 12; @@ -315,7 +315,7 @@ final class VanillaJigsawExportCompiler { addError(diagnostics, VanillaJigsawExportDiagnostic.Code.INVALID_POOL_WEIGHT, poolKey + "/pieces[" + index + "]", - "Minecraft 26.2 template-pool weights must be within 1..150."); + "Minecraft template-pool weights must be within 1..150."); } if (entry.getChance() != 1D) { addError(diagnostics, @@ -577,10 +577,12 @@ final class VanillaJigsawExportCompiler { JsonObject pack = new JsonObject(); pack.addProperty("description", request.description()); JsonArray minimum = new JsonArray(); - minimum.add(107); - minimum.add(1); + boolean current = VanillaStructureTemplateEncoder.targets263(); + int packFormat = current ? 121 : 107; + minimum.add(packFormat); + minimum.add(current ? 0 : 1); pack.add("min_format", minimum); - pack.addProperty("max_format", 107); + pack.addProperty("max_format", packFormat); JsonObject root = new JsonObject(); root.add("pack", pack); return root; diff --git a/core/src/main/java/art/arcane/iris/structure/export/VanillaJigsawExportRequest.java b/core/src/main/java/art/arcane/iris/structure/export/VanillaJigsawExportRequest.java index 912c0d032..0c93101c4 100644 --- a/core/src/main/java/art/arcane/iris/structure/export/VanillaJigsawExportRequest.java +++ b/core/src/main/java/art/arcane/iris/structure/export/VanillaJigsawExportRequest.java @@ -65,7 +65,7 @@ public final class VanillaJigsawExportRequest { private final Path output; private String namespace = "iris"; private String resourcePath; - private String description = "Iris vanilla jigsaw export for Minecraft 26.2"; + private String description = "Iris vanilla jigsaw export"; private VanillaJigsawExportFormat format = VanillaJigsawExportFormat.DIRECTORY; private VanillaJigsawExportSettings settings = VanillaJigsawExportSettings.defaults(); private boolean replaceExisting; diff --git a/core/src/main/java/art/arcane/iris/structure/export/VanillaStructureTemplateEncoder.java b/core/src/main/java/art/arcane/iris/structure/export/VanillaStructureTemplateEncoder.java index 8b8b6f655..dde8b69eb 100644 --- a/core/src/main/java/art/arcane/iris/structure/export/VanillaStructureTemplateEncoder.java +++ b/core/src/main/java/art/arcane/iris/structure/export/VanillaStructureTemplateEncoder.java @@ -1,6 +1,8 @@ package art.arcane.iris.structure.export; import art.arcane.iris.pack.value.IrisDirection; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport; import art.arcane.iris.structure.jigsaw.IrisJigsawConnector; import art.arcane.iris.structure.jigsaw.IrisJigsawPiece; import art.arcane.iris.structure.object.IrisObject; @@ -51,18 +53,26 @@ final class VanillaStructureTemplateEncoder { } } + boolean lowercase = targets263(); + MCABlockStateCodecSupport.Format format = lowercase + ? MCABlockStateCodecSupport.Format.LOWERCASE : MCABlockStateCodecSupport.Format.CAPITALIZED; CompoundTag root = new CompoundTag(); root.put("size", intList(object.getW(), object.getH(), object.getD())); - root.put("palette", palette(paletteStates)); + root.put("palette", palette(paletteStates, format)); root.put("blocks", blocks(orderedBlocks, paletteIndexes)); root.put("entities", new ListTag<>(CompoundTag.class)); - root.putInt("DataVersion", DATA_VERSION_26_2); + root.putInt("DataVersion", lowercase ? 5023 : DATA_VERSION_26_2); ByteArrayOutputStream output = new ByteArrayOutputStream(); NBTUtil.write(new NamedTag("", root), output, true); return output.toByteArray(); } + static boolean targets263() { + String minecraftVersion = IrisPlatforms.get().minecraftVersion(); + return "26.3".equals(minecraftVersion) || "26.3.0".equals(minecraftVersion); + } + private Map objectBlocks(IrisObject object) { Map blocks = new LinkedHashMap<>(); Vector3i center = object.getCenter(); @@ -135,10 +145,10 @@ final class VanillaStructureTemplateEncoder { }; } - private ListTag palette(List states) { + private ListTag palette(List states, MCABlockStateCodecSupport.Format format) { ListTag palette = new ListTag<>(CompoundTag.class); for (VanillaBlockState state : states) { - palette.add(state.toNbt()); + palette.add(state.toNbt(format)); } return palette; } diff --git a/core/src/main/java/art/arcane/iris/world/history/GenerationRegistryContractFactory.java b/core/src/main/java/art/arcane/iris/world/history/GenerationRegistryContractFactory.java index a8c4e7067..2f5368a57 100644 --- a/core/src/main/java/art/arcane/iris/world/history/GenerationRegistryContractFactory.java +++ b/core/src/main/java/art/arcane/iris/world/history/GenerationRegistryContractFactory.java @@ -110,7 +110,7 @@ public final class GenerationRegistryContractFactory { data, dimension, packFingerprint, - DataVersion.getLatest().get(), + DataVersion.getRuntime().get(), registries, registries.generationRegistry(), aliasPolicy @@ -224,7 +224,7 @@ public final class GenerationRegistryContractFactory { data, dimension, customBiome, - DataVersion.getLatest().get(), + DataVersion.getRuntime().get(), registry ); } @@ -508,7 +508,7 @@ public final class GenerationRegistryContractFactory { ) throws IOException { return captureRequiredDefinitions( requiredContracts, - DataVersion.getLatest().get(), + DataVersion.getRuntime().get(), IrisPlatforms.get().registries().generationRegistry() ); } @@ -519,7 +519,7 @@ public final class GenerationRegistryContractFactory { ) throws IOException { return captureRequiredDefinitions( requiredContracts, - DataVersion.getLatest().get(), + DataVersion.getRuntime().get(), generationRegistry ); } @@ -603,7 +603,7 @@ public final class GenerationRegistryContractFactory { return requireGeneratedSource( contract, key, - DataVersion.getLatest().get(), + DataVersion.getRuntime().get(), IrisPlatforms.get().registries().generationRegistry() ); } @@ -652,7 +652,7 @@ public final class GenerationRegistryContractFactory { return requireGeneratedSource( contract, key, - DataVersion.getLatest().get(), + DataVersion.getRuntime().get(), generationRegistry ); } diff --git a/core/src/main/java/art/arcane/iris/world/history/SavedTerrainChunk.java b/core/src/main/java/art/arcane/iris/world/history/SavedTerrainChunk.java index 46599f837..81bdcc83e 100644 --- a/core/src/main/java/art/arcane/iris/world/history/SavedTerrainChunk.java +++ b/core/src/main/java/art/arcane/iris/world/history/SavedTerrainChunk.java @@ -171,7 +171,7 @@ public final class SavedTerrainChunk { case "biomes" -> 3; case "noise" -> 4; case "surface" -> 5; - case "carvers" -> 6; + case "carvers", "terrain" -> 6; case "features" -> 7; case "initialize_light" -> 8; case "light" -> 9; diff --git a/core/src/main/java/art/arcane/iris/world/history/SavedTerrainChunkReader.java b/core/src/main/java/art/arcane/iris/world/history/SavedTerrainChunkReader.java index db0b7b9d6..23a593643 100644 --- a/core/src/main/java/art/arcane/iris/world/history/SavedTerrainChunkReader.java +++ b/core/src/main/java/art/arcane/iris/world/history/SavedTerrainChunkReader.java @@ -1,6 +1,9 @@ package art.arcane.iris.world.history; import art.arcane.volmlib.util.nbt.io.NBTUtil; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport; import art.arcane.volmlib.util.nbt.tag.CompoundTag; import art.arcane.volmlib.util.nbt.tag.ByteArrayTag; import art.arcane.volmlib.util.nbt.tag.ListTag; @@ -81,7 +84,8 @@ final class SavedTerrainChunkReader { if (sectionY * 16 < minimumY || sectionY * 16 >= (long) minimumY + height) { continue; } - if (sections.put(sectionY, decodeSection(section)) != null) { + if (sections.put(sectionY, decodeSection(section, root.getInt("DataVersion") >= 5023 + ? MCABlockStateCodecSupport.Format.LOWERCASE : MCABlockStateCodecSupport.Format.CAPITALIZED)) != null) { throw new IOException("Saved chunk contains duplicate section " + sectionY); } } @@ -158,16 +162,29 @@ final class SavedTerrainChunkReader { return section; } - private static Section decodeSection(CompoundTag section) throws IOException { + private static Section decodeSection(CompoundTag section, MCABlockStateCodecSupport.Format format) throws IOException { CompoundTag blocks = compound(section, "block_states"); CompoundTag biomes = compound(section, "biomes"); ListTag blockPalette = list(blocks, "palette"); List decodedBlocks = new ArrayList<>(blockPalette.size()); for (Tag entry : blockPalette) { - if (!(entry instanceof CompoundTag state)) { - throw new IOException("Saved block palette entry is not a compound"); + Tag state = entry; + if (format == MCABlockStateCodecSupport.Format.LOWERCASE + && state instanceof CompoundTag wrapped && wrapped.size() == 1 && wrapped.containsKey("")) { + state = wrapped.get(""); + } + if (format == MCABlockStateCodecSupport.Format.LOWERCASE && state instanceof StringTag name) { + PlatformBlockState resolved = IrisPlatforms.get().registries().blockOrNull(name.getValue()); + if (resolved == null) { + throw new IOException("Saved block state cannot be resolved: " + name.getValue()); + } + decodedBlocks.add(decodeBlock(MCABlockStateCodecSupport.encodeBlockState( + resolved.key(), name.getValue(), format), format)); + } else if (state instanceof CompoundTag compound) { + decodedBlocks.add(decodeBlock(compound, format)); + } else { + throw new IOException("Saved block palette entry is not a block state"); } - decodedBlocks.add(decodeBlock(state)); } ListTag biomePalette = list(biomes, "palette"); List decodedBiomes = new ArrayList<>(biomePalette.size()); @@ -181,10 +198,10 @@ final class SavedTerrainChunkReader { new Palette<>(decodedBiomes, packedData(biomes), 1, 64)); } - private static BoundaryColumnGeometry.Voxel decodeBlock(CompoundTag state) throws IOException { - String name = string(state, "Name"); + private static BoundaryColumnGeometry.Voxel decodeBlock(CompoundTag state, MCABlockStateCodecSupport.Format format) throws IOException { + String name = string(state, format.nameKey()); TreeMap properties = new TreeMap<>(); - if (state.get("Properties") instanceof CompoundTag values) { + if (state.get(format.propertiesKey()) instanceof CompoundTag values) { for (Map.Entry> property : values) { if (!(property.getValue() instanceof StringTag value)) { throw new IOException("Saved block property is not a string"); @@ -192,6 +209,13 @@ final class SavedTerrainChunkReader { properties.put(property.getKey(), value.getValue()); } } + return decodeBlock(name, properties); + } + + private static BoundaryColumnGeometry.Voxel decodeBlock(String name, Map properties) throws IOException { + if (name.isBlank()) { + throw new IOException("Saved block state has no resource key"); + } String stateKey = stateKey(name, properties); if (name.equals("minecraft:air") || name.equals("minecraft:cave_air") || name.equals("minecraft:void_air")) { return name.equals("minecraft:air") ? AIR diff --git a/core/src/main/java/art/arcane/iris/world/storage/region/NBTWorld.java b/core/src/main/java/art/arcane/iris/world/storage/region/NBTWorld.java index eefd37da6..a114d8544 100644 --- a/core/src/main/java/art/arcane/iris/world/storage/region/NBTWorld.java +++ b/core/src/main/java/art/arcane/iris/world/storage/region/NBTWorld.java @@ -19,6 +19,8 @@ package art.arcane.iris.world.storage.region; import art.arcane.iris.platform.bukkit.BukkitBlockResolution; +import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport; +import art.arcane.iris.platform.bukkit.nms.datapack.DataVersion; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.platform.bukkit.nms.INMS; @@ -30,6 +32,7 @@ import art.arcane.volmlib.util.nbt.mca.NBTWorldSupport; import art.arcane.iris.platform.reflect.KeyedType; import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import art.arcane.volmlib.util.nbt.tag.Tag; import art.arcane.iris.generation.concurrent.HyperLock; import org.bukkit.NamespacedKey; import org.bukkit.Registry; @@ -43,7 +46,7 @@ import java.util.Locale; public class NBTWorld { private static final class Holder { private static final BlockData AIR = BukkitBlockResolution.get("AIR"); - private static final NBTWorldSupport.BlockStateCodec BLOCK_STATE_CODEC = NBTWorldSupport.blockStateCodec( + private static final NBTWorldSupport.BlockStateCodec BLOCK_STATE_CODEC = NBTWorldSupport.blockStateCodec(new NBTWorldSupport.BlockStateCodecOptions<>( blockStateString -> BukkitBlockResolution.getOrNull(blockStateString, true), BukkitBlockResolution::getAir, blockData -> blockData.getAsString(true), @@ -53,8 +56,11 @@ public class NBTWorld { return "minecraft:" + blockData.getMaterial().name().toLowerCase(Locale.ROOT); } return key.getNamespace() + ":" + key.getKey(); - } - ); + }, + INMS.get().getDataVersion() == DataVersion.V26_3 + ? MCABlockStateCodecSupport.Format.LOWERCASE + : MCABlockStateCodecSupport.Format.CAPITALIZED + )); private static final Map BIOME_IDS = computeBiomeIDs(); } @@ -127,7 +133,7 @@ public class NBTWorld { ); } - public static BlockData getBlockData(CompoundTag tag) { + public static BlockData getBlockData(Tag tag) { return Holder.BLOCK_STATE_CODEC.decode(tag); } diff --git a/core/src/test/java/art/arcane/iris/pack/DefaultPackBootstrapProvisionerTest.java b/core/src/test/java/art/arcane/iris/pack/DefaultPackBootstrapProvisionerTest.java index 898e5be3f..cd83d273e 100644 --- a/core/src/test/java/art/arcane/iris/pack/DefaultPackBootstrapProvisionerTest.java +++ b/core/src/test/java/art/arcane/iris/pack/DefaultPackBootstrapProvisionerTest.java @@ -1,5 +1,7 @@ package art.arcane.iris.pack; +import art.arcane.iris.platform.bukkit.nms.datapack.DataVersion; + import art.arcane.iris.world.lifecycle.BukkitStartupPaths; import art.arcane.iris.world.history.GenerationRegistryContractFactory; import art.arcane.iris.generation.biome.IrisBiomeCustom; @@ -49,6 +51,28 @@ public class DefaultPackBootstrapProvisionerTest { assertTrue(DefaultPackBootstrapProvisioner.defaultPacks().isEmpty()); } + @Test + public void bootstrapUsesTheSelectedRuntimeBiomeSchema() throws Exception { + Path root = Files.createTempDirectory("iris-bootstrap-runtime"); + try { + Path dataDirectory = root.resolve("plugins/Iris"); + writePack(dataDirectory.resolve("packs/overworld"), "overworld", "runtime"); + BukkitStartupPaths startupPaths = BukkitStartupPaths.resolve(root, new String[0]); + for (DataVersion version : new DataVersion[]{DataVersion.V26_1_2, DataVersion.V26_2, DataVersion.V26_3}) { + DefaultPackBootstrapProvisioner.ProvisionResult result = DefaultPackBootstrapProvisioner.provision( + new DefaultPackBootstrapProvisioner.BootstrapRequest(dataDirectory, ignored -> {}, startupPaths, version)); + Path biomeRoot = result.datapackRoot().resolve("data/iris/worldgen/biome/biomes"); + try (Stream paths = Files.list(biomeRoot)) { + String json = Files.readString(paths.findFirst().orElseThrow()); + assertEquals(version != DataVersion.V26_3, json.contains("\"spawners\"")); + assertEquals(version == DataVersion.V26_3, json.contains("minecraft:gameplay/natural_mob_spawns")); + } + } + } finally { + delete(root); + } + } + @Test public void emptyInstallPublishesValidDatapackWithoutNetworkRequests() throws Exception { AtomicInteger requests = new AtomicInteger(); @@ -66,7 +90,8 @@ public class DefaultPackBootstrapProvisionerTest { 1, Duration.ZERO, 8L * 1024L * 1024L, - root + root, + DataVersion.getLatest() ); DefaultPackBootstrapProvisioner.ProvisionResult result = DefaultPackBootstrapProvisioner.provision( @@ -648,8 +673,8 @@ public class DefaultPackBootstrapProvisionerTest { BukkitStartupPaths startupPaths = BukkitStartupPaths.resolve(serverRoot, new String[0]); DefaultPackBootstrapProvisioner.ProvisionResult installed = - DefaultPackBootstrapProvisioner.provision(dataDirectory, ignored -> { - }, startupPaths); + DefaultPackBootstrapProvisioner.provision(new DefaultPackBootstrapProvisioner.BootstrapRequest(dataDirectory, ignored -> { + }, startupPaths, DataVersion.getLatest())); Path levelStem = installed.datapackRoot().resolve("data/iris/dimension/moon.json"); assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.INSTALLED, installed.status()); @@ -661,8 +686,8 @@ public class DefaultPackBootstrapProvisionerTest { StandardCharsets.UTF_8 ); DefaultPackBootstrapProvisioner.ProvisionResult updated = - DefaultPackBootstrapProvisioner.provision(dataDirectory, ignored -> { - }, startupPaths); + DefaultPackBootstrapProvisioner.provision(new DefaultPackBootstrapProvisioner.BootstrapRequest(dataDirectory, ignored -> { + }, startupPaths, DataVersion.getLatest())); assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.UPDATED, updated.status()); assertTrue(Files.readString(levelStem).contains("\"type\": \"iris:underworld\"")); @@ -709,7 +734,8 @@ public class DefaultPackBootstrapProvisionerTest { 1, Duration.ZERO, 8L * 1024L * 1024L, - serverRoot + serverRoot, + DataVersion.getLatest() ); } diff --git a/core/src/test/java/art/arcane/iris/pack/datapack/IrisDatapackCompilerTest.java b/core/src/test/java/art/arcane/iris/pack/datapack/IrisDatapackCompilerTest.java index 2be0a554e..de229cdd3 100644 --- a/core/src/test/java/art/arcane/iris/pack/datapack/IrisDatapackCompilerTest.java +++ b/core/src/test/java/art/arcane/iris/pack/datapack/IrisDatapackCompilerTest.java @@ -170,14 +170,16 @@ public class IrisDatapackCompilerTest { int maxFormat = pack.getInt("max_format"); assertEquals(101, minFormat); - assertEquals(107, maxFormat); + assertEquals(121, maxFormat); assertEquals(maxFormat, pack.getInt("pack_format")); - // One artifact serves both runtimes: each supported DataVersion's format must be in range. + // One artifact serves supported runtimes: each supported DataVersion's format must be in range. assertTrue(minFormat <= DataVersion.V26_1_2.getPackFormat() && DataVersion.V26_1_2.getPackFormat() <= maxFormat); assertTrue(minFormat <= DataVersion.V26_2.getPackFormat() && DataVersion.V26_2.getPackFormat() <= maxFormat); + assertTrue(minFormat <= DataVersion.V26_3.getPackFormat() + && DataVersion.V26_3.getPackFormat() <= maxFormat); } @Test @@ -496,14 +498,14 @@ public class IrisDatapackCompilerTest { Path dataDirectory = server.resolve("plugins/Iris"); BukkitStartupPaths startupPaths = BukkitStartupPaths.resolve(server, new String[0]); DefaultPackBootstrapProvisioner.ProvisionResult installed = - DefaultPackBootstrapProvisioner.provision(dataDirectory, ignored -> { - }, startupPaths); + DefaultPackBootstrapProvisioner.provision(new DefaultPackBootstrapProvisioner.BootstrapRequest(dataDirectory, ignored -> { + }, startupPaths, DataVersion.getLatest())); assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.INSTALLED, installed.status()); assertTrue(Files.isRegularFile(installed.datapackRoot().resolve("data/iris/dimension_type/upper.json"))); assertEquals(2, regularFileCount(installed.datapackRoot().resolve("data/iris/worldgen/biome/biomes"))); DefaultPackBootstrapProvisioner.ProvisionResult unchanged = - DefaultPackBootstrapProvisioner.provision(dataDirectory, ignored -> { - }, startupPaths); + DefaultPackBootstrapProvisioner.provision(new DefaultPackBootstrapProvisioner.BootstrapRequest(dataDirectory, ignored -> { + }, startupPaths, DataVersion.getLatest())); assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.UNCHANGED, unchanged.status()); assertFalse(IrisPlatforms.isBound()); } diff --git a/core/src/test/java/art/arcane/iris/platform/bukkit/nms/NmsBindingSelectorTest.java b/core/src/test/java/art/arcane/iris/platform/bukkit/nms/NmsBindingSelectorTest.java index c5fee3945..b1fa41857 100644 --- a/core/src/test/java/art/arcane/iris/platform/bukkit/nms/NmsBindingSelectorTest.java +++ b/core/src/test/java/art/arcane/iris/platform/bukkit/nms/NmsBindingSelectorTest.java @@ -7,6 +7,12 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public class NmsBindingSelectorTest { + @Test + public void selects263Revision() { + assertEquals("v26_3_R1", NmsBindingSelector.select(MinecraftVersion.fromBukkitVersion("26.3-R0.1-SNAPSHOT"))); + assertEquals("v26_3_R1", NmsBindingSelector.select(MinecraftVersion.fromBukkitVersion("26.3.0-R0.1-SNAPSHOT"))); + } + @Test public void selectsOnlyTheExactSupportedRevision() { MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.2-R0.1-SNAPSHOT"); @@ -23,12 +29,12 @@ public class NmsBindingSelectorTest { @Test public void rejectsUnsupportedVersionsWithoutProbingAnotherRevision() { - MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.3-R0.1-SNAPSHOT"); + MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.4-R0.1-SNAPSHOT"); IllegalStateException failure = assertThrows(IllegalStateException.class, () -> NmsBindingSelector.select(version)); - assertTrue(failure.getMessage().contains("26.3")); + assertTrue(failure.getMessage().contains("26.4")); } @Test diff --git a/core/src/test/java/art/arcane/iris/platform/bukkit/nms/datapack/DataVersionTest.java b/core/src/test/java/art/arcane/iris/platform/bukkit/nms/datapack/DataVersionTest.java index f036c6224..06ba0b463 100644 --- a/core/src/test/java/art/arcane/iris/platform/bukkit/nms/datapack/DataVersionTest.java +++ b/core/src/test/java/art/arcane/iris/platform/bukkit/nms/datapack/DataVersionTest.java @@ -7,15 +7,26 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; public class DataVersionTest { + @Test + public void runtimeSelectionPreservesEverySupportedVersion() { + assertSame(DataVersion.V26_1_2, DataVersion.forMinecraftVersion("26.1.2")); + assertSame(DataVersion.V26_2, DataVersion.forMinecraftVersion("26.2")); + assertSame(DataVersion.V26_2, DataVersion.forMinecraftVersion("26.2.0")); + assertSame(DataVersion.V26_3, DataVersion.forMinecraftVersion("26.3")); + assertSame(DataVersion.V26_3, DataVersion.forMinecraftVersion("26.3.0")); + assertSame(DataVersion.UNSUPPORTED, DataVersion.forMinecraftVersion("26.4")); + } + @Test public void latestRemainsNewestSupportedRelease() { - assertSame(DataVersion.V26_2, DataVersion.getLatest()); + assertSame(DataVersion.V26_3, DataVersion.getLatest()); } @Test public void packFormatsMatchServerVersionJson() { assertEquals(101, DataVersion.V26_1_2.getPackFormat()); assertEquals(107, DataVersion.V26_2.getPackFormat()); + assertEquals(121, DataVersion.V26_3.getPackFormat()); } @Test diff --git a/core/src/test/java/art/arcane/iris/structure/VillageImporterConnectorRoundTripTest.java b/core/src/test/java/art/arcane/iris/structure/VillageImporterConnectorRoundTripTest.java index f7a03a1fe..97a2eb607 100644 --- a/core/src/test/java/art/arcane/iris/structure/VillageImporterConnectorRoundTripTest.java +++ b/core/src/test/java/art/arcane/iris/structure/VillageImporterConnectorRoundTripTest.java @@ -15,6 +15,10 @@ import art.arcane.iris.structure.object.IrisObject; import art.arcane.iris.structure.placement.IrisStructure; import art.arcane.iris.structure.object.ObjectPlaceMode; import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import org.junit.Before; +import org.junit.After; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.nbt.io.NBTUtil; import art.arcane.volmlib.util.nbt.tag.CompoundTag; @@ -41,6 +45,19 @@ public class VillageImporterConnectorRoundTripTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Before + public void bindPlatform() { + IrisPlatforms.unbind(); + IrisPlatform platform = mock(IrisPlatform.class); + when(platform.minecraftVersion()).thenReturn("26.2"); + IrisPlatforms.bind(platform); + } + + @After + public void unbindPlatform() { + IrisPlatforms.unbind(); + } + @Test public void vanillaNbtMetadataSurvivesIrisPieceAndVanillaExport() throws Exception { FakeJigsawBlockInfo jigsaw = new FakeJigsawBlockInfo(-29, -17); diff --git a/core/src/test/java/art/arcane/iris/structure/export/VanillaBlockStateTest.java b/core/src/test/java/art/arcane/iris/structure/export/VanillaBlockStateTest.java index 0581e43b4..d8776bb09 100644 --- a/core/src/test/java/art/arcane/iris/structure/export/VanillaBlockStateTest.java +++ b/core/src/test/java/art/arcane/iris/structure/export/VanillaBlockStateTest.java @@ -1,6 +1,7 @@ package art.arcane.iris.structure.export; import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -9,6 +10,16 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public class VanillaBlockStateTest { + @Test + public void writes263NamesWithoutChangingTheOlderFormat() { + CompoundTag state = VanillaBlockState.parse("minecraft:oak_stairs[facing=north]") + .toNbt(MCABlockStateCodecSupport.Format.LOWERCASE); + assertEquals("minecraft:oak_stairs", state.getString("id")); + assertEquals("north", state.getCompoundTag("properties").getString("facing")); + assertNull(state.get("Name")); + assertNull(state.get("Properties")); + } + @Test public void aBareBlockKeepsItsNameAndNoProperties() { VanillaBlockState state = VanillaBlockState.parse(" minecraft:stone "); @@ -35,8 +46,8 @@ public class VanillaBlockStateTest { @Test public void theNbtFormCarriesTheNameAndOnlyRealProperties() { - CompoundTag bare = VanillaBlockState.parse("minecraft:air").toNbt(); - CompoundTag stairs = VanillaBlockState.parse("minecraft:oak_stairs[facing=north]").toNbt(); + CompoundTag bare = VanillaBlockState.parse("minecraft:air").toNbt(MCABlockStateCodecSupport.Format.CAPITALIZED); + CompoundTag stairs = VanillaBlockState.parse("minecraft:oak_stairs[facing=north]").toNbt(MCABlockStateCodecSupport.Format.CAPITALIZED); assertEquals("minecraft:air", bare.getString("Name")); assertNull(bare.get("Properties")); diff --git a/core/src/test/java/art/arcane/iris/structure/export/VanillaJigsawDatapackExporterTest.java b/core/src/test/java/art/arcane/iris/structure/export/VanillaJigsawDatapackExporterTest.java index b1bdd6ef2..a993073d6 100644 --- a/core/src/test/java/art/arcane/iris/structure/export/VanillaJigsawDatapackExporterTest.java +++ b/core/src/test/java/art/arcane/iris/structure/export/VanillaJigsawDatapackExporterTest.java @@ -71,6 +71,7 @@ public class VanillaJigsawDatapackExporterTest { when(registries.block(anyString())).thenReturn(stone); IrisPlatform platform = mock(IrisPlatform.class); when(platform.registries()).thenReturn(registries); + when(platform.minecraftVersion()).thenReturn("26.2"); IrisPlatforms.bind(platform); } @@ -141,6 +142,30 @@ public class VanillaJigsawDatapackExporterTest { assertEquals("north_up", paletteEntry.getCompoundTag("Properties").getString("orientation")); } + @Test + public void exports26_3RuntimeBlockStatesAndPackFormat() throws Exception { + when(IrisPlatforms.get().minecraftVersion()).thenReturn("26.3.0"); + try { + Path output = temporaryFolder.getRoot().toPath().resolve("village-263"); + VanillaJigsawExportResult result = new VanillaJigsawDatapackExporter().export( + request(connectorGraph(), output).namespace("studio").resourcePath("village/test").build()); + assertTrue(result.diagnostics().toString(), result.isSuccess()); + JsonObject pack = json(output.resolve("pack.mcmeta")).getAsJsonObject("pack"); + assertEquals(121, pack.get("min_format").getAsJsonArray().get(0).getAsInt()); + assertEquals(0, pack.get("min_format").getAsJsonArray().get(1).getAsInt()); + assertEquals(121, pack.get("max_format").getAsInt()); + CompoundTag template = (CompoundTag) NBTUtil.read(output.resolve( + "data/studio/structure/village/test/piece/pieces/start.nbt").toFile()).getTag(); + assertEquals(5023, template.getInt("DataVersion")); + CompoundTag palette = (CompoundTag) template.getListTag("palette").get(0); + assertEquals("minecraft:jigsaw", palette.getString("id")); + assertEquals("north_up", palette.getCompoundTag("properties").getString("orientation")); + assertFalse(palette.containsKey("Name")); + } finally { + when(IrisPlatforms.get().minecraftVersion()).thenReturn("26.2"); + } + } + @Test public void exportsZipWithPackFilesAtArchiveRoot() throws Exception { TestGraph graph = terminalGraph(); diff --git a/core/src/test/java/art/arcane/iris/world/history/SavedTerrainChunkTest.java b/core/src/test/java/art/arcane/iris/world/history/SavedTerrainChunkTest.java index 26e1c397c..f99083be3 100644 --- a/core/src/test/java/art/arcane/iris/world/history/SavedTerrainChunkTest.java +++ b/core/src/test/java/art/arcane/iris/world/history/SavedTerrainChunkTest.java @@ -5,6 +5,12 @@ import art.arcane.volmlib.util.nbt.tag.CompoundTag; import art.arcane.volmlib.util.nbt.tag.ListTag; import art.arcane.volmlib.util.nbt.tag.StringTag; import net.jpountz.lz4.LZ4BlockOutputStream; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformRegistries; +import org.junit.Before; +import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -18,6 +24,8 @@ import java.nio.file.Path; import java.util.zip.DeflaterOutputStream; import java.util.zip.GZIPOutputStream; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; @@ -27,6 +35,76 @@ public final class SavedTerrainChunkTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Before + public void bindPlatform() { + IrisPlatforms.unbind(); + IrisPlatform platform = mock(IrisPlatform.class); + PlatformRegistries registries = mock(PlatformRegistries.class); + when(platform.registries()).thenReturn(registries); + for (String state : new String[]{"minecraft:stone", "minecraft:water[level=0]", + "minecraft:sea_pickle[pickles=1,waterlogged=true]"}) { + PlatformBlockState block = mock(PlatformBlockState.class); + when(block.key()).thenReturn(state); + when(registries.blockOrNull(state.split("\\[")[0])).thenReturn(block); + } + IrisPlatforms.bind(platform); + } + + @After + public void unbindPlatform() { + IrisPlatforms.unbind(); + } + + @Test + public void reads263LowercasePaletteAtTerrainStatus() throws Exception { + Path world = temporaryFolder.newFolder("terrain-263").toPath(); + CompoundTag saved = root("minecraft:terrain"); + saved.putInt("DataVersion", 5023); + CompoundTag section = (CompoundTag) saved.getListTag("sections").get(0); + ListTag palette = section.getCompoundTag("block_states").getListTag("palette"); + for (int index = 0; index < palette.size(); index++) { + CompoundTag state = (CompoundTag) palette.get(index); + state.put("id", state.remove("Name")); + if (state.containsKey("Properties")) { + state.put("properties", state.remove("Properties")); + } + } + writeChunk(world, saved, 2, false); + SavedTerrainChunk chunk = SavedTerrainChunk.read(world, -1, -2, -16, 16); + assertEquals("minecraft:terrain", chunk.nativeStatus()); + assertEquals("minecraft:water[level=0]", chunk.column(-16, -32).geometry().voxelAt(-15).stateKey()); + } + + @Test + public void reads263StringAndWrappedPaletteEntries() throws Exception { + for (String name : new String[]{"minecraft:stone", "minecraft:water", "minecraft:sea_pickle"}) { + for (boolean wrapped : new boolean[]{false, true}) { + Path world = temporaryFolder.newFolder("terrain-string-" + name.substring(10) + "-" + wrapped).toPath(); + CompoundTag saved = root("minecraft:full"); + saved.putInt("DataVersion", 5023); + CompoundTag section = (CompoundTag) saved.getListTag("sections").get(0); + CompoundTag blocks = section.getCompoundTag("block_states"); + blocks.remove("data"); + if (wrapped) { + ListTag palette = new ListTag<>(CompoundTag.class); + CompoundTag state = new CompoundTag(); + state.putString("", name); + palette.add(state); + blocks.put("palette", palette); + } else { + ListTag palette = new ListTag<>(StringTag.class); + palette.add(new StringTag(name)); + blocks.put("palette", palette); + } + writeChunk(world, saved, 2, false); + SavedTerrainChunk chunk = SavedTerrainChunk.read(world, -1, -2, -16, 16); + BoundaryColumnGeometry.Voxel voxel = chunk.column(-16, -32).geometry().voxelAt(-16); + assertEquals(IrisPlatforms.get().registries().blockOrNull(name).key(), voxel.stateKey()); + assertEquals(name.equals("minecraft:stone") ? "" : "minecraft:water[level=0]", voxel.fluidStateKey()); + } + } + } + @Test public void readsModernPaletteAtNegativeCoordinatesWithEveryCompression() throws Exception { for (int compression = 1; compression <= 4; compression++) { diff --git a/gradle/generation-build-revision.gradle b/gradle/generation-build-revision.gradle index 9db8aec3e..e3f93f0aa 100644 --- a/gradle/generation-build-revision.gradle +++ b/gradle/generation-build-revision.gradle @@ -50,7 +50,7 @@ Closure> resolvedKernelDependencies = { TaskProvider revisionResources = tasks.register('generateGenerationBuildRevision') { group = 'build' description = 'Captures generation sources and resolved dependencies as build revision provenance.' - dependsOn(kernelDependencies, kernelAgentJar, ":adapters:bukkit:nms:v26_2_R1:convert") + dependsOn(kernelDependencies, kernelAgentJar, ":adapters:bukkit:nms:v26_2_R1:convert", ":adapters:bukkit:nms:v26_3_R1:convert") inputs.files(revisionScope.roots().collect { String source -> rootProject.file(source) }) inputs.property('revisionScope', revisionScope.toString()) inputs.files(catalogFile, kernelDependencies, kernelAgentJar.flatMap { Jar jar -> jar.archiveFile }) diff --git a/settings.gradle b/settings.gradle index f3e4cca6b..d5fa041aa 100644 --- a/settings.gradle +++ b/settings.gradle @@ -29,6 +29,7 @@ include(':probe') include(':spi') include(':adapters:bukkit:plugin') include(':adapters:bukkit:nms:v26_2_R1') +include(':adapters:bukkit:nms:v26_3_R1') /* * Opt-in: surface the three modded adapter builds to the IDE.