From b1e1fbc15edc3964fdb8cccf6f8393264864232f Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sat, 5 Sep 2026 11:57:01 -0400 Subject: [PATCH] Preserved terrain across updates - Saved chunks retain their natural geometry and generation ownership - Future chunks blend against frozen terrain boundaries - Build revisions capture generation source and dependencies automatically - Native terrain queries use contained transition geometry and final height caches - Historical searches retain recorded pack identities and placement facts - World creation and Studio updates preserve native lifecycle ordering - Hydrology containment and scoped content workers retain generation context - Pregeneration validates saved completion before trusting cached progress --- .../core/nms/v26_2_R1/IrisChunkGenerator.java | 97 +- .../iris/core/nms/v26_2_R1/NMSBinding.java | 398 +---- .../core/nms/v26_2_R1/NmsGenerationHooks.java | 61 + .../nms/v26_2_R1/NmsSavedTerrainCapture.java | 129 ++ .../core/nms/v26_2_R1/NmsWorldLifecycle.java | 403 +++++ .../GenerationHistoryRoutingContractTest.java | 3 + ...fecycleServerLevelAdviceContractTest.java} | 8 +- ...ifecycleShutdownBoundaryContractTest.java} | 14 +- .../NativeGenerationWriteGuardTest.java | 79 + .../nativegen/NativeTransitionColumnTest.java | 68 + .../runtime/BukkitEnginePlatformHooks.java | 23 +- .../runtime/BukkitSavedTerrainCapture.java | 23 + .../core/service/IrisProtocolService.java | 2 +- adapters/fabric/build.gradle | 2 +- adapters/forge/build.gradle | 2 +- .../nativegen/NativeGenerationWriteGuard.java | 46 + .../nativegen/NativeTransitionColumn.java | 53 + .../iris/modded/IrisModdedChunkGenerator.java | 44 +- .../iris/modded/ModdedForcedDatapack.java | 6 +- .../modded/ModdedNativeStructureStage.java | 17 +- .../modded/ModdedNativeTerrainReceipts.java | 53 + .../modded/ModdedSavedTerrainCapture.java | 142 ++ .../iris/modded/ModdedWorldEngines.java | 2 + .../modded/NativeTerrainReceiptHolder.java | 11 + .../iris/modded/command/ModdedPregenJob.java | 5 +- .../mixin/ChunkMapTerrainReceiptAccess.java | 18 + .../mixin/ChunkTerrainReceiptMixin.java | 37 + .../mixin/LevelChunkTerrainReceiptMixin.java | 21 + .../SerializableTerrainReceiptMixin.java | 89 + .../mixin/WorldGenerationWriteGuardMixin.java | 25 + .../service/ModdedStudioHotloadService.java | 14 + .../resources/irisworldgen.entity.mixins.json | 5 + .../iris/modded/ModdedForcedDatapackTest.java | 37 + ...dGenerationHistoryRoutingContractTest.java | 35 +- adapters/neoforge/build.gradle | 2 +- build.gradle | 5 +- ...Seal.java => GenerationBuildRevision.java} | 55 +- .../buildtools/GenerationRevisionScope.java | 57 + ....java => GenerationBuildRevisionTest.java} | 112 +- .../java/GenerationRevisionScopeTest.java | 163 ++ buildSrc/src/test/java/JarCompactorTest.java | 2 +- core/build.gradle | 2 +- core/generation-kernels/abi-1.seal | 1589 ----------------- .../iris/core/IrisDatapackCompiler.java | 126 +- .../iris/core/IrisStartupValidation.java | 59 +- .../iris/core/gui/ImageMapStudioGUI.java | 14 +- .../core/lifecycle/BukkitPublicBackend.java | 3 +- .../art/arcane/iris/core/loader/IrisData.java | 20 + .../art/arcane/iris/core/nms/INMSBinding.java | 14 +- .../pack/PackStructurePlacementValidator.java | 5 + .../core/pregenerator/IrisPregenerator.java | 16 - .../PregenPerformanceProfile.java | 68 +- .../cache/PregenSavedChunkStatus.java | 72 + .../methods/CachedPregenMethod.java | 45 +- .../iris/core/safeguard/IrisSafeguard.java | 8 +- .../iris/core/safeguard/task/Tasks.java | 9 +- .../core/service/ObjectStudioSaveService.java | 8 +- .../core/structure/StructureIndexService.java | 24 - .../arcane/iris/core/tools/IrisCreator.java | 26 +- .../arcane/iris/core/tools/IrisToolbelt.java | 4 +- .../iris/core/tools/IrisWorldCreator.java | 15 +- .../arcane/iris/engine/EngineDiagnostics.java | 63 + .../arcane/iris/engine/EngineHotloader.java | 112 +- .../iris/engine/EngineRuntimeBuilder.java | 33 + .../art/arcane/iris/engine/IrisComplex.java | 83 +- .../art/arcane/iris/engine/IrisEngine.java | 143 +- .../iris/engine/ResolvedTerrainProvider.java | 240 +++ .../iris/engine/StudioGenerationUpdate.java | 88 + .../iris/engine/UpperDimensionContext.java | 26 - .../engine/actuator/IrisBiomeActuator.java | 65 +- .../actuator/IrisDimensionStackActuator.java | 69 +- .../IrisTransitionGeometryActuator.java | 157 ++ .../decorator/IrisShoreLineDecorator.java | 11 +- .../decorator/IrisSurfaceDecorator.java | 2 +- .../arcane/iris/engine/framework/Engine.java | 17 +- .../framework/EngineLifecycleTasks.java | 36 +- .../iris/engine/framework/EngineMode.java | 28 +- .../engine/framework/EnginePlatformHooks.java | 13 + .../framework/GenerationSessionLease.java | 19 +- .../framework/GenerationSessionManager.java | 88 +- .../framework/GenerationTransitionGate.java | 200 +++ .../iris/engine/framework/IrisEngineMode.java | 23 + .../history/BoundaryColumnGeometry.java | 258 +++ .../history/BoundaryGeometryInfluence.java | 41 + .../history/ChunkGenerationOwnership.java | 29 + .../history/ChunkGenerationSemantics.java | 76 +- .../engine/history/DiskBoundaryCapture.java | 44 + .../iris/engine/history/GenerationBlend.java | 17 + .../GenerationBoundarySignatureSampler.java | 4 + .../history/GenerationBoundaryStore.java | 7 +- ...Seal.java => GenerationBuildRevision.java} | 30 +- .../iris/engine/history/GenerationEpoch.java | 5 - .../engine/history/GenerationFindCatalog.java | 139 +- .../engine/history/GenerationHistory.java | 146 +- .../GenerationHistoryRuntimeRouter.java | 250 +-- .../history/GenerationHistoryStore.java | 96 +- .../history/GenerationKernelCatalog.java | 10 +- .../engine/history/GenerationKernelV1.java | 4 +- .../engine/history/GenerationManifest.java | 17 +- .../history/GenerationPackRepository.java | 44 +- .../history/GenerationRegistryContract.java | 103 +- .../GenerationRegistryContractFactory.java | 23 +- .../history/GenerationSemanticCapture.java | 46 +- .../history/GenerationSemanticIndex.java | 116 +- .../history/GenerationSemanticQueries.java | 3 +- .../history/IrisBoundarySignatureSampler.java | 216 +-- .../engine/history/NativeTerrainReceipt.java | 196 ++ .../history/RegionGenerationOwnership.java | 22 + .../engine/history/SavedTerrainChunk.java | 208 +++ .../history/SavedTerrainChunkReader.java | 368 ++++ .../history/TerrainBoundarySignature.java | 8 +- .../TerrainBoundarySignatureStore.java | 92 +- .../history/TransitionBoundarySampler.java | 37 +- .../history/TransitionFluidContainment.java | 149 ++ .../history/TransitionGenerationPlan.java | 39 +- .../history/TransitionGeometryBlender.java | 205 +++ .../engine/history/WorldChunkInventory.java | 66 +- .../iris/engine/mantle/EngineMantle.java | 43 +- .../iris/engine/mantle/MantleComponent.java | 4 + .../iris/engine/mantle/MantleWriter.java | 96 + .../engine/mantle/MatterGenerationPhase.java | 11 + .../iris/engine/mantle/MatterGenerator.java | 84 +- .../iris/engine/mantle/TerrainMatterView.java | 94 + .../components/MantleCarvingComponent.java | 8 +- .../MantleHydrologyCaveVoxelView.java | 3 +- .../components/MantleHydrologyComponent.java | 41 +- .../components/MantleObjectComponent.java | 26 +- .../ObjectDestinationTransaction.java | 17 +- .../iris/engine/mode/ModeEnclosure.java | 2 +- .../arcane/iris/engine/mode/ModeIslands.java | 2 +- .../iris/engine/mode/ModeOverworld.java | 16 +- .../iris/engine/mode/ModeSuperFlat.java | 2 +- .../engine/modifier/IrisCarveModifier.java | 80 +- .../engine/modifier/IrisDepositModifier.java | 25 +- .../IrisFloatingChildBiomeModifier.java | 35 +- .../iris/engine/object/IObjectPlacer.java | 8 + .../engine/platform/BukkitChunkGenerator.java | 42 +- .../common/director/DirectorExecutor.java | 34 +- .../director/context/BiomeContextHandler.java | 11 +- .../context/DimensionContextHandler.java | 7 +- .../context/GeneratorContextHandler.java | 6 +- .../context/RegionContextHandler.java | 11 +- .../util/project/context/ChunkContext.java | 47 +- .../context/ChunkedDoubleDataCache.java | 19 + .../project/matter/PreObjectMatterCell.java | 36 +- .../matter/slices/PreObjectMatter.java | 25 +- .../stream/utility/CachedStream2D.java | 7 - .../arcane/iris/core/IrisSafeguardTest.java | 59 + .../iris/core/IrisStartupValidationTest.java | 84 + .../iris/core/gui/ImageMapStudioGUITest.java | 20 +- .../lifecycle/BukkitPublicBackendTest.java | 71 + .../loader/IrisDataAuthoringCacheTest.java | 75 + ...kValidatorStructureTerrainBackendTest.java | 88 + .../PregenPerformanceProfileTest.java | 163 +- .../cache/PregenSavedChunkStatusTest.java | 93 + .../CachedPregenMethodCompletionTest.java | 74 +- .../service/ObjectStudioSaveSourceTest.java | 51 + .../structure/StructureIndexServiceTest.java | 58 +- ...orStudioGenerationHistoryContractTest.java | 28 + .../engine/IrisComplexTransitionTest.java | 85 +- .../IrisEngineGenerationRuntimeScopeTest.java | 79 + .../engine/ResolvedTerrainProviderTest.java | 423 +++++ .../engine/StudioGenerationUpdateTest.java | 112 ++ .../engine/UpperDimensionTransitionTest.java | 44 +- .../IrisBiomeActuatorCoordinateTest.java | 43 +- ...risDimensionStackActuatorMetadataTest.java | 42 + .../IrisTransitionGeometryActuatorTest.java | 127 ++ .../IrisDecoratorCaveContextTest.java | 34 +- .../decorator/IrisShoreLineDecoratorTest.java | 34 +- .../framework/EngineLifecycleTasksTest.java | 64 +- .../GenerationTransitionGateTest.java | 255 +++ .../history/ArchivedRegistryCompilerTest.java | 131 ++ .../history/ArchivedSemanticQueryTest.java | 94 + .../history/BoundaryColumnGeometryTest.java | 168 ++ .../history/ChunkGenerationSemanticsTest.java | 22 +- .../history/GenerationBoundaryStoreTest.java | 70 + .../history/GenerationBuildRevisionTest.java | 56 + .../GenerationEpochContractFactoryTest.java | 2 +- .../history/GenerationFindCatalogTest.java | 293 +-- .../GenerationHistoryRuntimeRouterTest.java | 469 +++-- .../history/GenerationHistoryStoreTest.java | 50 +- .../engine/history/GenerationHistoryTest.java | 150 +- .../history/GenerationKernelCatalogTest.java | 10 +- .../GenerationKernelSourceSealTest.java | 56 - .../history/GenerationOrphanRecoveryTest.java | 61 + .../history/GenerationPackRepositoryTest.java | 42 + .../GenerationRegistryContractTest.java | 18 +- .../GenerationSemanticCaptureTest.java | 147 ++ .../history/GenerationSemanticIndexTest.java | 83 +- .../GenerationSemanticQueriesTest.java | 28 +- .../IrisBoundarySignatureSamplerTest.java | 139 +- .../engine/history/SavedTerrainChunkTest.java | 180 ++ .../history/SavedTerrainTestRegion.java | 88 + .../TerrainBoundarySignatureStoreTest.java | 6 +- .../history/TerrainBoundarySignatureTest.java | 2 +- .../TransitionFluidContainmentTest.java | 166 ++ .../history/TransitionGenerationPlanTest.java | 45 +- .../TransitionGeometryBlenderTest.java | 201 +++ .../history/WorldChunkInventoryTest.java | 112 ++ .../MatterGeneratorConcurrencyTest.java | 72 + .../mantle/ResolvedTerrainPlacementTest.java | 139 ++ .../engine/mantle/TerrainMatterViewTest.java | 87 + .../MantleHydrologyCaveVoxelViewTest.java | 4 + .../MantleHydrologyComponentTest.java | 75 + .../MantleObjectComponentCaveAnchorTest.java | 16 + .../IrisDepositModifierContextTest.java | 180 ++ ...eneratorGenerationHistoryContractTest.java | 23 + .../director/DirectorAuthoringDataTest.java | 94 + .../context/ChunkContextPrefillPlanTest.java | 40 +- .../ChunkedDoubleDataCacheOverrideTest.java | 24 + .../matter/slices/PreObjectMatterTest.java | 7 +- gradle/generation-build-revision.gradle | 91 + gradle/generation-kernel-seals.gradle | 133 -- ...> modded-generation-build-revision.gradle} | 20 +- 214 files changed, 11985 insertions(+), 4082 deletions(-) create mode 100644 adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NmsGenerationHooks.java create mode 100644 adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NmsSavedTerrainCapture.java create mode 100644 adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NmsWorldLifecycle.java rename adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/{NMSBindingServerLevelAdviceContractTest.java => NmsWorldLifecycleServerLevelAdviceContractTest.java} (95%) rename adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/{NMSBindingShutdownBoundaryContractTest.java => NmsWorldLifecycleShutdownBoundaryContractTest.java} (84%) create mode 100644 adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeGenerationWriteGuardTest.java create mode 100644 adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeTransitionColumnTest.java create mode 100644 adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/runtime/BukkitSavedTerrainCapture.java create mode 100644 adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeGenerationWriteGuard.java create mode 100644 adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeTransitionColumn.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeTerrainReceipts.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSavedTerrainCapture.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/NativeTerrainReceiptHolder.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/ChunkMapTerrainReceiptAccess.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/ChunkTerrainReceiptMixin.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/LevelChunkTerrainReceiptMixin.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/SerializableTerrainReceiptMixin.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/WorldGenerationWriteGuardMixin.java rename buildSrc/src/main/java/art/arcane/iris/buildtools/{GenerationKernelSeal.java => GenerationBuildRevision.java} (91%) create mode 100644 buildSrc/src/main/java/art/arcane/iris/buildtools/GenerationRevisionScope.java rename buildSrc/src/test/java/{GenerationKernelSealTest.java => GenerationBuildRevisionTest.java} (53%) create mode 100644 buildSrc/src/test/java/GenerationRevisionScopeTest.java delete mode 100644 core/generation-kernels/abi-1.seal create mode 100644 core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenSavedChunkStatus.java create mode 100644 core/src/main/java/art/arcane/iris/engine/EngineDiagnostics.java create mode 100644 core/src/main/java/art/arcane/iris/engine/ResolvedTerrainProvider.java create mode 100644 core/src/main/java/art/arcane/iris/engine/StudioGenerationUpdate.java create mode 100644 core/src/main/java/art/arcane/iris/engine/actuator/IrisTransitionGeometryActuator.java create mode 100644 core/src/main/java/art/arcane/iris/engine/framework/GenerationTransitionGate.java create mode 100644 core/src/main/java/art/arcane/iris/engine/history/BoundaryColumnGeometry.java create mode 100644 core/src/main/java/art/arcane/iris/engine/history/BoundaryGeometryInfluence.java create mode 100644 core/src/main/java/art/arcane/iris/engine/history/DiskBoundaryCapture.java rename core/src/main/java/art/arcane/iris/engine/history/{GenerationKernelSourceSeal.java => GenerationBuildRevision.java} (80%) create mode 100644 core/src/main/java/art/arcane/iris/engine/history/NativeTerrainReceipt.java create mode 100644 core/src/main/java/art/arcane/iris/engine/history/SavedTerrainChunk.java create mode 100644 core/src/main/java/art/arcane/iris/engine/history/SavedTerrainChunkReader.java create mode 100644 core/src/main/java/art/arcane/iris/engine/history/TransitionFluidContainment.java create mode 100644 core/src/main/java/art/arcane/iris/engine/history/TransitionGeometryBlender.java create mode 100644 core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerationPhase.java create mode 100644 core/src/main/java/art/arcane/iris/engine/mantle/TerrainMatterView.java create mode 100644 core/src/test/java/art/arcane/iris/core/IrisSafeguardTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/lifecycle/BukkitPublicBackendTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/loader/IrisDataAuthoringCacheTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/pregenerator/cache/PregenSavedChunkStatusTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/service/ObjectStudioSaveSourceTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/tools/IrisCreatorStudioGenerationHistoryContractTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/ResolvedTerrainProviderTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/StudioGenerationUpdateTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/actuator/IrisTransitionGeometryActuatorTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/framework/GenerationTransitionGateTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/history/ArchivedRegistryCompilerTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/history/ArchivedSemanticQueryTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/history/BoundaryColumnGeometryTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/history/GenerationBuildRevisionTest.java delete mode 100644 core/src/test/java/art/arcane/iris/engine/history/GenerationKernelSourceSealTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/history/GenerationOrphanRecoveryTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/history/GenerationSemanticCaptureTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/history/SavedTerrainChunkTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/history/SavedTerrainTestRegion.java create mode 100644 core/src/test/java/art/arcane/iris/engine/history/TransitionFluidContainmentTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/history/TransitionGeometryBlenderTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/mantle/ResolvedTerrainPlacementTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/mantle/TerrainMatterViewTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierContextTest.java create mode 100644 core/src/test/java/art/arcane/iris/util/common/director/DirectorAuthoringDataTest.java create mode 100644 core/src/test/java/art/arcane/iris/util/project/context/ChunkedDoubleDataCacheOverrideTest.java create mode 100644 gradle/generation-build-revision.gradle delete mode 100644 gradle/generation-kernel-seals.gradle rename gradle/{modded-generation-kernel-seals.gradle => modded-generation-build-revision.gradle} (70%) diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java index b41fa876c..93be0310c 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java @@ -1,5 +1,10 @@ package art.arcane.iris.core.nms.v26_2_R1; +import art.arcane.iris.nativegen.NativeTransitionColumn; +import art.arcane.iris.engine.history.TerrainBoundarySignature; +import java.util.Optional; +import art.arcane.iris.nativegen.NativeGenerationWriteGuard; +import java.util.function.LongPredicate; import art.arcane.iris.engine.DimensionStackContext; import art.arcane.iris.engine.DimensionStackLayout; import art.arcane.iris.engine.framework.Engine; @@ -12,6 +17,10 @@ import art.arcane.iris.engine.framework.NativeStructureStartPlan; import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; import art.arcane.iris.engine.IrisEngine; import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter; +import art.arcane.iris.engine.history.NativeTerrainReceipt; +import art.arcane.iris.engine.history.SavedTerrainChunk; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataType; import art.arcane.iris.engine.platform.BukkitChunkGenerator; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; @@ -112,7 +121,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; @@ -122,7 +130,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.IntBinaryOperator; import java.util.function.Predicate; -public class IrisChunkGenerator extends CustomChunkGenerator { +public class IrisChunkGenerator extends CustomChunkGenerator implements LongPredicate { private static final WrappedField BIOME_SOURCE; private static final WrappedReturningMethod SET_HEIGHT; private static final Runnable NO_OP = () -> { @@ -413,6 +421,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator { )); adjustGeneratedStructures( registryAccess, access, previousStarts, configuredStarts, templateManager); + if (route != null) { + access.persistentDataContainer.set(new NamespacedKey("iris", "structure_activation"), + PersistentDataType.LONG, route.activation().activationId()); + access.markUnsaved(); + } } } @@ -716,6 +729,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator { GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route); GenerationSessionLease lease = requireGenerationLease("bukkit_nms_build_surface"); IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + if (!engine.getComplex().allowsMantleChunkWrite(chunkPos.x(), chunkPos.z())) { + return; + } delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess); } } @@ -729,6 +745,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator { GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route); GenerationSessionLease lease = requireGenerationLease("bukkit_nms_apply_carvers"); IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + if (!engine.getComplex().allowsMantleChunkWrite(chunkPos.x(), chunkPos.z())) { + return; + } delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess); } } @@ -766,6 +785,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator { 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; } @@ -783,6 +804,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator { completion.completeExceptionally(completionFailure); } }); + lease.detachThread(); + if (route != null) { + route.detachThread(); + } return completion; } catch (RuntimeException | Error failure) { lease.close(); @@ -792,6 +817,19 @@ public class IrisChunkGenerator extends CustomChunkGenerator { } } + 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, @@ -933,19 +971,22 @@ public class IrisChunkGenerator extends CustomChunkGenerator { GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route); GenerationSessionLease lease = requireGenerationLease("bukkit_nms_biome_decoration"); IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { - if (allowsRoutedDiscreteGeneration(ichunkaccess, ChunkStatus.FEATURES)) { + if (!ichunkaccess.getPersistedStatus().isOrAfter(ChunkStatus.FEATURES)) { + placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager); + } + if (allowsRoutedDiscreteGeneration(ichunkaccess, ChunkStatus.FEATURES) + && NativeGenerationWriteGuard.allowsDecoration(engine, generatoraccessseed, chunkPos)) { // 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); - placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager); // 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, "bukkit_nms_biome_decoration"); + claimGeneratedSemantics(route, ichunkaccess, engine.getMinHeight()); } } @@ -998,7 +1039,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator { List resolvedPlacements = new ArrayList<>(starts.size()); for (StructureStart start : starts) { BoundingBox footprint = start.getBoundingBox(); - if (!engine.getComplex().allowsNewGenerationFootprint( + if (!isHistoricalStructureStart(world, start) + && !engine.getComplex().allowsNewGenerationFootprint( footprint.minX(), footprint.minZ(), footprint.maxX(), @@ -1098,6 +1140,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator { } } + 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 ""; @@ -1272,12 +1328,19 @@ public class IrisChunkGenerator extends CustomChunkGenerator { } } + @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 engine.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z()); + return NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, stage) + || engine.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z()); } private static WeightedList mergeSpawnTables( @@ -1314,6 +1377,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator { i, j, "bukkit_nms_base_height"); GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height"); IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + Optional resolved = engine.getComplex().resolvedTerrainColumn(i, j); + if (resolved.isPresent()) { + return NativeTransitionColumn.height(resolved.get(), heightmap_type, levelheightaccessor); + } boolean ignoreFluid = !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState()); int height = engine.getDimensionStackContext() == null ? engine.getHeight(i, j, ignoreFluid) @@ -1331,6 +1398,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator { 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) @@ -1412,16 +1483,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator { private static void claimGeneratedSemantics( GenerationHistoryRuntimeRouter.RuntimeRoute route, - String operation + ChunkAccess chunk, + int minimumY ) { if (route == null) { return; } try { - route.claimGeneratedSemantics(); + 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 " + operation - + " could not durably claim generated chunk semantics.", failure); + throw new IllegalStateException("Iris could not durably claim generated chunk semantics.", failure); } } diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java index aea901c43..28ee30413 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java @@ -1,5 +1,6 @@ package art.arcane.iris.core.nms.v26_2_R1; +import art.arcane.iris.engine.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; @@ -26,10 +27,8 @@ import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.nativegen.NativeStructureFactory; import art.arcane.iris.nativegen.NativeStructureGenerationException; import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata; -import art.arcane.iris.util.project.agent.Agent; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; -import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder; import art.arcane.iris.spi.PlatformBlockState; @@ -54,15 +53,6 @@ import art.arcane.volmlib.util.nbt.mca.palette.MCAWrappedPalettedContainer; import art.arcane.volmlib.util.nbt.tag.CompoundTag; import art.arcane.iris.util.common.scheduling.J; import it.unimi.dsi.fastutil.objects.Object2IntMap; -import it.unimi.dsi.fastutil.shorts.ShortList; -import net.bytebuddy.ByteBuddy; -import net.bytebuddy.agent.builder.AgentBuilder; -import net.bytebuddy.agent.builder.ResettableClassFileTransformer; -import net.bytebuddy.description.type.TypeDescription; -import net.bytebuddy.dynamic.DynamicType; -import net.bytebuddy.utility.JavaModule; -import net.bytebuddy.asm.Advice; -import net.bytebuddy.matcher.ElementMatchers; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.IdMapper; @@ -95,7 +85,6 @@ 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.Level; import net.minecraft.world.level.biome.BiomeSource; import net.minecraft.world.level.biome.Biomes; import net.minecraft.world.level.block.Block; @@ -108,7 +97,6 @@ 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.ProtoChunk; import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.chunk.status.WorldGenContext; import net.minecraft.world.level.dimension.LevelStem; @@ -127,7 +115,6 @@ import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; 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.storage.LevelStorageSource; import net.minecraft.world.level.levelgen.flat.FlatLayerInfo; import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; import org.bukkit.Bukkit; @@ -159,10 +146,8 @@ import org.jetbrains.annotations.NotNull; import java.awt.Color; import java.io.File; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -173,7 +158,6 @@ import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; public class NMSBinding implements INMSBinding { @@ -181,11 +165,7 @@ public class NMSBinding implements INMSBinding { private volatile DataVersion dataVersion; private final BlockData AIR = Material.AIR.createBlockData(); private final AtomicCache> biomeMapCache = new AtomicCache<>(); - private final AtomicBoolean injected = new AtomicBoolean(); - private volatile ResettableClassFileTransformer levelStorageAccessTransformer; - private volatile ResettableClassFileTransformer serverLevelTransformer; - private volatile ResettableClassFileTransformer pluginClassLoaderTransformer; - private boolean pluginClassLoaderCloseDeferred; + private final NmsWorldLifecycle worldLifecycle = new NmsWorldLifecycle(); private final AtomicCache> registryCache = new AtomicCache<>(); private final AtomicCache> globalCache = new AtomicCache<>(); private final AtomicCache registryAccess = new AtomicCache<>(); @@ -1131,6 +1111,17 @@ public class NMSBinding implements INMSBinding { } } + @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 { @@ -1552,170 +1543,37 @@ public class NMSBinding implements INMSBinding { @Override 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()); - 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()); - } - - 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; - } - } + return worldLifecycle.injectBukkit(); } @Override public void ensureServerLevelInjection() { - if (!injected.get()) - return; - try { - Agent.getInstrumentation().retransformClasses( - LevelStorageSource.LevelStorageAccess.class, - ServerLevel.class - ); - } catch (Throwable e) { - IrisLogging.error(C.RED + "Failed to re-apply Bukkit world lifecycle injection"); - } + worldLifecycle.ensureServerLevelInjection(); } @Override 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); - } - } - } + worldLifecycle.uninjectBukkit(); } @Override public boolean isServerStopping() { - return ((CraftServer) Bukkit.getServer()).getServer().hasStopped(); + return worldLifecycle.isServerStopping(); } @Override public ServerShutdownBoundary createServerShutdownBoundary() { - MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer(); - return new ServerShutdownBoundary( - () -> server.hasFullyShutdown, - server.getRunningThread() - ); + return worldLifecycle.createServerShutdownBoundary(); } @Override public void deferPluginClassLoaderClose() { - synchronized (injected) { - if (pluginClassLoaderTransformer == null || pluginClassLoaderCloseDeferred) { - return; - } - Agent.retainClassLoader(getClass().getClassLoader()); - pluginClassLoaderCloseDeferred = true; - } + worldLifecycle.deferPluginClassLoaderClose(); } @Override 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()); - } - } + worldLifecycle.releasePluginClassLoaderClose(); } @Override @@ -1761,220 +1619,4 @@ public class NMSBinding implements INMSBinding { settings.updateLayers(); return new FlatLevelSource(settings); } - - 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.util.project.agent.Installer", true, ClassLoader.getSystemClassLoader()); - return Boolean.TRUE.equals(installer.getMethod("deferClassLoaderClose", ClassLoader.class) - .invoke(null, loader)); - } - } - - 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; - } - } - - 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 { - org.bukkit.plugin.Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris"); - if (irisPlugin == null) { - return; - } - pluginClassLoader = irisPlugin.getClass().getClassLoader(); - generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader); - stagingType = Class.forName("art.arcane.iris.core.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; - } - - org.bukkit.plugin.Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris"); - if (irisPlugin == null) { - return; - } - pluginClassLoader = irisPlugin.getClass().getClassLoader(); - generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader); - stagingType = Class.forName("art.arcane.iris.core.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.core.nms.INMS", true, pluginClassLoader) - .getDeclaredMethod("get") - .invoke(null); - if (bindings == null) { - throw new IllegalStateException("Iris failed to resolve an INMSBinding instance."); - } - - java.lang.reflect.Method stemMethod = null; - for (java.lang.reflect.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_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NmsGenerationHooks.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NmsGenerationHooks.java new file mode 100644 index 000000000..43faccb97 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NmsGenerationHooks.java @@ -0,0 +1,61 @@ +package art.arcane.iris.core.nms.v26_2_R1; + +import art.arcane.iris.util.project.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.core.nms.v26_2_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_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NmsSavedTerrainCapture.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NmsSavedTerrainCapture.java new file mode 100644 index 000000000..33c08922b --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NmsSavedTerrainCapture.java @@ -0,0 +1,129 @@ +package art.arcane.iris.core.nms.v26_2_R1; + +import art.arcane.iris.engine.history.NativeTerrainReceipt; +import art.arcane.iris.engine.history.SavedTerrainChunk; +import art.arcane.iris.util.common.scheduling.J; +import ca.spottedleaf.moonrise.common.PlatformHooks; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +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.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.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +final class NmsSavedTerrainCapture { + 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(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(saveSnapshot(level, chunk)); + } catch (Throwable failure) { + snapshot.completeExceptionally(failure); + } + }).whenComplete((ignored, failure) -> { + if (failure != null) { + snapshot.completeExceptionally(failure); + } + }); + return snapshot.thenApplyAsync(data -> { + try { + if (data == null) { + return SavedTerrainChunk.read(world.getWorldFolder().toPath(), chunkX, chunkZ, minimumY, height); + } + 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(); + List> saves = new ArrayList<>(holders.size()); + for (NewChunkHolder holder : holders) { + CompletableFuture saved = new CompletableFuture<>(); + saves.add(saved); + J.runRegionFuture(world, holder.chunkX, holder.chunkZ, () -> { + try { + ChunkAccess chunk = holder.getCurrentChunk(); + if (chunk == null || !chunk.isUnsaved()) { + saved.complete(null); + return; + } + CompoundTag data = saveSnapshot(level, chunk); + byte[] receipt = data.getCompound("ChunkBukkitValues") + .flatMap(values -> values.getByteArray(NativeTerrainReceipt.NBT_KEY)).orElse(null); + saved.complete(new Checkpoint(holder.chunkX, holder.chunkZ, + BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString(), receipt, + data.getCompound("ChunkBukkitValues").map(values -> values.getLongOr( + NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY, 0)).orElse(0L))); + } catch (Throwable failure) { + saved.completeExceptionally(failure); + } + }).whenComplete((ignored, failure) -> { + if (failure != null) { + saved.completeExceptionally(failure); + } + }); + } + return CompletableFuture.allOf(saves.toArray(CompletableFuture[]::new)).thenRunAsync(() -> { + try { + MoonriseRegionFileIO.flush(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA); + MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA); + for (CompletableFuture future : saves) { + Checkpoint saved = future.join(); + if (saved == null) { + continue; + } + SavedTerrainChunk.verifyCheckpoint(world.getWorldFolder().toPath(), saved.chunkX(), saved.chunkZ(), + saved.status(), saved.receipt(), saved.structureActivation()); + } + } catch (IOException failure) { + throw new CompletionException(failure); + } + }); + } + + private static CompoundTag saveSnapshot(ServerLevel level, ChunkAccess chunk) { + SerializableChunkData snapshot = SerializableChunkData.copyOf(level, chunk); + PlatformHooks.get().chunkSyncSave(level, chunk, snapshot); + CompoundTag data = snapshot.write(); + MoonriseRegionFileIO.scheduleSave(level, chunk.getPos().x(), chunk.getPos().z(), data, + MoonriseRegionFileIO.RegionFileType.CHUNK_DATA); + return data; + } + + private record Checkpoint(int chunkX, int chunkZ, String status, byte[] receipt, long structureActivation) { + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NmsWorldLifecycle.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NmsWorldLifecycle.java new file mode 100644 index 000000000..986dec79f --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NmsWorldLifecycle.java @@ -0,0 +1,403 @@ +package art.arcane.iris.core.nms.v26_2_R1; + +import art.arcane.iris.core.nms.ServerShutdownBoundary; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.project.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.util.project.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.engine.platform.PlatformChunkGenerator", true, pluginClassLoader); + stagingType = Class.forName("art.arcane.iris.core.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.engine.platform.PlatformChunkGenerator", true, pluginClassLoader); + stagingType = Class.forName("art.arcane.iris.core.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.core.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_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/GenerationHistoryRoutingContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/GenerationHistoryRoutingContractTest.java index fcaf33f16..2876b0e7a 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/GenerationHistoryRoutingContractTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/GenerationHistoryRoutingContractTest.java @@ -43,6 +43,9 @@ public final class GenerationHistoryRoutingContractTest { assertBefore(noise, "openHistoryRoute(", "requireGenerationLease("); assertTrue(count(noise, "openHistoryRuntimeScope(route)") >= 2); assertBefore(noise, ".thenApply(filled ->", "pipeline.whenComplete("); + assertBefore(noise, "pipeline.whenComplete(", "lease.detachThread()"); + assertBefore(noise, "lease.detachThread()", "route.detachThread()"); + assertBefore(noise, "route.detachThread()", "return completion;"); assertTrue(noise.contains("closeNoisePipelineResources(\n failure, lease, route, stage)")); } diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingServerLevelAdviceContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NmsWorldLifecycleServerLevelAdviceContractTest.java similarity index 95% rename from adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingServerLevelAdviceContractTest.java rename to adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NmsWorldLifecycleServerLevelAdviceContractTest.java index fd1f4e4b8..15d80015d 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingServerLevelAdviceContractTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NmsWorldLifecycleServerLevelAdviceContractTest.java @@ -8,11 +8,11 @@ import java.nio.file.Path; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -public class NMSBindingServerLevelAdviceContractTest { +public class NmsWorldLifecycleServerLevelAdviceContractTest { @Test public void serverLevelAdviceSupportsCurrentSpigotAndPaperConstructorLayouts() throws Exception { String source = Files.readString( - Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n"); + Path.of(System.getProperty("iris.nmsBindingSource")).resolveSibling("NmsWorldLifecycle.java")).replace("\r\n", "\n"); String transformer = section( source, "public boolean injectBukkit()", @@ -35,7 +35,7 @@ public class NMSBindingServerLevelAdviceContractTest { @Test public void currentSpigotStorageAccessReceivesManagedIdentityBeforeConstruction() throws Exception { String source = Files.readString( - Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n"); + Path.of(System.getProperty("iris.nmsBindingSource")).resolveSibling("NmsWorldLifecycle.java")).replace("\r\n", "\n"); String transformer = section( source, "public boolean injectBukkit()", @@ -68,7 +68,7 @@ public class NMSBindingServerLevelAdviceContractTest { @Test public void ownedWorldPublishesItsManagedIdentityAndRuntimeStemTogether() throws Exception { String source = Files.readString( - Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n"); + Path.of(System.getProperty("iris.nmsBindingSource")).resolveSibling("NmsWorldLifecycle.java")).replace("\r\n", "\n"); String advice = section( source, "private static class ServerLevelAdvice", diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingShutdownBoundaryContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NmsWorldLifecycleShutdownBoundaryContractTest.java similarity index 84% rename from adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingShutdownBoundaryContractTest.java rename to adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NmsWorldLifecycleShutdownBoundaryContractTest.java index d0bab68ee..eefbf7f11 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingShutdownBoundaryContractTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NmsWorldLifecycleShutdownBoundaryContractTest.java @@ -8,10 +8,10 @@ import java.nio.file.Path; import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertFalse; -public class NMSBindingShutdownBoundaryContractTest { +public class NmsWorldLifecycleShutdownBoundaryContractTest { @Test public void shutdownClassificationUsesTheServerStopFlagInsteadOfPluginEnablement() throws Exception { - String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource"))); + String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")).resolveSibling("NmsWorldLifecycle.java")); String stopping = section(source, "public boolean isServerStopping()", "public ServerShutdownBoundary createServerShutdownBoundary"); assertTrue(stopping.contains("((CraftServer) Bukkit.getServer()).getServer().hasStopped()")); @@ -21,10 +21,10 @@ public class NMSBindingShutdownBoundaryContractTest { @Test public void pluginLoaderGuardUsesSystemAgentIdentityAndIsRemovedAtRelease() throws Exception { - String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource"))); + String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")).resolveSibling("NmsWorldLifecycle.java")); String injection = section(source, "public boolean injectBukkit()", "public void ensureServerLevelInjection()"); - String guard = section(source, "private static class PluginClassLoaderCloseAdvice", "private static class ChunkAccessAdvice"); - String release = section(source, "public void releasePluginClassLoaderClose()", "public KMap> getBlockProperties"); + String guard = section(source, "private static class PluginClassLoaderCloseAdvice", "private static class LevelStorageAccessAdvice"); + String release = section(source, "public void releasePluginClassLoaderClose()", "private static final class PluginClassLoaderInjectionListener"); assertTrue(injection.contains("Agent.requireClassLoaderCloseDeferral()")); assertTrue(injection.contains("getMethod(\"close\").getDeclaringClass()")); @@ -40,8 +40,8 @@ public class NMSBindingShutdownBoundaryContractTest { @Test public void shutdownBoundaryUsesPaperFullyShutdownStateAndServerThread() throws Exception { - String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n"); - String boundary = section(source, "public ServerShutdownBoundary createServerShutdownBoundary", "public KMap> getBlockProperties"); + String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")).resolveSibling("NmsWorldLifecycle.java")).replace("\r\n", "\n"); + String boundary = section(source, "public ServerShutdownBoundary createServerShutdownBoundary", "private static final class PluginClassLoaderInjectionListener"); assertTrue(boundary.contains("new ServerShutdownBoundary(")); assertTrue(boundary.contains("server.hasFullyShutdown")); diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeGenerationWriteGuardTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeGenerationWriteGuardTest.java new file mode 100644 index 000000000..46b4bb592 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeGenerationWriteGuardTest.java @@ -0,0 +1,79 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.IrisEngine; +import art.arcane.iris.engine.history.GenerationActivation; +import art.arcane.iris.engine.history.GenerationHistory; +import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter; +import java.util.Optional; +import art.arcane.iris.engine.framework.Engine; +import net.minecraft.SharedConstants; +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 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.when; + +public final class NativeGenerationWriteGuardTest { + @BeforeClass + public static void bootstrap() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @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.NOISE); + 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.NOISE); + assertTrue(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.FEATURES)); + assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.NOISE)); + 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_2_R1/src/test/java/art/arcane/iris/nativegen/NativeTransitionColumnTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeTransitionColumnTest.java new file mode 100644 index 000000000..38299c1f0 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeTransitionColumnTest.java @@ -0,0 +1,68 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.history.BoundaryColumnGeometry; +import art.arcane.iris.engine.history.TerrainBoundarySignature; +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.Test; + +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; + +public final class NativeTransitionColumnTest { + @BeforeClass + public static void bootstrap() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @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/plugin/src/main/java/art/arcane/iris/core/runtime/BukkitEnginePlatformHooks.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/runtime/BukkitEnginePlatformHooks.java index 56957a8b7..8497801ea 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/runtime/BukkitEnginePlatformHooks.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/runtime/BukkitEnginePlatformHooks.java @@ -23,6 +23,7 @@ import art.arcane.iris.core.ServerConfigurator; import art.arcane.iris.core.datapack.DatapackIngestService; import art.arcane.iris.core.events.IrisEngineHotloadEvent; import art.arcane.iris.core.gui.PregeneratorJob; +import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.project.IrisProject; import art.arcane.iris.core.project.IrisCodeWorkspace; @@ -30,6 +31,9 @@ import art.arcane.iris.core.service.IrisApiEventSVC; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.core.tools.WorldMaintenance; import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.history.SavedTerrainChunk; + +import java.util.concurrent.CompletableFuture; import art.arcane.iris.engine.framework.EngineMode; import art.arcane.iris.engine.framework.EnginePlatformHooks; import art.arcane.iris.engine.framework.NativeStructureVolume; @@ -47,6 +51,16 @@ import org.bukkit.World; import org.bukkit.WorldBorder; public final class BukkitEnginePlatformHooks implements EnginePlatformHooks { + @Override + public CompletableFuture flushSavedTerrainCapture(Engine engine) { + return INMS.get().flushSavedTerrainCapture(BukkitWorldBinding.world(engine.getWorld())); + } + + @Override + public CompletableFuture captureSavedTerrainChunk(Engine engine, int chunkX, int chunkZ) { + return BukkitSavedTerrainCapture.capture(engine, chunkX, chunkZ); + } + @Override public KList nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) { return INMS.get().nativeStructureVolumes(engine, minX, minZ, maxX, maxZ); @@ -54,12 +68,17 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks { @Override public void refreshWorkspace(Engine engine) { - new IrisCodeWorkspace(new IrisProject(engine.getData().getDataFolder())).updateWorkspace(); + new IrisCodeWorkspace(new IrisProject(engine.getPackSource().toFile())).updateWorkspace(); } @Override public void refreshDatapackWorkspace(Engine engine) { - DatapackIngestService.refreshWorkspace(engine.getData()); + IrisData data = IrisData.openRuntime(engine.getPackSource().toFile()); + try { + DatapackIngestService.refreshWorkspace(data); + } finally { + data.close(); + } } @Override diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/runtime/BukkitSavedTerrainCapture.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/runtime/BukkitSavedTerrainCapture.java new file mode 100644 index 000000000..ba50810ca --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/runtime/BukkitSavedTerrainCapture.java @@ -0,0 +1,23 @@ +package art.arcane.iris.core.runtime; + +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.history.SavedTerrainChunk; +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import org.bukkit.World; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +final class BukkitSavedTerrainCapture { + private BukkitSavedTerrainCapture() { + } + + static CompletableFuture capture(Engine engine, int chunkX, int chunkZ) { + World world = BukkitWorldBinding.world(engine.getWorld()); + if (world == null) { + return CompletableFuture.failedFuture(new IOException("The world is unavailable for terrain capture.")); + } + return INMS.get().captureSavedTerrainChunk(world, chunkX, chunkZ, engine.getMinHeight(), engine.getHeight()); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisProtocolService.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisProtocolService.java index ebaa91640..42f10b4b6 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisProtocolService.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisProtocolService.java @@ -192,7 +192,7 @@ public class IrisProtocolService implements IrisService, PluginMessageListener, World world = player.getWorld(); Engine engine = engineFor(world); if (engine != null) { - protocol.sendDimensionStatus(sessionId, engine.getDimension().getLoadKey(), engine.getData().getDataFolder().getName(), + protocol.sendDimensionStatus(sessionId, engine.getDimension().getLoadKey(), engine.getPackSource().getFileName().toString(), world.getSeed(), engine.getMinHeight(), engine.getMaxHeight(), true); return; } diff --git a/adapters/fabric/build.gradle b/adapters/fabric/build.gradle index cf232ed1c..439fa3d27 100644 --- a/adapters/fabric/build.gradle +++ b/adapters/fabric/build.gradle @@ -26,7 +26,7 @@ plugins { alias(libs.plugins.shadow) } -apply from: file('../../gradle/modded-generation-kernel-seals.gradle') +apply from: file('../../gradle/modded-generation-build-revision.gradle') Properties rootProperties = new Properties() file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) } diff --git a/adapters/forge/build.gradle b/adapters/forge/build.gradle index a86d70d36..1cc1e7588 100644 --- a/adapters/forge/build.gradle +++ b/adapters/forge/build.gradle @@ -26,7 +26,7 @@ plugins { alias(libs.plugins.shadow) } -apply from: file('../../gradle/modded-generation-kernel-seals.gradle') +apply from: file('../../gradle/modded-generation-build-revision.gradle') Properties rootProperties = new Properties() file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) } diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeGenerationWriteGuard.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeGenerationWriteGuard.java new file mode 100644 index 000000000..c3fe2428e --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeGenerationWriteGuard.java @@ -0,0 +1,46 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.IrisEngine; +import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter; +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; + +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.NOISE) + && !chunk.getPersistedStatus().isOrAfter(stage) + && !engine.getComplex().allowsMantleChunkWrite(chunk.getPos().x(), chunk.getPos().z()); + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeTransitionColumn.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeTransitionColumn.java new file mode 100644 index 000000000..731d3aff2 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeTransitionColumn.java @@ -0,0 +1,53 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.history.TerrainBoundarySignature; +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) { + StringReader reader = new StringReader(key); + 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/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java index 8a565fb9d..da7953d75 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java @@ -18,6 +18,10 @@ package art.arcane.iris.modded; +import art.arcane.iris.nativegen.NativeTransitionColumn; +import art.arcane.iris.engine.history.TerrainBoundarySignature; +import java.util.Optional; +import art.arcane.iris.nativegen.NativeGenerationWriteGuard; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.pack.PackValidationRegistry; import art.arcane.iris.engine.IrisEngine; @@ -1019,14 +1023,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { generationEngine.generate(pos.getMinBlockX(), pos.getMinBlockZ(), blocks, biomes, false); writeBlocks(chunk, blocks, dimMinY, height); + if (route != null) { + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + route.claimGeneratedSemantics((x, y, z) -> { + BlockState state = chunk.getBlockState(position.set(x, dimMinY + y, z)); + return state.isAir() || state.liquid(); + }); + } + ModdedNativeTerrainReceipts.persist(chunk, route); writeTerrainHeightmaps(chunk, generationEngine, pos, height); Heightmap.primeHeightmaps(chunk, EnumSet.of( Heightmap.Types.MOTION_BLOCKING, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES)); ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z()); - if (route != null) { - route.claimGeneratedSemantics(); - } return chunk; } catch (GenerationSessionException e) { if (generationEngine.isClosing() || e.isExpectedTeardown()) { @@ -1066,6 +1075,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { completion.completeExceptionally(completionFailure); } }); + route.detachThread(); return completion; } @@ -1205,6 +1215,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route); GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration"); IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { + if (chunk.getPersistedStatus().isOrAfter(ChunkStatus.FEATURES)) { + return; + } + nativeStructures.placeVanillaStructures(level, chunk, structureManager); if (!allowsRoutedDiscreteGeneration( current, route, @@ -1214,8 +1228,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { )) { return; } + if (!NativeGenerationWriteGuard.allowsDecoration(current, level, chunkPos)) { + return; + } importedFeatures.prepare(current); - nativeStructures.placeVanillaStructures(level, chunk, structureManager); // Vanilla's placed-feature pass, on THIS thread and never on ModdedGenPool: the FEATURES chunk // step writes into the eight neighbouring chunks and is not parallel-safe. Inert unless the // dimension set importedFeatures.enabled. @@ -1257,6 +1273,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { )); nativeStructures.adjustGeneratedStructures( registryAccess, chunk, previousStarts, configuredStarts, current, templateManager); + ModdedNativeTerrainReceipts.persistStructureActivation(chunk, route); } } @@ -1355,6 +1372,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { current, x, z, "modded_base_height"); GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height"); IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { + Optional resolved = current.getComplex().resolvedTerrainColumn(x, z); + if (resolved.isPresent()) { + return NativeTransitionColumn.height(resolved.get(), type, heightAccessor); + } int height = current.getDimensionStackContext() == null ? current.getHeight(x, z, ignoreFluid) : Engine.hostHeight(current, x, z, ignoreFluid); @@ -1367,12 +1388,16 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { @Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, RandomState randomState) { int minY = heightAccessor.getMinY(); - BlockState[] states = new BlockState[heightAccessor.getHeight()]; Engine current = requireDataQueryEngine("base column"); try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope( current, x, z, "modded_base_column"); GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column"); IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { + Optional resolved = current.getComplex().resolvedTerrainColumn(x, z); + if (resolved.isPresent()) { + return NativeTransitionColumn.column(resolved.get(), heightAccessor); + } + BlockState[] states = new BlockState[heightAccessor.getHeight()]; BlockState airState = Blocks.AIR.defaultBlockState(); boolean dimensionStack = current.getDimensionStackContext() != null; int surface = dimensionStack @@ -1394,6 +1419,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { } } + public boolean allowsNativeChunkWrite(int chunkX, int chunkZ) { + Engine current = engine; + return current == null || !current.isClosing() && !current.isClosed() + && current.getComplex().allowsMantleChunkWrite(chunkX, chunkZ); + } + private boolean allowsRoutedDiscreteGeneration( Engine current, GenerationHistoryRuntimeRouter.RuntimeRoute route, @@ -1407,7 +1438,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { if (route == null) { return allowsGenerationHistoryBypass(current); } - return current.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z()); + return NativeGenerationWriteGuard.allowsPendingStage(current, chunk, stage) + || current.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z()); } private void bindGenerationRuntimeRetirementListener(Engine current) { 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 7e052b11d..e7dbe3408 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 @@ -362,13 +362,15 @@ public final class ModdedForcedDatapack { KList presetIds = new KList<>(); Path worldRoot = worldRootOrNull(); Set legacySelections = unadoptedSelections(worldRoot); - for (File pack : compilationPackRoots(worldRoot)) { + List packRoots = compilationPackRoots(worldRoot); + for (File pack : packRoots) { PackStageContext context = packStageContext(pack.toPath(), legacySelections); if (stagePack(context, fixer, stagingDirectory, seenBiomes, presetIds)) { packCount++; } } + IrisDatapackCompiler.installRetainedRegistrySources(packRoots, List.of(stagingDirectory.toFile()), fixer); writePackMeta(stagingDirectory); // Forge only: FML has no block-drops event Iris can use, so drops are routed through a global loot // modifier. NeoForge does not need one - IrisNeoForgeBootstrap listens to BlockDropsEvent and both @@ -996,7 +998,7 @@ public final class ModdedForcedDatapack { return currentSource; } PhysicalResourceKey key = new PhysicalResourceKey(registryKey, resourceKey); - return GenerationRegistryContractFactory.requireGeneratedSource(retainedContract, key, fixer); + return GenerationRegistryContractFactory.renderGeneratedSource(retainedContract, key, fixer); } } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureStage.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureStage.java index 554d57ab4..9814681f4 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureStage.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureStage.java @@ -18,6 +18,10 @@ package art.arcane.iris.modded; +import net.minecraft.world.level.chunk.status.ChunkStatus; + +import art.arcane.iris.nativegen.NativeGenerationWriteGuard; + import art.arcane.iris.engine.DimensionStackContext; import art.arcane.iris.engine.DimensionStackLayout; import art.arcane.iris.engine.framework.Engine; @@ -341,7 +345,8 @@ final class ModdedNativeStructureStage { List resolvedPlacements = new ArrayList<>(starts.size()); for (StructureStart start : starts) { BoundingBox footprint = start.getBoundingBox(); - if (!current.getComplex().allowsNewGenerationFootprint( + if (!isHistoricalStructureStart(current, world, start) + && !current.getComplex().allowsNewGenerationFootprint( footprint.minX(), footprint.minZ(), footprint.maxX(), @@ -574,6 +579,16 @@ final class ModdedNativeStructureStage { return blockState; } + private boolean isHistoricalStructureStart(Engine current, WorldGenLevel world, StructureStart start) { + ChunkPos origin = start.getChunkPos(); + if (!current.getComplex().allowsMantleChunkWrite(origin.x(), origin.z())) { + return true; + } + ChunkAccess source = world.getChunk(origin.x(), origin.z(), ChunkStatus.EMPTY, false); + return source != null && NativeGenerationWriteGuard.isHistoricalStructure( + current, ModdedNativeTerrainReceipts.structureActivation(source)); + } + private BoundingBox writableArea(ChunkAccess chunk) { ChunkPos chunkPos = chunk.getPos(); int minX = chunkPos.getMinBlockX(); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeTerrainReceipts.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeTerrainReceipts.java new file mode 100644 index 000000000..7fec60106 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeTerrainReceipts.java @@ -0,0 +1,53 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter; +import art.arcane.iris.engine.history.NativeTerrainReceipt; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ImposterProtoChunk; + +import java.io.IOException; + +public final class ModdedNativeTerrainReceipts { + private ModdedNativeTerrainReceipts() { + } + + public static void persist(ChunkAccess chunk, GenerationHistoryRuntimeRouter.RuntimeRoute route) throws IOException { + if (route == null || route.naturalTerrain().isEmpty()) { + return; + } + set(chunk, NativeTerrainReceipt.encode(route.naturalTerrain().orElseThrow(), + route.activation().activationId(), route.epoch().epochId())); + chunk.markUnsaved(); + } + + public static long structureActivation(ChunkAccess chunk) { + return holder(chunk).iris$getStructureActivation(); + } + + public static void setStructureActivation(ChunkAccess chunk, long activation) { + holder(chunk).iris$setStructureActivation(activation); + } + + public static void persistStructureActivation(ChunkAccess chunk, GenerationHistoryRuntimeRouter.RuntimeRoute route) { + if (route != null) { + setStructureActivation(chunk, route.activation().activationId()); + chunk.markUnsaved(); + } + } + + public static byte[] get(ChunkAccess chunk) { + return holder(chunk).iris$getNaturalTerrain(); + } + + public static void set(ChunkAccess chunk, byte[] receipt) { + holder(chunk).iris$setNaturalTerrain(receipt); + } + + private static NativeTerrainReceiptHolder holder(ChunkAccess chunk) { + ChunkAccess target = chunk instanceof ImposterProtoChunk imposter ? imposter.getWrapped() : chunk; + if (!(target instanceof NativeTerrainReceiptHolder holder)) { + throw new IllegalStateException("Native terrain receipt storage is unavailable for " + chunk.getPos()); + } + return holder; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSavedTerrainCapture.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSavedTerrainCapture.java new file mode 100644 index 000000000..19e536604 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSavedTerrainCapture.java @@ -0,0 +1,142 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.history.SavedTerrainChunk; +import art.arcane.iris.modded.mixin.ChunkMapTerrainReceiptAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkMap; +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 java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +public final class ModdedSavedTerrainCapture { + private ModdedSavedTerrainCapture() { + } + + public static CompletableFuture capture(Engine engine, int chunkX, int chunkZ) { + ServerLevel level = requireLevel(engine); + CompletableFuture snapshot = new CompletableFuture<>(); + runOwned(level, () -> { + try { + ChunkMap map = level.getChunkSource().chunkMap; + ChunkHolder holder = map.getUpdatingChunkIfPresent(ChunkPos.pack(chunkX, chunkZ)); + ChunkAccess chunk = holder == null ? null : holder.getLatestChunk(); + if (chunk == null) { + map.read(new ChunkPos(chunkX, chunkZ)).whenComplete((data, failure) -> { + if (failure != null) { + snapshot.completeExceptionally(failure); + } else { + snapshot.complete(data.orElse(null)); + } + }); + return; + } + String status = BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString(); + if (!SavedTerrainChunk.hasTerrain(status)) { + throw new IOException("Native chunk " + chunkX + "," + chunkZ + " has no terrain at " + status); + } + CompoundTag data = SerializableChunkData.copyOf(level, chunk).write(); + if (chunk.isUnsaved()) { + ((ChunkMapTerrainReceiptAccess) map).iris$saveTerrainCheckpoint(chunk); + } + snapshot.complete(data); + } catch (Throwable failure) { + snapshot.completeExceptionally(failure); + } + }); + return snapshot.thenApplyAsync(data -> { + try { + if (data == null) { + throw new IOException("Saved native chunk is unavailable: " + chunkX + "," + chunkZ); + } + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (DataOutputStream output = new DataOutputStream(bytes)) { + NbtIo.write(data, output); + } + return SavedTerrainChunk.readNbt(bytes.toByteArray(), chunkX, chunkZ, engine.getMinHeight(), engine.getHeight()); + } catch (IOException failure) { + throw new CompletionException(failure); + } + }); + } + + public static CompletableFuture flush(Engine engine) { + ServerLevel level = requireLevel(engine); + CompletableFuture complete = new CompletableFuture<>(); + runOwned(level, () -> { + try { + ChunkMap map = level.getChunkSource().chunkMap; + List checkpoints = new ArrayList<>(); + for (ChunkHolder holder : ((ChunkMapTerrainReceiptAccess) map).iris$updatingChunks().values()) { + ChunkAccess chunk = holder.getLatestChunk(); + if (chunk == null) { + continue; + } + String status = BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString(); + if (!SavedTerrainChunk.hasTerrain(status) + && ModdedNativeTerrainReceipts.structureActivation(chunk) == 0) { + continue; + } + checkpoints.add(new Checkpoint(chunk.getPos().x(), chunk.getPos().z(), status, + ModdedNativeTerrainReceipts.get(chunk), ModdedNativeTerrainReceipts.structureActivation(chunk))); + if (chunk.isUnsaved()) { + ((ChunkMapTerrainReceiptAccess) map).iris$saveTerrainCheckpoint(chunk); + } + } + map.synchronize(true).thenRunAsync(() -> verifyCheckpoints(engine, checkpoints)) + .whenComplete((ignored, failure) -> { + if (failure != null) { + complete.completeExceptionally(failure); + } else { + complete.complete(null); + } + }); + } catch (Throwable failure) { + complete.completeExceptionally(failure); + } + }); + return complete; + } + + private static void verifyCheckpoints(Engine engine, List checkpoints) { + try { + for (Checkpoint checkpoint : checkpoints) { + SavedTerrainChunk.verifyCheckpoint(engine.getWorld().worldFolder().toPath(), + checkpoint.chunkX(), checkpoint.chunkZ(), checkpoint.status(), checkpoint.receipt(), + checkpoint.structureActivation()); + } + } catch (IOException failure) { + throw new CompletionException(failure); + } + } + + private record Checkpoint(int chunkX, int chunkZ, String status, byte[] receipt, long structureActivation) { + } + + private static ServerLevel requireLevel(Engine engine) { + if (!(engine.getWorld().platformWorld().nativeHandle() instanceof ServerLevel level)) { + throw new IllegalStateException("The level is unavailable for terrain capture."); + } + return level; + } + + private static void runOwned(ServerLevel level, Runnable action) { + if (level.getServer().isSameThread()) { + action.run(); + } else { + level.getServer().execute(action); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java index f1ebdf733..6066f3684 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java @@ -191,6 +191,8 @@ public final class ModdedWorldEngines { ModdedGenerationHistoryStorage.ActivePack opened ) throws IOException { GenerationHistory history = opened.history(); + history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()); + opened = ModdedGenerationHistoryStorage.resolveActive(history); long openedActivationId = history.activeActivation().activationId(); IrisEngine candidate = buildEngine(level, seed, opened); boolean ready = false; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/NativeTerrainReceiptHolder.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/NativeTerrainReceiptHolder.java new file mode 100644 index 000000000..5c0038228 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/NativeTerrainReceiptHolder.java @@ -0,0 +1,11 @@ +package art.arcane.iris.modded; + +public interface NativeTerrainReceiptHolder { + long iris$getStructureActivation(); + + void iris$setStructureActivation(long activation); + + byte[] iris$getNaturalTerrain(); + + void iris$setNaturalTerrain(byte[] receipt); +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenJob.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenJob.java index 651c11878..15ad2cf5d 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenJob.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenJob.java @@ -25,6 +25,7 @@ import art.arcane.iris.core.pregenerator.PregenPerformanceProfile; import art.arcane.iris.core.pregenerator.PregenTask; import art.arcane.iris.core.pregenerator.PregeneratorMethod; import art.arcane.iris.core.pregenerator.cache.PregenCache; +import art.arcane.iris.core.pregenerator.cache.PregenSavedChunkStatus; import art.arcane.iris.core.pregenerator.methods.CachedPregenMethod; import art.arcane.iris.engine.framework.Engine; import art.arcane.volmlib.util.format.Form; @@ -64,7 +65,9 @@ public final class ModdedPregenJob { ModdedPregenMethod moddedMethod = new ModdedPregenMethod(level, engine, sync); PregeneratorMethod method = moddedMethod; if (cached) { - method = new CachedPregenMethod(method, PregenCache.create(cacheDirectory(level)).sync(), task); + method = new CachedPregenMethod(new CachedPregenMethod.Configuration(method, + PregenCache.create(cacheDirectory(level)).sync(), task, + PregenSavedChunkStatus.fromWorld(engine.getWorld().worldFolder().toPath()))); } ActivePregen active = new ActivePregen(engine, moddedMethod); ACTIVE.set(active); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/ChunkMapTerrainReceiptAccess.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/ChunkMapTerrainReceiptAccess.java new file mode 100644 index 000000000..22616a0db --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/ChunkMapTerrainReceiptAccess.java @@ -0,0 +1,18 @@ +package art.arcane.iris.modded.mixin; + +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.world.level.chunk.ChunkAccess; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(ChunkMap.class) +public interface ChunkMapTerrainReceiptAccess { + @Invoker("save") + boolean iris$saveTerrainCheckpoint(ChunkAccess chunk); + + @Accessor("updatingChunkMap") + Long2ObjectLinkedOpenHashMap iris$updatingChunks(); +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/ChunkTerrainReceiptMixin.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/ChunkTerrainReceiptMixin.java new file mode 100644 index 000000000..80388f43a --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/ChunkTerrainReceiptMixin.java @@ -0,0 +1,37 @@ +package art.arcane.iris.modded.mixin; + +import art.arcane.iris.modded.NativeTerrainReceiptHolder; +import net.minecraft.world.level.chunk.ChunkAccess; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(ChunkAccess.class) +public abstract class ChunkTerrainReceiptMixin implements NativeTerrainReceiptHolder { + @Unique + private byte[] iris$naturalTerrain; + @Unique + private long iris$structureActivation; + + @Override + public long iris$getStructureActivation() { + return iris$structureActivation; + } + + @Override + public void iris$setStructureActivation(long activation) { + if (activation < 0) { + throw new IllegalArgumentException("Native structure activation cannot be negative"); + } + iris$structureActivation = activation; + } + + @Override + public byte[] iris$getNaturalTerrain() { + return iris$naturalTerrain == null ? null : iris$naturalTerrain.clone(); + } + + @Override + public void iris$setNaturalTerrain(byte[] receipt) { + iris$naturalTerrain = receipt == null ? null : receipt.clone(); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/LevelChunkTerrainReceiptMixin.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/LevelChunkTerrainReceiptMixin.java new file mode 100644 index 000000000..709ac0596 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/LevelChunkTerrainReceiptMixin.java @@ -0,0 +1,21 @@ +package art.arcane.iris.modded.mixin; + +import art.arcane.iris.modded.ModdedNativeTerrainReceipts; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.ProtoChunk; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LevelChunk.class) +public abstract class LevelChunkTerrainReceiptMixin { + @Inject(method = "(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/level/chunk/ProtoChunk;Lnet/minecraft/world/level/chunk/LevelChunk$PostLoadProcessor;)V", at = @At("RETURN")) + private void iris$transferTerrain(ServerLevel level, ProtoChunk source, LevelChunk.PostLoadProcessor processor, + CallbackInfo callback) { + ModdedNativeTerrainReceipts.setStructureActivation((LevelChunk) (Object) this, + ModdedNativeTerrainReceipts.structureActivation(source)); + ModdedNativeTerrainReceipts.set((LevelChunk) (Object) this, ModdedNativeTerrainReceipts.get(source)); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/SerializableTerrainReceiptMixin.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/SerializableTerrainReceiptMixin.java new file mode 100644 index 000000000..d6f96ddc5 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/SerializableTerrainReceiptMixin.java @@ -0,0 +1,89 @@ +package art.arcane.iris.modded.mixin; + +import art.arcane.iris.engine.history.NativeTerrainReceipt; +import art.arcane.iris.modded.ModdedNativeTerrainReceipts; +import art.arcane.iris.modded.NativeTerrainReceiptHolder; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.PalettedContainerFactory; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.storage.RegionStorageInfo; +import net.minecraft.world.level.chunk.storage.SerializableChunkData; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(SerializableChunkData.class) +public abstract class SerializableTerrainReceiptMixin implements NativeTerrainReceiptHolder { + @Unique + private byte[] iris$naturalTerrain; + @Unique + private long iris$structureActivation; + + @Override + public long iris$getStructureActivation() { + return iris$structureActivation; + } + + @Override + public void iris$setStructureActivation(long activation) { + if (activation < 0) { + throw new IllegalArgumentException("Native structure activation cannot be negative"); + } + iris$structureActivation = activation; + } + + @Override + public byte[] iris$getNaturalTerrain() { + return iris$naturalTerrain == null ? null : iris$naturalTerrain.clone(); + } + + @Override + public void iris$setNaturalTerrain(byte[] receipt) { + iris$naturalTerrain = receipt == null ? null : receipt.clone(); + } + + @Inject(method = "parse", at = @At("RETURN")) + private static void iris$parseTerrain(LevelHeightAccessor height, PalettedContainerFactory containers, + CompoundTag tag, CallbackInfoReturnable callback) { + SerializableChunkData parsed = callback.getReturnValue(); + if (parsed != null) { + ((NativeTerrainReceiptHolder) (Object) parsed).iris$setStructureActivation( + tag.getLongOr(NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY, 0)); + ((NativeTerrainReceiptHolder) (Object) parsed).iris$setNaturalTerrain( + tag.getByteArray(NativeTerrainReceipt.NBT_KEY).orElse(null)); + } + } + + @Inject(method = "copyOf", at = @At("RETURN")) + private static void iris$copyTerrain(ServerLevel level, ChunkAccess chunk, + CallbackInfoReturnable callback) { + ((NativeTerrainReceiptHolder) (Object) callback.getReturnValue()).iris$setStructureActivation( + ModdedNativeTerrainReceipts.structureActivation(chunk)); + ((NativeTerrainReceiptHolder) (Object) callback.getReturnValue()).iris$setNaturalTerrain( + ModdedNativeTerrainReceipts.get(chunk)); + } + + @Inject(method = "read", at = @At("RETURN")) + private void iris$restoreTerrain(ServerLevel level, PoiManager points, RegionStorageInfo storage, ChunkPos position, + CallbackInfoReturnable callback) { + ModdedNativeTerrainReceipts.set(callback.getReturnValue(), iris$naturalTerrain); + ModdedNativeTerrainReceipts.setStructureActivation(callback.getReturnValue(), iris$structureActivation); + } + + @Inject(method = "write", at = @At("RETURN")) + private void iris$writeTerrain(CallbackInfoReturnable callback) { + if (iris$structureActivation > 0) { + callback.getReturnValue().putLong(NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY, iris$structureActivation); + } + if (iris$naturalTerrain != null) { + callback.getReturnValue().putByteArray(NativeTerrainReceipt.NBT_KEY, iris$naturalTerrain.clone()); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/WorldGenerationWriteGuardMixin.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/WorldGenerationWriteGuardMixin.java new file mode 100644 index 000000000..8aac2b380 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/WorldGenerationWriteGuardMixin.java @@ -0,0 +1,25 @@ +package art.arcane.iris.modded.mixin; + +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(WorldGenRegion.class) +public abstract class WorldGenerationWriteGuardMixin { + @Inject(method = "ensureCanWrite", at = @At("RETURN"), cancellable = true) + private void iris$guardCompletedTerrain(BlockPos position, CallbackInfoReturnable result) { + WorldGenRegion region = (WorldGenRegion) (Object) this; + if (result.getReturnValue() && region.getLevel().getChunkSource().getGenerator() + instanceof IrisModdedChunkGenerator generator + && region.getChunk(position.getX() >> 4, position.getZ() >> 4) + .getPersistedStatus().isOrAfter(ChunkStatus.FULL) + && !generator.allowsNativeChunkWrite(position.getX() >> 4, position.getZ() >> 4)) { + result.setReturnValue(false); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java index 474690798..8733bc044 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java @@ -23,6 +23,10 @@ import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.tools.WorldMaintenance; import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.history.SavedTerrainChunk; +import art.arcane.iris.modded.ModdedSavedTerrainCapture; + +import java.util.concurrent.CompletableFuture; import art.arcane.iris.engine.framework.EnginePlatformHooks; import art.arcane.iris.engine.framework.NativeStructureVolume; import art.arcane.iris.engine.object.IrisDimension; @@ -64,6 +68,16 @@ public final class ModdedStudioHotloadService implements ModdedTickableService, private volatile ExecutorService executor; private long lastPollAt; + @Override + public CompletableFuture flushSavedTerrainCapture(Engine engine) { + return ModdedSavedTerrainCapture.flush(engine); + } + + @Override + public CompletableFuture captureSavedTerrainChunk(Engine engine, int chunkX, int chunkZ) { + return ModdedSavedTerrainCapture.capture(engine, chunkX, chunkZ); + } + @Override public void onEnable() { if (executor != null) { diff --git a/adapters/modded-common/src/main/resources/irisworldgen.entity.mixins.json b/adapters/modded-common/src/main/resources/irisworldgen.entity.mixins.json index e7b5a28e2..89973faef 100644 --- a/adapters/modded-common/src/main/resources/irisworldgen.entity.mixins.json +++ b/adapters/modded-common/src/main/resources/irisworldgen.entity.mixins.json @@ -4,6 +4,11 @@ "package": "art.arcane.iris.modded.mixin", "compatibilityLevel": "JAVA_21", "mixins": [ + "WorldGenerationWriteGuardMixin", + "ChunkMapTerrainReceiptAccess", + "ChunkTerrainReceiptMixin", + "LevelChunkTerrainReceiptMixin", + "SerializableTerrainReceiptMixin", "EntityPersistenceMixin", "LivingEntityLootMixin", "MobAwarenessMixin", 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 4f74a5172..25fb4a9b3 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,16 @@ package art.arcane.iris.modded; import art.arcane.volmlib.util.collection.KSet; +import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217; +import art.arcane.iris.engine.history.GenerationEpochContractFactory; +import art.arcane.iris.engine.history.GenerationRegistryContract; +import art.arcane.iris.engine.history.GenerationRegistryContractFactory; +import art.arcane.iris.engine.object.IrisCustomBiomeAliasResolver; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisDimensionType; +import art.arcane.iris.spi.PlatformGenerationRegistry; +import java.lang.reflect.Constructor; +import java.util.Set; import org.junit.Test; import java.io.IOException; @@ -39,6 +49,33 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public class ModdedForcedDatapackTest { + @Test + public void retainedDimensionSourceRendersBeforeNativeRegistryExists() throws Exception { + DataFixerV1217 fixer = new DataFixerV1217(); + IrisDimensionType type = new IrisDimension().getDimensionType(); + String source = type.toJson(fixer); + GenerationRegistryContract.PhysicalResourceKey key = new GenerationRegistryContract.PhysicalResourceKey( + GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY, "iris:test_type"); + GenerationRegistryContract.GeneratedSource saved = new GenerationRegistryContract.GeneratedSource( + GenerationRegistryContractFactory.DIMENSION_TYPE_EFFECTIVE_SOURCE_SCHEMA, + GenerationEpochContractFactory.fingerprintDimensionType(type), + GenerationEpochContractFactory.dimensionTypeSemanticJson(type), "test-renderer", + GenerationRegistryContractFactory.fingerprintDefinition( + key, PlatformGenerationRegistry.Definition.exactJson(source), "generated"), source); + GenerationRegistryContract contract = GenerationRegistryContract.fromDefinitionsAndGeneratedSources( + Map.of(key, GenerationRegistryContractFactory.fingerprintGeneratedSemantic(key, saved)), + Map.of(key, saved)); + Class stageType = Class.forName(ModdedForcedDatapack.class.getName() + "$PackStageContext"); + Constructor constructor = stageType.getDeclaredConstructor(Path.class, String.class, String.class, + String.class, boolean.class, boolean.class, boolean.class, Set.class, GenerationRegistryContract.class); + constructor.setAccessible(true); + IrisCustomBiomeAliasResolver resolver = (IrisCustomBiomeAliasResolver) constructor.newInstance( + Path.of("retained"), "test", "test", "iris:test_type", false, false, true, Set.of(), contract); + assertEquals(source, resolver.generatedSource(key.registryKey(), key.resourceKey(), "{}", fixer)); + assertThrows(IOException.class, () -> resolver.generatedSource( + key.registryKey(), "iris:missing_type", "{}", fixer)); + } + @Test public void packScopedIdsCannotCollideAndHaveReadableLabels() { String first = ModdedWorldgenIds.presetRef("overworld", "overworld"); diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedGenerationHistoryRoutingContractTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedGenerationHistoryRoutingContractTest.java index 57944bfae..552b0bbae 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedGenerationHistoryRoutingContractTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedGenerationHistoryRoutingContractTest.java @@ -25,7 +25,15 @@ public class ModdedGenerationHistoryRoutingContractTest { assertTrue(createHistoryEngine.contains("IrisBoundarySignatureSampler.INSTANCE")); assertTrue(createHistoryEngine.contains("getGenerationTransitionWidthBlocks()")); assertTrue(createHistoryEngine.contains("router.preloadActiveRuntimes();")); - assertBefore(createHistoryEngine, "close(candidate);", "ModdedGenerationHistoryStorage.resolveActive(history)"); + assertBefore(createHistoryEngine, "history.prepareCurrentGenerator(", + "opened = ModdedGenerationHistoryStorage.resolveActive(history)"); + assertBefore(createHistoryEngine, "opened = ModdedGenerationHistoryStorage.resolveActive(history)", + "IrisEngine candidate = buildEngine(level, seed, opened)"); + String promoted = method(createHistoryEngine, + "if (history.activeActivation().activationId() != openedActivationId)"); + assertBefore(promoted, "close(candidate);", "ModdedGenerationHistoryStorage.resolveActive(history)"); + assertBefore(promoted, "ModdedGenerationHistoryStorage.resolveActive(history)", + "candidate = buildEngine(level, seed, promoted)"); assertTrue(createHistoryEngine.contains("GenerationHistoryRuntimeRouter.attach(")); assertTrue(buildEngine.contains("active.packRoot().toFile()")); assertTrue(buildEngine.contains("history.paths().activationMantleRoot(activation.activationId())")); @@ -84,10 +92,16 @@ public class ModdedGenerationHistoryRoutingContractTest { assertTrue(fill.contains("generateTerrain(chunk, generationEngine, pos, air, route)")); assertTrue(fill.contains("return closeRouteOnCompletion(pipeline, route);")); assertTrue(terrain.contains("openHistoryRuntimeScope(route)")); - assertBefore(terrain, "generationEngine.generate(", "route.claimGeneratedSemantics();"); - assertBefore(terrain, "route.claimGeneratedSemantics();", "return chunk;"); + assertBefore(terrain, "generationEngine.generate(", "writeBlocks(chunk, blocks, dimMinY, height)"); + assertBefore(terrain, "writeBlocks(chunk, blocks, dimMinY, height)", "route.claimGeneratedSemantics("); + assertTrue(terrain.contains("chunk.getBlockState(position.set(x, dimMinY + y, z))")); + assertTrue(terrain.contains("state.isAir() || state.liquid()")); + assertBefore(terrain, "route.claimGeneratedSemantics(", "ModdedNativeTerrainReceipts.persist(chunk, route)"); + assertBefore(terrain, "ModdedNativeTerrainReceipts.persist(chunk, route)", "return chunk;"); assertTrue(completion.contains("pipeline.whenComplete(")); assertTrue(completion.contains("route.close();")); + assertBefore(completion, "pipeline.whenComplete(", "route.detachThread()"); + assertBefore(completion, "route.detachThread()", "return completion;"); } @Test @@ -108,7 +122,7 @@ public class ModdedGenerationHistoryRoutingContractTest { } @Test - public void discreteStagesResumeIncompleteHistoricalChunksAndRejectTransitionOwnership() throws IOException { + public void pendingNativeStagesPreserveSavedStartsAndGuardNewFeatureFootprints() throws IOException { String generator = source("IrisModdedChunkGenerator.java"); String discrete = method(generator, "private boolean allowsRoutedDiscreteGeneration("); String decoration = method(generator, "public void applyBiomeDecoration("); @@ -117,19 +131,28 @@ public class ModdedGenerationHistoryRoutingContractTest { String nativeStage = source("ModdedNativeStructureStage.java"); String adjust = method(nativeStage, "void adjustGeneratedStructures("); String place = method(nativeStage, "void placeVanillaStructures("); + String savedStart = method(nativeStage, "private boolean isHistoricalStructureStart("); assertTrue(discrete.contains("chunk.getPersistedStatus().isOrAfter(stage)")); assertFalse(discrete.contains("router.history().activeActivation().activationId()")); assertFalse(discrete.contains("return true;")); assertTrue(discrete.contains("allowsNewGenerationChunk(")); + assertTrue(discrete.contains("NativeGenerationWriteGuard.allowsPendingStage(current, chunk, stage)")); assertTrue(decoration.contains("ChunkStatus.FEATURES")); assertTrue(structures.contains("ChunkStatus.STRUCTURE_STARTS")); assertTrue(mobs.contains("ChunkStatus.SPAWN")); - assertBefore(decoration, "allowsRoutedDiscreteGeneration(", "nativeStructures.placeVanillaStructures("); + assertBefore(decoration, "chunk.getPersistedStatus().isOrAfter(ChunkStatus.FEATURES)", + "nativeStructures.placeVanillaStructures("); + assertBefore(decoration, "nativeStructures.placeVanillaStructures(", "allowsRoutedDiscreteGeneration("); + assertBefore(decoration, "allowsRoutedDiscreteGeneration(", "importedFeatures.run("); + assertBefore(decoration, "NativeGenerationWriteGuard.allowsDecoration(", "importedFeatures.run("); assertBefore(structures, "allowsRoutedDiscreteGeneration(", "super.createStructures("); assertBefore(mobs, "allowsRoutedDiscreteGeneration(", "NaturalSpawner.spawnMobsForChunkGeneration("); assertBefore(adjust, "allowsNewGenerationFootprint(", "recordWorldCheckStructureShift("); - assertTrue(place.contains("allowsNewGenerationFootprint(")); + assertBefore(place, "!isHistoricalStructureStart(current, world, start)", "allowsNewGenerationFootprint("); + assertTrue(savedStart.contains("world.getChunk(origin.x(), origin.z(), ChunkStatus.EMPTY, false)")); + assertTrue(savedStart.contains("ModdedNativeTerrainReceipts.structureActivation(source)")); + assertBefore(structures, "super.createStructures(", "ModdedNativeTerrainReceipts.persistStructureActivation("); } private static void assertRuntimeRoute(String source, String signature) { diff --git a/adapters/neoforge/build.gradle b/adapters/neoforge/build.gradle index b68018daa..81eff1590 100644 --- a/adapters/neoforge/build.gradle +++ b/adapters/neoforge/build.gradle @@ -26,7 +26,7 @@ plugins { alias(libs.plugins.shadow) } -apply from: file('../../gradle/modded-generation-kernel-seals.gradle') +apply from: file('../../gradle/modded-generation-build-revision.gradle') Properties rootProperties = new Properties() file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) } diff --git a/build.gradle b/build.gradle index ff1c3fa8f..7b8a375be 100644 --- a/build.gradle +++ b/build.gradle @@ -113,6 +113,7 @@ nmsBindings.each { key, value -> compileOnly(rootProject.libs.annotations) compileOnly(rootProject.libs.byteBuddy.core) testImplementation('junit:junit:4.13.2') + testImplementation('org.mockito:mockito-core:5.23.0') testImplementation(project(':core')) testCompileOnly(volmLibCoordinate) { transitive = false @@ -427,7 +428,7 @@ tasks.named('neoforgeJar').configure { List commonModdedArtifactEntries = [ 'META-INF/iris/generation-kernels/catalog.tsv', - 'META-INF/iris/generation-kernels/abi-1.seal', + 'META-INF/iris/generation-kernels/abi-1.revision', 'art/arcane/iris/engine/IrisEngine.class', 'art/arcane/iris/spi/IrisPlatform.class', 'art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class', @@ -533,7 +534,7 @@ tasks.register('verifyModdedArtifacts') { // them, so a shadowJar exclude that drops one is silent until a server hits that path. List requiredBukkitArtifactEntries = [ 'META-INF/iris/generation-kernels/catalog.tsv', - 'META-INF/iris/generation-kernels/abi-1.seal', + 'META-INF/iris/generation-kernels/abi-1.revision', 'plugin.yml', 'paper-plugin.yml', 'slimjar.dat', diff --git a/buildSrc/src/main/java/art/arcane/iris/buildtools/GenerationKernelSeal.java b/buildSrc/src/main/java/art/arcane/iris/buildtools/GenerationBuildRevision.java similarity index 91% rename from buildSrc/src/main/java/art/arcane/iris/buildtools/GenerationKernelSeal.java rename to buildSrc/src/main/java/art/arcane/iris/buildtools/GenerationBuildRevision.java index 8880d003c..d9b96e6e4 100644 --- a/buildSrc/src/main/java/art/arcane/iris/buildtools/GenerationKernelSeal.java +++ b/buildSrc/src/main/java/art/arcane/iris/buildtools/GenerationBuildRevision.java @@ -24,18 +24,18 @@ import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -public final class GenerationKernelSeal { - public static final String FORMAT = "iris-generation-kernel-seal-v1"; +public final class GenerationBuildRevision { + public static final String FORMAT = "iris-generation-build-revision-v1"; private static final Pattern HASH = Pattern.compile("[0-9a-f]{64}"); - private static final long MAXIMUM_SEAL_BYTES = 8L * 1024L * 1024L; + private static final long MAXIMUM_REVISION_BYTES = 8L * 1024L * 1024L; private static final int MAXIMUM_ENTRIES = 30_000; - private GenerationKernelSeal() { + private GenerationBuildRevision() { } public static void main(String[] arguments) throws IOException { if (arguments.length < 4 || arguments.length % 2 != 0 || !arguments[0].equals("verify-dependencies")) { - throw new IllegalArgumentException("Expected verify-dependencies pairs."); + throw new IllegalArgumentException("Expected verify-dependencies pairs."); } Map dependencies = new TreeMap<>(); for (int index = 2; index < arguments.length; index += 2) { @@ -46,8 +46,8 @@ public final class GenerationKernelSeal { verifyDependencySubset(Path.of(arguments[1]), dependencies); } - public static void verifyDependencySubset(Path seal, Map dependencies) throws IOException { - SourceManifest expected = read(seal); + public static void verifyDependencySubset(Path revision, Map dependencies) throws IOException { + SourceManifest expected = read(revision); for (Map.Entry dependency : dependencyHashes(dependencies).entrySet()) { if (!Objects.equals(expected.dependencies().get(dependency.getKey()), dependency.getValue())) { throw new IOException("Resolved platform dependency does not match generation ABI " + expected.abi() @@ -75,7 +75,7 @@ public final class GenerationKernelSeal { AlgorithmVersion algorithms = new AlgorithmVersion(positive(current[2]), positive(current[3])); SourceManifest selected = manifests.get(currentAbi); if (selected == null || !selected.algorithms().contains(algorithms)) { - throw new IllegalArgumentException("Current generation version has no matching sealed factory."); + throw new IllegalArgumentException("Current generation version has no matching build factory."); } TreeSet registered = new TreeSet<>(); int previous = 0; @@ -92,7 +92,7 @@ public final class GenerationKernelSeal { registered.add(abi); } if (!registered.equals(manifests.keySet())) { - throw new IllegalArgumentException("Kernel catalog must register exactly the retained sealed kernels."); + throw new IllegalArgumentException("Kernel catalog must register exactly the packaged kernels."); } } catch (IllegalArgumentException failure) { throw new IOException("Invalid generation kernel catalog.", failure); @@ -116,33 +116,32 @@ public final class GenerationKernelSeal { sourceHashes, dependencyHashes(dependencies)); } - public static void verify(Path repository, Path seal, Map dependencies) throws IOException { - SourceManifest expected = read(seal); + public static void verifySnapshot(Path repository, Path revision, Map dependencies) throws IOException { + SourceManifest expected = read(revision); SourceManifest actual = capture(new CaptureOptions(repository, expected.abi(), expected.factoryClass(), expected.algorithms(), expected.roots(), expected.exclusions()), dependencies); List failures = new ArrayList<>(); differences("source", expected.sources(), actual.sources(), failures); differences("dependency", expected.dependencies(), actual.dependencies(), failures); if (!failures.isEmpty()) { - throw new IOException("Generation ABI " + expected.abi() + " no longer matches its sealed implementation:\n" - + String.join("\n", failures) - + "\nRetain this kernel's implementation and add a separately versioned kernel for changed generation."); + throw new IOException("Generation build inputs changed after revision capture:\n" + + String.join("\n", failures)); } } - public static void writeNew(Path destination, SourceManifest manifest) throws IOException { + public static void write(Path destination, SourceManifest manifest) throws IOException { Files.createDirectories(destination.toAbsolutePath().getParent()); Files.writeString(destination, encode(manifest), StandardCharsets.UTF_8, - StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); } public static SourceManifest read(Path path) throws IOException { - if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.size(path) > MAXIMUM_SEAL_BYTES) { - throw new IOException("Missing or oversized generation kernel seal: " + path); + if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.size(path) > MAXIMUM_REVISION_BYTES) { + throw new IOException("Missing or oversized generation build revision: " + path); } List lines = Files.readAllLines(path, StandardCharsets.UTF_8); if (lines.size() < 4 || !FORMAT.equals(lines.getFirst()) || lines.size() > MAXIMUM_ENTRIES) { - throw new IOException("Invalid generation kernel seal: " + path); + throw new IOException("Invalid generation build revision: " + path); } int abi = 0; String factory = null; @@ -155,7 +154,7 @@ public final class GenerationKernelSeal { for (int index = 1; index < lines.size(); index++) { String[] entry = lines.get(index).split("\t", -1); if (entry.length < 2 || entry.length > 3) { - throw new IllegalArgumentException("Invalid seal entry."); + throw new IllegalArgumentException("Invalid revision entry."); } switch (entry[0]) { case "abi" -> { @@ -180,16 +179,16 @@ public final class GenerationKernelSeal { case "exclude" -> addPath(exclusions, entry); case "source" -> addHash(sources, entry, true); case "dependency" -> addHash(dependencies, entry, false); - default -> throw new IllegalArgumentException("Unknown generation kernel seal entry."); + default -> throw new IllegalArgumentException("Unknown generation build revision entry."); } } SourceManifest manifest = new SourceManifest(abi, factory, algorithms, roots, exclusions, sources, dependencies); if (!Files.readString(path, StandardCharsets.UTF_8).equals(encode(manifest))) { - throw new IllegalArgumentException("Generation kernel seal is not canonical."); + throw new IllegalArgumentException("Generation build revision is not canonical."); } return manifest; } catch (RuntimeException failure) { - throw new IOException("Invalid generation kernel seal: " + path, failure); + throw new IOException("Invalid generation build revision: " + path, failure); } } @@ -352,11 +351,11 @@ public final class GenerationKernelSeal { private static void addHash(Map hashes, String[] entry, boolean path) { if (entry.length != 3 || !HASH.matcher(entry[2]).matches()) { - throw new IllegalArgumentException("Invalid sealed hash."); + throw new IllegalArgumentException("Invalid revision hash."); } String key = path ? relative(entry[1]) : token(entry[1]); if (hashes.putIfAbsent(key, entry[2]) != null) { - throw new IllegalArgumentException("Duplicate sealed entry."); + throw new IllegalArgumentException("Duplicate revision entry."); } } @@ -377,9 +376,9 @@ public final class GenerationKernelSeal { } private static String token(String value) { - String required = Objects.requireNonNull(value, "seal token"); + String required = Objects.requireNonNull(value, "revision token"); if (required.isBlank() || required.chars().anyMatch(Character::isWhitespace)) { - throw new IllegalArgumentException("Invalid generation seal token."); + throw new IllegalArgumentException("Invalid generation revision token."); } return required; } @@ -442,7 +441,7 @@ public final class GenerationKernelSeal { sources = Map.copyOf(sources); dependencies = Map.copyOf(dependencies); if (sources.isEmpty() || dependencies.isEmpty() || sources.size() + dependencies.size() > MAXIMUM_ENTRIES) { - throw new IllegalArgumentException("Invalid generation kernel seal entry count."); + throw new IllegalArgumentException("Invalid generation build revision entry count."); } } } diff --git a/buildSrc/src/main/java/art/arcane/iris/buildtools/GenerationRevisionScope.java b/buildSrc/src/main/java/art/arcane/iris/buildtools/GenerationRevisionScope.java new file mode 100644 index 000000000..cac3eb581 --- /dev/null +++ b/buildSrc/src/main/java/art/arcane/iris/buildtools/GenerationRevisionScope.java @@ -0,0 +1,57 @@ +package art.arcane.iris.buildtools; + +import java.nio.file.Path; +import java.util.List; + +public final class GenerationRevisionScope { + private static final List SOURCE_ROOTS = List.of( + "lombok.config", + "core/agent/build.gradle", + "core/agent/src/main/java", + "core/src/main/java", + "spi/src/main/java", + "adapters/bukkit/plugin/src/main/java", + "adapters/bukkit/nms/v26_2_R1/src/main/java", + "adapters/minecraft-common/src/main/java", + "adapters/modded-common/src/main/java", + "adapters/client-common/src/main/java", + "adapters/fabric/src/main/java", + "adapters/forge/src/main/java", + "adapters/neoforge/src/main/java" + ); + private static final List OPERATIONAL_EXCLUSIONS = List.of( + "core/src/main/java/art/arcane/iris/engine/EngineDiagnostics.java", + "core/src/main/java/art/arcane/iris/engine/EngineMetricsReport.java", + "core/src/main/java/art/arcane/iris/core/IrisStartupValidation.java", + "core/src/main/java/art/arcane/iris/core/safeguard/IrisSafeguard.java", + "core/src/main/java/art/arcane/iris/core/safeguard/Mode.java", + "core/src/main/java/art/arcane/iris/core/safeguard/task/Task.java", + "core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java", + "core/src/main/java/art/arcane/iris/core/safeguard/task/Diagnostic.java", + "core/src/main/java/art/arcane/iris/core/safeguard/task/ValueWithDiagnostics.java", + "core/src/main/java/art/arcane/iris/core/splash/IrisSplashRenderer.java", + "adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NmsWorldLifecycle.java", + "adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java", + "adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPregen.java", + "adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java", + "adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java", + "adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java", + "adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java", + "adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/PaperCommandRegistrar.java", + "adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPack.java", + "adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java", + "adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandWhat.java", + "adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java", + "adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandEdit.java", + "adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDatapack.java" + ); + + private GenerationRevisionScope() { + } + + public static GenerationBuildRevision.CaptureOptions current(Path repository) { + return new GenerationBuildRevision.CaptureOptions(repository, 1, + "art.arcane.iris.engine.history.GenerationKernelV1", + List.of(new GenerationBuildRevision.AlgorithmVersion(1, 1)), SOURCE_ROOTS, OPERATIONAL_EXCLUSIONS); + } +} diff --git a/buildSrc/src/test/java/GenerationKernelSealTest.java b/buildSrc/src/test/java/GenerationBuildRevisionTest.java similarity index 53% rename from buildSrc/src/test/java/GenerationKernelSealTest.java rename to buildSrc/src/test/java/GenerationBuildRevisionTest.java index f83f6ca56..17ed01d8b 100644 --- a/buildSrc/src/test/java/GenerationKernelSealTest.java +++ b/buildSrc/src/test/java/GenerationBuildRevisionTest.java @@ -1,4 +1,4 @@ -import art.arcane.iris.buildtools.GenerationKernelSeal; +import art.arcane.iris.buildtools.GenerationBuildRevision; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -17,20 +17,20 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -public class GenerationKernelSealTest { +public class GenerationBuildRevisionTest { @Rule public final TemporaryFolder temporary = new TemporaryFolder(); @Test public void verifiesUnchangedSourceAndCanonicalDependencyContents() throws Exception { Fixture fixture = fixture(); - GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()); - String original = GenerationKernelSeal.artifactHash(fixture.dependency()); + GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()); + String original = GenerationBuildRevision.artifactHash(fixture.dependency()); artifact(fixture.dependency(), "same code", "another version", 1_700_000_000_000L); - assertEquals(original, GenerationKernelSeal.artifactHash(fixture.dependency())); - GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()); - assertEquals(GenerationKernelSeal.fingerprint(GenerationKernelSeal.read(fixture.seal())), - GenerationKernelSeal.fingerprint(GenerationKernelSeal.capture(fixture.options(), fixture.dependencies()))); + assertEquals(original, GenerationBuildRevision.artifactHash(fixture.dependency())); + GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()); + assertEquals(GenerationBuildRevision.fingerprint(GenerationBuildRevision.read(fixture.revision())), + GenerationBuildRevision.fingerprint(GenerationBuildRevision.capture(fixture.options(), fixture.dependencies()))); } @Test @@ -39,120 +39,118 @@ public class GenerationKernelSealTest { Path source = fixture.root().resolve("generation/Noise.java"); Files.writeString(source, "class Noise { int value = 2; }"); assertTrue(assertThrows(IOException.class, - () -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies())) + () -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies())) .getMessage().contains("source changed")); Files.writeString(source, "class Noise { int value = 1; }"); Path added = fixture.root().resolve("generation/Added.java"); Files.writeString(added, "class Added {}"); assertTrue(assertThrows(IOException.class, - () -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies())) + () -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies())) .getMessage().contains("source added")); Files.delete(added); Files.delete(source); assertThrows(IOException.class, - () -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies())); + () -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies())); } @Test public void rejectsChangedAddedAndDeletedDependencies() throws Exception { Fixture fixture = fixture(); - String original = GenerationKernelSeal.artifactHash(fixture.dependency()); + String original = GenerationBuildRevision.artifactHash(fixture.dependency()); artifact(fixture.dependency(), "changed code", "same version", 0L); - assertNotEquals(original, GenerationKernelSeal.artifactHash(fixture.dependency())); + assertNotEquals(original, GenerationBuildRevision.artifactHash(fixture.dependency())); assertTrue(assertThrows(IOException.class, - () -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies())) + () -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies())) .getMessage().contains("dependency changed")); artifact(fixture.dependency(), "same code", "same version", 0L); - assertThrows(IOException.class, () -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), + assertThrows(IOException.class, () -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), Map.of("math", fixture.dependency(), "new-math", fixture.dependency()))); assertThrows(IOException.class, - () -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), Map.of())); + () -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), Map.of())); } @Test public void rejectsMultiReleaseManifestChanges() throws Exception { Fixture fixture = fixture(); - String original = GenerationKernelSeal.artifactHash(fixture.dependency()); + String original = GenerationBuildRevision.artifactHash(fixture.dependency()); artifact(fixture.dependency(), "same code", "same version\r\nMulti-Release: true", 0L); - assertNotEquals(original, GenerationKernelSeal.artifactHash(fixture.dependency())); + assertNotEquals(original, GenerationBuildRevision.artifactHash(fixture.dependency())); assertThrows(IOException.class, - () -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies())); + () -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies())); } @Test public void rejectsAgentEntrypointAndCapabilityChanges() throws Exception { Fixture fixture = fixture(); - String original = GenerationKernelSeal.artifactHash(fixture.dependency()); + String original = GenerationBuildRevision.artifactHash(fixture.dependency()); for (String attribute : List.of("Agent-Class", "Premain-Class", "Launcher-Agent-Class", "Boot-Class-Path", "Can-Redefine-Classes", "Can-Retransform-Classes", "Can-Set-Native-Method-Prefix")) { artifact(fixture.dependency(), "same code", "same version\r\n" + attribute + ": changed", 0L); - assertNotEquals(attribute, original, GenerationKernelSeal.artifactHash(fixture.dependency())); + assertNotEquals(attribute, original, GenerationBuildRevision.artifactHash(fixture.dependency())); assertThrows(IOException.class, - () -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies())); + () -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies())); } } @Test - public void sealsExplicitAgentBuildInputsAndExecutableSources() throws Exception { + public void capturesExplicitAgentBuildInputsAndExecutableSources() throws Exception { Fixture fixture = fixture(); Path agentSource = fixture.root().resolve("agent/src/main/java/Installer.java"); Path agentBuild = fixture.root().resolve("agent/build.gradle"); Files.createDirectories(agentSource.getParent()); Files.writeString(agentSource, "class Installer {}"); Files.writeString(agentBuild, "plugins { id 'java' }"); - GenerationKernelSeal.CaptureOptions options = new GenerationKernelSeal.CaptureOptions( - fixture.root(), 1, "example.KernelV1", List.of(new GenerationKernelSeal.AlgorithmVersion(1, 1)), + GenerationBuildRevision.CaptureOptions options = new GenerationBuildRevision.CaptureOptions( + fixture.root(), 1, "example.KernelV1", List.of(new GenerationBuildRevision.AlgorithmVersion(1, 1)), List.of("generation", "agent/build.gradle", "agent/src/main/java"), List.of("generation/kernels")); - Path seal = fixture.root().resolve("agent.seal"); - GenerationKernelSeal.writeNew(seal, GenerationKernelSeal.capture(options, fixture.dependencies())); - GenerationKernelSeal.verify(fixture.root(), seal, fixture.dependencies()); + Path revision = fixture.root().resolve("agent.revision"); + GenerationBuildRevision.write(revision, GenerationBuildRevision.capture(options, fixture.dependencies())); + GenerationBuildRevision.verifySnapshot(fixture.root(), revision, fixture.dependencies()); Files.writeString(agentBuild, "plugins { id 'application' }"); assertThrows(IOException.class, - () -> GenerationKernelSeal.verify(fixture.root(), seal, fixture.dependencies())); + () -> GenerationBuildRevision.verifySnapshot(fixture.root(), revision, fixture.dependencies())); Files.writeString(agentBuild, "plugins { id 'java' }"); Files.writeString(agentSource, "class Installer { boolean defers; }"); assertThrows(IOException.class, - () -> GenerationKernelSeal.verify(fixture.root(), seal, fixture.dependencies())); + () -> GenerationBuildRevision.verifySnapshot(fixture.root(), revision, fixture.dependencies())); } @Test public void checksTheActualPlatformDependencySubset() throws Exception { Fixture fixture = fixture(); - GenerationKernelSeal.verifyDependencySubset(fixture.seal(), fixture.dependencies()); - assertThrows(IOException.class, () -> GenerationKernelSeal.verifyDependencySubset(fixture.seal(), + GenerationBuildRevision.verifyDependencySubset(fixture.revision(), fixture.dependencies()); + assertThrows(IOException.class, () -> GenerationBuildRevision.verifyDependencySubset(fixture.revision(), Map.of("unknown-library", fixture.dependency()))); artifact(fixture.dependency(), "platform override", "same version", 0L); assertThrows(IOException.class, - () -> GenerationKernelSeal.verifyDependencySubset(fixture.seal(), fixture.dependencies())); + () -> GenerationBuildRevision.verifyDependencySubset(fixture.revision(), fixture.dependencies())); } @Test - public void catalogCannotSelectAnUnsealedAlgorithmTuple() throws Exception { + public void catalogRequiresAPackagedAlgorithmTuple() throws Exception { Fixture fixture = fixture(); Path catalog = fixture.root().resolve("catalog.tsv"); Files.writeString(catalog, "iris-generation-kernel-catalog-v1\ncurrent\t1\t1\t1\nkernel\t1\n"); - Map manifests = Map.of(1, GenerationKernelSeal.read(fixture.seal())); - GenerationKernelSeal.verifyCatalog(catalog, manifests); + Map manifests = Map.of(1, GenerationBuildRevision.read(fixture.revision())); + GenerationBuildRevision.verifyCatalog(catalog, manifests); Files.writeString(catalog, "iris-generation-kernel-catalog-v1\ncurrent\t1\t2\t1\nkernel\t1\n"); - assertThrows(IOException.class, () -> GenerationKernelSeal.verifyCatalog(catalog, manifests)); + assertThrows(IOException.class, () -> GenerationBuildRevision.verifyCatalog(catalog, manifests)); Files.writeString(catalog, "iris-generation-kernel-catalog-v1\ncurrent\t1\t1\t1\nkernel\t1\nkernel\t2\n"); - assertThrows(IOException.class, () -> GenerationKernelSeal.verifyCatalog(catalog, manifests)); + assertThrows(IOException.class, () -> GenerationBuildRevision.verifyCatalog(catalog, manifests)); } @Test - public void permitsSeparatelyScopedFutureKernelsWithoutChangingRetainedSeal() throws Exception { + public void recapturesChangedGenerationWithoutChangingAbi() throws Exception { Fixture fixture = fixture(); - Path future = fixture.root().resolve("generation/kernels/v2/NewMath.java"); - Files.createDirectories(future.getParent()); - Files.writeString(future, "class NewMath {}"); - GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()); - GenerationKernelSeal.SourceManifest next = GenerationKernelSeal.capture(new GenerationKernelSeal.CaptureOptions( - fixture.root(), 2, "example.KernelV2", List.of(new GenerationKernelSeal.AlgorithmVersion(2, 3)), - List.of("generation/kernels/v2"), List.of()), fixture.dependencies()); - assertNotEquals(GenerationKernelSeal.fingerprint(next), - GenerationKernelSeal.fingerprint(GenerationKernelSeal.read(fixture.seal()))); - assertThrows(IOException.class, - () -> GenerationKernelSeal.writeNew(fixture.seal(), next)); + String original = GenerationBuildRevision.fingerprint(GenerationBuildRevision.read(fixture.revision())); + Files.writeString(fixture.root().resolve("generation/Noise.java"), "class Noise { int value = 9; }"); + artifact(fixture.dependency(), "new generation code", "new version", 0L); + GenerationBuildRevision.SourceManifest next = GenerationBuildRevision.capture(fixture.options(), fixture.dependencies()); + GenerationBuildRevision.write(fixture.revision(), next); + GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()); + assertEquals(1, next.abi()); + assertNotEquals(original, GenerationBuildRevision.fingerprint(next)); + assertEquals(next, GenerationBuildRevision.read(fixture.revision())); } private Fixture fixture() throws Exception { @@ -161,13 +159,13 @@ public class GenerationKernelSealTest { Files.writeString(root.resolve("generation/Noise.java"), "class Noise { int value = 1; }"); Path dependency = root.resolve("math.jar"); artifact(dependency, "same code", "same version", 0L); - GenerationKernelSeal.CaptureOptions options = new GenerationKernelSeal.CaptureOptions( - root, 1, "example.KernelV1", List.of(new GenerationKernelSeal.AlgorithmVersion(1, 1)), + GenerationBuildRevision.CaptureOptions options = new GenerationBuildRevision.CaptureOptions( + root, 1, "example.KernelV1", List.of(new GenerationBuildRevision.AlgorithmVersion(1, 1)), List.of("generation"), List.of("generation/kernels")); - Path seal = root.resolve("abi-1.seal"); + Path revision = root.resolve("abi-1.revision"); Map dependencies = Map.of("math", dependency); - GenerationKernelSeal.writeNew(seal, GenerationKernelSeal.capture(options, dependencies)); - return new Fixture(root, seal, dependency, dependencies, options); + GenerationBuildRevision.write(revision, GenerationBuildRevision.capture(options, dependencies)); + return new Fixture(root, revision, dependency, dependencies, options); } private static void artifact(Path path, String content, String version, long timestamp) throws IOException { @@ -186,7 +184,7 @@ public class GenerationKernelSealTest { } } - private record Fixture(Path root, Path seal, Path dependency, Map dependencies, - GenerationKernelSeal.CaptureOptions options) { + private record Fixture(Path root, Path revision, Path dependency, Map dependencies, + GenerationBuildRevision.CaptureOptions options) { } } diff --git a/buildSrc/src/test/java/GenerationRevisionScopeTest.java b/buildSrc/src/test/java/GenerationRevisionScopeTest.java new file mode 100644 index 000000000..c8fec653f --- /dev/null +++ b/buildSrc/src/test/java/GenerationRevisionScopeTest.java @@ -0,0 +1,163 @@ +import art.arcane.iris.buildtools.GenerationRevisionScope; +import art.arcane.iris.buildtools.GenerationBuildRevision; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +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 GenerationRevisionScopeTest { + private static final String ENGINE_ROOT = "core/src/main/java/art/arcane/iris/engine/"; + private static final String NMS_ROOT = "adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/"; + private static final String COMMAND_ROOT = "adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/"; + + @Rule + public final TemporaryFolder temporary = new TemporaryFolder(); + + @Test + public void operationalChangesPreserveTheGenerationFingerprint() throws Exception { + Fixture fixture = fixture(); + String original = GenerationBuildRevision.fingerprint(GenerationBuildRevision.read(fixture.revision())); + for (String exclusion : fixture.options().exclusions()) { + if (!exclusion.endsWith(".java")) { + continue; + } + Path operationalSource = fixture.root().resolve(exclusion); + write(operationalSource, "class Operational { String message = \"updated\"; }"); + GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()); + assertEquals(exclusion, original, GenerationBuildRevision.fingerprint( + GenerationBuildRevision.capture(fixture.options(), fixture.dependencies()))); + Files.delete(operationalSource); + GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()); + } + } + + @Test + public void generationChangesProduceANewBuildRevision() throws Exception { + Fixture fixture = fixture(); + for (String relative : generationSources()) { + Path source = fixture.root().resolve(relative); + Files.writeString(source, "class Generation { int sample = 2; }"); + GenerationBuildRevision.SourceManifest changed = GenerationBuildRevision.capture(fixture.options(), fixture.dependencies()); + assertNotEquals(relative, GenerationBuildRevision.fingerprint(GenerationBuildRevision.read(fixture.revision())), + GenerationBuildRevision.fingerprint(changed)); + GenerationBuildRevision.write(fixture.revision(), changed); + GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()); + Files.writeString(source, "class Generation { int sample = 1; }"); + GenerationBuildRevision.write(fixture.revision(), GenerationBuildRevision.capture(fixture.options(), fixture.dependencies())); + } + } + + @Test + public void newSourcesChangeRevisionAcrossPlatforms() throws Exception { + Fixture fixture = fixture(); + for (String root : fixture.options().roots()) { + if (!root.endsWith("/java")) { + continue; + } + assertNewSourceChangesRevision(fixture, root + "/NewSource.java"); + } + assertNewSourceChangesRevision(fixture, ENGINE_ROOT + "NewDiagnostics.java"); + assertNewSourceChangesRevision(fixture, NMS_ROOT + "NmsWorldLifecycleHelper.java"); + assertNewSourceChangesRevision(fixture, COMMAND_ROOT + "NewCommand.java"); + assertNewSourceChangesRevision(fixture, "core/src/main/java/art/arcane/iris/core/safeguard/task/NewTask.java"); + } + + @Test + public void onlyExplicitOperationalFilesAreExcluded() throws Exception { + Fixture fixture = fixture(); + List directoryExclusions = fixture.options().exclusions().stream() + .filter(path -> !path.endsWith(".java")) + .toList(); + assertTrue(directoryExclusions.isEmpty()); + GenerationBuildRevision.SourceManifest manifest = GenerationBuildRevision.read(fixture.revision()); + for (String relative : generationSources()) { + assertTrue(relative, manifest.sources().containsKey(relative)); + } + assertFalse(manifest.sources().containsKey(ENGINE_ROOT + "EngineDiagnostics.java")); + assertFalse(manifest.sources().containsKey(NMS_ROOT + "NmsWorldLifecycle.java")); + assertFalse(manifest.sources().containsKey(COMMAND_ROOT + "CommandIris.java")); + } + + private static void assertNewSourceChangesRevision(Fixture fixture, String relative) throws IOException { + Path source = fixture.root().resolve(relative); + write(source, "class NewSource {}"); + GenerationBuildRevision.SourceManifest changed = GenerationBuildRevision.capture(fixture.options(), fixture.dependencies()); + assertTrue(changed.sources().containsKey(relative)); + assertNotEquals(GenerationBuildRevision.fingerprint(GenerationBuildRevision.read(fixture.revision())), + GenerationBuildRevision.fingerprint(changed)); + Files.delete(source); + } + + private Fixture fixture() throws Exception { + Path root = temporary.newFolder().toPath(); + GenerationBuildRevision.CaptureOptions options = GenerationRevisionScope.current(root); + for (String scope : options.roots()) { + Path path = root.resolve(scope); + if (scope.endsWith("/java")) { + Files.createDirectories(path); + } else { + write(path, "build input"); + } + } + for (String relative : generationSources()) { + write(root.resolve(relative), "class Generation { int sample = 1; }"); + } + for (String exclusion : options.exclusions()) { + if (exclusion.endsWith(".java")) { + write(root.resolve(exclusion), "class Operational {}"); + } + } + Path dependency = root.resolve("dependency.jar"); + try (ZipOutputStream output = new ZipOutputStream(Files.newOutputStream(dependency))) { + output.putNextEntry(new ZipEntry("generation/Math.class")); + output.write("generation dependency".getBytes(StandardCharsets.UTF_8)); + output.closeEntry(); + } + Map dependencies = Map.of("volmlib", dependency); + Path revision = root.resolve("abi-1.revision"); + GenerationBuildRevision.write(revision, GenerationBuildRevision.capture(options, dependencies)); + return new Fixture(root, revision, options, dependencies); + } + + private static List generationSources() { + return List.of( + ENGINE_ROOT + "IrisEngine.java", + ENGINE_ROOT + "IrisComplex.java", + ENGINE_ROOT + "IrisEngineMantle.java", + ENGINE_ROOT + "EngineRuntimeBuilder.java", + ENGINE_ROOT + "history/GenerationKernelV1.java", + ENGINE_ROOT + "object/IrisDimension.java", + ENGINE_ROOT + "mode/ModeOverworld.java", + "core/src/main/java/art/arcane/iris/core/loader/IrisData.java", + "core/src/main/java/art/arcane/iris/core/IrisSettings.java", + "core/agent/src/main/java/GenerationTransformer.java", + NMS_ROOT + "NmsGenerationHooks.java", + NMS_ROOT + "NmsGenerationRegistry.java", + NMS_ROOT + "NMSBinding.java", + "spi/src/main/java/art/arcane/iris/spi/PlatformGenerationRegistry.java", + "adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java" + ); + } + + private static void write(Path path, String content) throws IOException { + Files.createDirectories(path.getParent()); + Files.writeString(path, content); + } + + private record Fixture(Path root, Path revision, GenerationBuildRevision.CaptureOptions options, + Map dependencies) { + } +} diff --git a/buildSrc/src/test/java/JarCompactorTest.java b/buildSrc/src/test/java/JarCompactorTest.java index ac1763ded..e1a9dfb06 100644 --- a/buildSrc/src/test/java/JarCompactorTest.java +++ b/buildSrc/src/test/java/JarCompactorTest.java @@ -63,7 +63,7 @@ public class JarCompactorTest { writeEntry(output, "META-INF/MANIFEST.MF", "Manifest-Version: 1.0\r\nMulti-Release: true\r\n\r\n" .getBytes(StandardCharsets.UTF_8)); writeEntry(output, "JarCompactorTest.class", classResource.readAllBytes()); - writeEntry(output, "META-INF/iris/generation-kernels/abi-1.seal", "registry fixture".getBytes(StandardCharsets.UTF_8)); + writeEntry(output, "META-INF/iris/generation-kernels/abi-1.revision", "registry fixture".getBytes(StandardCharsets.UTF_8)); writeEntry(output, "example/caf\u00e9.txt", "unicode name".getBytes(StandardCharsets.UTF_8)); for (int index = 0; index < 100; index++) { writeEntry(output, "example/item-" + index + ".txt", ("value-" + index).getBytes(StandardCharsets.UTF_8)); diff --git a/core/build.gradle b/core/build.gradle index 2ebfd5b5d..42711770d 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -522,4 +522,4 @@ tasks.named('check').configure { dependsOn('bukkitPurityRatchet') } -apply from: rootProject.file('gradle/generation-kernel-seals.gradle') +apply from: rootProject.file('gradle/generation-build-revision.gradle') diff --git a/core/generation-kernels/abi-1.seal b/core/generation-kernels/abi-1.seal deleted file mode 100644 index 625bdb916..000000000 --- a/core/generation-kernels/abi-1.seal +++ /dev/null @@ -1,1589 +0,0 @@ -iris-generation-kernel-seal-v1 -abi 1 -factory art.arcane.iris.engine.history.GenerationKernelV1 -algorithm 1 1 -scope adapters/bukkit/nms/v26_2_R1/src/main/java -scope adapters/bukkit/plugin/src/main/java -scope adapters/client-common/src/main/java -scope adapters/fabric/src/main/java -scope adapters/forge/src/main/java -scope adapters/minecraft-common/src/main/java -scope adapters/modded-common/src/main/java -scope adapters/neoforge/src/main/java -scope core/agent/build.gradle -scope core/agent/src/main/java -scope core/src/main/java -scope lombok.config -scope spi/src/main/java -exclude core/src/main/java/art/arcane/iris/engine/kernels -source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java a8987a308db171229d6379bae2698ad827ee58b6f86eee4164f3e594aeaf522f -source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/DatapackStructureStateFilter.java b1ae6b3ab4e0ed1d0292681e90a875dbbf449fef29cc7af281ec01a34100fa14 -source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/ImportedFeatureStage.java 8eafc6ddd5dc3023e80fd97a718183e142c58998444502a835a8f8331a53ff5a -source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java 145cadbccef9253ca8b29ba441c164a27d2c50e6a7c2177b18d1b999c0391ef7 -source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java 0b0823ddc0b03c7788ec33678f70acda0a26979158bbefd3809058cbf898a9b5 -source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NativeStructureWorldgenAccess.java a33a05dfcd1467a7b27a3245b819d21d9dac6024306901bcaafc43ff0f61bebc -source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NmsGenerationRegistry.java 8e7aa89be6166c40801edbe3c76bc7ff767e6a80874bdfc454c98ac7615da297 -source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/VanillaStructureBiomes.java bc9ef399119dd325e9c7256ffb9c9d070022f30af7c23c3711aeb2f736ba724b -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java cf9660620417c5a5a081c54f3ae8489ed386df08053d47eac29fde15e0cf75c8 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/IrisBootstrap.java fb6aa0c9b69f639dffd20a83e03760b01b6b2c23374a21e3521fd7b92c66697b -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/IrisPluginLoader.java f544908a6ad92e6a0a3a67ff939d1efdba45fcbdd4c0fc8d7f908b60837559b8 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/pregen/IrisPregenPhase.java 7605129e12bdd0ac5df72c1db3a83b5f2789b9fbb22593c75b69b534ccb42d91 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/pregen/IrisPregenProgress.java b0a84a5276fbfaece298f62256d39df2dda0d265be2160266efeec32bd38a9df -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/pregen/IrisPregenerationEvent.java 9c84409db4c426c599e473e516ba74515d7ec0d3b8ea9bc3fab830ad1a7a6777 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnField.java ce08d675a0b64c5b4af3e61442018024eb3d11510447075a0bdf2da0c835a3a8 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnQuery.java dd029894680f30e0cd65778c781073fdfc733ee21b766e70ef7994704e4783ef -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnSample.java b4097827f74fd0ad8780b5e35c1b1ad82181bd1990d7615ba49169fe62edde04 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnSink.java b6546b3a05309039f10ade65464b51122a486f7db8221b3ef76dfaf694e2f324 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisRiverState.java 104e7591d9779dad06fed389fa3960081d8cba22de54a5c5bb738babe5593ae9 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisSurfaceKind.java 219c5ac38cf885a3e78ec5357aa8384b04af1faf8bcb95fd05470eaf4c138408 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisTerrainService.java 95aa8ee1efb2696651c4d823c33ed782dec0a3c24b82eaf8b4fd8efc237a86e1 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisWorldInfo.java 166bf8502b2ea31e4ce854430b0ed0d743226ee6771a08ab266b657ec8ffe606 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/tree/IrisTreeFellerService.java 29f687164a144b0a4c821e20c59e23889abd38083479f478773db3ea0a1cc57a -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/tree/TreeFellerAccess.java dfb2d56b4bea6d1f076ba6f249d5921b013ada315065fdd6b55cf614737b2460 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/tree/TreeFellerOptions.java 94390f221326d6809a7a77ab634ce566e013553f2517473ca7c920c7401fc88c -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/tree/TreeFellerRunHooks.java 99a0be61a7c605bebc22a94bd8ae79ff7ece2a1dcf4bbf4d0872a5e8f783d788 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/world/IrisWorldEngineEvent.java 13669ed862bf3d47b32918ab1261a21e8290b1665a4c9ee86fe7aa832350d579 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/world/IrisWorldPhase.java 1ae836e7b447ed9ba9f131a2ec1672a684e4547c6232b5da6593ad3197e72cc0 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/BukkitWorldReconciler.java b90fdaa6f38081a2e24cb58a6f9ad5f94571fa6f550902d6b195572c7ea483df -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisFailClosedChunkGenerator.java a8737b548c90bab2c7c51e5ec3350c741607df191063aaade3ce8150a49ab3c5 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisStartupAdmissionListener.java 90b2ad47d82a8ac0f2f7bf817d5f670070037fefe6f8918b5a6deb768ad83248 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java 1fd8650149eeb40f1ab5fb04e922a2b69d4c8f538c38eaa88bcd71d7fae51c6f -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/PaperWorldReplacementEntryListener.java f184d47447d80de0ef575240c460f87bba776c9c224347b8c090ad0d544a7ba4 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/PendingWorldDeleteQueue.java 7d66da829880666260dff9e08fb06abb0f2bc2c26a32ef0b2702c78c4f76e999 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/PendingWorldReplacementManager.java 984140ff6f2dc6ce9ce557a80f57cbe81c2d101b69e8612f11ee6a1da25e8e8e -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDatapack.java 636eedaa93e181d6657fe2387582edcc5c819cdd601c30270b9cddd356bedaa3 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java 29f7b506942ed071e2693fb4f88aec08f80940cd9d7fc02353563589f2e36e4f -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandEdit.java 485c3ec7d31eabac2fffefa07ff5ba2e69c7645353890827089dbace8514bf53 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java b1e9470c8a506f72e79cc412aef911583bd611e31d4a38e65ea1b6c3d650b586 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java f24acdca32add8f304cea3a7a6c81e7875f46ebef47e46feb23997158288e143 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java 88cb72e493fa598ac2b0ebf05e6cbe0be36247fab52defd127734bc1a5652791 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java d8615226c6e7072a5149da289ffa560525f88ca3a4731485a3c17daa2530ead5 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPack.java 380b057617e958e160ce6416590606b7b7dfd49d02dbe8893935a0a8dee98893 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPregen.java 36debe4af8794bff860cf08d27773e433c2298945ab896783808d17f6092eea4 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java f263d228a51ea1c8b9ea21cebfe52d088e47a87299da392e4954a74b1c25d1f1 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java afc5fd80cc1ff3c2e281b735ae7bab93c1e9e832a9ee2342f8f430a23588d68b -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandWhat.java a8b8d36327424a8508de312213edef79365ea01c29f9b06a1dbeb740035921b5 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/ObjectPasteBounds.java 8d63943167d469935c714429952ac6cd53f0a76ee5e329abb7aa9dcc73304151 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/PaperCommandRegistrar.java 8b924642b04211d6d55d4c584635db194f2ea9f322258102b1dc583bbd0426a3 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/edit/BlockEditor.java f7c6a5d0c62078d2eb9abc6792f27e3345a78f0523c53ba0a4e68b68f049c9c2 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/edit/BukkitBlockEditor.java ced058b70ec2019fbe7b17cd7de185ccde5ef4de058eb12cbba32c624ffc2f1c -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/gui/BukkitGuiHost.java 1dc3da7c0c35985791aabb394d494abba2538f51377c71bc27c8fe45f4fff638 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java 5fe5b7ea98afa17be8f47e4a7958bf99b707a81cd6465e49d975815821522e7e -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiExpansion.java 398ddd36b788886abec956f5645c9133b406c9ca40836749ea1ab0ef7e5f0174 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiInstaller.java cad212fdb9a74daf8e2f59735ecd587f3f799b6e151772f3525c8aae0adc22c9 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiListener.java 5d8f1322bf6d702e45693002584e9d78f1e7593fc23c8337e76ef45cec9a3a96 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiPosition.java 9b5bab4bc3ac4e08855f5f1a1a9946085bb86eff6a4599fb0fe311a87b8d5466 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiPregenView.java bdc1963226d7e33c27ce1b899e6b20b4462edd7d0adf8f8d2c28c26a3facdff2 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiState.java 459d916dfcb6c3641c7d941a0ad59c8743714c10f034d4771e4d0cacf43a0d0a -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiWorldView.java 7480e4ca0c5f237e7f66742a62e672e6fb54acb41f33911462b72e4bf7455ca5 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/runtime/BukkitEnginePlatformHooks.java 1442d67cb0e8ac7e3a7625cd1b91fd5cb18cd86e550acb9f5c10f1db0d5c8e17 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/CommandSVC.java b72fca8e7923955e95198644f1e2890bc61e7a31e1c58fcc583404331ed471ab -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/DatapackStructureScopeSVC.java 05524cc261099f8e205367e16d708d41943203e108f4248baa2a23da5662d935 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/EditSVC.java 7b94aa7643809fe7031a620a6859947853058a27687d345eb381a5cfa677072a -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/FellingRun.java f160336b3d2094ba65fc19e96ac79e3dab87c89747dd7ad9a516e633b0f3b0d8 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisApiEventSVC.java 8ace4038bf4f3ceb29f4e75b46f90a9b90689b761474901f316a2af41507c784 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java 0c0972e90f42084c5b8658654ff9d0683121816514d692cb38474592e7e7c635 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineStatus.java 974c4dc3d4a808cbbfc2ef0dfc27f8dfdcec20a996a3586e7d709566dd56d8b1 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisIntegrationService.java 13b9d92307cd9b6a13d668d06f0d2d5335f73f4eec803a791409c6fd198f6697 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisProtocolService.java 411d34c393cc68391aea3c7f0f53fd088a69e822ad0784e6b025d6bd55f9c72c -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisTelemetrySnapshot.java 3ef396b6a29c6e50e7ba33a0228d73e8b38fb527661772a18552da32d58b7d26 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisTerrainSVC.java 2ca03961fceb8a4a7f4f639c52b780b7b901430631164e0c6bf71477ff2c6bcb -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisWorldPhaseLedger.java 2e8dafa5aa698c22a16f4ab08283bd1b21ba1c18f19a2ad39e77fe8b7990fce9 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/RoutedBlockBreakEvent.java 704d8d4fcc062b0c2a31b95a8fab4293d96f46ba599dbc46b751b446cbefd3d0 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerModel.java b7987e53d2a71dfd499f7d79e56a225f9eef54872ae1a50dd499009d4551c084 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerPresentation.java 876913f1b67306708e370497cb341b7c9a21ca75f908ae7ff3b5efa93c4e8c30 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerSVC.java 5265c07e4130521454949bb85b01c130859fce9d4198ba13ac3ec0aa9c209652 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellingRunner.java e077b08c8e054f08ade0bfbf8b76c53d6249c8917284193a0104863048c8761d -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeProvenance.java 09efb12817c6e357f3b1a5bf718f65b1eb4e6b0a7febeb7bf187d149a3b802ea -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/WandSVC.java 13bba0bd8b9d71e83a4ba242ddb71096860bdce0ccfb51642360e41453075e2e -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisApiFaultGuard.java af15d9aae4fc97f846f142428ee5be4c31ff683adab9ba691337f83a36f74cef -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisColumnWalk.java 95b1200d20cead79b9877eb811e01e50fb867076f5a48a66f7e05816a84836f1 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisSampleLimits.java 3d24e9aaee06b15b07133f900a30ed4ed2e932ee366d3a7e5c4b1bb0c49ce95e -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisSurfaceClassifier.java 25ef003a93514248f155be51ef985c00ed3773acabdd05f16e64c70be452ce48 -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisWorldInfoFactory.java 91d90523ae4d33f064007fe0935a1c7b4ec4823a70e73d7cc8ca6f609a1aeb6f -source adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/wand/WandSelection.java 3fbddab00ba23d6ed0fdd2925c6b22615f96ab22dab44f1c54020daf62fec486 -source adapters/client-common/src/main/java/art/arcane/iris/client/ClientPacketSink.java e53e0e73de4c0cfe69563d4df47fc8a2ec048b890ba5c67939e1ff55670f57a0 -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisClient.java 157b86a3f7b21f60005b98657734aa1b8fccb4b87be1d7f1b84d31b0b6705867 -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientCursor.java 848de6e66c93b899d411263925f97396bba3e363247a470b937799252acdae9e -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientDimension.java f7b5c974e6aef3d66b5bce4fc4dadde5f0f5c097f9f2e51bac4d78313f67fa42 -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientHud.java 2c2a1fff1293e017d2792dab7b1c43c1ff9623db1affb3ee182c3abde91aa5a3 -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientKeybinds.java fcad5f33d4d6caadadee42b1416c73b9b799ffcf11f71a2a81e3c63bea14c37a -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientMarkers.java a78d59d10c3dbe5cdd8d7fc064e3bc6be272109c878cd6650429afbe32c9198c -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientPregenState.java 079ba030df365ba93fb4f6de71cda1ab8f44772cb630824d3092dde82a4d03e4 -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientRegionMap.java 99922bcb3b61c66849bd292565083c488caeb6bc2cc8a136426ffe0766e08ff9 -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientSession.java 204f38014894611db2cd76d1b8d76b4b256bf5ef53e427e59d4c3ca152ecdf42 -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientTileCache.java 95c253e464636b01e75ad03c011b4f02677158da4d2765deb5c1cb0f9cbe90b0 -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientToasts.java 7ba6be6083a6c51ad2688f722d100db900f1d06ecc906198f028eada3b7a9036 -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisPregenHud.java f628f7a580c94c32cbd039f1f7f243e7e40a7516e1dbe47d627d92a26db32b4b -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileAssembler.java 27f82c251b8f0213af75b98739add8362bc01376cac84bb5a11bc5e054264b46 -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileCodec.java 50b0673452f9965d1bcf63a373b8eabdaa6ecf3c5a595ce52714b17e0a7fb29f -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileImage.java 534c350e4f2c6e9e6f2e034acb61486017c7dbdf22313a7080f01499a3e9503c -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileKey.java cb7507b6e0c4778c2b57cb38902ec47bae31e18be6f23dfbf540f81963fd560c -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisToastPresenter.java 5c8e05933fc2e0f66b9a9d66da10045a27c11b075d9bd7ea9a2d9e380517f798 -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisVisionScreen.java 321b02feb9ae07614ed0da66901f3d50871a309c129bef72180627aea95d99dd -source adapters/client-common/src/main/java/art/arcane/iris/client/IrisWhatOverlay.java dbbf674ef480870340e5afdb80e53bb577cefc7ba78191b34257cfc9fd4174c4 -source adapters/client-common/src/main/java/art/arcane/iris/client/mixin/IrisWorldOpenFlowsMixin.java a28656a13456e7bc914752afa607e3de97b8325f282245f2331df37a6f3738c8 -source adapters/client-common/src/main/java/art/arcane/iris/client/mixin/IrisWorldTypeEntryMixin.java 80b32f7c7f1ae0a7febebe3b47fd273952370075c1020f798009829a92e61021 -source adapters/fabric/src/main/java/art/arcane/iris/fabric/FabricForcedDatapackSources.java d93b8386427aed2b34c301bf4adeb1bc25af5e677a39ea5f64bbe39d43d94ef6 -source adapters/fabric/src/main/java/art/arcane/iris/fabric/FabricModdedLoader.java a08e326281cceff349581ff809c7260b523ce6264d7858c5598dc478d37d673b -source adapters/fabric/src/main/java/art/arcane/iris/fabric/FabricProtocolNetworking.java 617687a1dbca4bf5e096edeb97023f674c597fbd83b18e74544c9a2410606304 -source adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricBootstrap.java 0fea90cb82830579ba06a369769d07c4a49eb2786d2e2ae6da65b7d3d18284d0 -source adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricClient.java fcd2c6284ebec5546b0d934d7b311077ca6d70a36688aa4e8924e71efdc0ea07 -source adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/BlockItemMixin.java 5e30d60f38a9224bf40d3acf974a141d408cfedbb9e11c688bad1ac11d9fb830 -source adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/BlockMixin.java 5d22c29b208fed4031eff56c146e92c7e497f198ad47723e5f61324eeb9a6881 -source adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/PackRepositoryMixin.java 88128b0cc77e20866951639211c51577a47ef09e63bbe45f9e7bd57b972b2956 -source adapters/forge/src/main/java/art/arcane/iris/forge/ForgeClientProtocol.java 96806e77af41eef59435442d4851ccdc7937c06717d776fbbdb2579e56069775 -source adapters/forge/src/main/java/art/arcane/iris/forge/ForgeModdedLoader.java 4d9e05cbfdf46b6c0335fbfb75b1b8312c34c06e5147658b0631a36c926e2fce -source adapters/forge/src/main/java/art/arcane/iris/forge/ForgeProtocolNetworking.java 75a08ac153d0bb84d3658d375a65ccf7ff0c4353da904ffcfaae402bea03b285 -source adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBlockLootModifier.java 98070e54a6b83860d2b7f9df1d90de0d8ba4e37f3da1ec159efcf5d9cc09108a -source adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBootstrap.java f7d1afc599c5bb291f0c02295c037a0eb858da8b53e45630ae21f9a8080404be -source adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeClient.java cab2c100b6bbe03090468ba057e91438444a1a4d323e02fc9e94f8d6527e372a -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/ForcedStructureChunkGenerator.java 2d2a6adf6bb9366f51c228b6405484bf8a73e1ab7779becec74fe1ee9c088e00 -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeBuildFutures.java cf8255340cb2eeb237e0636800156c96e09033e2d48be88ce3593138ea01ad0d -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureFactory.java b17b7f24eebcaf2ecdd5f0dec3ce2d5c7b2385686cf0c56b339c0443ce91d5e5 -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureFoundationBuilder.java b00e69a91688daf9b47c7971f2636b64c142d36017c3d298a1d14b9453fd692b -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureGenerationException.java d4ded11b20d19fce2e2c5b8c96b85c12ed071bb614932f61acbdeac875ff0a56 -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureGenerationKeys.java 4ddcac8ff0e401272bb143ae09e73331495cd6a4e3ad1b17460cf55ff034d900 -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureLocatePersistence.java d1f2ce22b1d21447701b79986695c79855538206b9eb01e3ad02362fcfbc2db2 -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureLocateResults.java 71093b9323be5e2f92c69165a13b438afc70239d7a48668cf1da672eb6bb1914 -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureOwnershipFingerprint.java b008c3457a63fe59bbc02df942452f7c615daa2a6da74c017e6f2305a145f8e5 -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureOwnershipRecovery.java 38b90d78bcf4d3e22c3657737bce25a617073ccbd22373bc8413c5bd6c85e4b0 -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java 273aba5c6e2cdd00e13ac6f367bfb00ad9039b4aa11cca22bd5867b189abc2cc -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureReferenceEnvelope.java e92c041793ddff8c45420daf00fc32b95c6f11431c9f8e7007dd78ef17230d2f -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureReferenceRepair.java 209b3392869bef825ceb944261a1c6946d464486c57b76793f2f5607e840d478 -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureReflection.java ffe81e06ab09f7be9f376700074e51199f6d27333cd2ceed5e2ab552ff16862f -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureStartInjector.java ae4ab15e68da59cc8b7e0c95f1138b3fcc534047fefd16c68f57e74cd1702c0e -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureSurfaceFitter.java 40ba10f4891b114234d9bdfcda0b931220bbbdb26f68daa26023a2a0e6599103 -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureSurfaceSupportBuilder.java 7bc8b260551b54365dc82aa015071ad978382b6f1be05d2801d8ca7e7195796a -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancy.java 72ac1096ffc7f06123d9f48b025ce06fa45d576602094cb6fbdac5b2a1025375 -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTemplatePoolBounds.java 9f5166543ee377c177feeea9fa004e8344d2fba5b127def8a4d831a8a61db8bf -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTerrainIntegrator.java e127042121cb1fbbf19a1e52444f3fb8c820fb7d381dbe7408cd06f07deb70e1 -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVanillaLocator.java 5202a908d731182b06b6628ca5305d530004c152d46eddb11876a77ca3923c94 -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVegetationClearer.java 1e7c570c4ef7427f94e2e96972c57a98caf5fc6b7be014509b968e1f017c15a6 -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVerticalPlacer.java 6f48f9f3e359e84e2da0b5bf234a9047f8f4aa2380b235f419f87a144a443bd9 -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVolumeIndex.java 0aa89c9698d8b9a28a7d8c26f04bdf61a06fae39f2e6dfa35bb68228aff28c65 -source adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/WorldgenTerrainHeightmaps.java 3633876842907318ce93545e5de9253476dc5a30ccb34fa447c0b6b0934fab11 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/InitialSpawnQueue.java a56f2efbb4976a27e0cd2233938a5e7266d547467a75bd7a242519dca6639567 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedBiomeSource.java 896269c3f60f4f102425296d9fbfe432b739c2b72346e4b1df82a6bce4ccacd7 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java cfa0912affbfa32a9b4c9edf070c082edf06629d5da5867b1ccb06ff67554294 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/MainWorldService.java 6e10c7e89850e997b6ea865dba2fbd80585a71b4e5ce7001def52132e4e88b77 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBiome.java f1415ce9307d56d6457b8902948bb6373589ccca6188512ab5d15522e116c45f -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBiomeWriter.java 7e1a9dc43673927cd2657ba7d649bf81684658cd97b0ad9d1af14680e1d892b9 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockBreakHandler.java b675c8ff18b4ddded000cd589d3846cba4c05989a9d43fe435c41a011db0bb14 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockBuffer.java d0917cf1af55a81cadf26159e27e712aa71118c2285d87c2ee3f7eb2ace91c18 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockResolution.java a16d498b5eb0dcf252a05ea93b2369cac592326395cba3bff88ea2a6628c37af -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockState.java a9e76d82bf984b223734dfb4900d1f08d0132f92a452d8e3f1af5468a4c00a11 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDeathLoot.java 12606b4eef4f3f683cfce0af53077b0fca486f989e19f07b42fa192e1b0d85d5 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDecoratorHooks.java 4269442a7910127e584dfbf3a012bcc0762f0fe6b47e5bf07aa9cdaff5fdaa91 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionManager.java 98ab527d19f3c0230e00b190e58a414083c846f367f0495fc2dc63c13eea5902 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionMetadata.java 53cfded45dc0ceb7806c6c5e66bf7f634fcf4c53ed978d8087840b46bde0e4e9 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionRegistryStore.java e11fffecdb7b786bfb171c4fd2054397d75c6cb96806c3b077c99a726ebde853 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionStorage.java 976e2c0258d836f7f20c5dbac75b1ceaecfdffac38224816673bceb840bcdff2 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBinding.java 58de5ae3dd30e11fd1cae8074a6f83814d9e673677cb05ab5c978122146e3ac4 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java 3d9b02008bd263a1b44b9ff935c7b641b9ca85d26104ed09e4b2d24d3f595577 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineEffects.java df98d25ecf792af8853431369bc8a858681ad0c536eac7d19c8ee528159d25d8 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntityAwareness.java 2f30d3625cf8b9501d245dd2337b3d74919b87c6a86964d617871e368b4ddec9 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntityCommandRunner.java 3e77485ddb81778cddbaab40929f1ea40e256ecc94f2c6a69ac9c6c001a27a1c -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntityPersistence.java 63c76022ab75b1a8fd6e6129d787d8d0406905f941273492aca1ffffeb8e0bb3 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntitySpawner.java 4541b15d82469d3d954b6b12323d0fa2453e1461df5becb5e511cf0d0b829e92 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntityType.java f127a943bab820a1aad844a77481df95fa60cd22f17a9b61491990da7f488bec -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedForcedDatapack.java 8e0ade074974c5f7d7ed35c342abd95354b61a5fed03004d1fc2e95d31218295 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenPool.java 67458ff63087234f34d56191b1500f55b2f73069c2e337af060df1d474c06b57 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenerationHistoryStorage.java 44b55e2f028a281513c82b24afdb6745fc011579e635a9f379275773f14bd7f4 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenerationMode.java 5ec2158c9a78da99900d42f0a650ee336fc2b0728d2a0ad5e3e2fcd5e533d148 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenerationRegistry.java 21ceaf9189ae3411f8c740a11e1ecfddd50e1dc03a5a1996e0a6d3d6a5d39d43 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedHeightmaps.java 159aed3f54445f521301ebb3d2a6f129a89214725cb108d95de219c40fd3f852 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedImportedFeatureStage.java d1eff11d830dcbf532c728ff7c50d91904e18ff4b987ca4a40c36fb4ec543700 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisLog.java 62f2bc771b53878047e21fe6bfd22d2e339406b89e2fe849ab9f385ac7c50214 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisPayload.java e83ccddb4eb1f700fd1700fbe63379a338e66e854018d924808262877a077396 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisSplash.java deea21fb476c4970b926f9e114bf959e9c0e11fb8ab62e350128e539e6ee74ef -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedItem.java bc94d3314130d9534ed289ca210fc6320df693102e2b7faf3af443b7658c6e3a -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedItemTranslator.java c832a32d0f3ff3a63eeea25a4147d42a3242945a55ef42fd19cc2e7a30290d13 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedLoader.java f33df4cc272940bb78fab828e8e5fb92f806a94ad86872ef7d9fee61d83adab3 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedLootApplier.java 2ea310f89bce7ac0b21dffd0a56d3c5f403e1b109f353c1c95d8c2b36894acc0 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedMixinAudit.java 47af3a3d80a089e6244a4e27f1f7ef8b1c6b9796b7fad1680735057e4434ca65 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedMixinFlags.java ce320fadbc7f070bbea585ca08a6d95f8f4a6fc186dfb7b6c72848cb7ca35bf8 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedModConfig.java b638bbd0878ca6ab536b76ab27d94b814898dd6d61155389f020951d56a97faf -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureStage.java 162e4017b2995cde5a3ef46e8c1a1d976bd7f4c447c7f53f0c96e1313b13c159 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccess.java ed0a217c260f1853b64f738e9a3622055edef0576f9a4779ec41bd41ac97a15f -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedParityProbe.java 255494459d4b01f76ca8263246220b3a92b270f9a35d11ce3477b1fca8ebd584 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java 71192880396e7ec187d5af9941363f8c6a7eb4b6cb97afcaabe0f4c74a3b7b04 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatformWorld.java c5a411032b76f6400c901ecea74932ecc85c66834316694fee0b1b5a4d088919 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPrimaryWorldRouter.java 6d7fc0a3c809a6cfe56d6c3e80dbc84eb961b9976280e2fd11720df8f7f156f8 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolChannel.java 6c0e8f606972fdefaccb8d31dbaf663a4e19fe332da2b4f249bf0f7c64874e2f -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolHandler.java 60bd17268ac8b09c8fcf3dda54e9dca0a9284fb181ecf096672e479a90e7c105 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolTransport.java 27ddec12aba235c45bb8c9dfe93bac6bd300dbaedf7fe4967e86b7b89da2bd0f -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedRegistries.java bee33dcb7dc645ef0d2200aa3a26bdc32f3e62755b66f3a9a874867afcc6bbf9 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedRuntimeRegistry.java e51c0bfcecf8a16912859253d418d2211719a69f088e9a4eb6c1137f621d9fdf -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedScheduler.java 58da6477fba3cef89dbe9f5ea6b46003c3129285b8eebda0e56712fe24c3bc5a -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerAccess.java ccbfe88252680fe73307c9f4305aa1bc967eeec9a5ae05cdf0e628088ee339a8 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerCommands.java ae4655de25be05f79f234906c6a3de71ede257cc5621a43a6dbe2ce98ce0164b -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerLevels.java bb98d21fa6d4eb6c3acd925f2f389aa908fa4b21c30db5c5eeb466a3c7b61f04 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServiceManager.java 795a55ef5c6a5414308dfc684b760bfc0ac8308a63dbd091dc17838402c272dc -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSpawnTableMerger.java c5d28437a8449890c575fc6f5ec17539fc3ac0d93e5130d07e51f5cdc6fc9969 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java c5eb8451bba769513cc9b497672be9c8575b8bf82c0c6ea1bee520c31a642892 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStateMerger.java 2d3f43a0ee0909ea53cb0ff0be65982766d38bc705d8c84cf6b9fc2f96eeacc4 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStateRotator.java 06e1f17ac84cd45557cb146c1c924125b5038d2cff1514d658e1cb316579a075 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStructureHooks.java bcf32041849ff0f015956825044dd99af844f0ef01d136b42d79ab5d97e2bcc5 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStructureSetFrequencyOverrides.java 198400f1ad6179b1d470bcc0dfd412285c793a5ce75c6ae6d3a506acc28548c1 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedTeleportBounds.java e798e114950b9ccc12c3d8ddd872e4bac5678fc0db1bb7ee41a58c7b3343c6a8 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedTileData.java cfa39080e1ad37c148234a73d905eb26200a50faa4bc9b4e49bced812c63559d -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedTileReader.java 237e42b4762b345c803e4329ae4aab4fad7a9fc0f28bde2e5c851311a723b22e -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorkspaceGenerator.java b02b8f46bf808fc4ef8142c3189742fb7d10e937d4b5a925deaa04a04a33f956 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldCheck.java d8798d3022bb64263ff6cfcee5b88ed12fda8e9c968915b857abcaf192c2e510 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java 493d3697b8de9c740118c211af197b9ee3df2d94a6cfb618703f3ae76ad4534a -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldManager.java f9d7671625429ed63e354c4620ddd79d0df59c0dc24ca9135e6b3073b6000592 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldgenIds.java 34ce8b87c3eeba9bbd2e10c4e578aa558e33fc0eb1cb7bd9219556888c0a77eb -source adapters/modded-common/src/main/java/art/arcane/iris/modded/NativeSpawnTableMerger.java 68d0a981cb737733a2358325ce67b43c6057db5802f4d1c82c95e04cb60fff09 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckDimensionContract.java 291310b36e63f4e2424988dfe29310bcee01986fd8f3ee72a8a3973fe82720c1 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckFeaturePlacement.java c5cf84d7519027e08fdce47f07404adba4e5a1b834ce1b8cfb248ea91e258728 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckMaterials.java c532aa1febc49d3030569051cff81d93983fa184fc272867af94009b65b1472c -source adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckPredicates.java 9b68af294e8fb3559ff6141df2731be3c62b3db3edf1553035ffb115337ef562 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckStructureAudit.java 8a11d12ef7ffac604d1eb738439c45aa80ed065cc16affd67bd03b5f15113a94 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/api/IrisModdedAPI.java a218fdeac999b0effcabdee6972b6324324d14d829f5e7d27de84c7f26182ebe -source adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedBlockData.java 45b3a78b293c20f20653915bac2e58849ee76d0444fad7833dc271505b8bf2b6 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedBlockPlacementContext.java 1fb20ed06338c50ca4c70a22fae67e8c1cbf8fa0b177fd120923d6eb958df3d6 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedCustomContentRegistry.java 4845d72515d413be36d50b3c3e1a778afd0526476084a12d2e68885f90fefa2f -source adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedDataProvider.java b4c060c95afee495cb151876f305e2c15ab9bac042d6ef882a42759803e3b1b7 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedDataType.java 16b2dc90d445fdd1b2847ef4b3fd05f1140b54351d7a220c4fee82e4f225ac23 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java 80132c03eac9d242ac27a1e47a13063dd1bce9b9d99b9da842011778bb4abe31 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandFeedback.java 3e11d7c9af1b5718172a942a040d427cce7b5e96cfe026f49b4f9a5ceeb131cc -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java f1306269a76e8708f6191f7b8348d6d684c411bfdfdc807d7e22e370aca83532 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandSuggestions.java acb6dbb599fab95bc70d33b6c203898dfe68ff3c834b790c00c2bd0c2dec76b0 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandTree.java e9d05d9adafa4bcfd5134e5c4356d989b7fb503ecb9cfd351ca68dd9ed39b94f -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDatapackCommands.java 2f643389cd0f6063b842465948e326a076d0b69ad2943c236e4d244aed723271 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDeveloperCommands.java 94848f60d1198d32c8959f53b6f5b144a3f0392ed844d18d43a602ce834bb533 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDustRevealer.java fc70a6dcc9a193def8de368a92495b4c9e1ac3e3f4d05e9e896474f7cea47d82 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedEditCommands.java 35cb3deb1d45365c9491e3a303d17f8575da0e29a92b3eb5c52b5e95f91808c0 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedGoldenHash.java e79c39a9132b5a7cf17b326eb6520de78a777f0d7eb4481d43a6449a643e936a -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedGuiHost.java 6bb96135c36d49c32ec349df444f22f4702e91c4758cbbf4c12725e154ce13c9 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLanguageCommands.java d86ec60dc82766df66a75ca0972d067a5e07c4e28c43d4b47bc2f74c8f06e399 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java 00b2ca75d4df6a498b7cf5a6cdd03f210936a74a26fedc755f7d010ea8eacabb -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectCommands.java 2ae08263fb6ef83faa1b1dbc43c80ed3b8b1287a5d083a7a28de0d7762ab8bb8 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectPlacer.java 8be2015a9736fbbfe4b51d4a031d26346717db3241775bb97c3789aa7415d9da -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectUndo.java 5718c0b08781016a8e9d60c0418cf6e8fabc056fba9770361c6bfa861ae9369d -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPackCommands.java 8ec72be2810d77190d5ee4ac92836c4d24b41170114f2958feed57861d3b4a7a -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenBossBar.java b6d083a155121f1000f4c6b33b36e41c14d631a5a2b73b0c4fd69140f0d0be15 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenCommands.java 489a0dd63a519268bbc1ab2befc071f0b54d350c1a45143c5d3c86f21c08d1e6 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenJob.java 21ee87dc14051131ffd1a1b64acc1d8a067fff29304e8faa6a384651eed765d0 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenMethod.java 461f0621b6c13300b6f6eff4a06e9a0c313928ea8482e6afff1cf9893e4d914d -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedRegen.java 0dd8a976a9e89b03408ada5c0433699d204e2df02ea7b637a154700f8de84c51 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStructureCommands.java 6646261a1a48f27ebd6f192a1c403dcf5ab066114aec9d056a0406ad08856912 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java b60045ff5bb32c471165e1242273fb2bfa6eba98582c44e70e0c11f07f531763 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioTransitionQueue.java 090df22af548326554e52bd02ea4732c72a86ebb8d24d8101aa9d4039333d976 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedUnregisteredStructures.java 32cfe601c69c8dafcd86f07aec427a8b55ce462b424f56eb94ea8d5ba68addd6 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedVisionOverlay.java de9655cf1897798392146e2b9dc501cd61a9013ab23b962bf4201cdde001f6aa -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWandService.java f9b62e087b42d96127bdd7bd593e97231dce8fa9df954028a2f6674c4920dc3b -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWhatCommands.java 8adfacb6e9f731109f45191881bea08305efdd537be979f5442c8422b04a1878 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java e1519ebf2eb4a5fd904c415ccb5a2dae2f598eea11cdfeb46e1478685ce13e14 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/EntityPersistenceMixin.java dfed792bde45d2eb70a96833caec0f79006d3aa0deff45c3ed2096d2997318cb -source adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/LivingEntityLootMixin.java d3da0e23927d947cfe967152dbb124f568067fff8fbcab7107ae0b213a1a2e10 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/MobAwarenessMixin.java bfb431eb1dbbc7ccbc51249403093534d3c9e6fc24e926bc69717c725da719f6 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/StructureTemplatePaletteConcurrencyMixin.java 6a939219a54acc25e4410f9d332a9734c1f4b389b77f674c918b54a68b54cfb7 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedChunkUpdateService.java 8f1bad08e941e92424a547617a24d6c1543b4d1593b9867bd05c35b1cfd49690 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedEngineMaintenanceService.java efe3daf4d03bb65b6a228564fba73174b3c6f3e3a8269af8fd6e95df228eca5a -source adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedEntitySpawnService.java fb2bbcc54beb227e0e095f3d83fc592afa08ac4fdb54154f5e79661a890038e8 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedLogFilterService.java 4992f40b4d4e4a7a82f2a52ea9f20af2f18949fbda1dd80e8935ad3f54fd019f -source adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedPreservationService.java 45aa5d42d07f50cb440190d869179eb6adcb57172345f075c5beccc395752491 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedService.java 355579ee520992e3e488b68e97011591b51854b7b6f98f10a795de2d42bad595 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedSettingsHotloadService.java 599a549398a9a21663dcbce2a4f7162767703b4a60782933959197f984d841e4 -source adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java 7b6709e29c4fb96a3005f74580283120a5a276ff137280dec05a0f3106c6969b -source adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedTickableService.java ca3614152280551cbf31a6db943b65fb5330bb3c3181863bdac6fb7f4ee925af -source adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedTreeFellerPresentation.java ab5ab6f4030f82d259d8c41e244a652e68be2c60c67e6f8c2309a1d24bb1428d -source adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedTreeFellerService.java 1b153545c2f33ef2773f82c6ff45d7528307712ac09ec3b5d66ea1cdfc154a9b -source adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeBootstrap.java 61b83e5093be8e7715c4b655486ab652be561d8f4995b9dbf94e04a6b68ff7e3 -source adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeClient.java 47920a2adcda3c61188f449b3949a9018a81e48327c243147d4260c4e85d36ec -source adapters/neoforge/src/main/java/art/arcane/iris/neoforge/NeoForgeModdedLoader.java b6e2eecc0c4e8441fcf321e8881762ece2fff0146be5eca5fde28ac43b26b203 -source adapters/neoforge/src/main/java/art/arcane/iris/neoforge/NeoForgeProtocolNetworking.java c4a0ec708a661ffd06e42a0ca806bf3e79e0be5f10bbd88a7e58267136767c0c -source core/agent/build.gradle fd4259a77b7a5107e3f865f89ee1faa4264efd65de6180a55aba190fab7deeeb -source core/agent/src/main/java/art/arcane/iris/util/agent/Installer.java 390303a2585620e1d31146b7b1defdd384adbed354491ef778ad795c9dc076dd -source core/src/main/java/art/arcane/iris/core/DatapackInstallResult.java 7be2b6b369479b287927dbc816576b2b95f35a48c495d8f34fa3f3a7746bd4f7 -source core/src/main/java/art/arcane/iris/core/ExactWorldSlotPathPolicy.java 7bc43355f0c78fbd87c415bbd9904d74dfe7a6de7847bd7c395e3de772778c8b -source core/src/main/java/art/arcane/iris/core/IrisDatapackCompiler.java d7e41d55787f243b86a8908b3b756eaf514762fd7a73d5b1014ff42eadfada6b -source core/src/main/java/art/arcane/iris/core/IrisPaperLikeBackendMode.java 220867995c31d5ff2f3f941d6190b5274a4bd8001708b29d1177e737210ea283 -source core/src/main/java/art/arcane/iris/core/IrisRuntimeSchedulerMode.java 3366dce6fb33db8a9355b6d16d4feea119f78111da8c4cb4c021f6e144309cff -source core/src/main/java/art/arcane/iris/core/IrisSettings.java 55f47fc6288d6312dca4fae3792ea6f1349e1040c6e910fc097c1b8e12ea130a -source core/src/main/java/art/arcane/iris/core/IrisStartupValidation.java 0fbcb1e85c024d9ae761298eb8cff58e04118fa78cb39e32bb88b7703c38bc76 -source core/src/main/java/art/arcane/iris/core/IrisWorldStorage.java 83a34a3430ddfda929495d11b4ce61f5fc2cf722ee89a51b13b9a13a6c972578 -source core/src/main/java/art/arcane/iris/core/IrisWorlds.java 4fcceba0d5e502fc509dee6424f21dd5ef79df9db47d0445c15a8785c5a6659b -source core/src/main/java/art/arcane/iris/core/ServerConfigurator.java 3326f5e85b831e6dd40777326f7f8256e48cf3677156a6d1d584c34abbf73dea -source core/src/main/java/art/arcane/iris/core/SettingsHotloadWatch.java 722626042885fdc27f3eec37a496b079ca38193d4d6516e5718e56ce92090d37 -source core/src/main/java/art/arcane/iris/core/SnapshotDirectoryTreeDeleter.java 30580d7847c4527fa834f2832640c78f1b6ba675a4e37dd11c50cb73aadb04a4 -source core/src/main/java/art/arcane/iris/core/WorldCreatorCompat.java 68ab4da60dac12bda8c911928db612f8d8b26e4105d7329303ffcddbf7591454 -source core/src/main/java/art/arcane/iris/core/WorldRemovalPathPolicy.java 71e59d1244bda18d9a73a6a0c51fe0125ad542efa8989f966ea815a53f3df94b -source core/src/main/java/art/arcane/iris/core/WorldSlotKey.java 0adcf57eb1a61f867a334f0c7c456a3bd7f0dd5f601c9e8a15aaf928e1a6becf -source core/src/main/java/art/arcane/iris/core/compat/CompatAction.java 74ca80baedeaea62bc65c8ef4592a812b9bc020f426dd01a796d9553127f8527 -source core/src/main/java/art/arcane/iris/core/compat/CompatFinding.java 1ec638d73fca19ad6dd71ff216320a1fa69cf46a4aea7a129bfe47414c63cfad -source core/src/main/java/art/arcane/iris/core/compat/CompatRegistry.java 99f4de14517c16ad110c9ce3ab1fd1a202c6b429725fa0756a7eadeade371f93 -source core/src/main/java/art/arcane/iris/core/compat/CompatStatus.java ffb40e0cac779223eae01cbdec2599654b9e6aeeeb9de516634deb9e916aa441 -source core/src/main/java/art/arcane/iris/core/compat/CompatWalker.java 9a7463fd6c18e3ea2f28fbc956c2f99c3cf3b7ed8db360833345581160a03513 -source core/src/main/java/art/arcane/iris/core/compat/ContentGate.java 455df9cf0685711f8ec884faf445b243b2c89ebc0aaf56bd39bf58697e628004 -source core/src/main/java/art/arcane/iris/core/compat/KeyStatus.java d0bb444203ef07ee1367fc075e8fec8561bd47e94a2d77284bd777488a24ff55 -source core/src/main/java/art/arcane/iris/core/compat/LegacyBlockRenames.java a48b132f18640e828954aa579f3d40ce174e8b3e4598f6e2d6a149c27290cae5 -source core/src/main/java/art/arcane/iris/core/compat/MissingBlockState.java 7f1cbc274a97d9bcf4908ea5d7837d0b76ee8f842324cb12eedb5433c613d801 -source core/src/main/java/art/arcane/iris/core/compat/PackCompatReport.java 20ad8bcef783655f121e8befee2fc11b19408917f10760b2fa21b01fb7a225bb -source core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java 44b27e46a5fdce06a2e21e3521caf98f54f7e957df952406a3b0ac9084935401 -source core/src/main/java/art/arcane/iris/core/datapack/DatapackStructureScopeIndex.java 1d323a99b013f369ad740c5ad39c7672a606efd82d7e32134e0534fc9fc510dd -source core/src/main/java/art/arcane/iris/core/datapack/ModrinthResolver.java df34ef49c1a21fcae4a6730098f2b57c99fd04dda94b0744aafccc22fc930d4e -source core/src/main/java/art/arcane/iris/core/edit/BlockSignal.java 93771191d4e4c966c4b1251d4eb7d87309752a341e62112ef0ddc5bb458a6f6e -source core/src/main/java/art/arcane/iris/core/edit/DustRevealer.java ae654caaadb339011ff8766e4301827c33cdd718df77070758c7622ac57de945 -source core/src/main/java/art/arcane/iris/core/events/IrisEngineEvent.java e0374fd7bac1366a2678f46a1586605afaadc68b0901dafca309ac814f57fad3 -source core/src/main/java/art/arcane/iris/core/events/IrisEngineHotloadEvent.java 5328b316f78b9a5be5e5dd9ed904ad6522ecf5e70b42246d7ae78ac6cd2d8376 -source core/src/main/java/art/arcane/iris/core/events/IrisLootEvent.java cff694a692c53f19afd3821ab708799823a6fbd5829e6bd208594b54126de55b -source core/src/main/java/art/arcane/iris/core/gui/GuiHost.java b512f01895f9529667bf95e4f59524778a56463dad5ef8ec1c1f07ea341338ad -source core/src/main/java/art/arcane/iris/core/gui/GuiMarker.java 67c8a2763b484b61535d2ce534b4af3ad07e3466ff4ba406f9d7ee473c0b14c6 -source core/src/main/java/art/arcane/iris/core/gui/GuiOverlay.java 4dc15202ce94ead8b0ea0225ff7aa6a40fe4eefe9a1be099c1caeed2d8b84c36 -source core/src/main/java/art/arcane/iris/core/gui/ImageMapStudioGUI.java cc89b37d0126332970f3c4043867a485d97b6286d46cad93caf4757babfcb9f6 -source core/src/main/java/art/arcane/iris/core/gui/ImageMapStudioModel.java 425457201138eeb5d3f4954609e3654b08b2dcd23c0f461d78762b41f7004bbc -source core/src/main/java/art/arcane/iris/core/gui/ImageMapStudioPreviewPanel.java 80c5b0863df93889f537caeec494965b358af722fbc556bd9918840df12376c8 -source core/src/main/java/art/arcane/iris/core/gui/NoiseExplorerGUI.java 5e1ea38b0cb6c388868a83b9f1823c7a78c90570fb2542ba165a0a57ce2d30b2 -source core/src/main/java/art/arcane/iris/core/gui/NoisePalette.java 266e65c7b804c77f6726d117083201345e610eaefd2a7237f004755ea69d2132 -source core/src/main/java/art/arcane/iris/core/gui/NoiseRenderCoordinator.java 99165c4e280ce60fe1f5ac4bb274f2a3724efed03ea84d34afb3610fc25b312f -source core/src/main/java/art/arcane/iris/core/gui/NoiseViewport.java ff283b313b401f5cc3736e2870166265b54ca588314b0642eccdc12e7878710e -source core/src/main/java/art/arcane/iris/core/gui/PregenRenderSource.java 880d24027611c29cf43968ad08bc6608bb9442e126bac9b73613c6d0e84398ea -source core/src/main/java/art/arcane/iris/core/gui/PregenRenderer.java 6efefe10d618bc78b8c62b1d0353454b18af2f7cf650f1ebfefd2ce5322a945b -source core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java f2a3f09dfa72600b64101cf5e93c3da368b53f063e9c1c45a4b0bfed8bd5236b -source core/src/main/java/art/arcane/iris/core/gui/VisionGUI.java ef1474b624cc556ec756124ae4de6d60170cc137ffc66eeb34a44197b3cd6f84 -source core/src/main/java/art/arcane/iris/core/gui/VisionRenderController.java 2593c694bb6201ddad992c1cd9ba494d6f80dc62a0da5c59c6c421c12b0c4c16 -source core/src/main/java/art/arcane/iris/core/gui/VisionViewport.java 3bfb6301a080882e479148e170515749c371a478ffa94ce715a87dd2851ff7b1 -source core/src/main/java/art/arcane/iris/core/lifecycle/BukkitPublicBackend.java ca5352207b12daded63329ab37c76de9168ea6422eac12e296f2c9422bda90ca -source core/src/main/java/art/arcane/iris/core/lifecycle/BukkitStartupPaths.java 308768060e867e238d18ad824262a8db45199913319b9bb0dac32576bbafbcae -source core/src/main/java/art/arcane/iris/core/lifecycle/BukkitWorldConfiguration.java 109e541198aa636fe1a9a52ee4b8f5ae4094a3bbdcd3bf33dc145e165132748b -source core/src/main/java/art/arcane/iris/core/lifecycle/CapabilityResolution.java 6a1ee0f8868653d0ad7fe25e31dad3673c72b666bf0463604459d77bedb5a696 -source core/src/main/java/art/arcane/iris/core/lifecycle/CapabilitySnapshot.java a46e8f9b43b4db07761ea60335512335a90bf38a6041d29892cc4941b63c1f5b -source core/src/main/java/art/arcane/iris/core/lifecycle/DirectoryDurability.java 3bf12f3bcb74390fe1d5250de274a6b70012a1104688fc3b148aa78ad0980a25 -source core/src/main/java/art/arcane/iris/core/lifecycle/HuskWorldQuarantine.java 52408d6a3ea1e01e8d38b58a659f3a2bff0024eb8c9a84307a5b6755dc64bf63 -source core/src/main/java/art/arcane/iris/core/lifecycle/IrisWorldRemovalService.java e2226677842c4623f0eb4661bb79c09878c2310268be5c0f7622106d5cf996c7 -source core/src/main/java/art/arcane/iris/core/lifecycle/LifecycleOperationCoordinator.java 5b6f9c939f24c3868546da4b38aa20fbfa6da006afe4176749b721276f3e852e -source core/src/main/java/art/arcane/iris/core/lifecycle/ManagedWorldLoader.java 3314e5c112e90457d0f3afe822ed7540703b37c6b3dd39e9613c6610af66433e -source core/src/main/java/art/arcane/iris/core/lifecycle/MissingWorldStorageLog.java 9041970f68cdf7119f42376585f189f6e607289d6667cdf7b0613a49e892f264 -source core/src/main/java/art/arcane/iris/core/lifecycle/PaperLibBootstrap.java 17ac328180835f05beb4104a14a5c470f3ccde239f935a438d2971adbce87cc2 -source core/src/main/java/art/arcane/iris/core/lifecycle/PaperLikeRuntimeBackend.java 3360980851da86cd5604bddcdeeb784351757177858a2365b951eec9be6646ac -source core/src/main/java/art/arcane/iris/core/lifecycle/ServerFamily.java 4c6cd416ea277362c64d0ac55ed9f04ef2a099559b007e2068bce5818ba16129 -source core/src/main/java/art/arcane/iris/core/lifecycle/VanishedWorldStorage.java 74938a321b355ab84fa2ee6b597e0de8772fa87579a63628a3d263aef7f387e4 -source core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleBackend.java 9aea543c4431ee2af25ec874605b7dffaaea61e29b8e5e621e6c7625690e5ebb -source core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleCaller.java 7293d978b786befd2eecd4c406e90665c2b322e8ac23555c086fa9223c840fd3 -source core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleRequest.java f1c81dc7e41d8a74fe39ac00fe3ebebddb960ff8f515bd121af27372eb892ddd -source core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleService.java 184863570f74e850728726c1e666820aeb34646b7eb047e265a2d890f5abf4c1 -source core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleStaging.java af1e212f53cd8e2c7162b8b7601e8a3c4f68dc6453961d4e0a24e6faede01e67 -source core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleSupport.java 5887b932a5295e26e2c45d4604aaf7da3b8535ae5f31560606fd1f2231b33d7f -source core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementBootstrap.java 2776dcde425e4b9d7ce000cc5cac8260a24ce0268eb67a7154e5750a94abe0e0 -source core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementBootstrapMarker.java a2574c798f127cce116522263d0d0e59e9117484254487d8a2026db9bbcc2b54 -source core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementEntryGuard.java 648c352ac2d56c5eafcb11af769263b18242eedeaac506784e17fbe6ec46d9b0 -source core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementFilesystem.java f876a9ca7b6d83047462ecbc3598b0c2336f360cf06a7a9d5d5f95e3290455a1 -source core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementJournal.java 9e50adfa530ec7138359dfef497bd4c41508108788e9839670abf6fa773eb276 -source core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementSeed.java b3b5f994c9605151e3c1f9c0461de47b15f4958ba35a42af2c983794c6f0ff05 -source core/src/main/java/art/arcane/iris/core/lifecycle/WorldUnloadBoundaryRegistry.java cb1a0cdde3eea7b181d1f56a82fbfbcd2432ea028cc60abb35a53a62f4b52600 -source core/src/main/java/art/arcane/iris/core/lifecycle/WorldsProviderBackend.java 9c8ebedd7f3801ae1cd337697f9389f474aaba16e970ec31525ab8aa2867c749 -source core/src/main/java/art/arcane/iris/core/link/ExternalDataProvider.java 6c9f14fd54d6aa0f3447602709f22abe5111b57c0307ac350df3cb59787d403c -source core/src/main/java/art/arcane/iris/core/link/Identifier.java ebad82ddde1641b258b8ee3fefd7566fd09a984bda621d7ee26f9f291e1b09c8 -source core/src/main/java/art/arcane/iris/core/link/MultiverseCoreLink.java c26b06ace2ab1b74846a5010049d70f17f791571c95ff4229505beba305606b5 -source core/src/main/java/art/arcane/iris/core/link/MultiverseGuardListener.java de0839a519e9b2fd2cc94d713be3997157d40399148f8e3c50cbe22b2f109118 -source core/src/main/java/art/arcane/iris/core/link/WorldEditLink.java 6c4a3378aac26886e6a443ddf35bad36013f65e431df0bf0735d3f7b9e873787 -source core/src/main/java/art/arcane/iris/core/link/data/CraftEngineDataProvider.java 508d4e512fa9040ef4d83d15c7e10141eae758e459b9589dbb52387b4606e509 -source core/src/main/java/art/arcane/iris/core/link/data/DataType.java c3353364dce7959caaa9df093964c2a04eb44a0e8818c1f3dfdc9e8f4c7689c6 -source core/src/main/java/art/arcane/iris/core/link/data/EcoItemsDataProvider.java 1b1ae16c4327760860ebcbb020c885242f869f0f6104aa874e163071ccf8e075 -source core/src/main/java/art/arcane/iris/core/link/data/ExecutableItemsDataProvider.java 663ba05f0e759fbc108a257ad5611f7c3bab8d84779c2718070cc50a40da7dbc -source core/src/main/java/art/arcane/iris/core/link/data/ItemAdderDataProvider.java 31b63f60f7ac024d829c145a00df21dec9fc69350af3bf2e7ed2d9862845642b -source core/src/main/java/art/arcane/iris/core/link/data/KGeneratorsDataProvider.java 0173463f400b324b9898b78a2ac25831d0114e97830dbc0056429824a698c43e -source core/src/main/java/art/arcane/iris/core/link/data/MMOItemsDataProvider.java 5a476f7117140e4811045be74b14de5a43f3a5b6ab740a9acbebd31a7165bc69 -source core/src/main/java/art/arcane/iris/core/link/data/MythicCrucibleDataProvider.java 19d24c98be6fd2a6d095160b6935a64e5cd2906ebb3901e090465118fc6180d2 -source core/src/main/java/art/arcane/iris/core/link/data/MythicMobsDataProvider.java 52b091b058ffbfab866c2e126cd03120f1e206a681d087f7d0e8d67d66eb0815 -source core/src/main/java/art/arcane/iris/core/link/data/NexoDataProvider.java 7450f0967ac5848b051ac2e9239847d6b0a7a66695f26a65d7ad53e37e55343f -source core/src/main/java/art/arcane/iris/core/loader/ImageResourceLoader.java 5bfa853cd00b0ac3b5bcc99be81094cba4dee7d5b2ccd19cee83b947a3405835 -source core/src/main/java/art/arcane/iris/core/loader/IrisData.java e83626f106729f18290c87f75ff2eea10eaa1fe588220eddbe1b3c0e184509da -source core/src/main/java/art/arcane/iris/core/loader/IrisRegistrant.java 74fe1fbb52a56380c033a743c55710570ae9f661e9e6bf6c548781221d8a0b67 -source core/src/main/java/art/arcane/iris/core/loader/JsonSchemaValidator.java b6735fe7b9f0ddf6c75812c0f6c98a5ffa78949caf9e8692dd001deb27c4ff9e -source core/src/main/java/art/arcane/iris/core/loader/MatterObjectResourceLoader.java 971f70e4fdd349293c9e25fc69730d27428e12e97cacca71c5988cf1aa06776f -source core/src/main/java/art/arcane/iris/core/loader/ObjectResourceLoader.java a25232eaca96a17ba94720d34f9bbf52d08561c85f00c3d05d4c035aef206f64 -source core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java e9f4df3b428e7349816daff817bb065a7c743dad6d3dcbc9899ca1bac2629f1f -source core/src/main/java/art/arcane/iris/core/localization/BukkitCommandMessages.java 8b64aaaa630c2bef89f8e6a39371787453b4168d41f71b75d44d7783759452f0 -source core/src/main/java/art/arcane/iris/core/localization/BukkitCommandMessagesExtended.java 72b1b5035f73ff379a79987c57bfaec2e78252eea5f26f335379a0e8e3447f82 -source core/src/main/java/art/arcane/iris/core/localization/BukkitRuntimeMessages.java 5a46babcc6d8476bd4d3cbcb8ad7a9623791bd511978e1d656936d1ed0ae6acf -source core/src/main/java/art/arcane/iris/core/localization/BukkitUiMessages.java f13fb2e5cfe7af11a98691879d8d80b579d0287bde157a3ac93ce8f68ba95cba -source core/src/main/java/art/arcane/iris/core/localization/ClientUiMessages.java 9b9fc2b82114ee929647460e4bc6cfee689ac2e184ab9b97b3e1ffc64e96e316 -source core/src/main/java/art/arcane/iris/core/localization/DesktopUiMessages.java 943b81e8b1d2f8480aa0a4eb0dfc01392c29f59c9d7963f7f8ceba7f6b305413 -source core/src/main/java/art/arcane/iris/core/localization/DirectorCommandMessages.java 7ce97890d97bdbe7a3029662d4c4075376b7aab101ccf5207fec7869b6885e9d -source core/src/main/java/art/arcane/iris/core/localization/IrisLanguage.java 4327768a9d1af6aa4fdce488627ef3005e387c8a09a2f9cde3935e8348cd66bf -source core/src/main/java/art/arcane/iris/core/localization/IrisMessages.java 7e011e7727d367dc56061782cd1c7a79e32c74ffb7f78792fddb6e8d9769a75c -source core/src/main/java/art/arcane/iris/core/localization/LocaleHotloadSnapshot.java a68c62b03ff72ea01f91058890aafdc261f892014abc29ed84cd62a66faaf7c1 -source core/src/main/java/art/arcane/iris/core/localization/ModdedCommandMessages.java 47ef537dedc4490264d5ba336cc848954c43c7e12fc1dfa8c30e8424c673af69 -source core/src/main/java/art/arcane/iris/core/localization/ModdedHelpMessages.java de8cacc13fa4417f233921ba4bdd2ae7c1087c63bf67f8ccf44d3088c4f1aa41 -source core/src/main/java/art/arcane/iris/core/localization/PackDownloadMessages.java 1def6380432c341eec65cf7984f92845ba9b667fb7edf862e5674df927ced4ef -source core/src/main/java/art/arcane/iris/core/localization/RuntimeProgressMessages.java 4ec986db9171a1d92f96a84c5221907fe9426cd1b9a58850134d89b67706c689 -source core/src/main/java/art/arcane/iris/core/localization/RuntimeUiMessages.java 13cb09cdc52745e22ffe1547d0ca9984890bdcbdf9ebbc221510406dfc341056 -source core/src/main/java/art/arcane/iris/core/nms/DatapackStructureScopeResult.java 30faa3f5973e0f96618b5da18a8d166cb04bf07019dd00bc187f36d85f79ff54 -source core/src/main/java/art/arcane/iris/core/nms/INMS.java 3bb3598e7c35e629e3194757687cd1924ef9ed4c2d156a1f91d3133e4b121221 -source core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java 0bdb7a435c7f25bb0bc233dc3f5f82825e06ddc10e483b7e5b50b467c5630a96 -source core/src/main/java/art/arcane/iris/core/nms/MinecraftVersion.java 1fa721ce890a3393e374d5c214a7924eec10b00f6b4d07ffc637a97dccc52b04 -source core/src/main/java/art/arcane/iris/core/nms/NmsBindingSelector.java f2ca75425d7c51b57ceb9b6d3aa92bb7acd6d99bc8faba0b52684ab5dcb043a8 -source core/src/main/java/art/arcane/iris/core/nms/ServerShutdownBoundary.java ff79df31b623046e7a8f0e13142a0e23771346d6a783fd5c3176109ebd2f3569 -source core/src/main/java/art/arcane/iris/core/nms/container/BiomeColor.java 8ad3e00e29f6695aa23bd0c583707b2a25b1c2e206164d76e20737eb021e4947 -source core/src/main/java/art/arcane/iris/core/nms/container/BlockPos.java f63f1c7201a4f6276f195c2ab6f02b6278d98dac48f25ce38a57c1365301e043 -source core/src/main/java/art/arcane/iris/core/nms/container/BlockProperty.java b988803f1e02af1481ffb972b18d85b58f16ba85c368c6e43ce7268fba8f6e6e -source core/src/main/java/art/arcane/iris/core/nms/container/Pair.java 57435bb0532ad0a86624af0ca09e52db36a339e1d3e60a2de9372fdfa46131b2 -source core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java 9b9d2bcf705b0280b3080afadde6ada038b143bee43ea60a5afb059bfbafc27e -source core/src/main/java/art/arcane/iris/core/nms/datapack/IDataFixer.java 7d20eb16cf0eccb8064a0b8ac9006184b619ad5a8dcf8d359b92576acd0b092c -source core/src/main/java/art/arcane/iris/core/nms/datapack/v1192/DataFixerV1192.java b94915ec84037ddd7104ca604819fe1d214caee44154546873a5f00eb0c037f8 -source core/src/main/java/art/arcane/iris/core/nms/datapack/v1206/DataFixerV1206.java 2df52dbdc7ad1e1e2e67c5876cfacfac10ea133b4d0e812d8b910ef75e20cf4c -source core/src/main/java/art/arcane/iris/core/nms/datapack/v1213/DataFixerV1213.java 134cf12ab88100796eeff1b1cb3ae7cdf9e4d9e9f01d94f9df35ff546bfefc0c -source core/src/main/java/art/arcane/iris/core/nms/datapack/v1217/DataFixerV1217.java bee11359e9be80c3cd610e52f182b0405c2998c247ebf0cf7ba9352b421c3a93 -source core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java bd813ed2d0f3f6ebcfb688772c0c110bf3e06b045a079db79464764520f96e86 -source core/src/main/java/art/arcane/iris/core/pack/AtomicDirectoryPublisher.java 66207bc92ed2f9e7a8caaa23f91d9d98a5c72be3fa517ab6f16e91609e2f87f7 -source core/src/main/java/art/arcane/iris/core/pack/BrokenPackException.java 0033915832bfeb71872b90e972ee50d3f7d6f4fe9141bbff2669a3e65d2daa2e -source core/src/main/java/art/arcane/iris/core/pack/ContentKeyValidator.java 247b1d88b1d241843d9e73169dfbe1d3a5444a9004156113b3a7fb78d762b321 -source core/src/main/java/art/arcane/iris/core/pack/DefaultPackBootstrapProvisioner.java 10fecd5ea8e8616236def7fa1b9209fa808f454dc82d639c1b7a1a81c9a1a6c5 -source core/src/main/java/art/arcane/iris/core/pack/ImageMapPackageClosure.java 242c2dfe910667e829488c3f9fc91a0cbbc915dfd5254b5bf6b3685cbd3e41b9 -source core/src/main/java/art/arcane/iris/core/pack/IrisPack.java 23cf378635662fc0195ff6cb3f5c4f0e6b8e29a12903eb7d9e50dcd9faf87173 -source core/src/main/java/art/arcane/iris/core/pack/IrisPackRepository.java e1cd483e990faa5a8a9af998de4be4d7d9be7c0b138b439bbc33e166d11638f5 -source core/src/main/java/art/arcane/iris/core/pack/PackBiomeLayerValidator.java 13778b2f4708bacc70cf90a8e6036a5359bfad8b4725af025456c2b0b85c978e -source core/src/main/java/art/arcane/iris/core/pack/PackCaveProfileValidator.java 5fb21a5cff6d717c3fbcfc43ef001ea1b82a1686e9ba04f78d3540ac0158d8d0 -source core/src/main/java/art/arcane/iris/core/pack/PackDimensionValidator.java b04d3689434e4d8bcb3b97e1a9559665e87f0648e16088634013f8e17a4ae5ab -source core/src/main/java/art/arcane/iris/core/pack/PackDirectoryResolver.java 19cc02ce077c9bfadf94dacb2e58290518953dca79aa882af57c8282541e5c32 -source core/src/main/java/art/arcane/iris/core/pack/PackDownloadExecution.java 3275cc7709a865d95dffd8b4b406bac0ee0a4ae2648f67654b396aa95945eeab -source core/src/main/java/art/arcane/iris/core/pack/PackDownloader.java c1e28224b8a233628df76272fc666f9a82c5b38b9a5c25413d1c07966d85d1cf -source core/src/main/java/art/arcane/iris/core/pack/PackExportClosure.java 6b770dc6175bf753895f9914dc106ab35330fddef5444db2b1e6838fad9ceb93 -source core/src/main/java/art/arcane/iris/core/pack/PackGeneratorDuplicateValidator.java 6b42b685cc9ebd7e4b2b3eaffe131d73c0037e4a8a9d114593de5383fc18c391 -source core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java bcf9a4bc989386b59154ffe1166618053bb0267240b573a578ca7bfe16f20ab7 -source core/src/main/java/art/arcane/iris/core/pack/PackImageMapValidator.java 8b8c7e0211a17587bd3b5bbe29c4fff5bcc94840a1398a6a65551cfb21cab8e4 -source core/src/main/java/art/arcane/iris/core/pack/PackJsonFieldChecks.java 119b8a55673ac754aee819fdd0925f0cd666412d7e02746c7e6f2b7804f204d5 -source core/src/main/java/art/arcane/iris/core/pack/PackLootValidator.java fa303aa998b7d81bc6dfb76f729e40b3b7e4df08105de612a33a5f8c13328576 -source core/src/main/java/art/arcane/iris/core/pack/PackNativeStructureValidator.java 878765d22f222acbd347a35a94ab3dbba9a26d6802c4899c30e7b6d94782bea2 -source core/src/main/java/art/arcane/iris/core/pack/PackObjectSurfaceValidator.java eb6e50dae0c2268bc10551ab10efcc625e8f70ea48ff3fcfdaf87c58a6a6cd66 -source core/src/main/java/art/arcane/iris/core/pack/PackResourceCleanup.java 97db29cfd45f76cb6ce7166ac2d1234faaf5f6283bcdd7760ff28b3810bd6b1c -source core/src/main/java/art/arcane/iris/core/pack/PackSpawnValidator.java f7548ed3e4bdc62f321521b719f4956c05da785e03a31c24e173f52c1d5ce510 -source core/src/main/java/art/arcane/iris/core/pack/PackStaticObjectValidator.java 7a208ffad08f4c8264958aae0cac0933132ad07b6fae483bb300a6a18c0de9ce -source core/src/main/java/art/arcane/iris/core/pack/PackStructurePlacementValidator.java 194d66f40313ab2ddd3fc259d7f140b92590e515c7df66640f740591ccdf5cd1 -source core/src/main/java/art/arcane/iris/core/pack/PackStyledRangeDefaultValidator.java f09eb46e4b97e73b9db2d1e7e0d74496488ad4648ffb816a44e4befddc8b5775 -source core/src/main/java/art/arcane/iris/core/pack/PackValidationCache.java f78e37e77fae41da44614dc40bacc1c5dd30866e85f935bf48ad26a1fc932d0e -source core/src/main/java/art/arcane/iris/core/pack/PackValidationIo.java f4fd445405401269021abc749699ba0ec9963baf3441a78c3d945b08dfdac953 -source core/src/main/java/art/arcane/iris/core/pack/PackValidationRegistry.java 196417c667e06f93878c066f414b9037218ad6a43629b2b043c29e1cb5f17105 -source core/src/main/java/art/arcane/iris/core/pack/PackValidationResult.java c334c47dae069a1bd8682302933ee54c955744929ec80d5afb90fc47f3665783 -source core/src/main/java/art/arcane/iris/core/pack/PackValidator.java b51b82898fe5dc618880ff0af5553e21f82b41499eb2d2dc13fe7c3c1295be14 -source core/src/main/java/art/arcane/iris/core/pack/StructureGraphPackValidator.java 2e88ed567e1f477f1b77450932983ebf5c9e2fae866a66ad4713879f1f233d11 -source core/src/main/java/art/arcane/iris/core/pack/StructurePackageClosure.java 6d0f68a637ef260729d5e67da6b2211d204c78fb2d07866467923740e9f9ffb6 -source core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java f06951398de67eef6d2ea60c607673113353991b3c0db838ea4ce33d73d35be2 -source core/src/main/java/art/arcane/iris/core/pregenerator/MantleHeapPressure.java 8a01dfff59d07b75f90edffef9599d9a0ae44b3a33cced65af190fd9576690fb -source core/src/main/java/art/arcane/iris/core/pregenerator/PregenApiPhase.java aa6fca878aedd70c394c034cf62b6b7a0051875275682b154805121422da00d3 -source core/src/main/java/art/arcane/iris/core/pregenerator/PregenApiSink.java bdf3ba2e9e2ccd4b34d272635bd615de4be7a09166ed01798811c0574b1d24d2 -source core/src/main/java/art/arcane/iris/core/pregenerator/PregenListener.java f7058614dc01a8977075a2fa5dd428a2564bf26ba5e745aac67083ac07dad27f -source core/src/main/java/art/arcane/iris/core/pregenerator/PregenMantleBackpressure.java 8d0e49cf5d2a7ca022c626f74dbd0afb89b5c9150fcc6d7bec41c0652fa65a46 -source core/src/main/java/art/arcane/iris/core/pregenerator/PregenPerformanceProfile.java 25ee55fe54b2cb1b1dbf034e8a4bd514239c288ecb841ca5567233cdd77d9c48 -source core/src/main/java/art/arcane/iris/core/pregenerator/PregenPhaseTracker.java d57637b5a9e7c8e09c58e71fc8a1ff23eb295435c9fcd411746d5e8176a9e015 -source core/src/main/java/art/arcane/iris/core/pregenerator/PregenRateTracker.java 53304e35af5b0e32b86dd75bdb1d4052630150ace2751fc8ba86ca38e94ae527 -source core/src/main/java/art/arcane/iris/core/pregenerator/PregenRates.java d348acd7898d701e71bda83da20eb9ba09bad4f6779966ad87aa8a60522c3c8d -source core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java a52c1ff8be510f5357bc721b88e735b6bf14c389956f50e249f74f8970af2320 -source core/src/main/java/art/arcane/iris/core/pregenerator/PregeneratorMethod.java 6b259a5b14fd513c441f26c053e5713a646b7aac88542dc5c4e53f6aa4c008af -source core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCache.java e6af20dfaeea6967ee0d561631f612e0b2dae7e22182773001844d3522bd79de -source core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCacheImpl.java 2dafaaeb0b5bc8e65db9b0cf25ed2786cfa0f561c6712b7b5817568c239a9eb6 -source core/src/main/java/art/arcane/iris/core/pregenerator/cache/SynchronizedCache.java 22b8954a43725acbabaa1e8f37ecbdc3bb2f9c90dfd5c2833163ead500070669 -source core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncOrMedievalPregenMethod.java 7736a76250b1a2788743b5660da97b72c009fe63aaa0b5c47179d6fd008a32d4 -source core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java 629c893a2674d731192d04922888ce3c435b4b0dc0aa4ee64b95db17bf3cf5d4 -source core/src/main/java/art/arcane/iris/core/pregenerator/methods/CachedPregenMethod.java 0321a1c4e3c3766edece992b7377d7f64ce208d9b83e48b52b3da769c0544bd1 -source core/src/main/java/art/arcane/iris/core/pregenerator/methods/HybridPregenMethod.java 7911cf2d23b1cdc11dfc00ad0b6d49cd465bf0336187aa580b332ff5b121a24a -source core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java 6d1e48f1a8b2bbf5359b5231fd620ada6a193f7073de6ea8a28ad6a8271ed4ee -source core/src/main/java/art/arcane/iris/core/project/ImageMapStudioExporter.java 4c9ac3e780419457dde5651131db38c2a9543a11991a030da7ecf84466ff6b68 -source core/src/main/java/art/arcane/iris/core/project/IrisCodeWorkspace.java 405421807e4b4a62cca70deabca9466ea1f95cfb34de69c1a46e712c0d9ab3f2 -source core/src/main/java/art/arcane/iris/core/project/IrisPackageCompiler.java 214d2086f625b749024961545b4a6d03c7b3ee3b566073e1f142f7525c3acf7c -source core/src/main/java/art/arcane/iris/core/project/IrisProject.java 1b3daebe71e2d5651cf032fc7cb0e681704f6d507319cedd18953c8f12853b09 -source core/src/main/java/art/arcane/iris/core/project/IrisProjectCopier.java 07e3c17a5338fc579ebfb4fab60fa6d8ef8d8cf8f87b3d62da9d626dde6de29c -source core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java d6af3c1bb7df4922159daf69aea23493b59ebd095f4bf3cbf86922e8692f8fdd -source core/src/main/java/art/arcane/iris/core/project/SchemaKeyedTypes.java b326649564af4197e908216acabddcb6a07b2d5025252792f440a6f25eaf0acb -source core/src/main/java/art/arcane/iris/core/project/StudioOpenProgressReporter.java 3fa2ca5c70333e5b4b2c1ee5472c9d987ee91caf361c08e46f68a108f064a55a -source core/src/main/java/art/arcane/iris/core/protocol/CursorInfoRequestHandler.java 8922d56eb28cfa598214341f9c505a01f5b548bb14740847bc44ad697b43fa4e -source core/src/main/java/art/arcane/iris/core/protocol/EngineResolver.java 9b1b4fbcb3fab969812f6e9ff1147d21971d1eb3027e0705e85733d915354f9d -source core/src/main/java/art/arcane/iris/core/protocol/IrisCursorRequestService.java dd798abfbf387d29c65d1b581d2f82ba564e0dc5b25cb0187281393062915b34 -source core/src/main/java/art/arcane/iris/core/protocol/IrisCursorResolver.java ac0b6f29ee3c8e38ae40f844295775dcc478fe9fdbc7b466f4b9e8582371e4b1 -source core/src/main/java/art/arcane/iris/core/protocol/IrisProtocolServer.java 76d55691c7711acb131eb8df837debe1fa43fb80d929b4c4d31b4192eba3f015 -source core/src/main/java/art/arcane/iris/core/protocol/IrisServerTransport.java e82859ae3bf1b4ad60c99c49ce4dc9fdd40bb2257f74e276366cdb8a012be8f4 -source core/src/main/java/art/arcane/iris/core/protocol/IrisSession.java d3ad1a90301125908f0b814bb3250185f13c36179056aea5aaa7444a7d8c06d3 -source core/src/main/java/art/arcane/iris/core/protocol/IrisSessionRegistry.java ff49a87a6158105379d6c63514b4008629dfdca2a57854e020ad311c64b3f67b -source core/src/main/java/art/arcane/iris/core/protocol/IrisTileEncoder.java 789cb71f026dff4286d9948613fd29ced979f623db03786d7daa0e9dd2944c4b -source core/src/main/java/art/arcane/iris/core/protocol/IrisVisionRequestService.java a0ce1b436429ad1d7f6542a871d89fa044404b3c46c70e94cfb2c6129d59ddde -source core/src/main/java/art/arcane/iris/core/protocol/VisionTileRequestHandler.java 3ed8f9cadbb17996f40e021039a4985d32454249fe6b529099e2a08cca3140b3 -source core/src/main/java/art/arcane/iris/core/report/Report.java 2c8b24131637c03a8670e275d549f24843c9d62cdb09dd7ab8a7869c2864f3c3 -source core/src/main/java/art/arcane/iris/core/report/ReportType.java 65a83094386ea07b8cfd2cfd17ea441152158fbca162c8e61ff5f783f9288b6e -source core/src/main/java/art/arcane/iris/core/runtime/BukkitPublicRuntimeControlBackend.java dec12aad794a8c8989a25a33db6171d911ac429f1715f1e55f7bc4770643207c -source core/src/main/java/art/arcane/iris/core/runtime/ChunkClearer.java 127315079846b30e76e68c20124471d91133af7d81bd14e70d74d400e7d4a7e5 -source core/src/main/java/art/arcane/iris/core/runtime/ChunkJobReporter.java 6947e54164d2f0d2616b517c9f502b1b609927b42292e23c9e768ba8061f710e -source core/src/main/java/art/arcane/iris/core/runtime/GoldenHashEngine.java 2d5dc166e272d66ece78c7ce9989aeacebde32284cb826fdf17e76cab7bee130 -source core/src/main/java/art/arcane/iris/core/runtime/GoldenHashScanner.java 2b47df172ebcb7dc7f8cd29639661b9e1692710891740e9bf59227b351a124bf -source core/src/main/java/art/arcane/iris/core/runtime/InPlaceChunkRegenerator.java 3f47b05af871b8dbec12bdb8e269dda1287eb301f25a800d6048d20da2859859 -source core/src/main/java/art/arcane/iris/core/runtime/ObjectStudioActivation.java b3fa731a267ace9e052f1d4cbc437ecc1b6102c2c0ae030c21c9262c41069386 -source core/src/main/java/art/arcane/iris/core/runtime/ObjectStudioLayout.java b78868e5adefca27c3f1f9544256317b1949fe208f02ccee7dbffc61c389e129 -source core/src/main/java/art/arcane/iris/core/runtime/PaperLikeRuntimeControlBackend.java 443047b33624db675576a189c843952854fb8852c9df979eca044e8f0e3c23e6 -source core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java b501913f1704a3e59bcf6982d6683110565c549725d00311061a7dbb7dc0ef88 -source core/src/main/java/art/arcane/iris/core/runtime/TransientWorldCleanupSupport.java 54fe5614808ffa4cef920e6428f6a1fd70c23d01e3ab16d75fac862271dd2e97 -source core/src/main/java/art/arcane/iris/core/runtime/WorldDeletionQueue.java 4e77be2cb6c1bde9abc38253208c4a86bae0e575d8b6ddd7defdada414248ac3 -source core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlBackend.java 0827d2b8412ffdc33ab35f1abb732584f85935dac3e882e9e04994b0cfd3fcce -source core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java 1fcca53857eef148692ebd83c0fc92e691b79424914330d74c9653077a381fd0 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawPlanarArchetype.java 7ac518d17d1ce1759551fd9fa6b2763e8d6e976747e941e21cca75dce8a4a6e9 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawPlanarDirection.java d65637cea86b0e68258130b7bc265c3886fe9c094a3b39adc3a028febc5609d6 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawPlanarTopology.java 75e8515fa423f1aae90ad0b11077806340d9fdd53b6b8d58a1719d9ae8831646 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawPlanarTopologyKind.java 6987a27bac81d6c7a1deef244756e4b887325582da5ec72a058a2a9d62d97d8d -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioActivation.java 978182cc8d95b33fe86d7daf2e8b45d0495c3e682cf6d8225fbbfbdca89831f4 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioAuthoringAccess.java a021aba93078344f44df69e86c08b85b1dfb38ac0814795d5d98dbb7397f56d0 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioBay.java ef8674ae39cc9fac2c0b10d455e622740754af356607e7fe10f611eb47c303fc -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioBayKind.java 009ff01c4d87a35583703a7ef9b7c6ae8ddda3344b1b79b1d45374c8132fff60 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioBounds.java 13644dd511bfcc19bf19d40dddc2ab90e40bb5b96a7ab8b3c38d431b7ca09913 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioCellDimensions.java cf0ba6c8609e42651707152fef546d82a33bff53777d86babecc63979e2deaf1 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioCompatibilityTarget.java d454cbc32ffbc9fda805a32fc2c58c01c3ebd67f7ea3dd8ad596dffab80a56d3 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioControlPosition.java c02f47417e89cf96ace2ecd1d94d7c8309afce01985dc05eb3912e845175c87a -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioGraphEditor.java 1f3c472c64e30ce717f199b13910167ef697f4ea321d8bfa6df2e4b570fac165 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioGraphMapper.java 80aa753d91f42234586b3f056f7599c3bc34e654d0c90b0e26882e0cdbd55447 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioLayout.java 1840b746e1fb64f4a3c669c39b61f55556a7afa3dfaf20c2eeb44226bc4f6cad -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioMarkerKeyCodec.java 18b450bcf3751a5f9a3f95e8b2458e5404f137e1e855147e990a419456d2ed29 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioMode.java 278833b49bb9cc213bef0088ddb586d823b7479019ee0828d3ad76aaf2b6d1a4 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioPieceRules.java 1a1293e33a18122efba628d613b08fe5b7c36528ef3a926dccdbc9f6f8c1b9cf -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioPoolEditor.java f973b8763189a822b8c0d658dfc19c1962bb174d0c620e62dff65641ef4b77b2 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioPoolMembership.java 682cb4feee2aa7fbbb969e260e5633d058b85f208dcf1159f97db552fc420b87 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioProjectCreator.java ff29ccc8f3ea1665dccbb8b0310f649e2bdd1f8defbfc2aaa4ba3f7f4cfa20b8 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioProjectDeletionService.java aaebdf801a66baba401a57a5d4b53a8e0d9032b4b36de031f94c2950a0409dda -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioSession.java 70da806b2ac637486bd78fe14a80a051e0e41801262646162dae1cf3dbb40e08 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioStructureEditor.java 636964d593e7d1133b4c48cd48c736b2d9005db1577b197d1398ba35d8d708b5 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioToolAction.java 9f58c7f7b8503e23332bbc9f5046a867fffe536d3e729e96a81c68b96e6c448f -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioToolPayload.java 5aa47c18e949f55818364090f459e1e5acc2d9b0197590346f5532dd82aa5580 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioTripleSneakTracker.java a7cf91e40ac847f7af673cefbadd11d3e321fea624609c4713f7a37c206d5364 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioVariant.java 73d95289d7385ee38900409371516e8ecb4712f7d423241b7fa23a0ea6c12f43 -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioVariantCatalog.java caac4475f86254a733eafe8aee1b19e5f4a5bae4efa8b1b9ee54dba0720b3fef -source core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioWorkcellSpec.java c0bb579f674ef0d8a98c5ec78fe694a25cc77dccc64b51d95b4b62e4703c18ce -source core/src/main/java/art/arcane/iris/core/safeguard/IrisSafeguard.java 6e8ec60b98bd7940f208195e70a2af68310ebe5591660e66e3d78df021ddaf97 -source core/src/main/java/art/arcane/iris/core/safeguard/Mode.java 6a9f7b5d70c26982dab81ad2ceaa5e5380d546b746a86fc786d3e5e22e3d4007 -source core/src/main/java/art/arcane/iris/core/safeguard/task/Diagnostic.java 037f9d0f50b5ae3cd3a00cf0f143541f009f7b06173bf18bd831955ac815234d -source core/src/main/java/art/arcane/iris/core/safeguard/task/Task.java 9f6ddd3cad020d36ab35b63b5833fc66767e225b1653a0f80c3e7e0048b5f58a -source core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java 6a00df7216cabf8aaade159844ecb7bfd5d6430c48dd60602be237b9ae9dbb07 -source core/src/main/java/art/arcane/iris/core/safeguard/task/ValueWithDiagnostics.java f6690608c8a4f935f383dac79f91d7efd08d392889310fd6b6e03933f8aa62d9 -source core/src/main/java/art/arcane/iris/core/service/BoardSVC.java e6f9836b4cf5a7cd9121213b500f9bfa26e7aa85ae7c690ee15a765424b8c888 -source core/src/main/java/art/arcane/iris/core/service/EngineMaintenance.java 9ef59f55500e8fbe0b95b4048da962047653223ee1c23d0d8283fbe4f0d5792c -source core/src/main/java/art/arcane/iris/core/service/EntityRiseSVC.java b25c1ea511bb23905182675234f6297604c328dfbf96a3cbf2113e649ce345d4 -source core/src/main/java/art/arcane/iris/core/service/ExternalDataSVC.java 3f39470951486396ce81e00af9e8d1f4c20bd7848bfb75c8562fd76567ba4f5c -source core/src/main/java/art/arcane/iris/core/service/GlobalCacheSVC.java fe489117323f2713a0002c1d2a72ccfbd37c5fa79430b5147838b9d22a2f05f7 -source core/src/main/java/art/arcane/iris/core/service/JigsawStudioBoardContext.java b0058fad41aa4ce12e481a31e77503fbd2dc4be11665ca613cf98de309e8efd2 -source core/src/main/java/art/arcane/iris/core/service/JigsawStudioBoardState.java d54258d6581468835c3bf30f769d5ced534b0b3ec3d1f8390ae1c6d77902925d -source core/src/main/java/art/arcane/iris/core/service/JigsawStudioEvaluationState.java 8e9a93e4bb54070158a929d9e171115a83accff6ac63921f15e1238102376197 -source core/src/main/java/art/arcane/iris/core/service/JigsawStudioGraphEvaluation.java ebb768a6c8741fea4459f929b00a83a567c642e7b76b3f8ce463771620479380 -source core/src/main/java/art/arcane/iris/core/service/JigsawStudioHistoryStore.java e69d90723099c1eaf85366d4edd645a48393b526499ada860fdcc9ed151d0279 -source core/src/main/java/art/arcane/iris/core/service/JigsawStudioMarkerParser.java 6c315e71f46d69146fdbc0be86c2d5179701d35a35ef2a0d8c258bd85099bc55 -source core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuController.java 1272f8e7f5f7b17ee5cbd27a5f566c84905f3922ee29c8fb8b2b4531ada2cf5e -source core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuState.java 6962d2b1fc583d12aa955e82207cbe0b877cce7d80b5e1dbb2bc3a03a6edfb7e -source core/src/main/java/art/arcane/iris/core/service/JigsawStudioPreviewRenderer.java 3440d07233625e0188008ddfadae72b5ae51257f2df920beb60ef88ee1aaf33b -source core/src/main/java/art/arcane/iris/core/service/JigsawStudioResourceBundleAssembler.java 02fd237c6a8324a6e219e1afd09ac87ae030c5432a0140e153b0e0c9b059c219 -source core/src/main/java/art/arcane/iris/core/service/JigsawStudioService.java 061f927f557b12c70f8caf8886c75e98918f1dff7dd85a2ac49e11292436b77c -source core/src/main/java/art/arcane/iris/core/service/JigsawStudioToolCodec.java ff89fc0aa377a350ae6013a216784a2e00fe8dc15dd866a68364cf99eec0aa29 -source core/src/main/java/art/arcane/iris/core/service/LogFilterSVC.java 7a8e320acc2235e85452fac81d39c01f328e686b2ae2eac2a7d4261a0c27cf59 -source core/src/main/java/art/arcane/iris/core/service/MultiverseSVC.java 63e2d46b4e142d7049edb098b0f8d59e595fed30abfe035a0868298847938d55 -source core/src/main/java/art/arcane/iris/core/service/ObjectSVC.java f1b1ce5e4b40b366d5a92765b7ca0119af3e933767080401b82ccec09a325c38 -source core/src/main/java/art/arcane/iris/core/service/ObjectStudioSaveService.java 45104a4f7496ecdd9764837fea59fc580e8ab1493b3626c02cfce898bb520268 -source core/src/main/java/art/arcane/iris/core/service/PackDownloadProgressReporter.java db20ad4b9a2dafd9daae8fd05872168a5ff3ccd6eeff2823e128d3d5a720c371 -source core/src/main/java/art/arcane/iris/core/service/PreservationSVC.java 19efbaceba5a3445987ab0db9069ec7962c8a374ff03e5460222530e423a8287 -source core/src/main/java/art/arcane/iris/core/service/StudioSVC.java 986daa76b6feda6e7e365f12cc67d3f93b250e6bc8046c08583ef7c5b5295bf2 -source core/src/main/java/art/arcane/iris/core/service/TreeSVC.java 145b653f0f8f2432263df34af01535f423864925600dbe7bc246e491a00ba0bc -source core/src/main/java/art/arcane/iris/core/service/tree/BlockDropRouter.java ecaf3263d807f07cd67b83597767bfe4d53bd34e7456c40f6bc18e6d4f2e7a50 -source core/src/main/java/art/arcane/iris/core/service/tree/TreeDefinitionIndex.java 37bfe4dc7b97c9cfd601c9d2c1b858a674338e12a0daf2f9059b1be0fee1191b -source core/src/main/java/art/arcane/iris/core/service/tree/TreeMarkerTraversal.java 2d8870a50fcf451dd47c18e999c30ab24164540ebd35d45a78106fef436b8b74 -source core/src/main/java/art/arcane/iris/core/splash/IrisSplashComposer.java 5d281dececccfbb4f71079d842cfdd3999ef90f2ad7742d0e48a2be7fb4864f8 -source core/src/main/java/art/arcane/iris/core/splash/IrisSplashPackScanner.java b43ba3104d46d05a16203432c9d85eb1e88c0b9eec7eeb7e2c1a0b187cc537c2 -source core/src/main/java/art/arcane/iris/core/splash/IrisSplashRenderer.java 866de255a052166f76ebe68498ababe9edfc2abdc92c310655c8a7cc1f272768 -source core/src/main/java/art/arcane/iris/core/structure/BulkStructureImporter.java 3c8d9062ff38458f9eb841d01163642390b0360c01ceaccbcbedca1ea9d27522 -source core/src/main/java/art/arcane/iris/core/structure/FeatureImporter.java 0859766e81005afeca0cb09695de99d4704ad6e719fef6a179336ebc896ecb32 -source core/src/main/java/art/arcane/iris/core/structure/StructureCaptureImporter.java b57df7f806c499a07b7cf1263b6afa495e8f3f128732c877485138f502d84fe0 -source core/src/main/java/art/arcane/iris/core/structure/StructureImporter.java af50d86c34a135305037f203580938f86e287a3e170562398c35e091a276b22e -source core/src/main/java/art/arcane/iris/core/structure/StructureIndexService.java 6efd5e5071763884a1a7e7a99f0f1beb5388a8735dd570608b1c0486c376f9c8 -source core/src/main/java/art/arcane/iris/core/structure/StructureSchemaKeys.java eabe12088f14f7b76692c37e17494073960d93e310d95f519f1c829fddb7c2e7 -source core/src/main/java/art/arcane/iris/core/structure/VillageImporter.java f73042d512f177f7ed7732e0d08afc2f2a7ac961785b2005fb6c035e9023be8f -source core/src/main/java/art/arcane/iris/core/structure/authoring/IrisStructureBundleFactory.java 02da3743e55ec3930cd02660edf7879357694e737cfae1018529bb7abe54b05a -source core/src/main/java/art/arcane/iris/core/structure/authoring/StructureBackend.java ace3b5afe06f69dc1b70999efd527062963f39933ddcb995fbdf5438ce841d19 -source core/src/main/java/art/arcane/iris/core/structure/authoring/StructureCapability.java 9cadde94909c49f02fd959003225a0495fa51ac385390f9951cf5e33da8e4c88 -source core/src/main/java/art/arcane/iris/core/structure/authoring/StructureFileOperations.java 4f2805a067b2b312f3a6100a928b94984a942fe71a58eb5967783f9a35b5f1bc -source core/src/main/java/art/arcane/iris/core/structure/authoring/StructureHash.java 4967d159c691d9c4b6e904e4ea46da5f30d4be0ed78fc1da937bfe597781aa31 -source core/src/main/java/art/arcane/iris/core/structure/authoring/StructureKey.java cc5c440fb9aa262d8305c8efff5c922147cd51c83f7a963f10133edb07406606 -source core/src/main/java/art/arcane/iris/core/structure/authoring/StructureLoss.java 360daac29ace9d74416bda012f262987f474d3038197c0bf2ceb802c94930c31 -source core/src/main/java/art/arcane/iris/core/structure/authoring/StructureOwnershipManifest.java 3a064e816af6c2fb1054f3096fdc6725868ca09fde98ae38a35ea91a29d30bc1 -source core/src/main/java/art/arcane/iris/core/structure/authoring/StructureRecoveryResult.java c3f0d6dc90822a8e1371451d207fabebe8fc675321dc587371fedbfe8c996246 -source core/src/main/java/art/arcane/iris/core/structure/authoring/StructureResourceBundle.java 09588de332fa753cfcfba96d44b800873aabab0cd4cfadd77149eb7d0df26a3d -source core/src/main/java/art/arcane/iris/core/structure/authoring/StructureSource.java 3062cdd11c8a0b5da05592e3a346aa89f276613c5be3eb1c37d4d137a76f252e -source core/src/main/java/art/arcane/iris/core/structure/authoring/StructureTransactionJournal.java 6ff1b555a1dba60d217991d491c2038896f269647c68beae98c9ffff484fda38 -source core/src/main/java/art/arcane/iris/core/structure/authoring/StructureTransactionReadSet.java 90cbff5b223737455e891095896620ff35139fcd84c3d7b98e52d34c075d239c -source core/src/main/java/art/arcane/iris/core/structure/authoring/StructureTransactionWriter.java 8955d856c9e435c60207ca7d141c8e7992f3c8d2a5251db542b11164ff0c5469 -source core/src/main/java/art/arcane/iris/core/structure/authoring/StructureWriteMode.java ad751c43dc034bee19a9579386256ad5a79b4a26138bb15ec43b991c810b36b2 -source core/src/main/java/art/arcane/iris/core/structure/authoring/StructureWriteOptions.java 742b0ef567a48c82d1feae463fe40f25b2aeddeb31552798ecf80a5e3e58820f -source core/src/main/java/art/arcane/iris/core/structure/authoring/StructureWriteResult.java a011b069989e78c5a3d03ff5a56e516a9d5494902c8410a6ce9a55bee9d20bbc -source core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionDiagnostic.java d7907300dcdef922c44c4c0bc2e676a2efaf4bf4fd1bc908bbe3d395387dcffe -source core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionDisposition.java 676b24b2f15e2c3e1be019c773f775367c68bd0cd25754812472c689739cff94 -source core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionInputKind.java 97f10d973342d66d75e1a19107c323d3837921561f1da7f6248a5d7e08e672ab -source core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionLimits.java bf5eeaddaec24b136d758d1f33bf2a7ccd930044e12e3b158590e06759d3cfa0 -source core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionPlan.java c183acf72edf0e98ae27fa8208a14fe5a4db2b83283918093b0945d4d4688ff6 -source core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionReceipt.java e37b2a67f085fdea6b7fecedffcca2dbd2a3a9f4f9e107b301c561df88068d40 -source core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionRequest.java 9d11a2f5a828e382de7b0bb842eb5502979d1f78de4fa90cc2bc6a9650e76bb2 -source core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionResult.java afb0f7e2284207d6ccd1cd8ed0b3d9313216ddeb4338c4063d0d21a5cbf9ec1a -source core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionService.java 22d8278b06658a73fc0462175119997151d0c56a6808f3610add8512990b1e37 -source core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionStrategy.java 96e3f8df4d12ec829b687a0bb0e77f08ec1c9062fdd3d7325b200f6409892360 -source core/src/main/java/art/arcane/iris/core/structure/export/AtomicDatapackPublisher.java 2d23368731b42bee475d6f5fd91f36eb03156c198f41d95939f28c99ab704913 -source core/src/main/java/art/arcane/iris/core/structure/export/VanillaBlockState.java 80815c775d2c1b8df6153bc239788ab549708891341e0df03e44be619bbdad5d -source core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawDatapackExporter.java 0c6c1377dd7c07b65d0a65f4f1564e47d3709b7df94d319d8022dfffab2bcf36 -source core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportCompiler.java 635bffe86d3ae3935faa4696668adb0c2dca0e006fa1554a2a72586b5dbb4b69 -source core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportDiagnostic.java 00640fc4ac1e9f6bb5ce5faab0cb98f960aa1354240dbaf12cf1c39c57ba12c2 -source core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportFormat.java 331851b55f524c594a646716b61e9384a1419da9c2eb8a5e7e888f936366973a -source core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportRequest.java 88ec95660d7a5b9fa2c6a0cc9bc24f6a23993f61baab04d5f0e8fa924c36951b -source core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportResult.java 593733d6360278a6b32494216adff361cd0b24c2280e4950a6de03df718181fd -source core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportSettings.java 993378717d72a5eeab0d101d963021f7313ffdd44e0face6beb0c9a1224698f1 -source core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportSource.java 0da758a570f45f53c9dc665a44b1ba6d0821ce5561eb243515ce7ff1a6c3502e -source core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportValidation.java 2bde24455a7ea4e9c92a69e4e2d4c0daf0dd33be8291fd56a6ea8409738de0d0 -source core/src/main/java/art/arcane/iris/core/structure/export/VanillaResourceIdentifier.java 59eec41602fb65e398a39ebdec8bb66888fa1fc1e0d5f03e77d31cac8ce53cc9 -source core/src/main/java/art/arcane/iris/core/structure/export/VanillaStructureTemplateEncoder.java 49f907702a05e1752b0550333d8520c00c1d2720adf894f6a346b629cdecf7ea -source core/src/main/java/art/arcane/iris/core/tools/IrisConverter.java 27dd734ac27b6c0c61a59b864177a09a81747de96e7cf442fb33609ea73c856c -source core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java b9e2d8654385ee104936a070170ff6a930c076b9be49dbd85f163c3ec508e391 -source core/src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java aae81460e27818c657feac112375f3a006b2d52b4677d177dd00af7509ddd400 -source core/src/main/java/art/arcane/iris/core/tools/IrisReflectiveAPI.java 8f79765d8c267ef3ed56331771178dda3cbcaa7c231a0bb953c1af2e97dbcbc3 -source core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java bdeb0dd8d29cd4a27442257fd254be8fcddf6e8192f03f7c92b1cfc186c064a6 -source core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java 7600864f9ab2816a5a5a3aa9383e59af54d8801674730df44c233f20c4c2b2d1 -source core/src/main/java/art/arcane/iris/core/tools/TreePlausibilizeBatch.java 4e7a7da1047e4ed19f04391429134f970465d4a9237e2870fb6e98a56381d869 -source core/src/main/java/art/arcane/iris/core/tools/TreePlausibilizer.java d8bce0161edd72aadbd95f646f46eb6e4acc3e4feda8b061d39996bd7f865dfb -source core/src/main/java/art/arcane/iris/core/tools/WorldCreationProgressReporter.java 099aa1298326369af0f48b22f6eea627c3a9d94a6df4c04ba209d615723d9527 -source core/src/main/java/art/arcane/iris/core/tools/WorldMaintenance.java b8b8310ee456b841df1801ba96f3ba61329a84271c8d7553a0e4c7a9555431c7 -source core/src/main/java/art/arcane/iris/engine/DimensionStackContext.java 7b6b66d19a4488c034d5db5bcc7267b317c5fd50a63ebf0932eaad0f257b79d1 -source core/src/main/java/art/arcane/iris/engine/DimensionStackLayout.java af301cd642c12d7c986728a5af25de41608665e024f83129c30a4f07ec38f2ac -source core/src/main/java/art/arcane/iris/engine/DimensionTerrainContext.java f911f2b1dd3d3fc89d4a14e7f820a8cedd62c69549b48dbc602aaee85a843451 -source core/src/main/java/art/arcane/iris/engine/EngineBackgroundTasks.java 2a6816ad45ae2bad1dcf85c787b1c4b98373243a7614e05fc2e4e867abee04aa -source core/src/main/java/art/arcane/iris/engine/EngineDataStore.java fae22d19d7f159f132b79874f1a2d2e701e969fa156a5d54c6c300330e24cbad -source core/src/main/java/art/arcane/iris/engine/EngineHotloader.java 7c39d8ea19814ee51aa273774d419da71553e3d7d8a272350f81414fd4cc94a9 -source core/src/main/java/art/arcane/iris/engine/EngineMetricsReport.java ac15c31f8a3b2c1feae0dc7209816f728e39bd62ca6a74855e6926fb220d3774 -source core/src/main/java/art/arcane/iris/engine/EnginePanic.java bf6eaec2ecb1212f39706cf56759d927ca90dffa90acfe9bb75cde70019e4d61 -source core/src/main/java/art/arcane/iris/engine/EngineRuntime.java 423e70bf2aa84a465d8f3f643153d6436ac6748713247c8ecfa3f4de030f26c8 -source core/src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java f5123a38fbd4d0bb5564626212c486ef237275d3167a022d817c3894db06eb94 -source core/src/main/java/art/arcane/iris/engine/EngineShutdownSequence.java 3b2b78f4ff64903b288c66da7efd01adc25f25e5f2db20f4475a3d27fb1e53ce -source core/src/main/java/art/arcane/iris/engine/EngineTickRegistry.java 9ae20d3c6304ed7eb3676f0d5947d44f3f5591a2c33bab2d1087b8d68f3a662e -source core/src/main/java/art/arcane/iris/engine/GenerationCacheWarmer.java 821455b42549511328dfd90e5447dffcdf8ea1311490f1b0ada2e65ca1a311d2 -source core/src/main/java/art/arcane/iris/engine/GenerationRuntime.java 09e250e72e1ac63c38c29dc0860c951955ad71a7f9ec7a36ac27d7d27839b11c -source core/src/main/java/art/arcane/iris/engine/GenerationRuntimeScopeState.java a05de937d2d8ed1b5017549f1083e5f6592d944fa5a136dcfb5e9bc33eb78f2d -source core/src/main/java/art/arcane/iris/engine/IrisComplex.java 257916da58a83632887123debc9f0dea9a63087de61391550774982f5c613fb6 -source core/src/main/java/art/arcane/iris/engine/IrisEngine.java c64aabd254a8e75d0a52c5c4b7b43f06013d6762c7a1cac4bb1c324e9447db80 -source core/src/main/java/art/arcane/iris/engine/IrisEngineEffects.java 67cf96aed6c213dbd6d6cb81866afd29cd30b555cd4e88712395bc089e50f43c -source core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java 2c4251a6ad59b5435488fc3b83191c3753d5e356bc5c58832bba2c56a3a5aae9 -source core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java 5f441c4ba85045b4c273e934599e65df0122586d97cb23ddae3ed76c9471c759 -source core/src/main/java/art/arcane/iris/engine/MarkerSpawnScanner.java 8ef17c72732a387e49f401f53ae71f6e9c2e9d814018418d59ec4a6c4f84cfee -source core/src/main/java/art/arcane/iris/engine/NativeStructureBootstrapBarrier.java ba6545337f8547279e49f613fbbcb1c2f53181a8d3073cdbef0aad5bff425b91 -source core/src/main/java/art/arcane/iris/engine/SharedCornerBounds.java 75bb0a41f4707ef2435dcb9c48aa85816e685c7ac15dd3980eba09ae07a2e4d2 -source core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java 0fab515c2bedb02a7563b869bf8894ba99e21d82b14f96585158020ba8fc8a4b -source core/src/main/java/art/arcane/iris/engine/WorldBlockDropRouter.java bd39f4ea0d47a2c959cffbb0a516767d49d84c279d3ef7b6e032c941449c55ce -source core/src/main/java/art/arcane/iris/engine/WorldChunkMaintenance.java d3b3b2eaf6094661af53ebbcd85fe65ef8a4ec82f045623c3a585b4f89861903 -source core/src/main/java/art/arcane/iris/engine/WorldEntitySpawner.java d4ca0060064752e5651b46ca3f124c1e731b2c119d838dacdf86170048aa2639 -source core/src/main/java/art/arcane/iris/engine/WorldTeleportWarmup.java 4726b21c78898e45f90df758f87921f3b72d9fac8078893e06bbcc2f05308bfc -source core/src/main/java/art/arcane/iris/engine/actuator/HydrologyFluidLayerSelector.java b512d17cba3769f756144d1e7ff740a4b6ab4b9ea0e1b08162d5d53bf263d998 -source core/src/main/java/art/arcane/iris/engine/actuator/IrisBiomeActuator.java 465e2f693cb4d4278d7c654f0e46a5f530f5fbf9210ff10597bc72bcdab0ed44 -source core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java 9fa53a4e10473004f53a21a2a77687d83b0e48a84b9f3c533e04fd5829c8651a -source core/src/main/java/art/arcane/iris/engine/actuator/IrisDimensionStackActuator.java c20e4ff59ce96207184ca9221337448cf38feec3dc5ece393b5290a8f1f38c3f -source core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java af8a315ddd1a1ba4cb8194282bc470fea72fa30ebb4d429e118057952e05085d -source core/src/main/java/art/arcane/iris/engine/data/cache/AtomicCache.java d30843c3a1d726737164a96109a2be2cca9c7111611b769325c409beff2fd436 -source core/src/main/java/art/arcane/iris/engine/data/cache/Cache.java c5bb7071aad28a7992d005dff53ba2ef8accdbb77e5d537af4bd15874cbc0e57 -source core/src/main/java/art/arcane/iris/engine/data/cache/LazyBoundedCache.java 2feda25bc1ff17396e66e86346bae39ecf72fb48ecfc016b115d95107d2c68a0 -source core/src/main/java/art/arcane/iris/engine/data/chunk/LinkedTerrainChunk.java 443641621f5ecfb95334a37d86f925565720ca269d2a197858c2d1e537a7b4bc -source core/src/main/java/art/arcane/iris/engine/data/chunk/TerrainChunk.java b917e0d50bc2f285dbd94501f74bed594ff62af05c4b313aad243bb166c7dc55 -source core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java 9e9fed190f83b3efcc0da205de60551b422dace9d39cba97b507491aa443c891 -source core/src/main/java/art/arcane/iris/engine/decorator/DecoratorPlatformHooks.java 1294cbd6001a4a88b105ba59bcd83cef74473ab7cd5e50a2f45e181722390b60 -source core/src/main/java/art/arcane/iris/engine/decorator/FloatingDecorator.java ed178dfcc566b0d42d3df5f2bc4dec09d37fe76e5b79d84c411d76e47fe377a0 -source core/src/main/java/art/arcane/iris/engine/decorator/IrisCeilingDecorator.java 62e56a07facdce70cb235277b851fa356193bdf2258119d96bfb554690c1dd70 -source core/src/main/java/art/arcane/iris/engine/decorator/IrisEngineDecorator.java 033d20c8a8af3b51e80ea348e0d1249ac0ba297db735a77096fda1ee4eb1330e -source core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaFloorDecorator.java 0dd3099a46d431bdf0a22a3421ed15db6668117e48f915cb8a2d848a378328e9 -source core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaSurfaceDecorator.java 63d13573ebf6eefcdf699e05d748ceef5e666741d7ffaec3423a09dd1608c24d -source core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java 9fb4a007d87c13e11960cdf106c304c7f91777e4285e60e2a8d220422d31ade0 -source core/src/main/java/art/arcane/iris/engine/decorator/IrisSpeleothems.java 0be913b5f2f7fc3731ddf501982e78838432bb7aea91dfc3809ec52324621736 -source core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java 7eb072f6c062442b20f4aeca8631d682fc796b945c25b961df14aad03f2052bc -source core/src/main/java/art/arcane/iris/engine/framework/BlockEditAccess.java c27a4e22ec63a099648bca5bf60478b4410e035aaf16063819f5d571617469aa -source core/src/main/java/art/arcane/iris/engine/framework/BlockUpdater.java 453e0505b4afbf5e869fea947149d6dc9624724145e7af8db4be9533aa4f9134 -source core/src/main/java/art/arcane/iris/engine/framework/BukkitEngineWorldManager.java 19bde699f439a085aa35c97b383e75502f70aa2ea3f1564ed87cf7e593fdf8ed -source core/src/main/java/art/arcane/iris/engine/framework/Engine.java ed3e583f3ddbdfa76f2b77856a7e07888ba8dd649e616c5fd8528055142881ac -source core/src/main/java/art/arcane/iris/engine/framework/EngineActuator.java 2cdf4accb678a3d208c93d8015203f2c3deb0f96c5e83937c8b2997630300186 -source core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedActuator.java 19eadbaccd498c1998c3470a3d1f22c2a413e256d4df03cef635392c03d3da25 -source core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedComponent.java bcd525a0c57631cdfc861344a04769c8b14e2b9d68bd24b041693ad83f77ee3f -source core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedModifier.java cacc209c920c3a708462b6d50437ee81b6318650bbfb3215c7c828d27e0137b5 -source core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedWorldManager.java be45f07902215f0290a0c8aba70d5a67301a7886a0a29eb77ccb7f472473254d -source core/src/main/java/art/arcane/iris/engine/framework/EngineComponent.java 529005359f213ce1456eaca6ea86efcde5ab2973b83d0d792cf16c8bffd906f0 -source core/src/main/java/art/arcane/iris/engine/framework/EngineComponentCleanup.java a75ed490cdec2e52de958b572b670ac686a309119718218ea14744b0ccecd0c7 -source core/src/main/java/art/arcane/iris/engine/framework/EngineDecorator.java efbf2c8b7442fce1ee5df591a05a8a1a2af27c43a197d0f2d1390cf9dd24bd03 -source core/src/main/java/art/arcane/iris/engine/framework/EngineEffects.java 97bab6dbbd3502cb8f44e1e8ddec7c848ea23a903fb21b5d25a1ffc671e2d412 -source core/src/main/java/art/arcane/iris/engine/framework/EngineEffectsProvider.java 5cb096768fa3a59e677e0dff784ba76ee4f0fa806c9d993858c40590b4038fa0 -source core/src/main/java/art/arcane/iris/engine/framework/EngineLifecycleTasks.java 8363f850ad7b0f65340e0b0c97bef960cfc259a2c2e84329be74edc5c993fd9f -source core/src/main/java/art/arcane/iris/engine/framework/EngineMetrics.java fc1b0d5c90784da7fc25c7d1ced54e74c37745aed82dbb0e7eb59283a270d234 -source core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java 2e68d91b7e63fe5c575afe1bfc8b198c1ddaa20154a4f0ca5801c3bdc362e89d -source core/src/main/java/art/arcane/iris/engine/framework/EngineModifier.java 6cf63dc7142f19c33400525126728e6f72c4778a2d5ac1ec7471083119d8924a -source core/src/main/java/art/arcane/iris/engine/framework/EnginePlatformHooks.java 70abb258d41d809ef95c6946dbdd3b9504e7a870e2bdc7a336536e81d3ca1a0c -source core/src/main/java/art/arcane/iris/engine/framework/EnginePlayer.java 9b4b1b8aea343b8aa4f8c3c0603283a54e8988004e9927f84567761993efebaf -source core/src/main/java/art/arcane/iris/engine/framework/EngineStage.java 68f6fe0411a883f3be7296ef007b1ed6d36cba3f71536380921111da1ddc9283 -source core/src/main/java/art/arcane/iris/engine/framework/EngineTarget.java 2c2b4a10438f51e0cce580506de7a0e57cbb4e9e9024a36dba134422c1b43794 -source core/src/main/java/art/arcane/iris/engine/framework/EngineTelemetrySnapshot.java fd74a583f89c81748f7bcc37f3571f3e9bed2ecec087ee49d3eb33d265a59a7b -source core/src/main/java/art/arcane/iris/engine/framework/EngineWorldManager.java 4ef11d18c377c1fd55499a60bc4959d2c830063f680d179c500014243c85ac9f -source core/src/main/java/art/arcane/iris/engine/framework/EngineWorldManagerProvider.java 798bf4594bc8f59c3547ac2b3c56bae6cbad8397c43e4d7e94affadf8d3876f9 -source core/src/main/java/art/arcane/iris/engine/framework/Fallible.java 1fed3b4b882aa8450fe2595746d90855d43a0420b7dc2e587620cc37d22688bd -source core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionException.java 7e300e94b5d38636cbf35ec37d8ae4ef1e7154b30f4f7d66981523841d3e317b -source core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionLease.java 3590f8b6cd21d9f57ffb86445346c5f8bda9cb6159205b98196f20774e5c3cfc -source core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionManager.java 12df8e96c27776882b719fa9b8bc58ed1bef67bfdbaf37c46890227ce14e19b0 -source core/src/main/java/art/arcane/iris/engine/framework/HintedLocator.java 344a0851bcbae9afe6ff0b3b99bd70b11c732c7949cdcbe315488b68bd5e75de -source core/src/main/java/art/arcane/iris/engine/framework/HistoryAwareLocator.java a75d1db64618a7a51441ac2fe21c2047c2a47e2eda2eea9ed4902302b467373b -source core/src/main/java/art/arcane/iris/engine/framework/Hotloadable.java 167b07b43e2a95f840dcbb34bfc84dcf4a9f7b1c70936b25c7f08707af9934d7 -source core/src/main/java/art/arcane/iris/engine/framework/IrisEngineMode.java bc9814b96f7a550247e84e3594f99aca4459eaeeb3516575697c975c2e165f4b -source core/src/main/java/art/arcane/iris/engine/framework/IrisStructureLocator.java bbab3e73efa8ecb269ebaf75252c67af0948d82d57ba7f83f1616d556aba8e7b -source core/src/main/java/art/arcane/iris/engine/framework/ListFunction.java d2d743e0d01a17827e889e04d2e1be10dd146abde37f9c6fdd9cf279f753e4be -source core/src/main/java/art/arcane/iris/engine/framework/Locator.java cbb3910980f4c39de010e4883edcda9cbae67b650dcd73afa671a5c91c301c75 -source core/src/main/java/art/arcane/iris/engine/framework/LocatorCanceller.java a7b9a9ff4707d318542ee6c1dd6b60ca276694f68dd53835659380d27526de86 -source core/src/main/java/art/arcane/iris/engine/framework/LootResolver.java 7d12b1239d13201ee7d4ffa50f3376fa03de966cf2c9b43306d7d625d8cadc60 -source core/src/main/java/art/arcane/iris/engine/framework/MeteredCache.java 132068ab7b120ffe032ca58f9224d10a4e4ab3cf27ce1b6937be482f20488b6a -source core/src/main/java/art/arcane/iris/engine/framework/NativeFeatureGenerationPolicy.java 8d08e4f4c84a1b40782a02a906e7354fb40f37094b5adab4ff63357e1c8fd3a5 -source core/src/main/java/art/arcane/iris/engine/framework/NativeStructureFrequencyScale.java 669d0b3dd52f5b4be4a2c77d0d3b108aa1c08c1433321c1b98020d6f263f648c -source core/src/main/java/art/arcane/iris/engine/framework/NativeStructureGenerationPolicy.java f909b7a14390923c01f6e471b7a9cfb80e50fe25886177b2c4a94090f0d0d129 -source core/src/main/java/art/arcane/iris/engine/framework/NativeStructureOwnershipBundle.java 62fee5319135775ed3a5de40c1eb7a84ba771b6a063971c6b949bdbe737f95e6 -source core/src/main/java/art/arcane/iris/engine/framework/NativeStructureOwnershipRecord.java 30802a31e98bd802e88a149f6e23fcae53595df9bf80feb37f3e23cd4f422268 -source core/src/main/java/art/arcane/iris/engine/framework/NativeStructureOwnershipStore.java 4d2581de9c441e4bcd9199f9a929ca874ac9041360a7246bfca8480ae67a39c6 -source core/src/main/java/art/arcane/iris/engine/framework/NativeStructurePlacementPlanner.java 38fbec94120fcfe8b4c13227075a87a2f585f52538a74cb4bd749bedf45b1045 -source core/src/main/java/art/arcane/iris/engine/framework/NativeStructureStartPlan.java dff14250cb3d45c6a128fab52344f735ebdf5a248a8c473bd81565efc76f227b -source core/src/main/java/art/arcane/iris/engine/framework/NativeStructureVolume.java b0343519a3a34f8f1a8422ef3875d9ae87e9f18908d3e147e4430a4812cea7d8 -source core/src/main/java/art/arcane/iris/engine/framework/NativeStructureVolumeMemo.java bcf8821ab91afb81b88c7c7ae82f531deb85370ad04d0ba6b10a19edc4dfd357 -source core/src/main/java/art/arcane/iris/engine/framework/PlacedObject.java 533816a9d53c147b81bec266d6e02575ea3a491589d758659b89691db3d4b4a5 -source core/src/main/java/art/arcane/iris/engine/framework/PlacedStructurePiece.java 2d79a83d5c89b5c1914cf58bea7e4264f322968d77e0a038d0a2a5271085b586 -source core/src/main/java/art/arcane/iris/engine/framework/PreservationRegistry.java 9b15cdac5b1831c49b1861953d29235fea3c3ef3ad06d89641bb6313184ed4f1 -source core/src/main/java/art/arcane/iris/engine/framework/SeedManager.java 0738b6129967bef649f9bc0ec46928f811abbafbaff18aac2308823996650865 -source core/src/main/java/art/arcane/iris/engine/framework/Staged.java 7e6c7de380e58c6d39400795e5dfc0180fca8bd42886885f783e9db8671c4f3e -source core/src/main/java/art/arcane/iris/engine/framework/StructureAssembler.java 4030b4a74ff19f27e1e6f7b05e63cde4b0f5bf0fda4a0bdc75d1c962eb44439f -source core/src/main/java/art/arcane/iris/engine/framework/StructureCaveAnchorResolver.java 55dff6da5de9557f68eeb0deb7b51539f485c32dfc8013223c5f7a20187d31ec -source core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementGrid.java 632aa8e153584fbc564a9c1e0aeba3292ce453e0c45ddebb24d1781ed05c4699 -source core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementMarker.java 4596be075ab6d015aeb9f3472f7ab69ee08d5e098b02723e8244417c63a9f3bb -source core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementScope.java c6124b8a74301a4113ea88c16ec6eb23ec42b90fa83ea7dcdaa08b284025393b -source core/src/main/java/art/arcane/iris/engine/framework/StructureReachability.java c71fcfdc18d491fd3b5408af069f9435d83923003f4d66e04f007be25aa1f482 -source core/src/main/java/art/arcane/iris/engine/framework/StructureVerticalBounds.java 31534d99462922396fd89f323e395cdcf9536e3b3f33dde3ebd064c87dd2be20 -source core/src/main/java/art/arcane/iris/engine/framework/TreeBlockMaterial.java e69f4f932467f51667c22b6729555912cc6e1d6226b3d644fd4f3c567ca44d92 -source core/src/main/java/art/arcane/iris/engine/framework/WrongEngineBroException.java 0fb43e28006a20976361bc6d57061b789beb8e7e53d173c6fa688e95f06b73ee -source core/src/main/java/art/arcane/iris/engine/framework/placer/HeightmapObjectPlacer.java 8d467c3ff58f0b3a26ad98e8b168ece40bdb58c44be3f884eeab926174328439 -source core/src/main/java/art/arcane/iris/engine/framework/placer/WorldObjectPlacer.java 4d97f8f0ceb213677626ca780b3d0343a2426ce1e2bde64c4a4b9236b41d34f9 -source core/src/main/java/art/arcane/iris/engine/framework/render/IrisRenderer.java 84fac66e4cb0c8c9470c3d454cc0a5e6f71fe53396123d65e186dd3289b40fe3 -source core/src/main/java/art/arcane/iris/engine/framework/render/RenderType.java 60b374601a9ef6e43dc049278331176ccadad25f3915317fc2984f12b765481c -source core/src/main/java/art/arcane/iris/engine/framework/render/Renderer.java 095b96141acee8f1b142bddb50a767b2fc97243c3df9ab91d3d9ca8d4354d551 -source core/src/main/java/art/arcane/iris/engine/framework/structure/CompiledStructureGraph.java 2589e08727111aa3f62a52c50e64380b292cf9451fdda9ce4166aeaf12f7f8bd -source core/src/main/java/art/arcane/iris/engine/framework/structure/IrisObjectFrameReader.java 2a740825c4d6409229a12a4d27e620e31f8cb1ad5ce5f43e5b01d66e8461cc27 -source core/src/main/java/art/arcane/iris/engine/framework/structure/JigsawPoolSelection.java 949edbbfa1accbbbe8bb1733f6924b33aa234eea6bbfcaeeceb5dcf187297189 -source core/src/main/java/art/arcane/iris/engine/framework/structure/PlanarJigsawWorkcellResolver.java 489be3bcc7343517e0eeca636a0b96b26ef147a4d972876b8330fca848e339e1 -source core/src/main/java/art/arcane/iris/engine/framework/structure/StructureAssemblyResult.java 39db9401b1ca49f3018def61060e3e9066312e2d2839c45a48c6ac6d7d1a34e0 -source core/src/main/java/art/arcane/iris/engine/framework/structure/StructureAssemblyStatus.java ca8f8403b48e70f0bf43c11c8ae93448d9efff654aaaec4304586cb17b5e690c -source core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphAssemblySample.java 8696ac40916176142a42814056ee093a72bbef04ad7984233b8dba2a13d5068d -source core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphCatalog.java e5897ced312bb0e1d8c5a7870b05662671e4d4ae5758d9e2ff14e82a4fd0691e -source core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphCompilation.java 6acfcf85c48ed02ea5d23a2e635514dc1647b6bb0f729af2bf637cbd4fcdab50 -source core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphCompiler.java 0e6fea0b9eebad4bf855bcd0e540d5652f0351de2ce393dcd9a537e6718317d5 -source core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphDiagnostic.java 4ca011e67033f62843aa80703c13198c455570c89c4423e974502f53ef4518e0 -source core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphResolver.java 2df0f605ea0009f76ab68200ade964276bcc096a0ffb8a67bb6d42718de5f45c -source core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphValidationException.java 7872236245db6e94b12d523f21b63b183705456c1a9eafedea5d459b7138c699 -source core/src/main/java/art/arcane/iris/engine/framework/structure/StructureResourceBundleGraphCompiler.java a1c3670bf5645ed9d4fdda4ce7580e42e6c91bcd67eb8dd75b21892c941d2bed -source core/src/main/java/art/arcane/iris/engine/history/ChunkGenerationOwnership.java 77166bf1631597f0ba57159b8943b8f0d2446abc0c3ea317fe6be07770da2154 -source core/src/main/java/art/arcane/iris/engine/history/ChunkGenerationSemantics.java 737216a6b0918c34fec806685e064cda06103fe3138b666896a4eaa595c5325a -source core/src/main/java/art/arcane/iris/engine/history/GenerationActivation.java 7f50f7675c80595b84bc914bac80a341122ad9e6a60acf124f5519cfb4da80ec -source core/src/main/java/art/arcane/iris/engine/history/GenerationAdmission.java cef7b2c1a6152163943e527ea40605c4f7f6279420ae2a8da7d3d8f033c32550 -source core/src/main/java/art/arcane/iris/engine/history/GenerationBlend.java 10029ec9b6d6157c2fbf9040bed1f5834df99ae9f5d185ead6921f3780ced24a -source core/src/main/java/art/arcane/iris/engine/history/GenerationBoundary.java e91bffef9a977fad40dd883d49aae3a4c31d014ac7d4814cdce314ec64d080b6 -source core/src/main/java/art/arcane/iris/engine/history/GenerationBoundarySignatureSampler.java fc1dddbb5fa7638b111198be037cc6289b8cfb1f9b4862c1dbc79e69fe671760 -source core/src/main/java/art/arcane/iris/engine/history/GenerationBoundaryStore.java 28a23986559bb14b7637b89478098450dc2debe2b7fcc7e4a3315bff626dde62 -source core/src/main/java/art/arcane/iris/engine/history/GenerationEpoch.java d2c52d6566a4db09c1b43d08405067854cd806d30d055de819ea62ef5878eecc -source core/src/main/java/art/arcane/iris/engine/history/GenerationEpochContractFactory.java f2870dce0091f4f02707f902285ee9637824c7140e521ede47a9bb355d66eb9b -source core/src/main/java/art/arcane/iris/engine/history/GenerationFindCatalog.java 027b5a18d59803de55ee11a41544899d31bbc804e65962c138610e53199a022f -source core/src/main/java/art/arcane/iris/engine/history/GenerationHistory.java 83067b31473407397005722244d9a619d97a768dacbc6e770e592ebea1549093 -source core/src/main/java/art/arcane/iris/engine/history/GenerationHistoryPaths.java 200f0d6454b5e5258f113f4ede58e0f50eeef748c662d20660c3a064876b81bd -source core/src/main/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouter.java 8f77ecaa387f0b94951a64602ef88dbc25e2b075a514bdc7645ce3f07cb4405a -source core/src/main/java/art/arcane/iris/engine/history/GenerationHistoryStore.java ad28e74fac15de6fde17603edf6bc52086a9020df6ab89c05262a097e0e2764a -source core/src/main/java/art/arcane/iris/engine/history/GenerationKernelCatalog.java 2ecde868b74fb173a6f429fa5d3bb1c2da371c2a72e85c8144ad97000d2fc8b4 -source core/src/main/java/art/arcane/iris/engine/history/GenerationKernelRegistry.java 623ec226bea38eee2a73de8cd851b17bcc71036b7ee0869f5e89812add1530d4 -source core/src/main/java/art/arcane/iris/engine/history/GenerationKernelSourceSeal.java 17ef50f051d8a165c4af032b6cd53f200cb368ea4cdd4091433faf2e2e98097e -source core/src/main/java/art/arcane/iris/engine/history/GenerationKernelV1.java b56e2d5c32bdd99dba3918c44ef6db3d7ce23a320032407d55e5361e07811ffc -source core/src/main/java/art/arcane/iris/engine/history/GenerationManifest.java d3f787277d62681be62e5bd7943090c7ea3c3e3497588ef9f846b2d3cd1d57ff -source core/src/main/java/art/arcane/iris/engine/history/GenerationPackFingerprint.java b01f725ef66bcf64303e8d0b4807f6c740b864bd80d155ff908eb046d0ccd2d0 -source core/src/main/java/art/arcane/iris/engine/history/GenerationPackRepository.java a10a74379157027c440708f29ef848098e7bdc1f6917ea8f7e3afbc8a156f81c -source core/src/main/java/art/arcane/iris/engine/history/GenerationPublicationLock.java 852fefed8751bbd111011c64e4ddc161a76487e47896202372acfd5d6c89f891 -source core/src/main/java/art/arcane/iris/engine/history/GenerationRegistryContract.java b5ae41ffc7d15300f91c156c1cac69a74089fa39e6ff7333f17c3ce2b2fbe3b0 -source core/src/main/java/art/arcane/iris/engine/history/GenerationRegistryContractFactory.java 0c6d29055828ff6dffcfe78d1086ecb805edda465a8e13ea81f0dc7a89b81693 -source core/src/main/java/art/arcane/iris/engine/history/GenerationSemanticCapture.java 23ba9e97a353a7e2ca31f5b34f84f1ee90bc773808b72b7168c95a25a728ebd9 -source core/src/main/java/art/arcane/iris/engine/history/GenerationSemanticIndex.java f030ffa9c4275e269db6e1c25304e3825ef1b8317f9037982f048651912c539b -source core/src/main/java/art/arcane/iris/engine/history/GenerationSemanticQueries.java a62539b4893a88f1b4c953b58e411b09bddcb8c816feba31d33e6a2a023c7853 -source core/src/main/java/art/arcane/iris/engine/history/GenerationTransition.java bb5a21fe3b282c508416975bad8763a8b3a871188068bdb1e76daf06c9ab3fbd -source core/src/main/java/art/arcane/iris/engine/history/IrisBoundarySignatureSampler.java 3f6ae1a49f614b5ce929576ddfc67062464ffd755384145e0003b801b28f080c -source core/src/main/java/art/arcane/iris/engine/history/RegionGenerationOwnership.java 19eef0b83719461e373ea5175c1083b0b90cac2fa64d666daf91e6d6ee096a04 -source core/src/main/java/art/arcane/iris/engine/history/TerrainBoundarySignature.java 293dcd10a2417b7c53be6ae1d9ecf3b494e4b4acff8cdfc5e7e016acbcc2972e -source core/src/main/java/art/arcane/iris/engine/history/TerrainBoundarySignatureStore.java 5927cd7c3a07ebd5700757a41a2362610229ab5656a14120218949e2c332f653 -source core/src/main/java/art/arcane/iris/engine/history/TransitionBoundarySampler.java 2899dedf8863494c7fe0f7ecb4c19d404de3ae91217b3c928d46da3dd2813fab -source core/src/main/java/art/arcane/iris/engine/history/TransitionGenerationPlan.java 7b077551600ce61d098ad76ccb784f15cb7d4b3ad79724810181659803eb2333 -source core/src/main/java/art/arcane/iris/engine/history/WorldChunkInventory.java f1fc23646d07ec16b1aef7915c2752fd9f14d1ce8708b2b743acd799ea578659 -source core/src/main/java/art/arcane/iris/engine/hydrology/DrainageEdge.java ba1f1761fcda44a22f954436bfe8bd7131f779cc67dedce245263298ec183159 -source core/src/main/java/art/arcane/iris/engine/hydrology/DrainageNode.java 4cbf272410a4fed6731723c6fb3cbc66182a0a93c5b72b7d195eca5cc3a771d6 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydraulicSegment.java f2b259965faedef9db58c811aa0168ba7f4e86960326907b7738955dd94ab149 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCandidateKind.java 07228baa7ac628f3b711116080737b0645885a8deaaf7e264fe24f536d61ad37 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCandidateRejection.java 8ef0b47c6006bf3ac04b9a8f2c76252a4d5a9969bbec1985a9720afe54914869 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCaveCourseFilter.java 364b7f33b48a756b748317a0b98160a6e794ea4478f91d5912ba654d38a2fc7d -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCaveVoxelViewFactory.java 932a430287264ccac67086e019303aef5a12303c751bf65e33b9fe2d1f9e3997 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyColumnLayer.java f97523a78d7b0224eba9cfd3dbcd90029ac76ce689aa986515c08e839cc29b04 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyColumnSample.java 591e132654fe1b459f0a1e394b991593f9d793182d49a31eec1b422b8b88da76 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCrossTileCaveAdmission.java c364d91273fd7a6caeedf5f905c55519b090c25d3be8fe88d1cf53877629f42e -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCrossTileSurfaceAdmission.java e3edce502c45f10f6d8b282595417817eaf29c39a4e9c8d0e82d54989b633e71 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyDiagnosticCandidate.java 1296aebd409d5597d62a588434f7191f34c6b5f46dd09596286d9fb980f9f153 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyDiagnosticRenderSample.java 3f164f58120c978d2b25f53a820f2985d1bd1be090e3726f97cc98a1a281642e -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFeatureQuery.java 60e6478e03dd339f57e7d6633c02bb6308d2de27ea86a634b26745b697121708 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFeatureRef.java 6e02a66c7ec7c9ee4732176eea248e4ba3174c66c164da02c12dee7e229cb0ee -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFeatureType.java aa574734891b581bd140320cd23707d9f61bd9b3afa851909d4c8fe6e20dc1e4 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFootprintCompiler.java 076eb55abe053524cd1990c62458e007092fcba69118c620592edfef0fbad4f2 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyForkJoin.java 25005438b669b33bad5344ad6dd08127d0e928a3d383c2bb25610acb385322f1 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyGeometrySampler.java 77ce4ed588dd7238ae448e8997a688df8cc3a21ed4fc8f829d593b9bfbfdd7c5 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyHash.java 41771991828e67f219c36985ffc8dd3f65e9664b0a51f4e30be16b13b55e9c1d -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyNaturalTerrainSampler.java 2ddc53c8961e1db153ac36f7f5f4e71742a22316bb644187e02c52740dbcf41d -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyObservedPlannedSurface.java eac4e7f4ba41814d01c2fc4bade4653c89e53db7bd8ef498e0c54ad22179fb15 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyOceanBoundaryRefiner.java 7f6aa7fa674a4f0293cbd42de1808347d0c101874ba0f5c870117b9ede5a562f -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlanner.java b3c98015b4623049027effae6a2f8dccc5c82c97787304e2de1cac6338e62f51 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettings.java 5165f5b6faf5ab79156d93f94687c8f7249f16726a63592fb51c46c1b1ddb75c -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPoint.java 015a50924678d09c8447e0d66fa24c93da6a23178bc819bc0b8c320b9d82e7dd -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyRenderSample.java 03587336c3d95369ba7c2dba1e17346b637c5cda75b6ce93e1aa3761bc64815c -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyRoutingTerrainSampler.java 639735edf4431ea4b75e6009d674e698e8ccd9060cb891a5e294fb86380383af -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTerrainCaveVoxelView.java ed038c059020bdb6d10040e29946349be6ac50df894f5b5e1f71acecb4b553ef -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTerrainSample.java f1ea65b71befeb5d0cdb6d3c8402f0023f925fb13c16cb0c07ff1610f4079cd6 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTerrainSampler.java 190fd761a0be72700e05f5bd26626dcb51a87e59e280e9861ad3d79d5c86c494 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTile.java 33ee976cdba3b051d31738ab3acc6d5c4eea01c7043fa4d2ed24cb094b0cd704 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTileCache.java 0f39c34037f39945f99fbf17e27327d7cbc890956aab578a19129030f28f0da1 -source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTileKey.java b806c0df952d005f0f0543225b18dc1990ed6f9a85f68a9489c96011b3ef5a5c -source core/src/main/java/art/arcane/iris/engine/hydrology/RiverCourse.java f919c7ce0953a08a65e015d84195e6a43980d9c409bbf5d3dd9cde86a9307e7f -source core/src/main/java/art/arcane/iris/engine/hydrology/RiverCourseType.java 7bbb28a498f0d7705caf5e278e68922bacdec16a16fa75643e14feca721ebb40 -source core/src/main/java/art/arcane/iris/engine/hydrology/RiverFootprint.java 13e34daba714ed0e43b4ae4f59559840ae12ec1cb5feac5093de465f5998fe93 -source core/src/main/java/art/arcane/iris/engine/hydrology/RiverOutlet.java 2c413eafb48bfd3a6e4cf81bc14516a4b0e89057cbafc071d24d41000c9807f2 -source core/src/main/java/art/arcane/iris/engine/hydrology/StudioHydrologyTileStore.java 605ebeaa737b65274de99886a00893eb7f875ecd4a93d51100548863f544f5a0 -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/CavePosition.java 7fcbd256263acf6debc2d76e5cda0fb324253095144ba0a497889772ad3f0daf -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/CavePositionIndex.java fdf82e18f3123ed976cc7a2f688db113179941dd6d8337228df9f31f5475c784 -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveVoxel.java 6ab8b8407eb7e2f92e8284d8026e228951ae8074a679f82055456f5cb5dd6083 -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveVoxelPrecondition.java 84be0fd58827ed34c09aaa4c41c1f008dca34248b75c581e156906a78dcae742 -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveVoxelView.java a80a491c3bddfe65b52981e461e8a2ecd356e99f0bb8604810dab4416603f4bb -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveAction.java a7bdc7fd153f10e9789509f1fb2885792b19c7ad8d98d8e0777167a57ee1c78a -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveCandidate.java 4f5c03cad0fee52b1da5efc1abbfd931dddf34f68ff5e761a1681fc7deb26a68 -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveCell.java 928d5bb1a942a8c7d339668dd026841f0cdc4feb8c491be33e3d858a00655a85 -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveConflictPolicy.java c054815c89d861b687b09eade77e2bd262e481a31d196deb12ad596287dc58f7 -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveContainmentPlanner.java d3bfe93d1bf9758874463600b0f310770d0c3bec2a6f3f2eee1b234d993dedb3 -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveFluidPolicy.java c83ad9e7d90a13a2f3d3e7f199bc27fcd55cfa5f89fd405825e9716d3c0f9079 -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveGrottoShape.java 5367bd021c702ed49bf31a5db9c87e42d17fa894f63442b934c8b7193d4026fe -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveMode.java 4a23289e4a1225cf3b49f4eb63cef22b0bb7b0f64ddd5a625a0c6b1258ec6000 -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCavePlan.java 663e076a30bb37d4571c4cf83795a8f67dc26fc9abce712631edcb426efb0323 -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCavePlannerSettings.java 9d674dae223fcaa8a2ea46d866aa2834a85775821cb268b4d4bb2d7d75d7b2b0 -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCavePlanningResult.java d5e02895561b3bb00995b0a3e92162d3fddbe0207e53240d43a8842b4e45a776 -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveRejection.java 31021b9a21335b8941c5eb9e72336483433bec74853409a8f1a14087bbbd4140 -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveSource.java 69a6f64125daf30a21294b504c86b973edc7c1675333a94f95e87fabf5d0f5c2 -source core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveStorage.java 0a38293e05ea043564a0d9730da456c0519d29a0c5c4bb70680047b1ca02a602 -source core/src/main/java/art/arcane/iris/engine/hydrology/policy/EffectiveRiverPolicy.java 864f4783aa9461bb83829e9af356388949ea077b9cb4c231a2749bbb9b589b3b -source core/src/main/java/art/arcane/iris/engine/hydrology/policy/RiverConfinement.java eac8bb97d861650990c362d056e9360d3e16773601843347648d3d463e39e318 -source core/src/main/java/art/arcane/iris/engine/hydrology/policy/RiverPolicyResolver.java 3276c240e13279794f39c2ec99942a141da7dfb49a46e449a1b19a8f73c000d7 -source core/src/main/java/art/arcane/iris/engine/hydrology/runtime/HydrologyFeatureSearch.java e8c13f021ebf46adf1a41a8ea62c61c2f98b5e6114dc37ea670e12d30df5749b -source core/src/main/java/art/arcane/iris/engine/hydrology/runtime/HydrologyFeatureSearchBounds.java 01d2c49448bf7e2066259164e0bf72b97c94369b60d9b3c3bf21986198618a5e -source core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyNaturalHeightDescriber.java 2f57270103d92fc3a4d1b43ae14ab25ae8a0e375224ea5c357dffb809279d0d8 -source core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyNaturalHeightProvider.java 77fc411b5ab183af7c89f9cb566480ca28671efacf5084b6197589cb00a84d88 -source core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyNaturalOceanClassifier.java 4848892e471bffb9298b1b52d385a95ae013fa9230e8bb1df13629603ed44fe3 -source core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyNaturalSample.java 841c0f0247cc8a8bc545b6059cb9830a44b716142002220f979591623522bcf4 -source core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyNaturalSampleProvider.java 44235a4234bfc0a68c43e9388fdd06dbff103623d9a0126f96c94e0c2ba732d9 -source core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyRoutingTerrainSampler.java 444b226c065e3b7babf22c9f2d6c5b45ec86058bd32b136a604dc0bc5b01af6e -source core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyRuntime.java 855b6d19b16cd11b244af38707bbaf208887181f562a205536613160ae783a70 -source core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyRuntimeContext.java 97ebbaf84c00cfa4a4f5f57dbe25cb6f1793badabf9bd79e0bf33ebe09e630c3 -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/ChannelProfile.java fe0519eb051d51942c6ab974344965b0ccf511f519ce1d9fc78efebc76b29dd3 -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/ChannelProfileBuilder.java 328a2574e071e59e0d39e9330b7b11dfe90e6f46c250c1a6aa3a565fd06976dc -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/ErosionField.java a46e6e4e4a128a630d1e6650de705563f8d973d9072c710a8dc690b3b2ca879b -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/ErosionFieldCompiler.java 5c5fb78f59e8b745e97c50c2645babae417876a32d362934727c73275a9b503e -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceCellAdmission.java 8e8d102a8db3fea6b0d083a1c2fe0e7cc5304ba2729de067748ce3c7b57d7db8 -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceCenterline.java e773ba1da9bcf281b0c2673f8acd2dba1fb9730d02cd08e25266f2c601405757 -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceColumn.java 0553416db22228579f56eef8b2b5e115b149df86e7766d02ca2d5338428dcabe -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceCourseBuilder.java c24a31f41fa2d852956ccf2f8a77e1ab4080d042e6b3674a5a24919cbf2ecede -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceCourseResult.java 480d067b688e98a92046f39d869c18887d96bd54bb6d2849f460cbcc319b19ea -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceFeatureRefs.java 507b9ecf58214b0c923dda38e91e89cb6e4a9d5b9639c263bd25f20fd23df31e -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceFootprint.java a3871d85e5f9582c5a73fdd33329cb5e6cd8549ed5a0f416e885b84322ab5e50 -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceFootprintCompiler.java 9b25471a2ca368dc596ae7d0f64826d7a02445e076b4d865b1b352f6190e34ae -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceLayerColumn.java 235dcdd2df1ebb56a874578f02c5293e585c935db5781eeebdb1465394ba0a9d -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceNoise.java 320e864d0bb59f456fafa630f398510ec125d3c298a54f0f222062ac4a76956f -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceRole.java f5016d9082476c5ebf0a61a9dd7dc2d3de9883d0f26303f0b9d496df4ea0196a -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceSegmentLabeler.java 29ed68a65282648dbd52e71694bd0db5ed74fc6acd4189d90d05573285f8f64c -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceTerminal.java 1c6c1a807ef369cdc31a1214732729d4a18fa2589f3826e1f0e39fbe9d243fe6 -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/ValleyProfile.java 7ab6228c41a5b2152e2c05d09e1d92ef51b7f52743c4dc4601cdb46d3f96b3f0 -source core/src/main/java/art/arcane/iris/engine/hydrology/surface/ValleyProfileSolver.java 84888d4cc72f123650e2d4746942782173f8462af536f1170aef84cf36ecb717 -source core/src/main/java/art/arcane/iris/engine/image/CompiledIrisImageMap.java b24cba9ee930e24123a46dbf52ec878cfa066ecb08dc0212ed5b22e8db58b4e2 -source core/src/main/java/art/arcane/iris/engine/image/IrisImageMapCompiler.java b02d531c86bbf7666ad5c38290942c14c931d35bd7d150e3941e06976f11a298 -source core/src/main/java/art/arcane/iris/engine/image/IrisImageMapMaskSampler.java 5683d3db2ac95d9481a328f55ac508817639af18ab99ce1f5307869654c20845 -source core/src/main/java/art/arcane/iris/engine/image/IrisImageMapRuntime.java 841cf62cf792e181b6dffae756d0f28d14132750eaf2aa0060f09500965b69fd -source core/src/main/java/art/arcane/iris/engine/image/IrisImageMapSourceMetadata.java f38986b2c6bd3f0bbfc061ee08e082e253b3098bb6ff34610e414205deec6818 -source core/src/main/java/art/arcane/iris/engine/image/IrisImageMapValidationException.java 6bc109f5a1a97b6c17bbf8fb9505f247e4fa02884d2c87bfce907b03819bbdf6 -source core/src/main/java/art/arcane/iris/engine/mantle/ComponentFlag.java 9bcebb1c21e374f1a0be57bf9d9059359a2c820f2e2bcb158b021c7c8376901f -source core/src/main/java/art/arcane/iris/engine/mantle/EngineMantle.java 49254a8cd9d3ded14da55ca00c6f0b0aa9d1da3ee9d58a6ed3f81d425d865308 -source core/src/main/java/art/arcane/iris/engine/mantle/IrisMantleComponent.java 83abdcdc1e6928405c25a90fcd0a21158a41410cb5cbba2b5b72d14f6ceef0f2 -source core/src/main/java/art/arcane/iris/engine/mantle/MantleComponent.java dd9e939d71a344bbf2d0c2e1a750e895dcc827d67ff4336b799df00940415f4e -source core/src/main/java/art/arcane/iris/engine/mantle/MantlePass.java 5eabdc2598c092cbbcca358832bce14a87e34329e8606da20634606dae6d7b06 -source core/src/main/java/art/arcane/iris/engine/mantle/MantleSliceRetention.java 1bf17b10ea02662d89d7aaee9f5988194cc01d415feb9395a2f1dcfb399ee36b -source core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java e403c4bb5a4658bc3fd3806b380cae693efec54c802b1f8a35c13c233715d616 -source core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java ec06e4df7d1b7cbee870e6fc949cb29274f521de88c19a7e8fb9ee7462070d28 -source core/src/main/java/art/arcane/iris/engine/mantle/components/CarveOrphanSweep.java 6c06d7f015bc230132d19f92543ce5c17d6e34ee574a8a367b9f0a06c81cf492 -source core/src/main/java/art/arcane/iris/engine/mantle/components/CaveCarveScratch.java c87a6a782b85770c524a07a418fd12e07a729ff184fdd85b92746eb1edd95607 -source core/src/main/java/art/arcane/iris/engine/mantle/components/CaveFieldModuleState.java 3f734e2b32f0fa0ea37e706677ce41939fcdccfac8db419be30fed536b64b841 -source core/src/main/java/art/arcane/iris/engine/mantle/components/CaveFluidSupportPlan.java c0328f4a29ad9fc903ce6c026cfe71232da6f4f5c475663c3642b186d5137ac7 -source core/src/main/java/art/arcane/iris/engine/mantle/components/CaveObjectPlacementTransaction.java 4774d758cb366e7778b1cdf3f479c0a029db9de5ffc5fa6dcb9c30887443336b -source core/src/main/java/art/arcane/iris/engine/mantle/components/FloatingIslandSampleResolver.java b40d3996c47afc832467567c7f5b00739398c76821a7bf6e2fb5172213a83af6 -source core/src/main/java/art/arcane/iris/engine/mantle/components/FloatingObjectPlacementTransaction.java d3ae16cddedf58673d4ca50d4058b6d13500c1c6d85dbd7acf5a93fa72383ca1 -source core/src/main/java/art/arcane/iris/engine/mantle/components/GoldenDebugObjectPlacer.java fd1230b323182e5e44b1810a3ff4d2934c1d98704ae93ec8275e73c0be940634 -source core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java 2a767b374403b6285a9b010821ac17eb1163f2296134c1493ba76fe23c271ce6 -source core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java 9449a3e411e818d5e6e47dcc7eff357fa6735fe6b67236bc1d45f5c7762ccdf9 -source core/src/main/java/art/arcane/iris/engine/mantle/components/IslandObjectPlacer.java 617a452172d46289a1a9464ae32ca19996db8fc6fef31cf0819cecb3ee12b094 -source core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java 4462fbbe6d904be0eff1a1b9a8b2cb753709d60be63c135d0410b7a247a3b4fb -source core/src/main/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponent.java fee3db6abe5699398cc96b758ae3427f1481e38da1af8e12aea5d2763248c0fd -source core/src/main/java/art/arcane/iris/engine/mantle/components/MantleHydrologyCaveVoxelView.java 2bf58236a9ab2d2993bd05765cdae32bea45dbe6b2e21e827df55b18b804802b -source core/src/main/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponent.java 16180f714c38c3b91b80c263bd22d1a9e2f060b6392aafcd849a6860401b156a -source core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java 4e5b048a75d3297bc26d8230d212fcda8e0b9094bad85277d36bf43773ef7ab3 -source core/src/main/java/art/arcane/iris/engine/mantle/components/ObjectDestinationTransaction.java b791f909a2ca3b79819c1974067dfa3537a7ef06b887c761a684255477c79203 -source core/src/main/java/art/arcane/iris/engine/mantle/components/ObjectPassPlacer.java 519569552a31c8d72aa32254230cd962937428f2af228c1363375ee15f911434 -source core/src/main/java/art/arcane/iris/engine/mantle/components/ObjectSourcePlan.java a28da64e9d3a5678ac3bddaf405163cd2f313b748dcf1c60ab46e68e884a4820 -source core/src/main/java/art/arcane/iris/engine/mantle/components/ObjectSourcePlanCache.java 97c3073c49e66de797f98a8e80db9662ae19949f684ffdef4f40f052b2f3cfc0 -source core/src/main/java/art/arcane/iris/engine/mantle/components/StructureCarveEnvelope.java b1649c7614251bdacfc9a696d79bb51fe9a84cfb10c9e8aa2dc8897d056e3422 -source core/src/main/java/art/arcane/iris/engine/mantle/components/StructureCarvingFootprint.java 169cd6844a3257df8fbf71bddb60a87e05260de346e98c5c9571cccdb039d2de -source core/src/main/java/art/arcane/iris/engine/mantle/components/StructureFoundationPlanner.java f99651e5ff66615ac6ae5a0de250b70937470f0f6affa28f7c8d9bfb18743f59 -source core/src/main/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlan.java 247fdce9e80ed44c5cb0fdceea8730adec61cf7ec370a4ae3173f9652dd24aa3 -source core/src/main/java/art/arcane/iris/engine/mode/ModeEnclosure.java 5ff8bfd62a29e952c323f87981efc04ef0c8ba7e42b7f86ce7ebd6a016be5972 -source core/src/main/java/art/arcane/iris/engine/mode/ModeIslands.java 77650c09085581de220e92a9c7797531d0e9faa7efcc222a9b382ab1b4213ad5 -source core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java af51e4de5bfeaf80fe7c46c680b106932381f7a94879494e0bf2d4b6251b5f9d -source core/src/main/java/art/arcane/iris/engine/mode/ModeSuperFlat.java c1c132f08c63ab6b82f5d18b170903efa4c377c8e44bb5e55f82328e7ae13af2 -source core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java d7ba9b7a7ba652b79d567bf491624154408c1678341bddae28cb5fb50fa73241 -source core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveScratch.java 660d900a7dbf6be43b87e87db83c2d5453e4ecf2df769596054fb3268fc0e3de -source core/src/main/java/art/arcane/iris/engine/modifier/IrisCustomModifier.java 0c3a23f2f1cda4a616c422bcd6c42c1724149b638ce3982b723eb591775bf8fb -source core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java 543147c5150a496a01b73f7ed60c7a8571da14b12f44677ef62cfed07ad34cfe -source core/src/main/java/art/arcane/iris/engine/modifier/IrisFloatingChildBiomeModifier.java 52de3cfeb45d304fe3fcd772eb0110e6e88ffe9dda8a9872330b954966b2388b -source core/src/main/java/art/arcane/iris/engine/modifier/IrisPerfectionModifier.java 64ea8c7554b2c1f704a32e52c683d203330caac36da4fb769c2a1f9f3ce11fa8 -source core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java 23a9c38bab0191f05fe0f3d577624492902ed7b09685144fc861774dbd6f7320 -source core/src/main/java/art/arcane/iris/engine/object/BlockDataMergeSupport.java acdd8dbecb9a34f2fe46a60325fb53fbbb6efe08513e799afa25de367c2cb1a7 -source core/src/main/java/art/arcane/iris/engine/object/CarveResult.java 002fd8cf16c937e44dfcabccda6ceb7b249812bfaca3ce7881598676b1defbe1 -source core/src/main/java/art/arcane/iris/engine/object/CarvingMode.java 6264731af9f3de3a7e32b512a83398999a8b0bf18a8bebca5ce8c6097363a7e6 -source core/src/main/java/art/arcane/iris/engine/object/CaveResult.java 28e0792c304efd0a9642651a85c7f9fdd8630f2802c7afeb1451e97ad9b23477 -source core/src/main/java/art/arcane/iris/engine/object/CompatPools.java 29b8bac9ed15615fb271c731a8d2dfbef487b999736ef23ef9038d5d0d7bcaeb -source core/src/main/java/art/arcane/iris/engine/object/DecayControlPlacer.java 729926e5a3e989f3b5f9403fbacfc6c3f3f303b089680f85fe7a9d1f5c105abc -source core/src/main/java/art/arcane/iris/engine/object/FloatingBottomPaletteMode.java 33e6278cf5fe6279ef300804d44dd080de4badd67147b873b80f3aec1a13dfe1 -source core/src/main/java/art/arcane/iris/engine/object/FloatingIslandBoundarySampler.java 17422cd87c450b73ba01d9bee7ef4f4f63c4afcad6a28c2394d664d5d59d0851 -source core/src/main/java/art/arcane/iris/engine/object/FloatingIslandEdgeProfile.java 027cdf47558901ffc91593b3ec4259c4f918c51152f988d3de975153a5026c96 -source core/src/main/java/art/arcane/iris/engine/object/FloatingIslandSample.java 0d0b6594a138a2367f5a3a9f78a0d734328b704c56ba54d4ff409c78b27d832d -source core/src/main/java/art/arcane/iris/engine/object/FloatingObjectFootprint.java 0246aa29bf37fe1b9608ba26323e257916abfd9465b4b79ffdb9a9decc7f6f49 -source core/src/main/java/art/arcane/iris/engine/object/IObjectLoot.java 55dfe78884f5acd9026e76a6650592496a31b5412bb7048a76220126a569df37 -source core/src/main/java/art/arcane/iris/engine/object/IObjectPlacer.java 2377005797e05dc28d3030f5c82449364a18f1132725b47b297878ea2e653aa9 -source core/src/main/java/art/arcane/iris/engine/object/IPostBlockAccess.java 0d6161f0008b374f3232a8d063980719ae561b89579b005ad00b8e44f1080fc8 -source core/src/main/java/art/arcane/iris/engine/object/IRare.java 2ed7ab7a868bab87b9b00a3d36513fecefa3ea901d5b53d9a8a56bc236b67289 -source core/src/main/java/art/arcane/iris/engine/object/InferredType.java e1748de37d9141172816b9ff11e79871b2394087b6b4140de9415518fdbae9a8 -source core/src/main/java/art/arcane/iris/engine/object/InventorySlotType.java e2eaa8daa5c9320a534f39688c9c2fded737164179a0d97bacd504c875999e8f -source core/src/main/java/art/arcane/iris/engine/object/IrisAttributeModifier.java c285df180f48799d131a4ad6c0d92833ab3ecc8673e5cc24ddf0a23c1ef9cd6d -source core/src/main/java/art/arcane/iris/engine/object/IrisAxisRotationClamp.java 9db923cb3a1793b8e51ac280b83ca8b4903ac03d822d9f31f18d4d420495a1ac -source core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java 1306b10911b218c2d0e21c3b676b6a871f6328621f3cb32df98005a1df0a4608 -source core/src/main/java/art/arcane/iris/engine/object/IrisBiomeColorRenderer.java ebaccbbd2a13b10adede59e41a82b073e259d25b1b296e7f82b360613cd88592 -source core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustom.java 5b7ba7f9cfe7bc7ba1db5e115717b4e45fbc85e71f2d18fa4b9842bd87492aac -source core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomCategory.java 1aac3e93a08ccb0345cd45155608cd72f96a65b3de9b609959fe990b83f2f57e -source core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomParticle.java 2c5f9af511f7253f8bcdb4fd16eeb7193866d70d2f6319a1908d1355f6f9475e -source core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomPrecipType.java 3b1514b0c11f6043aee7b04c23428d2451d8e8207db053de93c1fcf01fc659dc -source core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomSpawn.java 90d9a3333fe37f3e1e0c81db9822af59e7dbde546a761b11a92e6c7fc6e815c8 -source core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomSpawnType.java 6852af298cbe3aedef954a87aad9f340fe6963aba9bd0ac4b0a3de93f1f44b2c -source core/src/main/java/art/arcane/iris/engine/object/IrisBiomeDerivatives.java 85de8587c19b1323453e818bae21e52e36592700a40bbfefa20aa1b67c14e53a -source core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGenLinks.java 4e80bf12caef43ace6f6220161220694d16e439e764ba3286805e99f92359250 -source core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGeneratorLink.java dd9ffd9eb3d4fa9625996ebba72ff0d111fbe86548bd79fc1614c0626bddbba3 -source core/src/main/java/art/arcane/iris/engine/object/IrisBiomeLayerGenerator.java 0cd4729cedc22bfa23068345edc7d5c1aa2c09561018d6e4ae31f4a076833109 -source core/src/main/java/art/arcane/iris/engine/object/IrisBiomeOres.java dba854c06c639d671e72c94aa80b5689a6920ca9ff4a566d8ed5b853a6288beb -source core/src/main/java/art/arcane/iris/engine/object/IrisBiomePaletteLayer.java 4a8d6ac6e4ea6c5e0abeb4ae2feea9670859392002487ec4ce280df07a328eaf -source core/src/main/java/art/arcane/iris/engine/object/IrisBlockData.java 0d0d61ec71d7afbef3eb493a2baa9caa1e6e435ef09fe42f89a45b86f230671e -source core/src/main/java/art/arcane/iris/engine/object/IrisBlockDrops.java d8e3217895573c1e4a31bd61735dd8d0065f3ca2faa244a48c094a73c6f3b7fb -source core/src/main/java/art/arcane/iris/engine/object/IrisCaveAnchorMode.java bd742762e9e7b7d7e7df29b0e86b7e8f235e64e66df1468d31f1c7ffef4bde98 -source core/src/main/java/art/arcane/iris/engine/object/IrisCaveFieldModule.java fd80a3eca5ba74672d9283881bfb1554a59121f8ccd3527271e5269f582a9b09 -source core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java 66336bd0f84e1332b93f55e7e9ffbdb40c4242779139b80022d8d69fc0decdf4 -source core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfileSampler.java 72cb85bd98fb74e18696f501a20c7fa44681f4ed1e995386575b002d1b215e20 -source core/src/main/java/art/arcane/iris/engine/object/IrisCoastalRiverGrottoConfig.java 492f557e348911331c23140850a018fbcf93ea50ef49b7f3512acf1d1288052d -source core/src/main/java/art/arcane/iris/engine/object/IrisColor.java dcb5c3a07a030cfbbdcf8394f5b35593634bafdf8fd0ff9070ffa755afad05be -source core/src/main/java/art/arcane/iris/engine/object/IrisCommand.java 606a62de009428a384b8e7873c3f03f838813cb44f905818e8e63af88c2b6ef1 -source core/src/main/java/art/arcane/iris/engine/object/IrisCommandRegistry.java f91e440d35f6da863e2f3234abc363a9a33198b31afb991773081dbd35eb5904 -source core/src/main/java/art/arcane/iris/engine/object/IrisCompat.java 9f24ce8a4f400289d5506fdb1b72e3dd793ae44e29091e41c5924eb5759df953 -source core/src/main/java/art/arcane/iris/engine/object/IrisCompatabilityBlockFilter.java 01ee6a8629e653afd6b7560d464184ec2fe2bbfa6094de3cf32e1874c63fc892 -source core/src/main/java/art/arcane/iris/engine/object/IrisCompatabilityItemFilter.java 20c2c9ff1ed906196a07fb5492e2dc4f2280af38bbff02c40b96c5a4f5b38239 -source core/src/main/java/art/arcane/iris/engine/object/IrisCoral.java ed256b40eaa2b2c5da6b8e8d53bfca6f019197e2ea4c62358a7f3f3fb91624bc -source core/src/main/java/art/arcane/iris/engine/object/IrisCoralBranchAzimuth.java ec39b19e08897dafda633b7852f1bb5fb8b5e7e0d7a31a4ac973d3cfd0545c9d -source core/src/main/java/art/arcane/iris/engine/object/IrisCoralForm.java 4a2f5ec3877ce1ccbf1321daebe3db4863599b8f178e17acb563b06f683fb979 -source core/src/main/java/art/arcane/iris/engine/object/IrisCrystal.java 2f00c8e7ce333d45398cd630bb1dd5b707e51dc244f89a7457fe83e6a042ae5c -source core/src/main/java/art/arcane/iris/engine/object/IrisCrystalDistribution.java e31ec96b1310d11e2a01591d591c88f07b1b077956815ea2e59895697b3dbad3 -source core/src/main/java/art/arcane/iris/engine/object/IrisCrystalSurface.java 9cb4e4031171f87b6c1c772d34a2c42953cf8d22cedf8d79a3f3be5d18d6b975 -source core/src/main/java/art/arcane/iris/engine/object/IrisCustomBiomeAliasResolver.java 70e6698d51ee1eaef6faf642f750261820d425928389a0d2076ac95e61ec2d0a -source core/src/main/java/art/arcane/iris/engine/object/IrisDecorationPart.java 7e713a3ade81bfb4add35e27732a2327baffc3851d1e4049fa820d3f96d2ad59 -source core/src/main/java/art/arcane/iris/engine/object/IrisDecorationStep.java 678ea725c13037c900f3a8267240354437077ae44673636d7882d136612c8fbc -source core/src/main/java/art/arcane/iris/engine/object/IrisDecorator.java 51b7306a743ec2b5bbc11cdfde6967960c81c23ac35508a5de01edece7d3eb8d -source core/src/main/java/art/arcane/iris/engine/object/IrisDeepFluidConfig.java ef3a10717ce05b75159023c59b6d5d4c18a508f747a1541d08b8324c3ad48bd4 -source core/src/main/java/art/arcane/iris/engine/object/IrisDepositBiomeScope.java 1c1328711aa25702e61013897f63c2d75994f3236e6cd7b3c4d7e416bd92ce6b -source core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java bd54bc2702af646dfa1811016b0faaa409f72ceb6636ce5d232cbb44a60f7659 -source core/src/main/java/art/arcane/iris/engine/object/IrisDepositHeightDistribution.java 4652f04172c8decde8405afb9904664e0f89bf80148bcb306fc1f5db5b1b57ca -source core/src/main/java/art/arcane/iris/engine/object/IrisDepositPlacementScope.java 0b1b15482a85d473eac3b9d58be5b681b8e210846900ab5a2b4e945bf3873105 -source core/src/main/java/art/arcane/iris/engine/object/IrisDepositShape.java 07647e01b010a338e1cf01f5372f8dfbccf34f3915742032baecacccda5293fc -source core/src/main/java/art/arcane/iris/engine/object/IrisDepositVariant.java 0234c4caa8c723047be7293fea0452df2221450b05ef3102bbc152170a5e5038 -source core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java 806dcdc5b7c57b766bff0e48655c1bc9381854f9ba83d08a22cca37409ae218d -source core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingEntry.java a0c5971a455b2ff2e4c82c7a0387110ab4f1e0dd27238fbcd57b7a0b3b8899dc -source core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java 51ecf9f7f108311951baa621614f33085a569ffa88f7d314daf0fcdaf39e44b2 -source core/src/main/java/art/arcane/iris/engine/object/IrisDimensionContractException.java 6d64f24dd2ff4b759ade515a2f6bff218fa7b59a68cd856f7e82cf2ae7d0fb65 -source core/src/main/java/art/arcane/iris/engine/object/IrisDimensionMode.java 0365ec35f06d0cd6d59d0d74e1a9b0d1ab8757c327ad974b31384ec284cd2577 -source core/src/main/java/art/arcane/iris/engine/object/IrisDimensionModeType.java a26e4213c6370fd41f5c264e7e67050507d0b1bb49732173170f7e6dc2290e37 -source core/src/main/java/art/arcane/iris/engine/object/IrisDimensionRuntimeContract.java b1a3f4ddff860e71f9d93d867b6e484b7956beff5cfeae161c539753c3f11acd -source core/src/main/java/art/arcane/iris/engine/object/IrisDimensionStack.java acd40ce733a6bf7abf4b15d9a4d67ff6c439951a58b374b2ee6c1032b98a705d -source core/src/main/java/art/arcane/iris/engine/object/IrisDimensionStackBlend.java f24ceced41a73246fa2d45c3b495258b2bb6fea136862d7e49aa60831b128ba2 -source core/src/main/java/art/arcane/iris/engine/object/IrisDimensionType.java 91119dc5bf04a8a4760294fe8756d0a95f1aa1041f85e2dd04b24a911d553740 -source core/src/main/java/art/arcane/iris/engine/object/IrisDimensionTypeOptions.java 6498d1841a9755c0ff5b8dc0e69a70f7facb19666638195a7be0b0e92bd88c8f -source core/src/main/java/art/arcane/iris/engine/object/IrisDirection.java bf942f58caf6d814eea2c61088f47b51efb1b20f5c21ff3ffc83454ebfb4a1b3 -source core/src/main/java/art/arcane/iris/engine/object/IrisDuration.java ca2358c75f0c38aecf4f67a25168b2d6a6cadd5ed73f7ed4409d807f41c0fd17 -source core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java e4bc736071f43e270d1a698f21a3fd5a1463465e2b2910bd542c6463f89b81ba -source core/src/main/java/art/arcane/iris/engine/object/IrisEnchantment.java 99e56d9c891ed62b75300cb6f524707df4d6db3e1494ce5531809c45e18e2243 -source core/src/main/java/art/arcane/iris/engine/object/IrisEngineData.java 98f0c9f865d6251ba831a44f7d88bf8427fee6c2b36224f6d9d348f62a8e2fd9 -source core/src/main/java/art/arcane/iris/engine/object/IrisEngineSpawnerCooldown.java 3ed656dfbd3d33520e25839d80efdecffe7ef510ef8b6dd849be37b4193bd2d5 -source core/src/main/java/art/arcane/iris/engine/object/IrisEngineStatistics.java fd322600f410aa8212b54a19734d796e1547b79dac2455390154dbee76a60484 -source core/src/main/java/art/arcane/iris/engine/object/IrisEngineStreamType.java a1ada0938ec368980212fd22c02732f418c76350c1c531e337c7d6b92bcc7286 -source core/src/main/java/art/arcane/iris/engine/object/IrisEngineValueType.java 287b1de2617be341e6815dda28fa066d3cbb0e4d2c6e780f7c3fd2d732f5a44d -source core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java 977c6b130dba4218bc25b705eb8d1272483c0527d85316e2b535c6237630c65c -source core/src/main/java/art/arcane/iris/engine/object/IrisEntitySpawn.java 0739bc688adb0e506b79978f48c9651886888bb344a7d3558ad2cabc02eaebec -source core/src/main/java/art/arcane/iris/engine/object/IrisEnvironment.java 8a9fdd3898bcf89a2d4991c3d4972630be5c3e73655705c7b47a9a99a9990e93 -source core/src/main/java/art/arcane/iris/engine/object/IrisExpression.java 53c1237fef12f7f95901206efbf6bc61786abbc24bf0c4ecfeefc266dbab53dc -source core/src/main/java/art/arcane/iris/engine/object/IrisExpressionFunction.java 876504f198f15bd565af8ecb29562e51ca2086e2861832556e41a908527f4090 -source core/src/main/java/art/arcane/iris/engine/object/IrisExpressionLoad.java 8768b79b885381ea8812b7a5c2aa9d264b525e045d899fae7295597566b87b27 -source core/src/main/java/art/arcane/iris/engine/object/IrisFloatingChildBiomes.java cb58881275b1f9834fce2b37465f397c421d42ab05d3b7c12f0970de324c6d4c -source core/src/main/java/art/arcane/iris/engine/object/IrisFormation.java 231c926fc4589bf8ec1ca9839a262f118ec88f0d13391243b0e82717250e0503 -source core/src/main/java/art/arcane/iris/engine/object/IrisFormationForm.java 830a54a825e49d9ffa4715aea771d008c61b2c789afd8eadaf31d6fac984053b -source core/src/main/java/art/arcane/iris/engine/object/IrisFormationProfile.java 24bfe493a25624593c9a869055034e6a6f7d170aca394013da83711592a9516e -source core/src/main/java/art/arcane/iris/engine/object/IrisFungus.java 4edf5e93158a16a3bf041cec57b88463e8f41eb1a7b12965f6239593631e9997 -source core/src/main/java/art/arcane/iris/engine/object/IrisFungusCapShape.java fc73287ce22b472c6494c440aa79c4bea41f18d71cd3c2c738339fef06e86770 -source core/src/main/java/art/arcane/iris/engine/object/IrisGenerator.java 6de0a72c2e20214cf6a239cb7ec2ce3f0da1c33078afd80ab730a75556121fe5 -source core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java e79547388638f9d2a59dffbc69045e504fb748e78a93b6c0512f4244a21daf56 -source core/src/main/java/art/arcane/iris/engine/object/IrisGrottoPoolLevel.java 18a27f37ed2555578b334493779d42dc2ed44b538f9e435e6c0eb322addb3594 -source core/src/main/java/art/arcane/iris/engine/object/IrisHydrology.java 4d2ad747194e89729b9762224978066fb26e7e475a0e5dc0d2ba14f995c41baa -source core/src/main/java/art/arcane/iris/engine/object/IrisImage.java 9ac67fe87646810497d2d1210f625ce6fee9d1d67a31ff344927a97e7eee87d1 -source core/src/main/java/art/arcane/iris/engine/object/IrisImageColorMode.java 1c75d95d1d02fbe45effce4535a6eab48901f95467e8742090e181ad83e538a9 -source core/src/main/java/art/arcane/iris/engine/object/IrisImageMap.java 33bc41a8f2501955e00be59b8ded48cffec972ca1b1a836b6f951f3643314888 -source core/src/main/java/art/arcane/iris/engine/object/IrisImageMapAlpha.java 3373807fae5afdb60858bb0f65030516700094701ec35cd122b75591cf0aa3dc -source core/src/main/java/art/arcane/iris/engine/object/IrisImageMapApplication.java d438486bced5e418d5259fea229d9b8c96eef35eed5823634cf28611b512d776 -source core/src/main/java/art/arcane/iris/engine/object/IrisImageMapBinding.java 677dba2cebcb3d343e66af18aec97938a931a54275f31e9baaf32eeaac7cc2b9 -source core/src/main/java/art/arcane/iris/engine/object/IrisImageMapMask.java 4134d1b217a7fad2aea381072f4996f05dbf51a5a1f43f7637462c4c00b9c1da -source core/src/main/java/art/arcane/iris/engine/object/IrisImageMapMaskOperation.java dfcc81092395dc11206b0d15a86b11a07780b845df539015501cc95bc2793222 -source core/src/main/java/art/arcane/iris/engine/object/IrisImageMapOrigin.java f7b5ac5548d3467de82f14815edc5e25158b7ad59ce08c007a9c999dd4dec461 -source core/src/main/java/art/arcane/iris/engine/object/IrisImageMapOutOfBounds.java fabc3aee529e0e595b29ccbe1feb936111a57b90699c18977939e399773418af -source core/src/main/java/art/arcane/iris/engine/object/IrisImageMapRotation.java 606526a2848eb0cdf4c3fdf81eb2bf24c52f575170de08c5c34480efa6cd58ee -source core/src/main/java/art/arcane/iris/engine/object/IrisImageMapSampling.java b715fb9ae345b6efa1ac48e177987cbcb4d62baef29cd8973d7855c6cea5d25d -source core/src/main/java/art/arcane/iris/engine/object/IrisImageMapType.java 14d1b9b3f41a6af5916a158a5eb6da8f82ac62b5c645bf390a2831f91cbd57d2 -source core/src/main/java/art/arcane/iris/engine/object/IrisImageMapUnknownColor.java 1b33d4d80239671b95e4ee68f1aea1c48b64fc1b3b627bb27ac6170a3404c435 -source core/src/main/java/art/arcane/iris/engine/object/IrisImportedFeatureControl.java edde159d74e074b0181f32ce518ef94885b78f752e23463859f2004d4d4775ff -source core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java 915392cf65141822915f7695d13006636e118f7e2f343c44f2cbc5394e1a54f9 -source core/src/main/java/art/arcane/iris/engine/object/IrisInlandRiverGrottoConfig.java a3f36f44404dabc02cd8a84f3d5bd727e31e1fd243893c497ff5977b4af2fa12 -source core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator.java 8423df656b2dc0995123a92d3a794dfb130ce2f9a3ba49b0199187025209a009 -source core/src/main/java/art/arcane/iris/engine/object/IrisJigsawBranchFailurePolicy.java 2b7d09dd7ec092633c05aa1eedd6f4e217641f47c97b2dd34b215bacaccdba1c -source core/src/main/java/art/arcane/iris/engine/object/IrisJigsawCompatibility.java fc471a3d2f638d63228d45d0710e85513700476bf47a6b9b8d0a117cc92950ff -source core/src/main/java/art/arcane/iris/engine/object/IrisJigsawConfiguration.java 7945925e8e8fb8f851dd70da7f8b68c060c391cac746bc882c9962c7e1edb013 -source core/src/main/java/art/arcane/iris/engine/object/IrisJigsawConnector.java 65ba86abf90155ea86e4f30167d5ba7d0064eefc6d0ef8234362d267e425da05 -source core/src/main/java/art/arcane/iris/engine/object/IrisJigsawHeightmap.java c184b95c7e8d198f8d5e045fe0b20c1e94cb29d4234e25ea3290f896c3f654f9 -source core/src/main/java/art/arcane/iris/engine/object/IrisJigsawLiquidSettings.java 78a05c72b456b8ae0dd9b90155ec17fc62022b53e6d8c16742fa6085448778cd -source core/src/main/java/art/arcane/iris/engine/object/IrisJigsawMode.java ef208336caa476abeb97ade1f32e64dd65d189b2a55948aa02af6943e2c122f8 -source core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPiece.java 21e45af5faa6b81c350925e69ebebee14d089c66e9d5a0565d37dcc40a00c507 -source core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPieceEntry.java 03473b37b1e194337660c4b391ccb5f6d00e7770d8ac0bc8379711ca8dec5a84 -source core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPieceRules.java ff0fdc1da7b5f84f06d3e148fbbb07809fba5828c5b5f55b0af0fb99e19fa3bf -source core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPool.java dc3ae79959ac0a464093076753ad518a724d87a8758793318dc933b9217e8853 -source core/src/main/java/art/arcane/iris/engine/object/IrisJigsawThemeSet.java 00781cffa14975a5a3f94cca3c3c87dd26cf64c58939e8e730c58f3f97dfe7ea -source core/src/main/java/art/arcane/iris/engine/object/IrisJigsawWorkcell.java d0da640adaa458b4c44cd243e8d207f3c459e659d159950e6d1c70de3281ece2 -source core/src/main/java/art/arcane/iris/engine/object/IrisJigsawWorkcellArchetype.java 869cf45a51743366e4b04ee803ac0c00d5feec5c750f459eb585c1b5350a2c71 -source core/src/main/java/art/arcane/iris/engine/object/IrisLoot.java 35b4cf97deb5e4136127349b5734f10a27e7971f90d33f1b4220648241d58122 -source core/src/main/java/art/arcane/iris/engine/object/IrisLootMode.java 85cde69ebe613f667f8faa4212933fbb3cfccb3a17d244085d1697d1957b0507 -source core/src/main/java/art/arcane/iris/engine/object/IrisLootReference.java d4d70fc55603a10c6eed3491917f3553b1b3e3b0588499f5a7aa20e9eca89d38 -source core/src/main/java/art/arcane/iris/engine/object/IrisLootTable.java 8f7c9d292addca0fb3c859a41a04f04dd87ab504030925257748b54db8fcc14b -source core/src/main/java/art/arcane/iris/engine/object/IrisMarker.java ec49402a3eb300369a30ae7b85e33c4b12eec5b61550a50347856bfd9bef4506 -source core/src/main/java/art/arcane/iris/engine/object/IrisMaterialPalette.java 41c67bec2b9ff70ca53bd5290136ea7ad1a0c9f971b7b43593ebbcf0b1346119 -source core/src/main/java/art/arcane/iris/engine/object/IrisMod.java a520cd12ff1750c7c2ff310e5569e636065c687c0d003e3d69fa6ee91e1237cc -source core/src/main/java/art/arcane/iris/engine/object/IrisModBiomeInjector.java c3b167161d64a0700533c576ad6219a0464c948722713f69b3e58823e41408d4 -source core/src/main/java/art/arcane/iris/engine/object/IrisModBiomeReplacer.java 5020c12524c5c475fd9fd474f4f30f03c12468f5c1c04bf80abefa5f5089f28f -source core/src/main/java/art/arcane/iris/engine/object/IrisModNoiseStyleReplacer.java 066217f3bd688619d5be39c598632b593e1448330f98827f20bf58ecc2a61f48 -source core/src/main/java/art/arcane/iris/engine/object/IrisModObjectPlacementBiomeInjector.java 9182b958b968a84bcd3d4bcaa129cc3b2b867505d71b0fa58064f30026ba870d -source core/src/main/java/art/arcane/iris/engine/object/IrisModObjectPlacementRegionInjector.java c7c9152464d502bdbe3a683afb26baaddf0ec7ed93019d798a8e4936d1731b14 -source core/src/main/java/art/arcane/iris/engine/object/IrisModObjectReplacer.java 0090708e15a592ac8b97c44529d45416aba390749df86ec80f718c61cbef1a74 -source core/src/main/java/art/arcane/iris/engine/object/IrisModRegionReplacer.java 093fde6b3491b3fd104a627b2ed3d26c4734312789eb97a9b43dfd23fe5f3cae -source core/src/main/java/art/arcane/iris/engine/object/IrisNativeStructure.java 6fd983bd640ad94a22bc9fd8986d18132c3a39f02f092739453b4c41f71246cb -source core/src/main/java/art/arcane/iris/engine/object/IrisNativeStructureDecision.java f0792c1fa4a13f0e826696eafc88c00505f9b91c49b9e0251c7958aa29eecdd3 -source core/src/main/java/art/arcane/iris/engine/object/IrisNoiseGenerator.java 3e638ea7a65852b84617d2ce069e62c7ed03032460eff6e3e17d7e7462b4011e -source core/src/main/java/art/arcane/iris/engine/object/IrisObject.java 48f88cd3652b9308c7f23564b0381ffe7eea8cca85352ad6a80083c6d848e8a4 -source core/src/main/java/art/arcane/iris/engine/object/IrisObjectCompat.java 417790a22c3967654c6e2460779376a3bfb9d561a1f5440c574ef84cbf59bd91 -source core/src/main/java/art/arcane/iris/engine/object/IrisObjectIO.java 1e317a730f43620c91275793fa4e04d67cfe6002526809b26a11d82e8c062dc7 -source core/src/main/java/art/arcane/iris/engine/object/IrisObjectLimit.java 1949b1d16676cec7e28c8572a6a4a504a4b3bb17c31f6cb243dc07345dc01731 -source core/src/main/java/art/arcane/iris/engine/object/IrisObjectLoot.java 0a2e19df0183baf6e60c6a15386386291c231d4404f907798216cda2b26fa81f -source core/src/main/java/art/arcane/iris/engine/object/IrisObjectMarker.java dfac232dc147d36e865ed9ff1acf7b0a4da2fe46d81fc95d6449fa52de1d26b6 -source core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java 244133fbd44a297986f64e39b849f3fe15e60057e2bc5183d28b9dce73b0c72d -source core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java 2e1746d636efd21b66081ccd1936d35fe37f21812f815151d5c91a93d6e0224b -source core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementScaleInterpolator.java 9d0b2b3928d05c9a4addeb3815cf67f9f407e63c5b5ce1a8ef74d1dcb208ee61 -source core/src/main/java/art/arcane/iris/engine/object/IrisObjectReplace.java f07b155a1239055114666288d941474ba1341623cdcac30998dc56441e913182 -source core/src/main/java/art/arcane/iris/engine/object/IrisObjectRotation.java 42e129047a26439bc1b993e17a802c5ec797eaf31023a193b9002e1fc858a847 -source core/src/main/java/art/arcane/iris/engine/object/IrisObjectScale.java c4fc4ffeed0073f873daebbfa0ab2bb9b7a0b57656d4159772aed25221891e13 -source core/src/main/java/art/arcane/iris/engine/object/IrisObjectShaping.java 14995204a83b09656e432d7463c2ba858f99522bd2762e3ae4d82f6e8c0a91d8 -source core/src/main/java/art/arcane/iris/engine/object/IrisObjectTransforms.java 4665c9580d2181cb27768ad5224c9312cbd23350e92d8fbd09ea220b9ea4e3e9 -source core/src/main/java/art/arcane/iris/engine/object/IrisObjectTranslate.java ea183f928e92392948bd8fe2a64650a1828951308e0258b45b7779ff2dca1134 -source core/src/main/java/art/arcane/iris/engine/object/IrisObjectVacuum.java 65b351ffdc3bad69faff4ffee039727111cd9a8013980f665337f0790e391cc8 -source core/src/main/java/art/arcane/iris/engine/object/IrisObjectVanillaLoot.java d44c84a06eb62fd86117dfc626bbdca9f4f1364eecb184330f2e4a68b08d021c -source core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java 679bf899c475e331a9f95a49c3aa031b26ac261155df5784daf906ee9860d481 -source core/src/main/java/art/arcane/iris/engine/object/IrisOreGeneratorBounds.java 3e74a0da0d10b5db886adb3903780da4fbba3595964f86fa5ab44e3f58923e4b -source core/src/main/java/art/arcane/iris/engine/object/IrisPosition.java 38c165372170dcdb7f66a534bb84b48ab1a47cfc27466ceb915d0d9f74dcd88c -source core/src/main/java/art/arcane/iris/engine/object/IrisPotionEffect.java 6680513e0e11c40fe71608ecd3f3ba8e00083e7aab9b3ad3732cb53950da49e8 -source core/src/main/java/art/arcane/iris/engine/object/IrisProceduralBlocks.java b48374bc4e988f4869d20759d3a624b46f803d7e2f64664737c54815e2bdc07a -source core/src/main/java/art/arcane/iris/engine/object/IrisProceduralObjects.java 9ec87c0491818eaf0fe43f9f9d903660de9086ad54ff8623fe38d3e983ec3bfc -source core/src/main/java/art/arcane/iris/engine/object/IrisProceduralPlacement.java 23f850273146dd7e5debf2c216ee208486ac5bbef599d5940dd6bde9085295a8 -source core/src/main/java/art/arcane/iris/engine/object/IrisProceduralTree.java cab26334e539779f272807954470f5a8418a14ee245d68500f249c67a941d71b -source core/src/main/java/art/arcane/iris/engine/object/IrisRange.java 86ebad3ea8789e496fdb0b0b16be61fc0773c76e3f6203b8fe37bead4c28f223 -source core/src/main/java/art/arcane/iris/engine/object/IrisRate.java 2dd62cad7775329fec24308cca1f6878cacf1a7d92a1c5b66c3ca9a3637891c2 -source core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java dfb1a2260bc65f8fc1ff5c695bf66b48b7f9567a1f3c715907d4b64dd1f1384a -source core/src/main/java/art/arcane/iris/engine/object/IrisRiverBedProfile.java 210ece0d816ef18add37a2c1ea42154d8963186f3abd88f2c8179e22e24ab2f0 -source core/src/main/java/art/arcane/iris/engine/object/IrisRiverBlendStyle.java e055cfa3adf5090292211b46659c3a9917e3b8792c94b643cc2e540c80e08635 -source core/src/main/java/art/arcane/iris/engine/object/IrisRiverChannelShapeConfig.java eca518a2eb1620cd532b2f67c653059b70f4004d6a16478c36749c2e4d28d931 -source core/src/main/java/art/arcane/iris/engine/object/IrisRiverDropShapeConfig.java 8cbf11bd685af008a97291ec5c68bc69c87ae0acf436eb97c5f2179444bd5baf -source core/src/main/java/art/arcane/iris/engine/object/IrisRiverGeometryConfig.java 1b04e5c83be25b2ef2a7dfc9b0aee07786092807ccdcda018531475e9f89617c -source core/src/main/java/art/arcane/iris/engine/object/IrisRiverGrottoConfig.java 0646b95b9977e8842e62bc83a9f29a8201fd1e43e305057ae269606f86af38e1 -source core/src/main/java/art/arcane/iris/engine/object/IrisRiverHydrology.java 0e72706022a3fab1532c40cd59587c9cb196b36607960216c119f4c59ec3f1df -source core/src/main/java/art/arcane/iris/engine/object/IrisRiverInlandOutlet.java 876028d782ce40583d0d6a6073241f072a1598eea79d3e094536de71b7797962 -source core/src/main/java/art/arcane/iris/engine/object/IrisRiverMaterialConfig.java 742ff0448a5c6df3adbdc4267c38a595d13ecbe04cfd447dc72fea34e5e6e1f6 -source core/src/main/java/art/arcane/iris/engine/object/IrisRiverMeanderConfig.java c7ad6ddf041f82c12e24225ace3baade969d3d3cf89196767f48d5de95570d88 -source core/src/main/java/art/arcane/iris/engine/object/IrisRiverMouthConfig.java 7c2497c5b0ffff278a1db13bf852a8ac9cbd6f783ece733f992f571f0f9b3d23 -source core/src/main/java/art/arcane/iris/engine/object/IrisRiverPlacementMode.java 2dac7958a9b088dd40c0851baa508d6decb1a63c6a1af5168201b2e1106addb3 -source core/src/main/java/art/arcane/iris/engine/object/IrisRiverPolicy.java 48f7303335cc801886e2e5d7e260af88f05ee71b5bda537b3b23f30ff593a9d3 -source core/src/main/java/art/arcane/iris/engine/object/IrisRiverProfile.java 0ca093428359b22707dab5832f639dc32d06ea7c9b64cdaee019999dd7d69873 -source core/src/main/java/art/arcane/iris/engine/object/IrisRiverRoutingConfig.java e39718e3a350bb28b4c42a7534f829701b7121e7e4b02a45f118fed881c8438c -source core/src/main/java/art/arcane/iris/engine/object/IrisRiverRoutingMode.java 7523243c17b0c1e19a8f27cc41d4de563e482c612087ed26ee2251e5fbddc674 -source core/src/main/java/art/arcane/iris/engine/object/IrisRuin.java b4166e50c9a896df12ec703d601869ef613f6fbefdf18410b4bdf23d1feaf3a7 -source core/src/main/java/art/arcane/iris/engine/object/IrisRuinDecorator.java c5a0036561c312f941242d52194abc90d193a2c7de780c42d57c924c84ceeaf0 -source core/src/main/java/art/arcane/iris/engine/object/IrisRuinDecoratorTarget.java 417adb5db3e4a88379947970273b22130b109ea1f05f469dd308beeef9de2dfc -source core/src/main/java/art/arcane/iris/engine/object/IrisRuinForm.java 9570b5d986d5a05af4d02b2d9101cc48c623fea572e55e35df3819c612b1286e -source core/src/main/java/art/arcane/iris/engine/object/IrisSeaCaveConfig.java 888e6a32d952e28f23a76f33b57d8a2bcf1d508212d86057569eb300682b85df -source core/src/main/java/art/arcane/iris/engine/object/IrisShapedGeneratorStyle.java 707b49bc5f9ccad49923a4f084e23bb24bc54114d1bcd519ea48219f132b87f8 -source core/src/main/java/art/arcane/iris/engine/object/IrisSlopeClip.java 750a0cdd661a6bc4aa9da8c5d45834f6906a3105ed5c83f24ce043e080a3ead3 -source core/src/main/java/art/arcane/iris/engine/object/IrisSpawnGroup.java 76a2266a83c209d50bd7602cfc963e7d508c48fdce8148833c2da1edc8a3823d -source core/src/main/java/art/arcane/iris/engine/object/IrisSpawner.java 71adbbb8bc462f473c376f10c8d47c7a27003f270a4997a6d2933ce87089a548 -source core/src/main/java/art/arcane/iris/engine/object/IrisSpawnerCooldowns.java e943726ca0addac01ea6b9e41f19f3c57d2b0abbe7e5d1ddf6e73ce5319b65e0 -source core/src/main/java/art/arcane/iris/engine/object/IrisStaticObject.java 9aa979656ba165d43f80dd809568ba82cae4bff7d2b609a760cd4e0bdf031193 -source core/src/main/java/art/arcane/iris/engine/object/IrisStaticObjectLayer.java 0adbf222ed7bc09ea551096ad3af0a693d46ad2dcbc7f7f8bf11812ead24039c -source core/src/main/java/art/arcane/iris/engine/object/IrisStaticObjectRotation.java 1bce5d7e4ba804c6c564161be14bb1e8ddc2482bbf5f5338f032b4ba7738cedd -source core/src/main/java/art/arcane/iris/engine/object/IrisStiltSettings.java 92493fe250b2c3e9942a33a554890599cc3c9e7721f306d1a14d56e45876b838 -source core/src/main/java/art/arcane/iris/engine/object/IrisStructure.java 514738f0cb6ced3f1815619a70d2d50caadd4d5a6063dc6ad6d825f32cf4056b -source core/src/main/java/art/arcane/iris/engine/object/IrisStructureAnchorMode.java 109f98c953a17a7890eabd82af2d455fb39626f2a4ac8f50601b6ece520cb05e -source core/src/main/java/art/arcane/iris/engine/object/IrisStructureCarveShape.java 6b6d46ab1b258c5ba667889a9548c69ba7b5b05636dc31b4add8b4d71cf3a708 -source core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java 0810f243920f439d62a94e5c8fea332562cf99abdb8b4aaeeed7a36af8bbd5e2 -source core/src/main/java/art/arcane/iris/engine/object/IrisStructureSetFrequencyOverride.java 4eb597d8dfe58142b44a6bc760e1a89a64d9fbce7e21138271236247ef2d43b3 -source core/src/main/java/art/arcane/iris/engine/object/IrisStructureStiltSettings.java 433a716468fc908dbe7ba89d6d1ccfa31615f42adb4f707f33ae0c19edebbecd -source core/src/main/java/art/arcane/iris/engine/object/IrisStructureTerrain.java 9019db1db1f80ee62e339e658594fa4b5c955ab2595685b1e5a2f63d3f584268 -source core/src/main/java/art/arcane/iris/engine/object/IrisStructureTerrainMode.java fd353b4362e2bdcac61ec5b79fa2edf9f47904887bedbef4e914807883f182dc -source core/src/main/java/art/arcane/iris/engine/object/IrisStructureYBand.java f9523bcb8bc94fd9edfa1c982ea7b486f199c4263ace5b7e21c42e28d56dfdcc -source core/src/main/java/art/arcane/iris/engine/object/IrisStyledRange.java 1155c6f598676cbc41ecfee2af6697867787200ba26bc64cec4ea1c5b3502b86 -source core/src/main/java/art/arcane/iris/engine/object/IrisSurface.java 35f98a084a8fb91e50678ae668e7d3c8e1723d44bba6d027a0d64d51faf5e3f8 -source core/src/main/java/art/arcane/iris/engine/object/IrisSurfacePoolConfig.java 1db61fc2c2b1b851235f15dd8c48ace06f6914df9f6ddd0641e0509ca7850916 -source core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceRiverBankConfig.java 9fba00b099f42e433e6d77581eb38418b8f9a3704fb357e8f3c4065a98fdb8d6 -source core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceRiverBedConfig.java 577b218d04464313846105aa422d53f65845b78ebd8838e1c71469b2f35eeaaa -source core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceRiverChannelConfig.java f146cb85f2c4e332572f4e6a8adecf9ca9f2dc93809f6d8e59877227e5f809b7 -source core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceRiverConfig.java 3068bf2cada585142704c396ea226e3c31ede31c1596d38c9e09ac168dfc65e9 -source core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceRiverErosionConfig.java 65cce3c6373987c4ec049cc1508f48674f4c130781200681732553bb0e3fa2d8 -source core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceRiverFlowConfig.java 7fb44dbe3999b644a4c5dc1d7df006b07b7f0e2fac38af25e7821ff7713950d6 -source core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceRiverPondConfig.java c985f07af4bdc872001c79f57c56b0dec532bb65cd6a275f3d2bb297870c14b4 -source core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceRiverPondsConfig.java 5da7da41d10560c971039fb1318e58ead86ab793fd468dfee0969e1c4a1cb83a -source core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceRiverShapeConfig.java 9a868eedde1d5e647e2d6bb59ce7842d9a79e42e22976b4f48612961e94eed55 -source core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceRiverSourceConfig.java 1256866ae92b37d7190bad78f534eb6ebab9f3c580b098d4517e01ed50adb717 -source core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceSupport.java 1c8698aa066f0f2fb26b452b5abb63edce99cabc167169de1e0644d5b928479f -source core/src/main/java/art/arcane/iris/engine/object/IrisTimeBlock.java 9cae2c622fe8359a01f1ae7ff30598a6fe044382f6738df8b903ec7b1a331123 -source core/src/main/java/art/arcane/iris/engine/object/IrisTree.java 9f0e170a9e15d597ddbd4d30d1d52d4a6448ebb166ccbf1d5d5b25270b40bbe1 -source core/src/main/java/art/arcane/iris/engine/object/IrisTreeAzimuthMode.java 60fa87bcbdecb43685b718b4638bc5624d96ebc4907e0cdcb79ab163cfc8012d -source core/src/main/java/art/arcane/iris/engine/object/IrisTreeBranchProbability.java f3097dcd786c0c71d648b8aae81ec56c41b87d2a3463c206c262cc6e510a0bd5 -source core/src/main/java/art/arcane/iris/engine/object/IrisTreeBranches.java 52490a2a7b724851ed2842ba0deede783af49f9e3b00b61a8d325b6d5cb43f73 -source core/src/main/java/art/arcane/iris/engine/object/IrisTreeCanopy.java 1cd2dd2a719050a90534fda53d5ed73d473f1ea313f39476e6ca2483a3d75db9 -source core/src/main/java/art/arcane/iris/engine/object/IrisTreeDecorator.java 07512ba13e6cd3bc91fd82a1ddc66b87d962dd4e28efe7fb261d81b2127d3493 -source core/src/main/java/art/arcane/iris/engine/object/IrisTreeDecoratorTarget.java f19121cb3ffdac18aca6a38f6724a8d7694396fa81045335cbe1523df90c6560 -source core/src/main/java/art/arcane/iris/engine/object/IrisTreeFunction.java 1e33e8c88d0953c5f52f7210ee4ba769c076e691db317f110350d2dc9cd3b8c5 -source core/src/main/java/art/arcane/iris/engine/object/IrisTreeLayer.java 1ab82828fea136849a9e5a4f0bfae4bad1f36364e9689f3f83a2a5386262b129 -source core/src/main/java/art/arcane/iris/engine/object/IrisTreeLeafMode.java 35930479f3bd5892b06f54d7865daa946affc199e6b69aba637f5c80bd98fa3c -source core/src/main/java/art/arcane/iris/engine/object/IrisTreeModes.java ce070224488a8b4d02566162475406253d7d11ce990bffde4703401a38cab1d7 -source core/src/main/java/art/arcane/iris/engine/object/IrisTreeProfile.java dc2846b836efbc9b935621ff05df1935d3698c3e14880fad98a3cf66fb8b7573 -source core/src/main/java/art/arcane/iris/engine/object/IrisTreeRootStyle.java b62b9e6016f5dcb2644d8d324cf74164e565a0ec93c4ca78af4fa13a2f748eb1 -source core/src/main/java/art/arcane/iris/engine/object/IrisTreeSecondaryLeaf.java 1a63c61e7a0a723b14fbed4e906217dd023a992eeac2a8488c05ed834bdd2eab -source core/src/main/java/art/arcane/iris/engine/object/IrisTreeSettings.java 22e95abe2290cc8b7f27ffc83522b7f45d90f014ab7cc95a226225d60bf2f70f -source core/src/main/java/art/arcane/iris/engine/object/IrisTreeSize.java e6d078de687cd9ef7b39d4aefee81a6a4b3bca5f4b8dea55cd3eb876b4d696c0 -source core/src/main/java/art/arcane/iris/engine/object/IrisTreeSubBranches.java 9c51c8a0609cfba78a5bcb333ea69b83475bcc3181e9adaaf77964382338b38a -source core/src/main/java/art/arcane/iris/engine/object/IrisUndergroundRiverConfig.java 0e0a15ffb2bfad94f8537aa11e0164a93e6b2958fe10623a3d8bb8b1dc6a7870 -source core/src/main/java/art/arcane/iris/engine/object/IrisUndergroundRiverSourceConfig.java d898a754f581513cb56d3ed351cb9439179ac1d8110f015e712cd181ede321e4 -source core/src/main/java/art/arcane/iris/engine/object/IrisVacuumSettings.java 5535e0630ffc40494bc5857ea73eba0d9ad4dfb8b44b1203059c053e46e5af7a -source core/src/main/java/art/arcane/iris/engine/object/IrisVanillaBiomeTags.java ab3f3e14ddaea85d00bad70ec87d6471266e695a309767578621559671e1def5 -source core/src/main/java/art/arcane/iris/engine/object/IrisVanillaLootTable.java d3460a96784b2dd5457f7da16e03a69330852d650952b51413ff30dd85bba732 -source core/src/main/java/art/arcane/iris/engine/object/IrisVanillaStructureAdjustment.java 7b7e05f15065377a6e284775cf059f13cf6c52370e85a33b0e32dfc9fbed2fe7 -source core/src/main/java/art/arcane/iris/engine/object/IrisWeather.java 1007a0c390653664dbc0aad2a6904f065efa55d1ded043366a64f124a68b76b7 -source core/src/main/java/art/arcane/iris/engine/object/IrisWorld.java af2c9dddd7f3f648b4c82545b469bb23aa517a4d1e0a0fe25ecc933397849886 -source core/src/main/java/art/arcane/iris/engine/object/IrisWorldBoundary.java a058c0eddfdc5feef81abde73df041b97a8c6b6ac4e4846d31b7c051f0e26589 -source core/src/main/java/art/arcane/iris/engine/object/IrisWorldBoundaryCenter.java 700c715324682d69afebfcc27e58594f3cd0515a867e0b7bbb299402170ab309 -source core/src/main/java/art/arcane/iris/engine/object/JigsawJoint.java b4f25e63a4ddfc2f250b885a03f228c19213b76f7aaa48c7ea2959b0f5e66769 -source core/src/main/java/art/arcane/iris/engine/object/LegacyTileData.java 8a80a7b40fe7429be0f796603c6caaeb2c530721505390567cfb9c5a0f15f0d7 -source core/src/main/java/art/arcane/iris/engine/object/NativeFeatureGenerationStatus.java 29e69fff49065096464a67d9bdbb245df8b3a438a4942db9b01b9e0249d6b5bd -source core/src/main/java/art/arcane/iris/engine/object/NativeStructureGenerationStatus.java a2ec06101b36998d94ea6bc5e49401b9943c2e9dc037edcbd4a642b791b523ef -source core/src/main/java/art/arcane/iris/engine/object/NativeStructureSuppression.java 02fd9d1e56fccc92252ab5e352515e736a09bfe391c60d57948f5d45ff5ac813 -source core/src/main/java/art/arcane/iris/engine/object/NoiseStyle.java 1b9fac39f37200d87adede7c36116ff4495eefd19bbc84f8dfb3ef97143f589a -source core/src/main/java/art/arcane/iris/engine/object/ObjectPlaceMode.java 6d5aec1eca0a2b57c34f79b18fe3bc494911caa8bbf14fad9bea6041c4ebbd95 -source core/src/main/java/art/arcane/iris/engine/object/OverrideMode.java ab63c9fd45bdb2bd6a0579d818a12f5f05aedbffd42024adc2fc8c427b09bf43 -source core/src/main/java/art/arcane/iris/engine/object/PotionEffectTypes.java f9dddd9a786d71357acae117e29dc52ed99815b7ce61909fe65e54cd6dc32548 -source core/src/main/java/art/arcane/iris/engine/object/StructureDistribution.java 6cb6675d49e17857a82baa998f866d99100cecb3a3e89ff0d2c25325d5272735 -source core/src/main/java/art/arcane/iris/engine/object/StudioMode.java 1e64934c03d5cca634e19ea44a99ee5b7cba68fda6fbc16aefaeaacbc5a4651d -source core/src/main/java/art/arcane/iris/engine/object/TileData.java f95ecb5098df6c27e99013be6f85cfc57fc8bc995e487f953191eadee9c7ddb0 -source core/src/main/java/art/arcane/iris/engine/object/TopShapeMode.java 305b303d00b187f4da9e36a256c9e9dbe258b909f15fe3f9ba7697efd26f818c -source core/src/main/java/art/arcane/iris/engine/object/annotations/ArrayType.java e5edacb489c4de16c4102401266ce3973083fd522609d44c8d813533f64e854c -source core/src/main/java/art/arcane/iris/engine/object/annotations/DependsOn.java 56e2ad466f31cd0c98418105ea7830208ac261a1be12723fe0cc9237b616228f -source core/src/main/java/art/arcane/iris/engine/object/annotations/Desc.java e640e607958e20063e9383a7cda1fc0feab78f5292ac90674375e4fb26fb27ac -source core/src/main/java/art/arcane/iris/engine/object/annotations/MaxNumber.java 401ea8fb5f76f2c4e9bac14f660212aba746fd6e48794a32fd0fdc02a5948818 -source core/src/main/java/art/arcane/iris/engine/object/annotations/MinNumber.java 00108e553586886ca29b4290f66bd21edfbaa6f19e2dc9dd68427e738b62175b -source core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBiome.java 0bd430dfd750c0942d87510a4a841336bc214365d1756f5dc65695c3c1583387 -source core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBlockType.java 875546db8db492dcff2d07dd4dedd4586f522070e8d7a852a909d17685a6f53f -source core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListEnchantment.java 2654737786accb978faddf0b407b8bdaafb3a6781e5c37bd325a02adddae4535 -source core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListEntityType.java 2d445bf62bd356c5f398f705796414b0cfd08190757d25319cd6df9cf37b08f2 -source core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListFont.java aac86e9b558e5629bbb82403e16d9c1e008c993f8ac4429f58de1460dbeb037d -source core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListFunction.java 940e739215d2420d473bedde4e1a459ebf3ef3a7727e8a82861777a656c7d53a -source core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListItemType.java 65ca1e1b9250b545b51acc17ddb5de17fd79b6f94c86c49cdbd7f2d3fb3c1ba5 -source core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListNativeJigsawPool.java 571c6f313b5ac8025009881358cf709a65f8ff10e2f8ffe61e3afd0343b63090 -source core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListPotionEffect.java 5f9267b2522b07a4bed1488f2c615e373250f740863abc981c9e474a6dbf7547 -source core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListResource.java 48e5fa3f53b85f038f479b14b14c0665af21c3a9f30ca95e2b060908b60a2222 -source core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListSpecialEntity.java d2763229278f3d83641cadea4deb26c580ec5d1c670d8787ef627f2699ab03e7 -source core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListStructure.java 494207ecbde7467de5f82bb2bd5d3cabd61e35d95837842a5218aed0bff631b3 -source core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructure.java d03f9eec68c3a8de2854941337f1992140737e5a78f4e273a851fdf2d643d084 -source core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructureSet.java 3c948e3c5e746789bcc2cf9a8f134196094ddc48b30f9042541d9174d4898e39 -source core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryMapBlockState.java 4c95567aee2dd9521c716f8c708f9503774fc0ecef8683f3be16bdd808a141c2 -source core/src/main/java/art/arcane/iris/engine/object/annotations/Required.java bd06bb734b8bddb7971c82db64f5ae23a8168ad5690dbd598a8b30726dcd2c8a -source core/src/main/java/art/arcane/iris/engine/object/annotations/Snippet.java 5a3f23a4d9955dbb1a3827e31ecb59a27e3a49e4a4cbeef70f15c2c938963f9f -source core/src/main/java/art/arcane/iris/engine/object/annotations/functions/ComponentFlagFunction.java 632fa21a73bf4e6474fe181e090d6a7ad2812b30dbafa5b8c40b4ce62815655f -source core/src/main/java/art/arcane/iris/engine/object/annotations/functions/LootTableKeyFunction.java 5290972c68b26049fc0dc4dd8ecd9009dbbc58369a72033133c73a17468b6295 -source core/src/main/java/art/arcane/iris/engine/object/coral/CoralCanvas.java 54b4981245a8eb4f326929de3f8be2c20197aaa6efe6f8f7e8a7c7c69a141864 -source core/src/main/java/art/arcane/iris/engine/object/coral/CoralGenerator.java 35199093847d5baed1577c2c276064c8c88d28a3fc062df76a1c1f1e817aeac5 -source core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalBaseBuilder.java ffc52e9664e23cfa3dda220e2d080b2077926b66aa9f84034adc49e7cfe38d6d -source core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalCanvas.java e6261162be929c50217536ac5197e2cf53bb389700cb34ce60b0d8ac35239d32 -source core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalGenerator.java d3d6b585025407a6e4541fd5871e5986ee4406f1e1ec0fcc53696e0e8a80d884 -source core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalGeometry.java f4101c62f94247941d3e4fa14fdbd7d27a60d297226abfa389c4bd1dce29cbbd -source core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalRole.java efe3b4ea13ca24b787c823c46fa6ded2d8d705faff4df34ae822fd82c212ebbd -source core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalShardBuilder.java fcce96cd30192ef729e06b575eae263b4291b986cf5ecc663c7fb13d79757ac7 -source core/src/main/java/art/arcane/iris/engine/object/formation/FormationBlockResolver.java 36038971f58dedd46a888913629dd4ba529e8fa07635e55e5204a7645cbc03b9 -source core/src/main/java/art/arcane/iris/engine/object/formation/FormationCanvas.java ad42efdb50c28b1acce2fce736d5a585d3d56cb7e7bcddce0812c4ac25aaeae4 -source core/src/main/java/art/arcane/iris/engine/object/formation/FormationGenerator.java 14abd7b35404a34ffc92c85143034ad88e0e8544c2d01424d6bd85f0ebcb195c -source core/src/main/java/art/arcane/iris/engine/object/formation/FormationProfiles.java 1b88edf26487c7cd8bbb0d25d256744f70061f53b0cd91e1458300aad880d6cd -source core/src/main/java/art/arcane/iris/engine/object/formation/FormationShapeBuilder.java 5e3cf340c33b9ee161a3730177359746dc88ef4f367c255db54e31a30020ee13 -source core/src/main/java/art/arcane/iris/engine/object/fungi/FungusCapBuilder.java b270399107bb075721b62f51e7eea267359d50079707f9424866c85d5bcdc8ac -source core/src/main/java/art/arcane/iris/engine/object/fungi/FungusCellRole.java 110bb130ed09c858193630f8985fa71c523db77d0ad43481c60c7f1967b60e54 -source core/src/main/java/art/arcane/iris/engine/object/fungi/FungusGenerator.java ce19b708af6355f80c6d49294791f1e2aec86b2cc1b4771b9aad90eb0be59f7f -source core/src/main/java/art/arcane/iris/engine/object/fungi/FungusShelfBuilder.java c8891944dd30234d49dab15ea842936665dab148e627f1154a913fba1b83533e -source core/src/main/java/art/arcane/iris/engine/object/fungi/FungusStemBuilder.java fc3d600fa5341c93116185bf3ad34a65dde2db7a48d3e3e88f1a74103398bb92 -source core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterObject.java 5934e22c0c145e3d41a6167bd5ae2714f578d0654ae0b8a7b2ef4a4d79fda4a6 -source core/src/main/java/art/arcane/iris/engine/object/ruin/RuinBlockCanvas.java 4f34080210e484a093e32cbc82d8859c84600e48d5f0f5cffd3fc2945d11b418 -source core/src/main/java/art/arcane/iris/engine/object/ruin/RuinDecoratorApplier.java 340aac794784b18654e0e605f1cc2764fa867593be927945340b480c9d08e50b -source core/src/main/java/art/arcane/iris/engine/object/ruin/RuinGenerator.java f81e211499be7869421bc8afc892ec2036ffe169dc0d0752d532598d1dd3ea29 -source core/src/main/java/art/arcane/iris/engine/object/ruin/RuinShapes.java c13c727390e7b4fc6cb61af233199aa270c26e3d4389d4ce8a96da1b3b06313b -source core/src/main/java/art/arcane/iris/engine/object/tree/ProceduralTreeGenerator.java f88f37003efd089707cceb1642bc868ac59bd49a499092774e0f678854dfa4d0 -source core/src/main/java/art/arcane/iris/engine/object/tree/TreeBlockCanvas.java e22dbfa8e39db43150baca70476be9626417b899cf7c2a6e672c89818cdfe0ba -source core/src/main/java/art/arcane/iris/engine/object/tree/TreeBlockResolver.java 402727f10a403bad2aba35621464fd0606124a71b46411f53c13c72e746e6409 -source core/src/main/java/art/arcane/iris/engine/object/tree/TreeCanopyBuilder.java 1ddf30c73466f9ca9e89df80137c8b5d5fca42539dd5b757e2380372e9b3fe12 -source core/src/main/java/art/arcane/iris/engine/object/tree/TreeDecoratorApplier.java d751001eef389ba0264a7434f742106409196d8f950212da84833b538ecf1f14 -source core/src/main/java/art/arcane/iris/engine/object/tree/TreeFunctions.java 2dc29ed64041b0bf2202d445878df65b1f5445d73ca0fb774eb472b6adb2b9ed -source core/src/main/java/art/arcane/iris/engine/object/tree/TreePlausibility.java 8d0ae9211fc370d7ca081effcb136e3379fd630627cab8a982b6cce9ae8a2088 -source core/src/main/java/art/arcane/iris/engine/object/tree/TreeProfiles.java dc15c4acedc709a2cc73d36aba148899cb3e129cb5d1656f0b6e5a6df8f5363b -source core/src/main/java/art/arcane/iris/engine/object/tree/TreeRootBuilder.java df4248d6dd1508b4856508a2a8363b7cffae0d8db66a10495ffe9593b8127f68 -source core/src/main/java/art/arcane/iris/engine/object/tree/TreeSupport.java 11c72c83a632d34945e3032d68ba89f0727ff3e7af3fae64988d17a8e9bf0fd5 -source core/src/main/java/art/arcane/iris/engine/object/tree/TreeTrunkBuilder.java 856d0b46117a485df832cfadd2c31b491fde8609b43799b060110246524527d1 -source core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java 6b036e6e3246df6a8e850344c67b0b5b1f8cc5e36914d304b5e9eaeb3a214243 -source core/src/main/java/art/arcane/iris/engine/platform/DummyBiomeProvider.java 560036f370d550f7916e6ed4b79df907fc0787567c27eabc49df1ef108cf5b13 -source core/src/main/java/art/arcane/iris/engine/platform/DummyChunkGenerator.java 12ecd07d0189672a7c161734bce487f6cb6b4797a71df3692f29fa7ccc34cf32 -source core/src/main/java/art/arcane/iris/engine/platform/EngineBukkitOps.java bee55e2704b0e2405b631408fa42ca4c7baef8c83c0b00af8c1dc98e8f3792f0 -source core/src/main/java/art/arcane/iris/engine/platform/PlatformChunkGenerator.java 5450184e6474e78dac7788c9ef395a9a0fce430a2c6cd97b3f08383b319378bb -source core/src/main/java/art/arcane/iris/engine/platform/studio/EnginedStudioGenerator.java 5e279de24da76030fdfaf8b037bebf375fd35bd3e388fbaa2c4a24942ec1cedb -source core/src/main/java/art/arcane/iris/engine/platform/studio/StudioGenerator.java a94909957b4a6c60b9ac995312a72ef4208d5fd8862c4c03c2525693e9ebe69f -source core/src/main/java/art/arcane/iris/engine/platform/studio/generators/BiomeBuffetGenerator.java 5d169970dcd15011a6dfb9a8ddf76b0c03de2bef09b0f7c8ffbe3ce6787194f9 -source core/src/main/java/art/arcane/iris/engine/platform/studio/generators/JigsawStudioGenerator.java 65b7b09536bcc9a92b8588f06f0d1e94d1301fd19be8e8e26a17b79ff67e3e86 -source core/src/main/java/art/arcane/iris/engine/platform/studio/generators/ObjectStudioGenerator.java 93d9b05fbbcb579ac898d83aadc4bddc084aa05ffaebd84f72c3e51b5f4115b3 -source core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBiome.java 4d6cbcac0788b593616e063ed102dbbd93db57f9354f8b5294f19b8660748889 -source core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBiomeWriter.java 8c37ba79a8299a4b05f5e561380b03a54a10cc2f32f7d79b25ffd544211a7d54 -source core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockResolution.java a032f34aa55fb67d8f8435689f75ae2323edcb80204c93716a8b5fd8cf86b784 -source core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockState.java edecf9a49a5372e63cf3dd04999eb31b14a366ffa7f35d1340c3588edaf5593f -source core/src/main/java/art/arcane/iris/platform/bukkit/BukkitEntityType.java d1e5704ea5a4a9de73c0a119312f10e8888cd26d373e0c888ae59e1aecbacead -source core/src/main/java/art/arcane/iris/platform/bukkit/BukkitEnvironment.java 701e3365e2767e387d291de92926698a30f0125392c4d8fa209c6b5cb72d6be8 -source core/src/main/java/art/arcane/iris/platform/bukkit/BukkitItem.java ed5a7a0d0b1d33f6f83a09ee546018f631676fde47865b6168655b5afc3572c1 -source core/src/main/java/art/arcane/iris/platform/bukkit/BukkitPlatform.java 66feea66899c8f657b460bc957b399c07e10573bc53495c0336fd5d3a722a124 -source core/src/main/java/art/arcane/iris/platform/bukkit/BukkitRegistries.java e9ac71f90672b9c69d28124f7a2302cd5510478eb9a8177318440bbc9e774f4e -source core/src/main/java/art/arcane/iris/platform/bukkit/BukkitScheduler.java 6e70b1081763190048e6df1271328caa749465d5572db77892ac205dd99d16b7 -source core/src/main/java/art/arcane/iris/platform/bukkit/BukkitStructureHooks.java 2a6ec7a8ffd682e99476debda475d49273ff5eee2237dfbdc8fda80dc080c6e5 -source core/src/main/java/art/arcane/iris/platform/bukkit/BukkitWorld.java ac8a80f986ea4c8923a8d18c020902ce174e0bf136d35304963cee09a48bc3a5 -source core/src/main/java/art/arcane/iris/platform/bukkit/BukkitWorldBinding.java cb27266059ea0e2831654bcc38c479345b1faaa8fe19dbdaede80e84370e2e8c -source core/src/main/java/art/arcane/iris/util/common/board/BoardSettings.java 2d4028c4e8d4b84d8709c8da81fa90c446a5e6102b6eebc4816d89d8ed6aea7e -source core/src/main/java/art/arcane/iris/util/common/board/ScoreDirection.java 3389b64fac4546e95d4e343b967cbfd4338ea06b5a9bc9bfb4ac2c053f726eb5 -source core/src/main/java/art/arcane/iris/util/common/data/B.java c4a2ca1949b25d664803a9331271c8cf9ff07cdeb4a390e8827633b079f50662 -source core/src/main/java/art/arcane/iris/util/common/data/DataProvider.java 499f1a1b265fb2f504ac40b35ff5bd0ab2b21abc0bbe2ec4afed09f14e09a029 -source core/src/main/java/art/arcane/iris/util/common/data/IrisCustomData.java 3fb98125822014cd36254ba153e46a12331ba4f2c1fa3e8c23e8da45d83d9dec -source core/src/main/java/art/arcane/iris/util/common/data/VectorMap.java 7120c0caec76c0e29a98d050ff0dac8711ca218d959953f5f8d9d74f1b657844 -source core/src/main/java/art/arcane/iris/util/common/data/registry/Attributes.java 6fc3ab6299d8a75754f48a87270bfeabb063b00529c536d4797f5822d21153e0 -source core/src/main/java/art/arcane/iris/util/common/data/registry/KeyedRegistry.java a0506c114e251bdc76dc6363f2f045bab95aa946c1aee4ee7c899524c958361a -source core/src/main/java/art/arcane/iris/util/common/data/registry/Materials.java 1a6766d5b434de63cb1e6505baa06a3c73ea27e79d2afce0f5834d08c373bc68 -source core/src/main/java/art/arcane/iris/util/common/data/registry/Particles.java 768c2aa7ae16e7c10bc242033b9375777c0cd62a224a9887bc02244bfb156230 -source core/src/main/java/art/arcane/iris/util/common/data/registry/RegistryTypeAdapter.java 3b28af25693dcb6659e508299b136aa4d89a1b24481f8d58293f600ef443d256 -source core/src/main/java/art/arcane/iris/util/common/data/registry/RegistryUtil.java ea0d71132b3bf9a937333c2cefed745b4a966583b5ac038d9247b3884e6265bc -source core/src/main/java/art/arcane/iris/util/common/director/DirectorContext.java 241f1a1694be6d6688add7ccb7d01825768c2c499fe867810703427d6094163d -source core/src/main/java/art/arcane/iris/util/common/director/DirectorContextHandler.java 3190790afee9f33412ef099e7177421f52fbd11e445d9f42e70255dcf3ee0cbb -source core/src/main/java/art/arcane/iris/util/common/director/DirectorExecutor.java 8c617fb719f4051224e7483bb033f2b85ded9797a6954b9c4be278ea501bda9c -source core/src/main/java/art/arcane/iris/util/common/director/DirectorParameterHandler.java b7606427e94cffbd4871f5aad96e20554811ecc8d252e4220388fea57fae1410 -source core/src/main/java/art/arcane/iris/util/common/director/DirectorSystem.java 0ccc9d69a0dd52ec3a301d2b175aa9f9793c156729c993e5cd071af2d5403f3f -source core/src/main/java/art/arcane/iris/util/common/director/context/BiomeContextHandler.java 60d8c2f43c23f5083b97b5d5d042cd6cfcb3328bbaaf13c1fc0c00814cba2892 -source core/src/main/java/art/arcane/iris/util/common/director/context/DimensionContextHandler.java af70356e729654442f1d31ec5ad5764cde485602eaa7977d6a31c9c7465436fb -source core/src/main/java/art/arcane/iris/util/common/director/context/GeneratorContextHandler.java 943912dec8527c95fc629855696f4569c65590756efb6e2dd6a0ec8c1afd329d -source core/src/main/java/art/arcane/iris/util/common/director/context/RegionContextHandler.java 00c602eb42b44fcfd40595e0ca6807916cc058321621f9b773d6b2c4b7fd4876 -source core/src/main/java/art/arcane/iris/util/common/director/context/VectorContextHandler.java df76e6e2c4661a4159e07a49b219700f91968f86c70f004482b2e8ec97135e28 -source core/src/main/java/art/arcane/iris/util/common/director/context/WorldContextHandler.java c52e1d9a0f2229c6944a4370664429ece62e253f4e403b13569738d83aad587d -source core/src/main/java/art/arcane/iris/util/common/director/handlers/BiomeHandler.java d98db91c3957f3130a655353610372953429acfc2ada03497add6a9b260ec75e -source core/src/main/java/art/arcane/iris/util/common/director/handlers/BlockVectorHandler.java 02862855b97c3bc4de662bc0b916084e6b09f073426b24574aca91f40a610c7e -source core/src/main/java/art/arcane/iris/util/common/director/handlers/BooleanHandler.java b5f2384ee4df5bf2e78c46d2b0acbc2f34bff3f02531df5b7d22973849cd3e7d -source core/src/main/java/art/arcane/iris/util/common/director/handlers/ByteHandler.java 3d50e2515854537deef85a74f3d2b43818ef5b679fa4bdd715ef39532c274a73 -source core/src/main/java/art/arcane/iris/util/common/director/handlers/DataVersionHandler.java 25941ce2e039180454e8ed733e2f603f57e4c5527cec94f87de0b35c8acf6466 -source core/src/main/java/art/arcane/iris/util/common/director/handlers/DimensionHandler.java b991f51b4bcdccbb7d561676f4319e88eb875cd990f823bd3fad7d8838718ec0 -source core/src/main/java/art/arcane/iris/util/common/director/handlers/DoubleHandler.java 79188a98a32fd21d3ea65ecf5bedd6f6221e88a093ff08e99b4f325510e2d77b -source core/src/main/java/art/arcane/iris/util/common/director/handlers/EntityHandler.java 0f926d62f1a9ca8fc80a7c15f50f88c2b29368f51ccdfc1487aa4a6b9dce0b76 -source core/src/main/java/art/arcane/iris/util/common/director/handlers/FloatHandler.java eeb0157418834453b87ddd67c591a24b5fb039555c1ef73b140173b1266dc1bb -source core/src/main/java/art/arcane/iris/util/common/director/handlers/GeneratorHandler.java b9c310c27af1b34c1ab1e1154d8c27792c99746b4f8f52b8086ed58766860cff -source core/src/main/java/art/arcane/iris/util/common/director/handlers/IntegerHandler.java df8342c7c9cdc89f8e76cee57ff8ab0020c2a5a1cc6fdca76064e1594aedd783 -source core/src/main/java/art/arcane/iris/util/common/director/handlers/LongHandler.java 9e3ee5b7f1c5e3444d762b471d9485654159d168bdf4e332aec371515778767a -source core/src/main/java/art/arcane/iris/util/common/director/handlers/PlayerHandler.java 05c60b0b4e8d67c97ecbbedbe068d5b920fd930eb1e5726368499a3dd15de98b -source core/src/main/java/art/arcane/iris/util/common/director/handlers/RegionHandler.java e81ba147bd7fd2ce53f1950a393a276dad3b3a9505d39b194b67c363589fa3ba -source core/src/main/java/art/arcane/iris/util/common/director/handlers/ShortHandler.java f8cd415ddb63b0e76c96b5dbeec1071a3d10486ce374357a4d643cc8ae1ec32a -source core/src/main/java/art/arcane/iris/util/common/director/handlers/StringHandler.java 2ba059b3f029bf20198d74297c843c77954e229f2ba8e564bc18ae2a191ec661 -source core/src/main/java/art/arcane/iris/util/common/director/handlers/VectorHandler.java b87879a9bf6977d3e6912e5961765d2a75123dad774ab3f3aba0f16aae56e1af -source core/src/main/java/art/arcane/iris/util/common/director/handlers/WorldHandler.java cbfe0d8abb2d7c854a7d042aa3ae479845096796e501b7e3ce48419b46b6b6b6 -source core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/HydrologyTypeHandler.java 02b535ec317ccbda3293a368266fd2b4f230aafdd86db737b84971358380a0e5 -source core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/IrisStructureHandler.java f0f1175fd202efbacdbafe73ffc81bd0a5cf0b1b8b9e59ae4775f339900d14db -source core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/LocatableObjectHandler.java 385fb9a2e04f630cb3005a9859a4e16ea37416bb9de1a262111588b612c37c9b -source core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/NullableDimensionHandler.java 030665aa59347df09c08de416d9a262099e05ddc0f7bb27a0ec170261999aace -source core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/NullablePlayerHandler.java f914779d13aec15640dec63a88d5a05b43bf093bbc6d305bd0697296a5ce8357 -source core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ObjectHandler.java 659975224155bf2cc505ffe2ae922f7c71f137ef25d94be62cbf60c32310b65b -source core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ObjectTargetHandler.java 02ebb5f35b5b79949373a2a22434592fcb824d97c5cff2f6d8f8caa8137e1f82 -source core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ReachableBiomeHandler.java 41e0516afb968b57740f086faca631bc48a39b589e2e5cd711fe41e5fe5b0203 -source core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ReachableRegionHandler.java c06fe8c3e586429c0c78c4bfc5453912d81a31c089078253034f069f50e36dc6 -source core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/RegistrantHandler.java 7b5306f1cce0a4a510320412b59b8fd7c0e0c8c1bdf23448fc8c287964a165a5 -source core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/StructureHandler.java 0e5e93f4b44c39e491b2ed26b17e076b727171778268d3df7d01f8c926b101be -source core/src/main/java/art/arcane/iris/util/common/format/C.java bf8cddb1a0e2ea10f8ffeca03b9e7b033066b839d01c1f3867776d634b65facd -source core/src/main/java/art/arcane/iris/util/common/inventorygui/UIStaticDecorator.java 3841cbb595929bf67f72a8ac88f7cb6879d1f9ebae25e73147e98bde01b3794e -source core/src/main/java/art/arcane/iris/util/common/inventorygui/Window.java e14517b148a65c9e954a3cc7777445083b98fca2a1b532c2167509f95fdc5344 -source core/src/main/java/art/arcane/iris/util/common/inventorygui/WindowResolution.java 71aea9d541f3afb299e8b74fb2b4fb04aa104b77aa76b35ae76dc3f5e93f4c7f -source core/src/main/java/art/arcane/iris/util/common/math/AxisAlignedBB.java 69dffa2f3b44276d39f146108cca525f2734b54dfd2528b11618380faaa91072 -source core/src/main/java/art/arcane/iris/util/common/math/ChunkSpiral.java 233d36a41d8144437b1023329685f9b5b690f85d5d4c3a7fde16411ae6bce795 -source core/src/main/java/art/arcane/iris/util/common/math/Direction.java 4623f70902bfb1b23f156765575f0e9e33acc24206ef7b92aeeb7ec18bf1de5d -source core/src/main/java/art/arcane/iris/util/common/math/IrisBlockVector.java f91db04a00af69e6a10c05ab98b5dc76db22da629504e11bca61188ac6cabce9 -source core/src/main/java/art/arcane/iris/util/common/math/IrisVector.java 04aeed731584d3e28fcddc0f53b32c07e76091e8ed82e957518ab0aa319e0402 -source core/src/main/java/art/arcane/iris/util/common/math/Vector3i.java 39da42ce65ffac27c0122e354301b4e329350e65ba3b026c9b658af072eaef16 -source core/src/main/java/art/arcane/iris/util/common/math/VectorMath.java 55c5adcc531c4dbd8b0d616c2e7b2ae6a4aa6e4682f644c5d99d8800b57cdd77 -source core/src/main/java/art/arcane/iris/util/common/misc/Bindings.java 4d254794cdc808dd49c2481d06b25b0392416848ab45bf8f491dea15e792f31b -source core/src/main/java/art/arcane/iris/util/common/misc/ServerProperties.java e72647a7ec0ba85454af2799b7ce74d38b4934d38d4c96ecfccabb1aa0b77422 -source core/src/main/java/art/arcane/iris/util/common/misc/SlimJar.java a50e26f6f530a6ec588cec8a0ca4bb32c60e31b427e3c1502b486d6d97e76627 -source core/src/main/java/art/arcane/iris/util/common/misc/WebCache.java ac44fd2da7d1ccadfb4ecc795dd167124163b7a219f4ba9213707e1cb1668c64 -source core/src/main/java/art/arcane/iris/util/common/misc/getHardware.java 2f2546acf31f62f490951b93d3b0739b7b032440af9abdaa832815fd36684dcd -source core/src/main/java/art/arcane/iris/util/common/nbt/mca/Chunk.java 9ae7326436498ef5e896d43ef8144b7c02cc89fa5c1a7890d0e0d699b68be4bf -source core/src/main/java/art/arcane/iris/util/common/nbt/mca/MCAFile.java b52cff0c7388c053db3ec5776ec9ddb31e3dc30480da01936a0a9888be0776e2 -source core/src/main/java/art/arcane/iris/util/common/nbt/mca/MCAUtil.java f7b170fd91519e4a55b06f602269ca4836c8148f38c6637e1e1abc3155b44dc7 -source core/src/main/java/art/arcane/iris/util/common/nbt/mca/NBTWorld.java 88dfef5359ad041f3db198fff43a170797714de7ad7d7c78370c42bf07522663 -source core/src/main/java/art/arcane/iris/util/common/nbt/mca/Section.java a36b901345c80768b71ef1d840dfbfa3354e9e63877bb5426455a3f8edd60f77 -source core/src/main/java/art/arcane/iris/util/common/parallel/BurstExecutor.java 7d50b3b577a4c3e50146d600897feec3c64ad6acd00f73fdaced20d11616bb68 -source core/src/main/java/art/arcane/iris/util/common/parallel/GridLock.java 0a9caf4ddd647968a1febfaeb9a9c17663f4226c8fb9034bdfa72edde470aee6 -source core/src/main/java/art/arcane/iris/util/common/parallel/HyperLock.java 6d14685ed41b87ae00c60db4cc9f57921957e5d79d09b774f8cc67b9b04ed4f2 -source core/src/main/java/art/arcane/iris/util/common/parallel/MultiBurst.java 73b81e425a5bed9b51e1bcc1a6f7b4cce2caba9938b57604fa2428c545e9a7ac -source core/src/main/java/art/arcane/iris/util/common/plugin/Chunks.java 0e30c03eee9435b11e5c8317cc63982afc09eb3adb9c73220f58af2bc942a2ad -source core/src/main/java/art/arcane/iris/util/common/plugin/IrisService.java a036c877f9127189b8f5e11bdd75dd898aec87bce99faab9487632580cdcd35e -source core/src/main/java/art/arcane/iris/util/common/plugin/VolmitPlugin.java cb128175a5d2c50395126e19f9794688cfda29abe273a3b815433e0e10fa902c -source core/src/main/java/art/arcane/iris/util/common/plugin/VolmitSender.java 9fe6fdcb380d88580a57b3a20495bd1bfaa890443e9ad6a479a99ba542222bec -source core/src/main/java/art/arcane/iris/util/common/plugin/chunk/ChunkTickets.java d57009b24b8c300bcee1b89dbc28ad3539db4d32f751b0725ea80bb2b67fa2a7 -source core/src/main/java/art/arcane/iris/util/common/plugin/chunk/TicketHolder.java 7fc88f5eac464e543d1b8ea85591b2adeccb93cd1be573af07706f8e1eb54f0a -source core/src/main/java/art/arcane/iris/util/common/reflect/KeyedType.java 8476552092b1fc15868fadbe58eaf298e64b8dae27f2e7559706c5d757ac740d -source core/src/main/java/art/arcane/iris/util/common/reflect/W.java 71aebec938f0486ce46e91ab86690d4b77259282dbbf97b8a2954c34575d0cbd -source core/src/main/java/art/arcane/iris/util/common/reflect/WrappedField.java 1098f887ada2e4ff1a3dec58b5a3cbd344cfc021a29dee4faca85c6209b566a2 -source core/src/main/java/art/arcane/iris/util/common/reflect/WrappedReturningMethod.java f5b5057deb468c9669861f53c4b43636aa88913b121428195393711b6173dc09 -source core/src/main/java/art/arcane/iris/util/common/scheduling/J.java 68e323bc2b948427b736a9d2475439aecb77a73f6e25000a20a7ccb6e857e78e -source core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/DownloadJob.java b2ac86872f1899149c40ac369e7d222a98175a6d38dd9db2f511d8b4ff639566 -source core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/Job.java 7d1e01589b0fd4dc0e0ad3b7702c8d8c11db5ec2581ec031c016486cae46bbf2 -source core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/JobCollection.java f4eb59e9a5d3109ff04a9f440cda444bd0f77080972f29908c44ec545a5c70ce -source core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/ParallelQueueJob.java 3840e0d11d7453b60c0d93bdbc73f207425022df0c358216c028f4f079305c38 -source core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/QueueJob.java 178d5535ea3b3e2543641c4177a7e6917062b4b72f871af303c2e7872c289086 -source core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/SingleJob.java 467858e6fb0a287dc40eb62f0d3514adc071d629d6ed4ef3730daf7dfe7a337c -source core/src/main/java/art/arcane/iris/util/project/agent/Agent.java ab3f625bb74e91cbe6e16bc8de000978e5bb6571ae50f74259ef701d5af80874 -source core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java 2be385d5a23cd7cb9b638c5479682a140742c31553b34733d467de2181a8cc89 -source core/src/main/java/art/arcane/iris/util/project/context/ChunkedDataCache.java 366c349e9260e4e9f329b5afaa1b7145da47a6946e2fab18515af98ce027a003 -source core/src/main/java/art/arcane/iris/util/project/context/ChunkedDoubleDataCache.java cef35eac773115991933b0a8897f39f2a9b4681c4913e3f5e431b2505bf95ca6 -source core/src/main/java/art/arcane/iris/util/project/context/IrisContext.java 246b419ccb142754c8811cf6dae2cc8c7b9aa6d696a538df64c8aef43430866e -source core/src/main/java/art/arcane/iris/util/project/hunk/Hunk.java 2d7e3ce55cc90aee9d2361a89e2759fad8da8a7864f230b1289db9b0ac1c2c8a -source core/src/main/java/art/arcane/iris/util/project/hunk/HunkFace.java 4cd1912f5c88d8debf00620b2e697652be2d9b2457947c71e4c38094c1b28338 -source core/src/main/java/art/arcane/iris/util/project/hunk/SharedHunkAdapter.java 2d15b0446a3d7867616acb9088ae37629d04c9a4f4991a96f19dcea8ef8d6102 -source core/src/main/java/art/arcane/iris/util/project/hunk/storage/ArrayHunk.java f991504f354c1aac494c80d59e09a66f5d860e967a5c2ac944be88ee89019a54 -source core/src/main/java/art/arcane/iris/util/project/hunk/storage/AtomicDoubleHunk.java 42fe5835ab8e2441725b8022c01f30d5b61825fa6934870f82f05178f82a9e0f -source core/src/main/java/art/arcane/iris/util/project/hunk/storage/AtomicHunk.java 5f91d541d29e0b12dc42410f04588b796e49d3ac85ad506008a7726aa00cc33c -source core/src/main/java/art/arcane/iris/util/project/hunk/storage/AtomicIntegerHunk.java f17c9091de3e5f65ab6565411e6688f9d154b987e1b0411c05cfd058908cc6e8 -source core/src/main/java/art/arcane/iris/util/project/hunk/storage/AtomicLongHunk.java 3cfe22a63b6c1d8982d013df2eae3c3282acbe1cd06d1c043d46a33ee2afdffd -source core/src/main/java/art/arcane/iris/util/project/hunk/storage/StorageHunk.java 497ba6a9036495d186f9b6bc51511ff3b00ec3156e25c0ed430f1dc7b670a770 -source core/src/main/java/art/arcane/iris/util/project/hunk/storage/SynchronizedArrayHunk.java d0454878719e6fd549e04ed7c76d8b0d227a4e63e977b621ac612a7339d55afc -source core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkBiomeHunkView.java 267e9bbab55773b5cbdf875b420329a023d39092e7e905dc7341eb24bafb60f6 -source core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkDataHunkHolder.java ea6d723c5fe167da92bbea7986f415b5206dcacebee8de7fc9de21d0564ad5fe -source core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkDataHunkView.java 6dc3f7d0a243a86eeb43071d02b51da4e77c6d61d372a44e1d07ea5897eb810a -source core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkHunkView.java 4142c7bea9702bb5826fc14a4f5045c2dba047b8ab1f32b3a5074e6a2fe94237 -source core/src/main/java/art/arcane/iris/util/project/hunk/view/TerrainChunkBiomeHunkView.java 17b887f1dbd705430071761845d353bd65dccdcfda84122238273cb62f54b336 -source core/src/main/java/art/arcane/iris/util/project/interpolation/Interpolation3D.java c7b7ae82442e6fe49f5c05364f1857ab7f7ffa5b1a5964e6c0b19e796c0c4b4a -source core/src/main/java/art/arcane/iris/util/project/interpolation/InterpolationMethod.java 8948ead4aeb36e734b32dd7c247c0546c59e41d7792bf913053ee4f5154793e2 -source core/src/main/java/art/arcane/iris/util/project/interpolation/InterpolationMethod3D.java 4d5831fc15fe506432ec0f747ccd8cadc1f4976c561812d18a53fea3af121e2e -source core/src/main/java/art/arcane/iris/util/project/interpolation/InterpolationType.java 50266ebcd1552f781fee3409f01ff24259f6a33dd7c1e9876075b7462f5bca2e -source core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java b217803f57df1bcff429bbc9319598294ec3f1c68bab3e34b7bc4fd722136aef -source core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBounds.java 484d465f6e7ee86c567570a3d044bf871d716728b936ea1909fcc5b6fb8834d8 -source core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsProvider.java 5da0eaf2db701a21db2fd3009660449b5c2ee306ac3bfaf0522b15f82890008a -source core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsSampleCache2D.java 7a8928a4e3649c7120c5da17e11eab45bd1f39ed9ebfa4b3a58ff42c5a0f60c5 -source core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseSampleCache2D.java 2a7c13310ec82c1211b73e4925bd0262fab32fc7e474ab4c41c9f803e18e107b -source core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterContext.java b8eca540d775aac3ff015b12e2d1c56967443c186ea8e28678c210ce5a67c0be -source core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterSupport.java 0ed6c5fceaed9c5f88a6f431e5264707f402a63fc95307b1fee175fea6d50354 -source core/src/main/java/art/arcane/iris/util/project/matter/PreObjectMatterCell.java 341b5359d510650bda08568e9da8d503c1efb8f9b37474e84164b78edb6eb640 -source core/src/main/java/art/arcane/iris/util/project/matter/TileWrapper.java ff216e098814b35677cd177292319160b62fd996cf8b6788f0c11fd94f019448 -source core/src/main/java/art/arcane/iris/util/project/matter/WorldMatter.java 8c3c2dd2bf40737ff8ae6025fddba4fa34a414b9c735332e19732d17f8965580 -source core/src/main/java/art/arcane/iris/util/project/matter/slices/EntityMatter.java a9a3a4113ec4ba13e842b87e51622f1f614072ff871b02c59bbcfc5f50b48b3a -source core/src/main/java/art/arcane/iris/util/project/matter/slices/HydrologyCaveMatter.java fb79e741e638c13538c48b3cc7eddc1ca03614bfff25aed43c7147a7e8f6e081 -source core/src/main/java/art/arcane/iris/util/project/matter/slices/IdentifierMatter.java 72eccdc8be832810f19460787e787e9a7d6bd4610903309b1a9a2e5dd15c47d9 -source core/src/main/java/art/arcane/iris/util/project/matter/slices/NativeStructureOwnershipMatter.java 629b3784ee52f888987ac6fde90acca09b51268c3a7b01d99d6168f070fb2480 -source core/src/main/java/art/arcane/iris/util/project/matter/slices/PlatformBlockMatter.java 93ab902f5c8970b91c79521c76c181d189bec686daec6cb2a1611d1e05fc4be7 -source core/src/main/java/art/arcane/iris/util/project/matter/slices/PreObjectMatter.java a2afaf996085d4c0eb898ee5028550ec0681e941de8e2da3deb66826a54465ca -source core/src/main/java/art/arcane/iris/util/project/matter/slices/RegistryMatter.java ced506ce57bb218993de707ad766b5cb48f1945d135e463aee1bf631f11a6745 -source core/src/main/java/art/arcane/iris/util/project/matter/slices/SpawnerMatter.java b89275e7fbdf9261122ad8387b3dbbff9990641699054086745ef51bb1afd0a2 -source core/src/main/java/art/arcane/iris/util/project/matter/slices/TileMatter.java 3b004600848805c71ba11992b1f63a0f68c0e6ff2bce7a6b0ace61397deea572 -source core/src/main/java/art/arcane/iris/util/project/matter/slices/TreeBlockMaterialMatter.java 363672ec989ffe185a1bafdb975bafc0cca0c34bc866d202ce75c059edfb1809 -source core/src/main/java/art/arcane/iris/util/project/noise/CNG.java d6dda93ac0c9927664e47b44f86d3aa1d5fba63eecb5986eaf8866943d7d2ff9 -source core/src/main/java/art/arcane/iris/util/project/noise/CNGFactory.java 02814eeaf9cabc6c1105d7c88d63ab4823c2ff57ab2673c32bb2f3b7bd69b78f -source core/src/main/java/art/arcane/iris/util/project/noise/CellGenerator.java 41a9a7372dffe939e59c8e2cacad3f938b886aab73bdb6aa0e271495637f53d9 -source core/src/main/java/art/arcane/iris/util/project/noise/CellHeightNoise.java be8e387a5782ffcb42561bc4e17e6ccd4d0c708debc592a6dbaff0f1329baca2 -source core/src/main/java/art/arcane/iris/util/project/noise/CellularNoise.java adc76fe90c137b936cf16be88a19c21fd6efa6a7f47906c1ecd4ef0770160a20 -source core/src/main/java/art/arcane/iris/util/project/noise/CloverNoise.java 482e45a205e8b151580e39f3f7b07a8c4ac77b33bda6e2a7ad207d775bdd497e -source core/src/main/java/art/arcane/iris/util/project/noise/CubicNoise.java bdb5475a2506148f34bd4120a7f7d321811b1fa2f9cb97488e12609412878aa4 -source core/src/main/java/art/arcane/iris/util/project/noise/ExpressionNoise.java f0a8491b3dd3b073b73d752709c9a2232a71cb5bc592b4cad6cb41cffd8bff84 -source core/src/main/java/art/arcane/iris/util/project/noise/FastNoise.java 7b66688b7a5c62354d3e37921538fc1be4c24b9f0be7e38c296321d5c6787e3f -source core/src/main/java/art/arcane/iris/util/project/noise/FastNoiseDouble.java 2d285201ce056cd650f6a9d9ba84cccad07b566767cdc97ac3466986eceb2b35 -source core/src/main/java/art/arcane/iris/util/project/noise/FlatNoise.java af74540f237cd14757109668ea91875de1b466c15ca13d5df116003bf71a4cdd -source core/src/main/java/art/arcane/iris/util/project/noise/FractalBillowPerlinNoise.java 8d7bee43cee226e41963f78a1c5a79a5bef6ce9541246fcb55c1f3de616f0bc1 -source core/src/main/java/art/arcane/iris/util/project/noise/FractalBillowSimplexNoise.java 7626a6c220460568b900b288891da9a4cc8a70ad1c0d79bd432fe3f2b83c3c52 -source core/src/main/java/art/arcane/iris/util/project/noise/FractalCubicNoise.java 859fc69c64302e8868dae59613d723e3c6ba6149e9b377f9bcfaa4bb811b061a -source core/src/main/java/art/arcane/iris/util/project/noise/FractalFBMSimplexNoise.java f3285abcf6061090e8886f09ba192c0d53eaa19bab2706560442a2d22a906e94 -source core/src/main/java/art/arcane/iris/util/project/noise/FractalRigidMultiSimplexNoise.java be682487079f4f1c5ecd77d881ffb1a1d62a1f96d155cd1478d13d2741a6a34f -source core/src/main/java/art/arcane/iris/util/project/noise/GlobNoise.java f0b38f6c189252cc3f48d545d1715a69d2fbf9bb6149e1fa1bbe1a6439bbd22c -source core/src/main/java/art/arcane/iris/util/project/noise/HexJamesNoise.java 44cd644606ae636c89f5fa89e0542da292b9c8492f7b67235313f093bc04b476 -source core/src/main/java/art/arcane/iris/util/project/noise/HexRandomSizeNoise.java 7d7963ab023ed0743f3c92059cff472608608e5f56e317fdecbe6f527f296917 -source core/src/main/java/art/arcane/iris/util/project/noise/HexSimplexNoise.java d9f5c3fb8944de289381050ae376aed48e8d5252cd9b340ab029f155f0e9c7b7 -source core/src/main/java/art/arcane/iris/util/project/noise/HexagonNoise.java 91c1bf026e06bf6f16c6c1935d8f42df71ca9a963ac89b54d755c4a5d9f44f7b -source core/src/main/java/art/arcane/iris/util/project/noise/ImageNoise.java 20a4d70126b3e62b8a79df4285345a6b2a386d12d60ce5b905a2df71aaf2e384 -source core/src/main/java/art/arcane/iris/util/project/noise/InterpolatedNoise.java b596394e3929252d2423054a9a2c56c0019c34de9961972a6962caab5a3c9ff5 -source core/src/main/java/art/arcane/iris/util/project/noise/NoiseFactory.java 813cd569be725db7d2e5bb5744d0655bfc37dd20f72b41e66bd601f13f0d215f -source core/src/main/java/art/arcane/iris/util/project/noise/NoiseGenerator.java cc088b0ad8cea28405616121e9005d9571e63e718de659a7613d3f975ead44ed -source core/src/main/java/art/arcane/iris/util/project/noise/NoiseType.java 4e20aca06cdde422ad7cd4149fa31432b268c959ecbe483e07a1c470df2b99f6 -source core/src/main/java/art/arcane/iris/util/project/noise/OctaveNoise.java ad9f27127d853ff645c00cd204c0f8064124435ad027b1d279fb05763589259d -source core/src/main/java/art/arcane/iris/util/project/noise/OffsetNoiseGenerator.java 469094b6fff1451a720c4bb3e8611c1247cb8ceb7d80c604c3d7f78e905cf11b -source core/src/main/java/art/arcane/iris/util/project/noise/PerlinNoise.java dead31997bca4ad87a9cf36b7e5180322d172f1c3f84ec5e4da08a3e4608791e -source core/src/main/java/art/arcane/iris/util/project/noise/SierpinskiTriangleNoise.java 7d39b5c23e66f13bfd032abe2492676a1c216ebc80be54fb9f2ddceddb38b9de -source core/src/main/java/art/arcane/iris/util/project/noise/SimplexNoise.java ad148e206fdc560a3511bd2aec3fe88fb44df12673de64da5f7bc7b92bcfa3d0 -source core/src/main/java/art/arcane/iris/util/project/noise/VascularNoise.java 992f445c6143925c39bd3b4e04c7fee99fb4f60482f8aea7a90e09e3774c6b9f -source core/src/main/java/art/arcane/iris/util/project/noise/WhiteNoise.java 7768ed89609a6eca5b1a73a8c4f22d677dda8661d292ae2dac542bc3ad6383a8 -source core/src/main/java/art/arcane/iris/util/project/stream/BasicStream.java 3d248d3800b17cdc53c121aa5fa9df91cc39fbca8ffd3dcf79ae5ebd8e03c8c9 -source core/src/main/java/art/arcane/iris/util/project/stream/ProceduralStream.java 8b6df300b0919c9fe3f38518a2af2bf766e0c815b82efcab55c74ea1793d562a -source core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/AddingStream.java 77c703257669672f4fea25636e41d408e38f34127c100f6bd377b30b186e1c58 -source core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/ClampedStream.java 6b7f5666323c585776eba0c03b073eff881fbec5a23e180172d020fa9d38a41f -source core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/CoordinateBitShiftLeftStream.java 2458f0e87a16cdd78997410b28d4f108843ca536001529c7a95ae7151d7b6986 -source core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/CoordinateBitShiftRightStream.java 1ee649bea1b3712f1518b23b8e5eb6a55dbb6cda75303216f86beb80db7c2eab -source core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/DividingStream.java 166f91f51ad6849c9553492e223219848aa6c4ef65aeb50883f74b3aaf29c019 -source core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/FittedStream.java 9874586f9edcbb5374b37fc72abb88c0aeaadd910e935e9d7178cc8bb9fd9883 -source core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/MaxingStream.java a6ef3010edda88bc354a2e034f49240f8a1887ab8a16c374b32f1fd07ce6ebc1 -source core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/MinningStream.java 5470b60a521b377d0d29bbe07d327c636064dac5574ee5b9681b2ce69fb64368 -source core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/ModuloStream.java b42669c491e6cc41e3aa7a1343d3f9eac8b4237dd76d0b12125ee437eabf4c32 -source core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/MultiplyingStream.java b793f7f827f1c04d418be4afe79378270fe0160c43ad876171a78031ad99bfc0 -source core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/OffsetStream.java 099772511c7bb56af01bb7fa186f6a9760d768df5d4da737d441dab08486bc72 -source core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/RadialStream.java e660fac479b334dd68baa4311460736c5f552a48af9381b1027c4c5a389800c9 -source core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/RoundingDoubleStream.java a9f3603a5beb025bc432273e4d0591ac9add82c7381357e6a5b9722e98c181d0 -source core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/SlopeStream.java fd7db87b17556ee2c06cba7d5a76d0531d534dda14c2c03dca3452a1f7a9ab59 -source core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/SubtractingStream.java e318c3c22348a2b2b7bf2dd2891c69fc3fc87234130cf4eb4ea50d59f1a23aa8 -source core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/ZoomStream.java ae1c378826a92f34741818f723a6956869c40e5e61ae9e75bdb9045e4ace0f77 -source core/src/main/java/art/arcane/iris/util/project/stream/convert/AwareConversionStream2D.java 567e939e218e9e07fb7d015aeaa54c28652ba62da3a0381f1648a983433f22e2 -source core/src/main/java/art/arcane/iris/util/project/stream/convert/AwareConversionStream3D.java 25a9458450d4688a92d492ac20cc12aaee46aa45c1279c0b8a667cc3127e6ecd -source core/src/main/java/art/arcane/iris/util/project/stream/convert/CachedConversionStream.java b2a6ee1d0d30ec2646b9d60bd8dca61340a89fd6869db3c65af6df74021ffe88 -source core/src/main/java/art/arcane/iris/util/project/stream/convert/ConversionStream.java f7a539c7c4225bdf9ed559a5124f0ef8204fcced17fa853224aca7a58c13f7c9 -source core/src/main/java/art/arcane/iris/util/project/stream/convert/ForceDoubleStream.java 469c035c04b7d938a4254e3aa4777a5f0adaee626558fd982eeabb26fadc422e -source core/src/main/java/art/arcane/iris/util/project/stream/convert/RoundingStream.java d599a5cbfbd36f00ca75ad83975b46e0aed73885c548415203afb0e0cc8ac2bd -source core/src/main/java/art/arcane/iris/util/project/stream/convert/SelectionStream.java 9b69c39d182fbd2199370d8ead3b6e1afa00ed9eb6fa18df04377bcd919cde1a -source core/src/main/java/art/arcane/iris/util/project/stream/convert/SignificanceStream.java 8feacf800b54f9adbbdc22f757cb93cbec513bcb895069e58c915ab718314611 -source core/src/main/java/art/arcane/iris/util/project/stream/convert/To3DStream.java cb5d29788fd809c47cbb902e8d6ffca84e5f1f64276570d6623c3c4245a5862d -source core/src/main/java/art/arcane/iris/util/project/stream/interpolation/BiHermiteStream.java 6920b2150a31f4c923f109b1d82ffd8fba5df254facb63aa48cbfd3f036078ba -source core/src/main/java/art/arcane/iris/util/project/stream/interpolation/BiStarcastStream.java 2b90e49be7a06823ce8962144a2e5e4de1b275aad701019d9277a435ad31e833 -source core/src/main/java/art/arcane/iris/util/project/stream/interpolation/BicubicStream.java 4709379c135d5b9aaa828aa35674ab282137ea3ad6970ee2299e330abe2d6976 -source core/src/main/java/art/arcane/iris/util/project/stream/interpolation/BilinearStream.java d51fc68851d7fb7e67102c0c995b8c7eaa21dc2061331c7f1cc9cc6f78236414 -source core/src/main/java/art/arcane/iris/util/project/stream/interpolation/Interpolated.java aa4e0d928f4908829048103d524c64c51d39967dad785f8e5ad14badec25e5a6 -source core/src/main/java/art/arcane/iris/util/project/stream/interpolation/InterpolatingStream.java 095892a9c52b1248003e93e7ebb5924930d3ebfd60327851bd3c27c2138c2155 -source core/src/main/java/art/arcane/iris/util/project/stream/interpolation/Interpolator.java adfc343b5c172d1da145a025c179e3b4406e8f6dc6f992544dc09205ebbadb16 -source core/src/main/java/art/arcane/iris/util/project/stream/interpolation/InterpolatorFactory.java 0e3424ab3006c2ad65075e1c13785a711f02839cf0843a224fdb22da8cc8b41f -source core/src/main/java/art/arcane/iris/util/project/stream/interpolation/TriStarcastStream.java 7b23e16f3dfd9b75a1fd819f3e8df36c7d264c2261af7c27388f0b8f29617fd9 -source core/src/main/java/art/arcane/iris/util/project/stream/interpolation/TricubicStream.java 01aef030f87acabd621d253c2593d2eb232c70509619d8edd03cb65eb5da7fc5 -source core/src/main/java/art/arcane/iris/util/project/stream/interpolation/TrilinearStream.java 515a3baf44bac1cc067c9d95dc71cba05f21b59972dcf439393809c05b4037c9 -source core/src/main/java/art/arcane/iris/util/project/stream/sources/CNGStream.java e154372335404e7f986eb320ecfa603277a8a4608591dfb4452a58d986459508 -source core/src/main/java/art/arcane/iris/util/project/stream/sources/FunctionStream.java 5833555ca8f0c78c1fe2d4b989a38162dffd096d5f7a2e24d1267a08d8b82bb8 -source core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedDoubleStream2D.java e330019adce955e894bc4222c1bfb21b9b059a6017ac1b72e99f88eae066ff8e -source core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream2D.java 22291cac0c48f9dbcdea7638c0b1cd98d5b04810a017f69596c544add0666f6d -source core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream3D.java 8c57948b8b336b97917ae9efc8af036a3b0d79bf91fd85ed0f678dbdcf6a3049 -source core/src/main/java/art/arcane/iris/util/project/stream/utility/ChunkFillableDoubleStream2D.java 69ca3858df4418c1e5776d2723bb7e115022c01bbe65b1195355138e04ecf4f0 -source core/src/main/java/art/arcane/iris/util/project/stream/utility/ChunkFillableStream2D.java 5755a03f6baaf5f6062e206c5f566358610411b29cfd059db39fc08634c2876a -source core/src/main/java/art/arcane/iris/util/project/stream/utility/ContextInjectingStream.java 42ff53e2074a1db1924975c6dd2e2ac415d19722b905508c98b3e336ef282aaa -source core/src/main/java/art/arcane/iris/util/project/stream/utility/NullSafeStream.java f1e221b963db32124e996ac28e3fd97945390261084e93ff6cf7bc29f1cf73e6 -source core/src/main/java/art/arcane/iris/util/project/stream/utility/SynchronizedStream.java c3d22d944b6a8ab70f92070998ba3927629948378fdf2317632925afefcdb9c3 -source core/src/main/java/art/arcane/iris/util/simd/NoiseKernels2D.java 40a9fed6a1fedbecb3452c8877274b6ab9d1923ae47f2d38c55c164d244e6f07 -source core/src/main/java/art/arcane/iris/util/simd/ScalarNoiseKernels2D.java f6e4ac5809b5e0a80a1db9c244bc083d29f80fb04e511bb9351efa4deae9f3a5 -source core/src/main/java/art/arcane/iris/util/simd/ScalarSimdKernels.java 594239aa4ab1dc4bae6dcbfd581dea78ea546d879261ffe0a288a10e2298fb04 -source core/src/main/java/art/arcane/iris/util/simd/SimdKernels.java a02e2256e762e1a1ef4318cca3f5fb241bdda76ebf3f561ae9d961b66708c3bf -source core/src/main/java/art/arcane/iris/util/simd/SimdSupport.java 8a1af96feee77b39b38ad050b6bdf5ef86e390095113bec445027af6d32c44b8 -source core/src/main/java/art/arcane/iris/util/simd/VectorNoiseKernels2D.java 00e57c742f31e934dbd6327f97c6892320f192af515e230911010ec4e0887754 -source core/src/main/java/art/arcane/iris/util/simd/VectorSimdKernels.java 7425b052c9f72cc0eae343fd799836d75791cb9efeb908cb8905a3069d44ea4f -source lombok.config 0742cd68410f4004b443c5f07c2962aa803a6fcd5dbb8d43833a72155a07accf -source spi/src/main/java/art/arcane/iris/spi/IrisLogging.java a94ad539296aac3a194391f8062ab004bf0b5d70361d8dc3c4f282ad29cbe663 -source spi/src/main/java/art/arcane/iris/spi/IrisPlatform.java 5177479ec58e2258269db196c4273d06b46d960136d3fba6a491cc3b418312c4 -source spi/src/main/java/art/arcane/iris/spi/IrisPlatforms.java 30ae684541a4a5fa1d95e6c497aac9e8e41ae4a9adeedb735b90b72d5fa71285 -source spi/src/main/java/art/arcane/iris/spi/IrisServices.java ac57dcd5f056123c24b46b4f37b990a39e153d188b84639b598407a3490567f1 -source spi/src/main/java/art/arcane/iris/spi/LogLevel.java eaf5dcee19619518911a0c42a14943522bd3db69ccba9d247e0163dd593be523 -source spi/src/main/java/art/arcane/iris/spi/PlatformBiome.java 8b2005ee6c9121d33241733438c6f0a9ceba4c80d5b0bb199974a506407ba2c8 -source spi/src/main/java/art/arcane/iris/spi/PlatformBiomeWriter.java 172bb739a9bcb0966c8ebc355fc63fd190da7220a40cf38dad23b487777ea509 -source spi/src/main/java/art/arcane/iris/spi/PlatformBlockProperty.java 62f214b18f9ea8530b00b66226044b51a66220d3dbbf2fb2d1b898f165ebf113 -source spi/src/main/java/art/arcane/iris/spi/PlatformBlockState.java 3d5c5f030bc597645c9cbce89d931ac39ac3c40f9cba42390284eb90ea5771b9 -source spi/src/main/java/art/arcane/iris/spi/PlatformEntityType.java 12dce3025e3434a28ab58836afdc3c602cd620a923410ba70e465f791227e0bd -source spi/src/main/java/art/arcane/iris/spi/PlatformGenerationRegistry.java 340a1f75a21feb5e62fe7f9f5263d8cbec3441b79db9f2146e319892909102d5 -source spi/src/main/java/art/arcane/iris/spi/PlatformItem.java 237da8d4c02bac3fc9d4de166d1b2d58df41d220a9eb4f29d550c0fad562ffac -source spi/src/main/java/art/arcane/iris/spi/PlatformNumericRange.java bab334a3652499c6fbce52fde4c268d1852a6c1379100da2f72ae712b4560cc3 -source spi/src/main/java/art/arcane/iris/spi/PlatformRegistries.java 9f18016d144ab0582b3a89ce2f40de03971f9d6e91fbdd8652686bcf110f709e -source spi/src/main/java/art/arcane/iris/spi/PlatformScheduler.java ea0e59ba7d4088d74ba12eb6fa497b0cf61be345a4d21327a7ecd3cd7d3b0e0b -source spi/src/main/java/art/arcane/iris/spi/PlatformStructureHooks.java f4a5d70f1ff8dceb0bd425cfe2ab356597c9e19bc0e1a5e418a9b777039cf371 -source spi/src/main/java/art/arcane/iris/spi/PlatformWorld.java b388f9a364dff30a100c01ddab2ea7dfc3ada1b9dfd0f1e6351c6fc3a7d040c5 -source spi/src/main/java/art/arcane/iris/spi/protocol/IrisMessage.java 40e71fa6c7aa7e3cf6157480cc6da3d26b7f0841634f35d68b8f727b4c090280 -source spi/src/main/java/art/arcane/iris/spi/protocol/IrisMessageCodec.java ba0b520028a8e5a2c4712733afccc5f64b48991b41b060afc393ef94c5f91f54 -source spi/src/main/java/art/arcane/iris/spi/protocol/IrisProtocol.java 553e468417c93426d5b5029109604ec365a1883d986a4f7927e72780108182c3 -source spi/src/main/java/art/arcane/iris/spi/protocol/IrisWireReader.java 73ac10e79ece110d81cfb90c3affd95826fbb1a6fd361e82689798af24c4ca45 -source spi/src/main/java/art/arcane/iris/spi/protocol/IrisWireWriter.java 159b4b9b7fcfe31ff8bed288bda439afe963aab26813d62c074757705c75e4f8 -source spi/src/main/java/art/arcane/iris/spi/protocol/ProtocolException.java 8e71f2ee052c35caa923df55e00b3e92ff00f34703a736e4a48d118f8017df2d -dependency at.yawk.lz4:lz4-java c67308251e7b3743bc2a8bd351165ed1e3faa4b7957084f79b2be1ebbd476e60 -dependency com.dfsek:paralithic adaac82e69de20b99e7871436c5792b5b76eec4043008567ff0a95b2e94db872 -dependency com.github.ben-manes.caffeine:caffeine 913092bf9a378180d823f2677c6bb2d585f0968a2d83e090b787a9f051ad91b5 -dependency com.github.oshi:oshi-core ecb17b22ee14bc8726bf3ed90223e5e54031c5126d024c71ca1f93f40cd2703b -dependency com.google.code.gson:gson 43ef9252659feb811b42fa2bba20c70c53883f0917c4d5cc59879721f0cbf79f -dependency com.google.errorprone:error_prone_annotations 329255aeae8684ecc89f2941c32643a0d9e6cdf3e28862c8c99b055649ab2de5 -dependency com.google.guava:failureaccess 07feb13fb715e571deb2c3e951f10ff4eb57b87c2d556f1c34eb88e800a4a24c -dependency com.google.guava:guava 9e6e694038a4e28fb89ef58304f831ea49df920199a5ef2ca61958ca4821b343 -dependency com.google.guava:listenablefuture e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -dependency com.google.j2objc:j2objc-annotations d11db6735c998c26d64d62283b371192e223f4f93168bfaa599ed42ee1dbfcfb -dependency com.googlecode.concurrentlinkedhashmap:concurrentlinkedhashmap-lru 696dab17679d566ac8f780e4f880a0ebecbc335d746008432e18df05e93b3f96 -dependency commons-io:commons-io 540193a4d84d8b7356cda1819db4e52d7edeef353fe9bd6a594b99f9c70380cb -dependency de.crazydev22.slimjar.helper:spigot 5223dd60dc00e5a4258edb381352a3e9d76289d969f2f52d852809aabf52adff -dependency de.crazydev22.slimjar:runtime 84b58dea2d5054fc4940a5d28a1ad2039e06ac332f8fb33dabf1b1d41755e9c6 -dependency io.papermc:paperlib 1dbb89986f419aaa7ce45dab86846ebb575dd17da0c87298a339c65282a0ac64 -dependency iris-agent 60f191ebe8512f7d9e68e417c66b0148f06cf7d7f5decd072bde5f1a304bdfb6 -dependency it.unimi.dsi:fastutil 0c8bca8fad13026a6c6ebfe0d317e413f3a291dca8e6898cfb8f15fe17db38b7 -dependency jaxen:jaxen 925cd684a108929ae4ae3b6022bde74a9a50c50e71afbe2b8c0165112b9b88e5 -dependency net.bytebuddy:byte-buddy 213e0ff38c1cd2c03824ad8a9b33b20bb3faa9d5912a2629b19ff7cf4bb98da7 -dependency net.bytebuddy:byte-buddy-agent c51f4e08d8f1a89bdb275acf473e8cf43a6f85d7f3c28e25dc4aac64e2bc6904 -dependency net.java.dev.jna:jna 9dff785a156994c2b7fcc986fd8d93f8a602619043e6af4c390418359ab07f75 -dependency net.java.dev.jna:jna-platform 53258fb4209f4e320e84e0ad182d4d27117b80b7b4c1c298ef27358ee70806ae -dependency net.kyori:adventure-api b714cbc92d705ea6d236c734c88aa453bf9d9b72014a6633088f493ae22ed51f -dependency net.kyori:adventure-key b02f15bd36239cfded8a04091f5c576ba468e2b8820984c6869366bbbf4414f5 -dependency net.kyori:adventure-text-minimessage 4aaec48729d5eb33e8b13c65f33ba9507315fd9c176cae015c6aceff5a00c633 -dependency net.kyori:adventure-text-serializer-legacy a2a3a6c4c0655c8a4ff648c46b1dc9714991ed84fd1b69c2e4383a2605a86cf3 -dependency net.kyori:adventure-text-serializer-plain 8771f9bbf6d17af7fb3bcf70660bdc0544d66f46d5b4aa4b96c733c622a02a5f -dependency net.kyori:examination-api ec189e7cf15d05c74967946ac18244518c899505211673ac85bae7e25ed7e29b -dependency net.kyori:examination-string ecdf4c03d397b8e8f7c8169ecfad7c85eb5ca79d8b205f7cc4212bd7d0d552f3 -dependency org.apache.commons:commons-lang3 19f7ddf766a74256fbf93a662001924f931d0f36dd5e507398317e5ff8544b5f -dependency org.bstats:bstats-base 7ba522921bebd6fa0ccb76f8d7173c7616fd0f2d3ec1fa519b4cfa4e41ed9093 -dependency org.bstats:bstats-bukkit 2ddcd4312dbb28185da284420c7251dd7af3c9917ff4e40b74ca083051ac019e -dependency org.dom4j:dom4j 0fd042dea173096d8d3f4142d63b6e2e5c7c7f168ba54e87053fd03b91023937 -dependency org.jetbrains:annotations 3957c697cd9e6ffb3706d1d636191752167822a16ec116728ea727457d82336b -dependency org.jspecify:jspecify 94530fa28be17f84b2cdedf409975168329537c377fc72ee6746ec3d482d6b2b -dependency org.ow2.asm:asm 2598039f7bbf6acf6aa5072984f797f001b1c4a9fc5e12f528f9b405211399a9 -dependency org.projectlombok:lombok a85edb86453d6e0d4cc3f598e227fb2617a7bb38a884ef1f23a6a153e9378da5 -dependency org.slf4j:slf4j-api 5f56cfefb5a20874110d80289351e3382f45f91e988fe6068505f8e3ee684a05 -dependency org.zeroturnaround:zt-zip 3332982ad8688f417d8e7aebd50f33eaa5ba7c29f32fa974eef8c9668e36006a -dependency volmlib 5d80d75be1249e2109b9be5fc5ea06517c1a6cb2d8ad9d0131bfdb26e321cada diff --git a/core/src/main/java/art/arcane/iris/core/IrisDatapackCompiler.java b/core/src/main/java/art/arcane/iris/core/IrisDatapackCompiler.java index d0e1ece04..2301be54d 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisDatapackCompiler.java +++ b/core/src/main/java/art/arcane/iris/core/IrisDatapackCompiler.java @@ -22,6 +22,11 @@ import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KSet; +import com.google.gson.JsonObject; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -53,7 +58,7 @@ import java.util.regex.Pattern; import java.util.stream.Stream; public final class IrisDatapackCompiler { - private static final int INPUT_FINGERPRINT_SCHEMA = 3; + private static final int INPUT_FINGERPRINT_SCHEMA = 4; private static final int INPUT_BUFFER_BYTES = 64 * 1024; private static final int WORLD_PACK_SCAN_DEPTH = 8; private static final Pattern REGISTRY_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+"); @@ -177,6 +182,9 @@ public final class IrisDatapackCompiler { } } } + for (GenerationRegistryContract contract : retainedRegistryContracts(packRoots)) { + updateDigestString(digest, contract.fingerprint()); + } return HexFormat.of().formatHex(digest.digest()); } @@ -220,6 +228,14 @@ public final class IrisDatapackCompiler { data.close(); } } + for (GenerationRegistryContract contract : retainedRegistryContracts(packRoots)) { + for (PhysicalResourceKey key : contract.generatedSources().keySet()) { + putRegistryRequirement(requirements, registryPath(key) + "/" + key.resourceKey(), + fingerprintContent(GenerationRegistryContractFactory.renderGeneratedSource(contract, key, fixer))); + } + contract.biomeTags().forEach((biome, tags) -> + biomeTags.computeIfAbsent(biome, ignored -> new TreeSet<>()).addAll(tags)); + } addBiomeTagRequirements(requirements, biomeTags); return Map.copyOf(requirements); } @@ -494,11 +510,111 @@ public final class IrisDatapackCompiler { } IrisDimension.writeShared(datapackRoots, height, adjustVanillaHeight); + installRetainedRegistrySources(packRoots, datapackRoots, fixer); installLevelStemBindings(normalizedBindings, dimensions, datapackRoots); validateOutputs(datapackRoots, dimensionCount, normalizedBindings.size()); return new CompilationResult(packCount, dimensionCount, countBiomes(biomes)); } + public static void installRetainedRegistrySources(List packRoots, Collection datapackRoots, + IDataFixer fixer) throws IOException { + Map sources = new TreeMap<>(); + Map> tags = new TreeMap<>(); + for (GenerationRegistryContract contract : retainedRegistryContracts(packRoots)) { + for (PhysicalResourceKey key : contract.generatedSources().keySet()) { + String source = GenerationRegistryContractFactory.renderGeneratedSource(contract, key, fixer); + String previous = sources.putIfAbsent(key, source); + if (previous != null && !JsonParser.parseString(previous).equals(JsonParser.parseString(source))) { + throw new IOException("Conflicting retained registry source for " + key); + } + } + contract.biomeTags().forEach((biome, memberships) -> { + for (String tag : memberships) { + tags.computeIfAbsent(tag, ignored -> new TreeSet<>()).add(biome); + } + }); + } + for (File root : datapackRoots) { + for (Map.Entry entry : sources.entrySet()) { + Path output = registryOutput(root.toPath(), registryPath(entry.getKey()), entry.getKey().resourceKey()); + if (Files.exists(output)) { + String previous = Files.readString(output, StandardCharsets.UTF_8); + if (!JsonParser.parseString(previous).equals(JsonParser.parseString(entry.getValue()))) { + throw new IOException("Conflicting retained registry output at " + output); + } + continue; + } + Files.createDirectories(output.getParent()); + Files.writeString(output, entry.getValue(), StandardCharsets.UTF_8); + } + for (Map.Entry> entry : tags.entrySet()) { + writeRetainedBiomeTag(registryOutput(root.toPath(), "tags/worldgen/biome", entry.getKey()), entry.getValue()); + } + } + } + + private static void writeRetainedBiomeTag(Path output, Set biomes) throws IOException { + Set merged = new TreeSet<>(biomes); + if (Files.exists(output)) { + JsonObject existing = JsonParser.parseString(Files.readString(output, StandardCharsets.UTF_8)).getAsJsonObject(); + for (JsonElement value : existing.getAsJsonArray("values")) { + merged.add(value.getAsString()); + } + } + JsonArray values = new JsonArray(); + merged.forEach(values::add); + JsonObject tag = new JsonObject(); + tag.addProperty("replace", false); + tag.add("values", values); + Files.createDirectories(output.getParent()); + Files.writeString(output, tag.toString(), StandardCharsets.UTF_8); + } + + private static Path registryOutput(Path root, String registryPath, String resourceKey) throws IOException { + int separator = resourceKey.indexOf(':'); + Path directory = root.toAbsolutePath().normalize().resolve("data") + .resolve(resourceKey.substring(0, separator)).resolve(registryPath); + Path output = directory.resolve(resourceKey.substring(separator + 1) + ".json").normalize(); + if (!output.startsWith(directory)) { + throw new IOException("Unsafe retained registry resource key " + resourceKey); + } + return output; + } + + private static String registryPath(PhysicalResourceKey key) { + return key.registryKey().substring(key.registryKey().indexOf(':') + 1); + } + + private static List retainedRegistryContracts(List packRoots) throws IOException { + Map contracts = new TreeMap<>(); + Set loadedWorlds = new LinkedHashSet<>(); + for (File pack : packRoots) { + Path dimensionRoot = historyDimensionRoot(pack.toPath()); + if (dimensionRoot == null || !loadedWorlds.add(dimensionRoot)) { + continue; + } + for (GenerationEpoch epoch : GenerationHistory.open(dimensionRoot).manifest().epochs()) { + contracts.putIfAbsent(epoch.registryContract().fingerprint(), epoch.registryContract()); + } + } + return List.copyOf(contracts.values()); + } + + private static Path historyDimensionRoot(Path packRoot) { + Path pack = packRoot.toAbsolutePath().normalize(); + Path epoch = pack.getParent(); + Path epochs = epoch == null ? null : epoch.getParent(); + Path generation = epochs == null ? null : epochs.getParent(); + Path iris = generation == null ? null : generation.getParent(); + if (iris == null || iris.getParent() == null || !"pack".equals(pack.getFileName().toString()) + || !"epochs".equals(epochs.getFileName().toString()) + || !"generation".equals(generation.getFileName().toString()) + || !"iris".equals(iris.getFileName().toString())) { + return null; + } + return iris.getParent(); + } + private static void installLevelStemBindings( List bindings, Map> dimensions, @@ -648,11 +764,9 @@ public final class IrisDatapackCompiler { && (Files.exists(generationRoot, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(generationRoot))) { GenerationHistory history = GenerationHistory.open(worldRoot); - for (GenerationEpoch epoch : history.manifest().epochs()) { - Path retainedPack = history.paths().packRoot(epoch.epochId()); - if (hasDimensions(retainedPack)) { - candidates.add(retainedPack); - } + candidates.add(history.activePackRoot()); + if (history.pendingActivation().isPresent()) { + candidates.add(history.packRoot(history.pendingActivation().orElseThrow().activationId())); } return true; } diff --git a/core/src/main/java/art/arcane/iris/core/IrisStartupValidation.java b/core/src/main/java/art/arcane/iris/core/IrisStartupValidation.java index 17c5222ce..f17f04592 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisStartupValidation.java +++ b/core/src/main/java/art/arcane/iris/core/IrisStartupValidation.java @@ -10,25 +10,50 @@ public final class IrisStartupValidation { } public static synchronized void begin() { - snapshot = new Snapshot(true, ValidationState.PENDING, ValidationState.PENDING, List.of(), List.of()); + snapshot = new Snapshot(true, ValidationState.PENDING, ValidationState.PENDING, List.of(), List.of(), ValidationState.DISABLED, ""); } public static synchronized void disable() { snapshot = Snapshot.disabled(); } + public static synchronized void beginRuntimeValidation() { + if (!snapshot.enforced()) { + return; + } + snapshot = new Snapshot(true, snapshot.datapacks(), snapshot.packs(), + snapshot.datapackFailures(), snapshot.packFailures(), ValidationState.PENDING, ""); + } + + public static synchronized void markRuntimeReady() { + if (!snapshot.enforced()) { + return; + } + snapshot = new Snapshot(true, snapshot.datapacks(), snapshot.packs(), + snapshot.datapackFailures(), snapshot.packFailures(), ValidationState.READY, ""); + } + + public static synchronized void markRuntimeInvalid(String failure) { + if (!snapshot.enforced()) { + return; + } + snapshot = new Snapshot(true, snapshot.datapacks(), snapshot.packs(), + snapshot.datapackFailures(), snapshot.packFailures(), ValidationState.INVALID, + normalizeFailure(failure, "Iris runtime injection failed. Resolve the startup errors and restart the server.")); + } + public static synchronized void beginDatapackValidation() { if (!snapshot.enforced() || snapshot.datapacks() == ValidationState.RESTART_REQUIRED) { return; } - snapshot = new Snapshot(true, ValidationState.PENDING, snapshot.packs(), List.of(), snapshot.packFailures()); + snapshot = new Snapshot(true, ValidationState.PENDING, snapshot.packs(), List.of(), snapshot.packFailures(), snapshot.runtime(), snapshot.runtimeFailure()); } public static synchronized void markDatapacksReady() { if (!snapshot.enforced() || snapshot.datapacks() == ValidationState.RESTART_REQUIRED) { return; } - snapshot = new Snapshot(true, ValidationState.READY, snapshot.packs(), List.of(), snapshot.packFailures()); + snapshot = new Snapshot(true, ValidationState.READY, snapshot.packs(), List.of(), snapshot.packFailures(), snapshot.runtime(), snapshot.runtimeFailure()); } public static synchronized void markDatapacksInvalid(String failure) { @@ -40,7 +65,7 @@ public final class IrisStartupValidation { ValidationState.INVALID, snapshot.packs(), List.of(normalizeFailure(failure, "External datapack validation failed.")), - snapshot.packFailures()); + snapshot.packFailures(), snapshot.runtime(), snapshot.runtimeFailure()); } public static synchronized void requireRestart(String reason) { @@ -52,14 +77,14 @@ public final class IrisStartupValidation { ValidationState.RESTART_REQUIRED, snapshot.packs(), List.of(normalizeFailure(reason, "A restart is required to load validated datapacks.")), - snapshot.packFailures()); + snapshot.packFailures(), snapshot.runtime(), snapshot.runtimeFailure()); } public static synchronized void markPacksReady() { if (!snapshot.enforced()) { return; } - snapshot = new Snapshot(true, snapshot.datapacks(), ValidationState.READY, snapshot.datapackFailures(), List.of()); + snapshot = new Snapshot(true, snapshot.datapacks(), ValidationState.READY, snapshot.datapackFailures(), List.of(), snapshot.runtime(), snapshot.runtimeFailure()); } public static synchronized void markPacksInvalid(List failures) { @@ -72,7 +97,7 @@ public final class IrisStartupValidation { .map(failure -> normalizeFailure(failure, "Iris dimension-pack validation failed.")) .toList(); snapshot = new Snapshot(true, snapshot.datapacks(), ValidationState.INVALID, - snapshot.datapackFailures(), normalized); + snapshot.datapackFailures(), normalized, snapshot.runtime(), snapshot.runtimeFailure()); } public static boolean isReady() { @@ -92,6 +117,7 @@ public final class IrisStartupValidation { Snapshot current = snapshot; if (force && current.enforced() + && isRuntimeReady(current) && current.datapacks() == ValidationState.RESTART_REQUIRED && current.packs() == ValidationState.READY) { return Optional.empty(); @@ -103,6 +129,11 @@ public final class IrisStartupValidation { if (!current.enforced() || isReady(current)) { return Optional.empty(); } + if (!isRuntimeReady(current)) { + return Optional.of(current.runtime() == ValidationState.INVALID + ? current.runtimeFailure() + : "Iris is still validating runtime injection."); + } if (current.datapacks() == ValidationState.RESTART_REQUIRED) { return Optional.of(firstFailure(current.datapackFailures(), "Iris installed validated datapacks and requires a restart before the server is safe.")); @@ -131,6 +162,7 @@ public final class IrisStartupValidation { public static void requireWorldReplacementStagingReady() { Snapshot current = snapshot; if (current.enforced() + && isRuntimeReady(current) && current.datapacks() == ValidationState.RESTART_REQUIRED && current.packs() == ValidationState.READY) { return; @@ -150,9 +182,14 @@ public final class IrisStartupValidation { return failures == null || failures.isEmpty() ? fallback : failures.getFirst(); } + private static boolean isRuntimeReady(Snapshot current) { + return current.runtime() == ValidationState.DISABLED || current.runtime() == ValidationState.READY; + } + private static boolean isReady(Snapshot current) { return !current.enforced() - || current.datapacks() == ValidationState.READY + || isRuntimeReady(current) + && current.datapacks() == ValidationState.READY && current.packs() == ValidationState.READY; } @@ -169,7 +206,9 @@ public final class IrisStartupValidation { ValidationState datapacks, ValidationState packs, List datapackFailures, - List packFailures + List packFailures, + ValidationState runtime, + String runtimeFailure ) { Snapshot { datapackFailures = List.copyOf(datapackFailures); @@ -177,7 +216,7 @@ public final class IrisStartupValidation { } private static Snapshot disabled() { - return new Snapshot(false, ValidationState.DISABLED, ValidationState.DISABLED, List.of(), List.of()); + return new Snapshot(false, ValidationState.DISABLED, ValidationState.DISABLED, List.of(), List.of(), ValidationState.DISABLED, ""); } } } diff --git a/core/src/main/java/art/arcane/iris/core/gui/ImageMapStudioGUI.java b/core/src/main/java/art/arcane/iris/core/gui/ImageMapStudioGUI.java index 48c3d40f8..74728cf9d 100644 --- a/core/src/main/java/art/arcane/iris/core/gui/ImageMapStudioGUI.java +++ b/core/src/main/java/art/arcane/iris/core/gui/ImageMapStudioGUI.java @@ -143,7 +143,7 @@ public final class ImageMapStudioGUI { private ImageMapStudioGUI(Engine engine) { this.engine = Objects.requireNonNull(engine, "Image Map Studio engine"); - this.packFolder = engine.getData().getDataFolder(); + this.packFolder = engine.getPackSource().toFile(); this.dimensionKey = engine.getDimension().getLoadKey(); this.boundary = engine.getDimension().getWorldBoundary() == null ? null @@ -163,7 +163,6 @@ public final class ImageMapStudioGUI { } static void reloadActiveEngine(Engine engine) { - engine.getData().getImageMapLoader().clearCache(); engine.hotloadSilently(); } @@ -431,9 +430,14 @@ public final class ImageMapStudioGUI { } private void refreshPresets() { - String[] keys = engine.getData().getImageMapLoader().getPossibleKeys(); - Arrays.sort(keys); - presetPicker.setModel(new DefaultComboBoxModel<>(keys)); + IrisData data = IrisData.openRuntime(packFolder); + try { + String[] keys = data.getImageMapLoader().getPossibleKeys(); + Arrays.sort(keys); + presetPicker.setModel(new DefaultComboBoxModel<>(keys)); + } finally { + data.close(); + } } private void loadPreset(String key) { diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitPublicBackend.java b/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitPublicBackend.java index 291842f50..90b67b930 100644 --- a/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitPublicBackend.java +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitPublicBackend.java @@ -32,9 +32,8 @@ final class BukkitPublicBackend implements WorldLifecycleBackend { WorldLifecycleStaging.stageStemGenerator(request.worldName(), request.generator()); } - INMS.get().ensureServerLevelInjection(); - try { + INMS.get().ensureServerLevelInjection(); World world = creator.createWorld(); return CompletableFuture.completedFuture(world); } catch (Throwable e) { diff --git a/core/src/main/java/art/arcane/iris/core/loader/IrisData.java b/core/src/main/java/art/arcane/iris/core/loader/IrisData.java index 7932c5654..302951712 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/IrisData.java +++ b/core/src/main/java/art/arcane/iris/core/loader/IrisData.java @@ -189,6 +189,26 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory { return Optional.ofNullable(dataLoaders.get(dataFolder)); } + public static void invalidateLoadedAuthoringResources(File source) { + Path requested = dataFolderIdentity(Objects.requireNonNull(source, "authoring pack source")); + for (Map.Entry entry : dataLoaders.entrySet()) { + if (dataFolderIdentity(entry.getKey()).equals(requested)) { + entry.getValue().invalidateAuthoringResources(); + } + } + } + + private synchronized void invalidateAuthoringResources() { + if (generationRegistryContract != null) { + throw new IllegalStateException("Cannot invalidate an immutable generation pack as an authoring source."); + } + dump(); + clearLists(); + generationPackFingerprint = null; + contentGate = null; + dimensionCustomBiomeResourceKeys.clear(); + } + public String customBiomeResourceKey(IrisDimension dimension, IrisBiomeCustom customBiome) { IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension"); IrisBiomeCustom requiredBiome = Objects.requireNonNull(customBiome, "customBiome"); diff --git a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java index 623156ced..12e88f147 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java +++ b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java @@ -18,6 +18,8 @@ package art.arcane.iris.core.nms; +import art.arcane.iris.engine.history.SavedTerrainChunk; +import java.util.concurrent.CompletableFuture; import art.arcane.iris.core.datapack.DatapackStructureScopeIndex; import art.arcane.iris.engine.object.IrisImportedStructureControl; import art.arcane.iris.core.lifecycle.WorldLifecycleCaller; @@ -57,9 +59,19 @@ import org.bukkit.inventory.ItemStack; import java.awt.Color; import java.util.List; import java.util.Set; -import java.util.concurrent.CompletableFuture; public interface INMSBinding { + default CompletableFuture flushSavedTerrainCapture(World world) { + return CompletableFuture.failedFuture(new UnsupportedOperationException( + "Native saved terrain checkpoints are unavailable.")); + } + + default CompletableFuture captureSavedTerrainChunk( + World world, int chunkX, int chunkZ, int minimumY, int height) { + return CompletableFuture.failedFuture(new UnsupportedOperationException( + "Native saved terrain capture is unavailable.")); + } + default PlatformGenerationRegistry generationRegistry() { throw new UnsupportedOperationException( "The active NMS binding does not expose generation registry definitions." diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackStructurePlacementValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackStructurePlacementValidator.java index 4d95b5e4a..871012bcf 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackStructurePlacementValidator.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackStructurePlacementValidator.java @@ -110,6 +110,11 @@ final class PackStructurePlacementValidator { continue; } if (hasNativeStructures) { + Object anchor = placement.opt("anchor"); + if (anchor != null && anchor != JSONObject.NULL && !"LEGACY".equals(anchor)) { + blockingErrors.add(placementPath + + ".anchor must be omitted, null, or LEGACY for nativeStructures."); + } validateNativeStructures( placementPath, nativeStructures, registries.structures(), registries.jigsaws(), diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java b/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java index f1b63d029..18cec301c 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java @@ -211,7 +211,6 @@ public class IrisPregenerator { historyLastCached.set(cached.get()); historyLastMillis.set(startedAt); ticker.start(); - checkRegions(); int[] regionBounds = task.regionBounds(); generator.onRegionBounds(regionBounds[0], regionBounds[1], regionBounds[2], regionBounds[3]); generator.onPregenStart(task.getCenter().getX(), task.getCenter().getZ()); @@ -280,10 +279,6 @@ public class IrisPregenerator { + "] chunks/s"; } - private void checkRegions() { - task.iterateRegions(this::checkRegion); - } - private void init() { generator.init(); } @@ -413,9 +408,6 @@ public class IrisPregenerator { } generatedRegions.add(pos); - // No checkRegions() here: re-spiraling every region after every completed region - // was O(regions^2) dead work on the submitter thread (result discarded, and the - // only side effect was CachedPregenMethod faulting plates against eviction). } } @@ -437,14 +429,6 @@ public class IrisPregenerator { MantleHeapPressure.requestPanicReclaim(); } - private void checkRegion(int x, int z) { - if (generatedRegions.contains(new Position2(x, z))) { - return; - } - - generator.supportsRegions(x, z, listener); - } - public long getFailedChunks() { return failed.get(); } diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenPerformanceProfile.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenPerformanceProfile.java index 15d2dfe86..8c38746db 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenPerformanceProfile.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenPerformanceProfile.java @@ -21,22 +21,15 @@ package art.arcane.iris.core.pregenerator; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.framework.MeteredCache; -import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.util.common.parallel.MultiBurst; import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D; -import art.arcane.iris.util.project.stream.utility.CachedStream2D; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.Objects; public final class PregenPerformanceProfile { - private static final long HOTLOAD_ACQUISITION_TIMEOUT_SECONDS = 30L; - private static final AtomicBoolean JVM_HINT_LOGGED = new AtomicBoolean(false); - private PregenPerformanceProfile() { } @@ -44,7 +37,6 @@ public final class PregenPerformanceProfile { IrisSettings.IrisSettingsPerformance performance = IrisSettings.get().getPerformance(); int previousNoiseCacheSize = performance.getNoiseCacheSize(); int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, 4_096); - boolean fastCacheEnabledBefore = Boolean.getBoolean("iris.cache.fast"); boolean changed = false; if (targetNoiseCacheSize != previousNoiseCacheSize) { @@ -52,73 +44,41 @@ public final class PregenPerformanceProfile { changed = true; } - if (!fastCacheEnabledBefore) { - System.setProperty("iris.cache.fast", "true"); - changed = true; - } if (MultiBurst.burst.raiseParallelism(pregenBurstParallelism(Runtime.getRuntime().availableProcessors()))) { changed = true; } - if (JVM_HINT_LOGGED.compareAndSet(false, true) && !fastCacheEnabledBefore) { - IrisLogging.info("For startup-wide cache-fast coverage, set JVM argument: -Diris.cache.fast=true"); - } - return changed; } public static void apply(Engine engine) { apply(); - if (!prepareNoiseCacheInPlace(engine)) { - engine.hotloadComplex(); - logApplied(); + if (engine != null) { + prepareNoiseCacheInPlace(engine); + IrisLogging.info("Pregen profile applied: noiseCacheSize=" + + IrisSettings.get().getPerformance().getNoiseCacheSize()); } } public static void applyToGenerator(PlatformChunkGenerator generator) { - apply(); - Engine engine = generator == null ? null : generator.getEngine(); - if (!prepareNoiseCacheInPlace(engine)) { - generator.hotloadComplexAsync(HOTLOAD_ACQUISITION_TIMEOUT_SECONDS, TimeUnit.SECONDS).join(); - logApplied(); - } + apply(generator == null ? null : generator.getEngine()); } static int pregenBurstParallelism(int availableProcessors) { return Math.max(4, availableProcessors * 2); } - private static boolean prepareNoiseCacheInPlace(Engine engine) { - if (engine == null) { - return true; - } - IrisComplex complex = engine.getComplex(); - if (complex == null) { - return false; - } - ProceduralStream caveBiomeStream = complex.getCaveBiomeStream(); - if (!(caveBiomeStream instanceof CachedStream2D cachedStream) || !cachedStream.usesFastCache()) { - return false; - } + private static void prepareNoiseCacheInPlace(Engine engine) { + IrisComplex complex = Objects.requireNonNull(engine.getComplex(), "Pregeneration requires an active biome complex"); int configuredChunks = IrisSettings.get().getPerformance().getNoiseCacheSize(); - if (!resizeNoiseCache(complex.getNaturalHeightStream(), configuredChunks)) { - return false; - } - return resizeNoiseCache(complex.getHeightStream(), configuredChunks); + resizeNoiseCache(complex.getNaturalHeightStream(), configuredChunks); + resizeNoiseCache(complex.getRawHeightStream(), configuredChunks); } - private static boolean resizeNoiseCache(ProceduralStream stream, int configuredChunks) { - if (stream instanceof CachedDoubleStream2D cachedStream) { - cachedStream.setMaximumChunks(configuredChunks); - return true; + private static void resizeNoiseCache(ProceduralStream stream, int configuredChunks) { + if (!(stream instanceof CachedDoubleStream2D cachedStream)) { + throw new IllegalStateException("Pregeneration requires a mutable double terrain cache"); } - if (!(stream instanceof MeteredCache cache)) { - return false; - } - return cache.getMaxSize() == (long) configuredChunks * 256L; - } - - private static void logApplied() { - IrisLogging.info("Pregen profile applied: noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() + " iris.cache.fast=" + Boolean.getBoolean("iris.cache.fast")); + cachedStream.setMaximumChunks(configuredChunks); } } diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenSavedChunkStatus.java b/core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenSavedChunkStatus.java new file mode 100644 index 000000000..ccd8f66c1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenSavedChunkStatus.java @@ -0,0 +1,72 @@ +package art.arcane.iris.core.pregenerator.cache; + +import art.arcane.iris.engine.history.SavedTerrainChunk; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.BitSet; +import java.util.LinkedHashMap; +import java.util.Objects; + +public final class PregenSavedChunkStatus { + private static final int MAXIMUM_REGIONS = 256; + + private final StatusReader reader; + private final LinkedHashMap regions = new LinkedHashMap<>(16, 0.75f, true); + + public PregenSavedChunkStatus(StatusReader reader) { + this.reader = Objects.requireNonNull(reader); + } + + public static PregenSavedChunkStatus fromWorld(Path dimensionRoot) { + Path root = Objects.requireNonNull(dimensionRoot).toAbsolutePath().normalize(); + return new PregenSavedChunkStatus((x, z) -> readFull(root, x, z)); + } + + public synchronized boolean isFull(int chunkX, int chunkZ) { + long key = ((long) (chunkX >> 5) << 32) | ((chunkZ >> 5) & 0xffffffffL); + RegionStatus status = regions.get(key); + if (status == null) { + status = new RegionStatus(new BitSet(1024), new BitSet(1024)); + regions.put(key, status); + if (regions.size() > MAXIMUM_REGIONS) { + regions.remove(regions.keySet().iterator().next()); + } + } + int index = ((chunkZ & 31) << 5) | (chunkX & 31); + if (!status.checked().get(index)) { + try { + status.full().set(index, reader.isFull(chunkX, chunkZ)); + status.checked().set(index); + } catch (IOException failure) { + throw new UncheckedIOException("Cannot verify saved pregeneration chunk " + chunkX + "," + chunkZ, failure); + } + } + return status.full().get(index); + } + + private static boolean readFull(Path root, int chunkX, int chunkZ) throws IOException { + Path region = root.resolve("region/r." + (chunkX >> 5) + "." + (chunkZ >> 5) + ".mca"); + if (Files.notExists(region)) { + return false; + } + try (RandomAccessFile input = new RandomAccessFile(region.toFile(), "r")) { + input.seek((long) (((chunkZ & 31) << 5) | (chunkX & 31)) * Integer.BYTES); + if (input.readInt() == 0) { + return false; + } + } + return SavedTerrainChunk.isComplete(SavedTerrainChunk.readStatus(root, chunkX, chunkZ)); + } + + @FunctionalInterface + public interface StatusReader { + boolean isFull(int chunkX, int chunkZ) throws IOException; + } + + private record RegionStatus(BitSet checked, BitSet full) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/CachedPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/CachedPregenMethod.java index c5073d9c8..a68e73daf 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/CachedPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/CachedPregenMethod.java @@ -4,19 +4,22 @@ import art.arcane.iris.core.pregenerator.PregenListener; import art.arcane.iris.core.pregenerator.PregenTask; import art.arcane.iris.core.pregenerator.PregeneratorMethod; import art.arcane.iris.core.pregenerator.cache.PregenCache; +import art.arcane.iris.core.pregenerator.cache.PregenSavedChunkStatus; import art.arcane.volmlib.util.mantle.runtime.Mantle; public class CachedPregenMethod implements PregeneratorMethod { private final PregeneratorMethod method; private final PregenCache cache; private final PregenTask task; + private final PregenSavedChunkStatus savedStatus; private volatile PregenListener wrappedSource; private volatile PregenListener wrappedListener; - public CachedPregenMethod(PregeneratorMethod method, PregenCache cache, PregenTask task) { - this.method = method; - this.cache = cache.sync(); - this.task = task; + public CachedPregenMethod(Configuration configuration) { + this.method = configuration.method(); + this.cache = configuration.cache().sync(); + this.task = configuration.task(); + this.savedStatus = configuration.savedStatus(); } @Override @@ -26,7 +29,6 @@ public class CachedPregenMethod implements PregeneratorMethod { @Override public void close() { - cache.write(); method.close(); cache.write(); } @@ -39,12 +41,12 @@ public class CachedPregenMethod implements PregeneratorMethod { @Override public boolean supportsRegions(int x, int z, PregenListener listener) { - return cache.isRegionCached(x, z) || method.supportsRegions(x, z, listener); + return isSavedRegionCached(x, z) || method.supportsRegions(x, z, listener); } @Override public String getMethod(int x, int z) { - if (cache.isRegionCached(x, z)) { + if (isSavedRegionCached(x, z)) { return "Cached"; } return method.getMethod(x, z); @@ -52,7 +54,7 @@ public class CachedPregenMethod implements PregeneratorMethod { @Override public void generateRegion(int x, int z, PregenListener listener) { - if (cache.isRegionCached(x, z)) { + if (isSavedRegionCached(x, z)) { listener.onRegionGenerated(x, z); task.iterateChunks(x, z, (cX, cZ) -> { listener.onChunkGenerated(cX, cZ, true); @@ -60,13 +62,16 @@ public class CachedPregenMethod implements PregeneratorMethod { }); return; } - method.generateRegion(x, z, listener); - cache.cacheRegion(x, z); + if (!method.supportsRegions(x, z, listener)) { + task.iterateChunks(x, z, (chunkX, chunkZ) -> generateChunk(chunkX, chunkZ, listener)); + return; + } + method.generateRegion(x, z, cachingListener(listener)); } @Override public void generateChunk(int x, int z, PregenListener listener) { - if (cache.isChunkCached(x, z)) { + if (cache.isChunkCached(x, z) && savedStatus.isFull(x, z)) { listener.onChunkGenerated(x, z, true); listener.onChunkCleaned(x, z); return; @@ -74,6 +79,20 @@ public class CachedPregenMethod implements PregeneratorMethod { method.generateChunk(x, z, cachingListener(listener)); } + private boolean isSavedRegionCached(int x, int z) { + if (!cache.isRegionCached(x, z)) { + return false; + } + for (int localX = 0; localX < 32; localX++) { + for (int localZ = 0; localZ < 32; localZ++) { + if (!savedStatus.isFull((x << 5) + localX, (z << 5) + localZ)) { + return false; + } + } + } + return true; + } + private PregenListener cachingListener(PregenListener listener) { PregenListener source = wrappedSource; PregenListener wrapped = wrappedListener; @@ -185,6 +204,10 @@ public class CachedPregenMethod implements PregeneratorMethod { method.onRegionSubmitted(regionX, regionZ); } + public record Configuration(PregeneratorMethod method, PregenCache cache, PregenTask task, + PregenSavedChunkStatus savedStatus) { + } + @Override public Mantle getMantle() { return method.getMantle(); diff --git a/core/src/main/java/art/arcane/iris/core/safeguard/IrisSafeguard.java b/core/src/main/java/art/arcane/iris/core/safeguard/IrisSafeguard.java index 92232b937..326fd929d 100644 --- a/core/src/main/java/art/arcane/iris/core/safeguard/IrisSafeguard.java +++ b/core/src/main/java/art/arcane/iris/core/safeguard/IrisSafeguard.java @@ -1,5 +1,6 @@ package art.arcane.iris.core.safeguard; +import art.arcane.iris.core.IrisStartupValidation; import art.arcane.iris.core.safeguard.task.Diagnostic; import art.arcane.iris.core.safeguard.task.Task; import art.arcane.iris.core.safeguard.task.Tasks; @@ -24,6 +25,7 @@ public final class IrisSafeguard { } public static void execute() { + IrisStartupValidation.beginRuntimeValidation(); List tasks = Tasks.getTasks(); LinkedHashMap> resultValues = new LinkedHashMap<>(tasks.size()); LinkedHashMap contextValues = new LinkedHashMap<>(tasks.size()); @@ -36,9 +38,13 @@ public final class IrisSafeguard { try { result = task.run(); } catch (Throwable e) { + boolean injectionFailure = "injection".equals(task.getId()); + if (injectionFailure) { + IrisStartupValidation.markRuntimeInvalid("Iris runtime injection failed. Resolve the startup errors and restart the server."); + } IrisLogging.reportError(e); result = new ValueWithDiagnostics<>( - Mode.WARNING, + injectionFailure ? Mode.UNSTABLE : Mode.WARNING, new Diagnostic(Diagnostic.Logger.ERROR, "Error while running task " + task.getId(), e) ); } diff --git a/core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java b/core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java index 1e45cb74a..cf0dc153f 100644 --- a/core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java +++ b/core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java @@ -1,6 +1,7 @@ package art.arcane.iris.core.safeguard.task; import art.arcane.iris.BuildConstants; +import art.arcane.iris.core.IrisStartupValidation; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.core.IrisWorlds; import art.arcane.iris.core.IrisWorldStorage; @@ -119,18 +120,22 @@ public final class Tasks { private static final Task INJECTION = Task.of("injection", () -> { if (!Agent.install()) { + IrisStartupValidation.markRuntimeInvalid("Iris Java agent is unavailable. Add -javaagent:" + + Agent.AGENT_JAR.getPath() + " to the JVM arguments before -jar and restart the server."); return withDiagnostics(Mode.UNSTABLE, Diagnostic.Logger.ERROR.create("Java Agent"), - Diagnostic.Logger.ERROR.create("- Please enable dynamic agent loading by adding -XX:+EnableDynamicAgentLoading to your jvm arguments."), - Diagnostic.Logger.ERROR.create("- or add the jvm argument -javaagent:" + Agent.AGENT_JAR.getPath())); + Diagnostic.Logger.ERROR.create("- Add -javaagent:" + Agent.AGENT_JAR.getPath() + " before -jar in your startup command and restart the server."), + Diagnostic.Logger.ERROR.create("- Dynamic attachment requires -XX:+EnableDynamicAgentLoading and a host that permits JVM attachment.")); } if (!INMS.get().injectBukkit()) { + IrisStartupValidation.markRuntimeInvalid("Iris runtime injection failed. Resolve the startup errors and restart the server."); return withDiagnostics(Mode.UNSTABLE, Diagnostic.Logger.ERROR.create("Code Injection"), Diagnostic.Logger.ERROR.create("- Failed to inject code. Please contact support")); } + IrisStartupValidation.markRuntimeReady(); return withDiagnostics(Mode.STABLE); }); diff --git a/core/src/main/java/art/arcane/iris/core/service/ObjectStudioSaveService.java b/core/src/main/java/art/arcane/iris/core/service/ObjectStudioSaveService.java index e51dc1e32..f91200350 100644 --- a/core/src/main/java/art/arcane/iris/core/service/ObjectStudioSaveService.java +++ b/core/src/main/java/art/arcane/iris/core/service/ObjectStudioSaveService.java @@ -91,7 +91,7 @@ public class ObjectStudioSaveService implements IrisService { Map objectsDirs = new ConcurrentHashMap<>(); for (Map.Entry e : sources.entrySet()) { - File dir = resolveObjectsDir(e.getValue()); + File dir = resolveObjectsDir(e.getValue(), engine); if (dir != null) { objectsDirs.put(e.getKey(), dir); } @@ -200,9 +200,13 @@ public class ObjectStudioSaveService implements IrisService { return false; } - private static File resolveObjectsDir(IrisData data) { + static File resolveObjectsDir(IrisData data, Engine engine) { File root = data.getDataFolder(); if (root == null) return null; + if (root.toPath().toAbsolutePath().normalize().equals( + engine.getData().getDataFolder().toPath().toAbsolutePath().normalize())) { + root = engine.getPackSource().toFile(); + } File objects = new File(root, "objects"); if (!objects.exists()) { objects.mkdirs(); diff --git a/core/src/main/java/art/arcane/iris/core/structure/StructureIndexService.java b/core/src/main/java/art/arcane/iris/core/structure/StructureIndexService.java index b0a1715cf..6c1ab1b94 100644 --- a/core/src/main/java/art/arcane/iris/core/structure/StructureIndexService.java +++ b/core/src/main/java/art/arcane/iris/core/structure/StructureIndexService.java @@ -21,8 +21,6 @@ package art.arcane.iris.core.structure; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.loader.ResourceLoader; -import art.arcane.iris.engine.object.IrisStructure; import com.google.gson.GsonBuilder; import java.io.File; @@ -38,8 +36,6 @@ import java.util.concurrent.ConcurrentHashMap; public final class StructureIndexService { private static final String INDEX_PATH = ".iris/structure-index.json"; - private static final String LEGACY_INDEX_PATH = "structures/structure-index.json"; - private static final String LEGACY_INDEX_KEY = "structure-index"; private static final Set GENERATED = ConcurrentHashMap.newKeySet(); private StructureIndexService() { @@ -66,7 +62,6 @@ public final class StructureIndexService { } public static File write(IrisData data) { - removeLegacyIndex(data); List structures = IrisPlatforms.get().structureHooks().structureKeys(); List sets = IrisPlatforms.get().structureHooks().structureSetKeys(); @@ -93,7 +88,6 @@ public final class StructureIndexService { List iris = new ArrayList<>(); if (data.getStructureLoader() != null) { Collections.addAll(iris, data.getStructureLoader().getPossibleKeys()); - iris.removeIf(LEGACY_INDEX_KEY::equals); } Collections.sort(vanilla); @@ -138,22 +132,4 @@ public final class StructureIndexService { return new File(dataFolder, INDEX_PATH); } - static File legacyIndexFile(File dataFolder) { - return new File(dataFolder, LEGACY_INDEX_PATH); - } - - static void removeLegacyIndex(IrisData data) { - File legacyFile = legacyIndexFile(data.getDataFolder()); - try { - Files.deleteIfExists(legacyFile.toPath()); - ResourceLoader structureLoader = data.getStructureLoader(); - if (structureLoader != null) { - structureLoader.getLoadCache().invalidate(LEGACY_INDEX_KEY); - structureLoader.clearList(); - } - } catch (Throwable e) { - IrisLogging.reportError("Failed to remove legacy structure index '" - + legacyFile.getAbsolutePath() + "'", e); - } - } } diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java index d6ee9fdb4..56dae386b 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java @@ -118,9 +118,9 @@ public class IrisCreator { */ private String name = "irisworld"; /** - * Studio mode makes the engine hotloadable and uses the dimension in - * your Iris/packs folder instead of copying the dimension files into - * the world itself. Studio worlds are deleted when they are unloaded. + * Studio mode snapshots edits from your Iris/packs folder into generation history. + * Existing chunks retain their generation while new chunks use accepted edits. + * Studio worlds are deleted when they are unloaded. */ private boolean studio = false; /** @@ -260,18 +260,15 @@ public class IrisCreator { + "Restart the server, then run the command again."); } - IrisDimension installedDimension = resolvedDimension; - if (!studio() || benchmark) { - reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack"); - StudioSVC studioService = IrisServices.get(StudioSVC.class); - installedDimension = benchmark - ? studioService.installIntoTransientWorld(sender, resolvedDimension, dimensionRoot) - : studioService.installIntoWorld(sender, resolvedDimension, dimensionRoot, seed); - if (installedDimension == null) { - throw new IrisException("Failed to install dimension pack for " + dimension()); - } - dimension = installedDimension.getLoadKey(); + reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack"); + StudioSVC studioService = IrisServices.get(StudioSVC.class); + IrisDimension installedDimension = benchmark && !studio + ? studioService.installIntoTransientWorld(sender, resolvedDimension, dimensionRoot) + : studioService.installIntoWorld(sender, resolvedDimension, dimensionRoot, seed); + if (installedDimension == null) { + throw new IrisException("Failed to install dimension pack for " + dimension()); } + dimension = installedDimension.getLoadKey(); if (studio()) { IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen()); IrisLogging.debug("Studio create scheduling: mode=" + runtimeSchedulerMode.name().toLowerCase(Locale.ROOT) @@ -285,6 +282,7 @@ public class IrisCreator { long generatorPrepareStart = System.nanoTime(); WorldCreator wc = new IrisWorldCreator() .dimension(installedDimension) + .studioPackSource(resolvedDimension.getLoader().getDataFolder()) .name(name) .seed(seed) .studio(studio) diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java b/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java index 28e622278..7de4eb6fa 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java @@ -33,6 +33,7 @@ import art.arcane.iris.core.pregenerator.PregenPerformanceProfile; import art.arcane.iris.core.pregenerator.PregenTask; import art.arcane.iris.core.pregenerator.PregeneratorMethod; import art.arcane.iris.core.pregenerator.cache.PregenCache; +import art.arcane.iris.core.pregenerator.cache.PregenSavedChunkStatus; import art.arcane.iris.core.pack.PackDirectoryResolver; import art.arcane.iris.core.pack.PackDownloader; import art.arcane.iris.core.project.IrisProject; @@ -316,7 +317,8 @@ public class IrisToolbelt { useCachedWrapper = runtimeSchedulerMode != IrisRuntimeSchedulerMode.FOLIA; } PregeneratorMethod activeMethod = useCachedWrapper - ? new CachedPregenMethod(method, resolvePregenCache(engine.getWorld()), task) + ? new CachedPregenMethod(new CachedPregenMethod.Configuration(method, resolvePregenCache(engine.getWorld()), task, + PregenSavedChunkStatus.fromWorld(engine.getWorld().worldFolder().toPath()))) : method; return new PregeneratorJob(new PregeneratorJob.Configuration( task, diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java index 030a3445a..eadf2dd39 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java @@ -33,6 +33,7 @@ import org.bukkit.generator.ChunkGenerator; import java.io.File; import java.io.IOException; +import java.util.Objects; public class IrisWorldCreator { private String name; @@ -41,6 +42,7 @@ public class IrisWorldCreator { private IrisDimension dimension; private long seed = 1337; private boolean persistent; + private File studioPackSource; public IrisWorldCreator() { @@ -83,6 +85,11 @@ public class IrisWorldCreator { return this; } + public IrisWorldCreator studioPackSource(File source) { + this.studioPackSource = Objects.requireNonNull(source, "Studio authoring source"); + return this; + } + public WorldCreator create() { IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension; NamespacedKey worldKey = IrisWorldStorage.keyFromName(name); @@ -102,13 +109,13 @@ public class IrisWorldCreator { .seed(seed) .worldFolder(worldFolder) .build(); - GenerationHistory generationHistory = persistent + GenerationHistory generationHistory = persistent || studio ? requireGenerationHistory(w.worldFolder(), seed) : null; - File packRoot = generationHistory != null + File packRoot = studio + ? Objects.requireNonNull(studioPackSource, "Studio authoring source") + : generationHistory != null ? requireActivePack(generationHistory) - : studio - ? dim.getLoader().getDataFolder() : new File(w.worldFolder(), "iris/pack"); ChunkGenerator g = new BukkitChunkGenerator( w, diff --git a/core/src/main/java/art/arcane/iris/engine/EngineDiagnostics.java b/core/src/main/java/art/arcane/iris/engine/EngineDiagnostics.java new file mode 100644 index 000000000..c6507cd9e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineDiagnostics.java @@ -0,0 +1,63 @@ +package art.arcane.iris.engine; + +import art.arcane.iris.core.compat.PackCompatReport; +import art.arcane.iris.core.pack.PackValidationResult; +import art.arcane.iris.core.pack.PackValidationRegistry; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; + +import java.io.File; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +final class EngineDiagnostics { + private final IrisEngine engine; + + EngineDiagnostics(IrisEngine engine) { + this.engine = engine; + } + + /** + * One line per engine naming what this pack cannot generate on the running Minecraft version. The published + * validation result is complete (validation force-loads the whole pack), so it is preferred; an unvalidated pack + * falls back to what this engine has gated while building its runtime (dimension, regions, biomes). Never throws: + * a report failure must not take an otherwise working world down with it. + */ + void logPackCompatSummary() { + try { + File folder = engine.getData().getDataFolder(); + // Runtime data comes from an immutable snapshot; the dimension key identifies the pack. + String pack = engine.getDimension().getLoadKey(); + String world = engine.getTarget().getWorld().name(); + String version = IrisPlatforms.isBound() ? IrisPlatforms.get().minecraftVersion() : null; + PackValidationResult published = PackValidationRegistry.get(folder.toPath()); + if (published == null) { + published = PackValidationRegistry.get(folder.getName()); + } + PackCompatReport report = published != null && !published.getCompatFindings().isEmpty() + ? PackCompatReport.of(published.getCompatFindings()) + : engine.getData().getCompatReport(); + if (!report.isEmpty()) { + IrisLogging.info("World '" + world + "' pack '" + pack + "' " + report.summaryLine(version)); + } + if (engine.getDimension().isCompatExcluded()) { + IrisLogging.error("World '" + world + "' pack '" + pack + "' cannot generate on Minecraft " + + (version == null || version.isBlank() ? "unknown" : version)); + } + } catch (Throwable e) { + IrisLogging.debug("Pack compat summary failed: " + e.getMessage()); + } + } + + void logStudioInitializationPhase(String phase, long startedAtNanos, boolean skipped) { + if (!engine.isStudio()) { + return; + } + IrisLogging.info("[Studio engine timing] world=%s kind=%s phase=%s duration=%dms skipped=%s", + engine.getTarget().getWorld().name(), + engine.getInitializationMode().name().toLowerCase(Locale.ROOT), + phase, + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAtNanos), + Boolean.toString(skipped)); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EngineHotloader.java b/core/src/main/java/art/arcane/iris/engine/EngineHotloader.java index e83fb249c..3a6ee0db8 100644 --- a/core/src/main/java/art/arcane/iris/engine/EngineHotloader.java +++ b/core/src/main/java/art/arcane/iris/engine/EngineHotloader.java @@ -19,6 +19,7 @@ package art.arcane.iris.engine; import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.protocol.IrisProtocolServer; import art.arcane.iris.engine.GenerationRuntime.BiomeMaxes; import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly; @@ -27,6 +28,10 @@ import art.arcane.iris.engine.framework.EngineTarget; import art.arcane.iris.engine.framework.IrisStructureLocator; import art.arcane.iris.engine.framework.StructureReachability; import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.history.GenerationHistory; +import art.arcane.iris.engine.history.IrisBoundarySignatureSampler; +import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter; +import art.arcane.iris.engine.history.GenerationManifest; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisServices; import art.arcane.iris.util.project.context.IrisContext; @@ -34,12 +39,13 @@ import art.arcane.iris.util.project.context.IrisContext; import static art.arcane.iris.engine.EngineShutdownSequence.runCleanup; /** - * Performs the two supported live reload transitions for an {@link IrisEngine}: a full pack hotload - * and a biome-complex-only rebuild. Both run entirely under the engine lifecycle lock, seal and - * drain generation before swapping anything, and roll the previous runtime back on failure. + * Reloads mutable engine runtimes and publishes immutable Studio generation activations. + * Studio transitions drain sessions outside the lifecycle lock before freezing chunk ownership. + * Failures before publication preserve the previous runtime; durable publication failures stop generation. */ final class EngineHotloader { private final IrisEngine engine; + private final Object studioTransitionLock = new Object(); EngineHotloader(IrisEngine engine) { this.engine = engine; @@ -51,6 +57,10 @@ final class EngineHotloader { } void hotloadComplex() { + if (engine.isStudio() && engine.getGenerationHistoryRuntimeRouter().isPresent()) { + hotloadSilently(); + return; + } synchronized (engine.lifecycleLock) { engine.requireRunning("rebuild the biome complex"); requireMutableGenerationRuntime("rebuild the biome complex"); @@ -114,6 +124,11 @@ final class EngineHotloader { } void hotloadSilently() { + GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElse(null); + if (engine.isStudio() && router != null) { + hotloadStudio(router); + return; + } synchronized (engine.lifecycleLock) { engine.requireRunning("hotload"); requireMutableGenerationRuntime("hotload"); @@ -192,6 +207,95 @@ final class EngineHotloader { } } + private void hotloadStudio(GenerationHistoryRuntimeRouter router) { + synchronized (studioTransitionLock) { + EngineRuntime previous = null; + GenerationManifest previousManifest = router.history().manifest(); + GenerationHistoryRuntimeRouter.StudioCutover cutover = null; + StudioGenerationUpdate update = null; + boolean backgroundAdmissionClosed = false; + try { + engine.requireRunning("hotload the Studio pack"); + GenerationHistory history = router.history(); + update = StudioGenerationUpdate.prepare(engine, history); + if (update == null) { + return; + } + engine.awaitNativeStructureBootstrap("Studio generation cutover"); + engine.backgroundTasks.closeBackgroundTaskAdmission(); + backgroundAdmissionClosed = true; + engine.backgroundTasks.drainBackgroundTasks("Studio generation cutover") + .requireComplete("Studio generation cutover"); + cutover = router.beginStudioCutover(IrisEngine.SESSION_DRAIN_TIMEOUT_MILLIS); + synchronized (engine.lifecycleLock) { + engine.requireRunning("prepare the Studio pack cutover"); + previous = engine.runtime; + engine.studioGenerationTransition = true; + engine.lifecycleState = LifecycleState.HOTLOADING; + } + engine.sealForTransition("Studio generation cutover", false); + IrisBoundarySignatureSampler.checkpoint(engine); + synchronized (engine.lifecycleLock) { + if (engine.lifecycleState != LifecycleState.HOTLOADING || engine.runtime != previous) { + throw new IllegalStateException("Studio engine changed while its generation cutover was draining."); + } + engine.prepareRuntimeHotload(); + history.stageUpdate(update.source(), update.fingerprint(), update.dimensionContract(), + update.registryContract(), IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()); + cutover.promotePending(); + IrisStructureLocator.invalidate(engine); + StructureReachability.invalidate(engine); + engine.runtimeBuilder.refreshStudioRuntimeServices(); + engine.getPrefetchSaveStarted().set(false); + engine.getEngineData().getStatistics().hotloaded(); + engine.getPlatformHooks().applyWorldBoundary(engine); + } + broadcastStudioHotload(false, ""); + } catch (Throwable failure) { + if (previous != null) { + synchronized (engine.lifecycleLock) { + if (engine.lifecycleState == LifecycleState.HOTLOADING + || engine.lifecycleState == LifecycleState.RUNNING) { + boolean unchanged = false; + try { + unchanged = router.history().manifest() == previousManifest; + } catch (Throwable historyFailure) { + failure.addSuppressed(historyFailure); + } + if (unchanged) { + engine.runtimeBuilder.restoreRuntimeAfterFailedTransition(previous); + } else { + engine.lifecycleState = LifecycleState.FAILED; + engine.getClosing().set(true); + engine.backgroundTasks.closeBackgroundTaskAdmission(); + } + } + } + } else if (backgroundAdmissionClosed && engine.lifecycleState == LifecycleState.RUNNING + && !engine.getClosing().get()) { + engine.backgroundTasks.openBackgroundTaskAdmission(); + } + IrisLogging.reportError("Studio generation update failed for '" + engine.getWorld().name() + "'.", failure); + broadcastStudioHotload(true, failure.getClass().getSimpleName() + ": " + failure.getMessage()); + throw EngineShutdownSequence.propagate(failure); + } finally { + synchronized (engine.lifecycleLock) { + engine.studioGenerationTransition = false; + } + if (cutover != null) { + cutover.close(); + } + if (update != null) { + try { + update.close(); + } catch (Throwable cleanupFailure) { + IrisLogging.reportError("Failed to remove the staged Studio pack after its update.", cleanupFailure); + } + } + } + } + } + private void requireMutableGenerationRuntime(String operation) { if (engine.getGenerationHistoryRuntimeRouter().isPresent()) { throw new IllegalStateException("Cannot " + operation @@ -210,7 +314,7 @@ final class EngineHotloader { if (protocolServer == null) { return; } - String packKey = engine.getData().getDataFolder().getName(); + String packKey = engine.getPackSource().getFileName().toString(); protocolServer.broadcastStudioHotload(packKey, 0, failed, message); } catch (Throwable broadcastFailure) { IrisLogging.error("Iris studio hotload broadcast failed: " + broadcastFailure.getClass().getSimpleName() diff --git a/core/src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java b/core/src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java index 624b4fbcb..099affb6a 100644 --- a/core/src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java +++ b/core/src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java @@ -248,6 +248,39 @@ final class EngineRuntimeBuilder { IrisLogging.debug("Engine Setup Complete " + next.generation().cacheId()); } + void refreshStudioRuntimeServices() { + EngineRuntime previous = requireRuntime("refresh Studio runtime services"); + EngineEffects effects = null; + EngineWorldManager worldManager = null; + try { + effects = Objects.requireNonNull(IrisServices.get(EngineEffectsProvider.class).create(engine), + "Studio engine effects"); + worldManager = Objects.requireNonNull(IrisServices.get(EngineWorldManagerProvider.class).create(engine), + "Studio world manager"); + } catch (Throwable failure) { + if (effects != null) { + EngineEffects createdEffects = effects; + EngineShutdownSequence.runCleanup(failure, createdEffects::close); + } + throw propagate(failure); + } + EngineRuntime next = new EngineRuntime(previous.generation(), effects, worldManager); + Throwable retirementFailure = EngineShutdownSequence.runCleanup(null, previous.worldManager()::close); + retirementFailure = EngineShutdownSequence.runCleanup(retirementFailure, previous.effects()::close); + if (retirementFailure != null) { + retirementFailure = EngineShutdownSequence.runCleanup(retirementFailure, worldManager::close); + retirementFailure = EngineShutdownSequence.runCleanup(retirementFailure, effects::close); + throw new IllegalStateException("Failed to retire Studio runtime services.", retirementFailure); + } + engine.runtime = next; + worldManager.start(); + engine.getGenerationSessions().activateNextSession(); + engine.lifecycleState = LifecycleState.RUNNING; + engine.getClosing().set(false); + engine.backgroundTasks.openBackgroundTaskAdmission(); + scheduleRuntimeTasks(next); + } + private void scheduleRuntimeTasks(EngineRuntime engineRuntime) { schedulePackHash(engineRuntime.generation()); try { diff --git a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java index e38b4af44..7d0e42adb 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java @@ -18,6 +18,8 @@ package art.arcane.iris.engine; +import art.arcane.iris.util.project.context.IrisContext; + import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisRegistrant; @@ -25,6 +27,7 @@ import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.history.GenerationBlend; import art.arcane.iris.engine.history.TransitionGenerationPlan; +import art.arcane.iris.engine.history.TerrainBoundarySignature; import art.arcane.iris.engine.hydrology.HydrologyColumnLayer; import art.arcane.iris.engine.hydrology.HydrologyColumnSample; import art.arcane.iris.engine.hydrology.HydrologyFeatureType; @@ -79,6 +82,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.OptionalInt; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; @@ -140,6 +144,8 @@ public class IrisComplex implements DataProvider { private ProceduralStream trueBiomeDerivativeStream; private ProceduralStream naturalHeightStream; private ProceduralStream unblendedNaturalHeightStream; + private final ResolvedTerrainProvider resolvedTerrain; + private ProceduralStream placementHeightStream; private ProceduralStream heightStream; private ProceduralStream roundedHeighteightStream; private ProceduralStream maxHeightStream; @@ -188,6 +194,7 @@ public class IrisComplex implements DataProvider { IrisComplex(Engine engine, boolean simple, TransitionGenerationPlan transitionGenerationPlan) { this.transitionGenerationPlan = transitionGenerationPlan; + this.resolvedTerrain = new ResolvedTerrainProvider(engine); int cacheSize = noiseCacheSize(engine, IrisSettings.get().getPerformance().getNoiseCacheSize()); IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE); UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes()); @@ -314,11 +321,7 @@ public class IrisComplex implements DataProvider { (x, z) -> sampleUnblendedNaturalTerrainHeight(engine, x, z), Interpolated.DOUBLE ).cache2DDouble("unblendedNaturalHeightStream", engine, cacheSize); - naturalHeightStream = transitionGenerationPlan == null ? unblendedNaturalHeightStream - : unblendedNaturalHeightStream.convertAware2D((newHeight, x, z) -> blendNaturalTerrainHeight( - newHeight, - transitionGenerationPlan.terrainSampleAt(blockCoordinate(x), blockCoordinate(z)))) - .cache2DDouble("naturalHeightStream", engine, cacheSize); + naturalHeightStream = unblendedNaturalHeightStream; naturalTrueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D, b -> focusBiome)) .cache2D("naturalTrueBiomeStream-focus", engine, cacheSize) : naturalHeightStream @@ -355,10 +358,11 @@ public class IrisComplex implements DataProvider { } heightStream = ProceduralStream.ofDouble((x, z) -> resolveHydrologyTerrainHeight(x, z)) .cache2DDouble("heightStream", engine, cacheSize); - roundedHeighteightStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z)) + placementHeightStream = ProceduralStream.ofDouble(this::samplePlacementHeight); + roundedHeighteightStream = placementHeightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z)) .round(); - slopeStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z)) - .slope(3).cache2DDouble("slopeStream", engine, cacheSize); + slopeStream = placementHeightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z)) + .slope(3); naturalSlopeStream = naturalHeightStream.slope(3).cache2DDouble("naturalSlopeStream", engine, cacheSize); trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D, b -> focusBiome)) @@ -480,14 +484,7 @@ public class IrisComplex implements DataProvider { } private double sampleNaturalTerrainHeight(Engine engine, double x, double z) { - double newHeight = sampleUnblendedNaturalTerrainHeight(engine, x, z); - if (transitionGenerationPlan == null) { - return newHeight; - } - TransitionGenerationPlan.TerrainSample sample = transitionGenerationPlan.terrainSampleAt( - blockCoordinate(x), - blockCoordinate(z)); - return blendNaturalTerrainHeight(newHeight, sample); + return sampleUnblendedNaturalTerrainHeight(engine, x, z); } private double sampleUnblendedNaturalTerrainHeight(Engine engine, double x, double z) { @@ -495,20 +492,6 @@ public class IrisComplex implements DataProvider { return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight); } - static double blendNaturalTerrainHeight( - double newHeight, - TransitionGenerationPlan.TerrainSample sample - ) { - Objects.requireNonNull(sample, "terrain transition sample"); - if (sample.newEpochWeight() == 1D || !sample.hasHistoricalSignature()) { - return newHeight; - } - double historicalHeight = sample.nearestSignature().fluidHeight().isPresent() - ? sample.historicalOceanFloorHeight() - : sample.historicalSurfaceHeight(); - return GenerationBlend.interpolate(historicalHeight, newHeight, sample.newEpochWeight()); - } - static double calculateNaturalSlope(double naturalHeight, double easternHeight, double southernHeight) { double deltaX = easternHeight - naturalHeight; double deltaZ = southernHeight - naturalHeight; @@ -808,6 +791,9 @@ public class IrisComplex implements DataProvider { int naturalHeight, double hydrologyWeight ) { + if (layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid()) { + return layer; + } return new HydrologyColumnLayer( layer.feature(), GenerationBlend.interpolateHeight(naturalHeight, layer.bedY(), hydrologyWeight), @@ -847,6 +833,42 @@ public class IrisComplex implements DataProvider { || hydrologyRuntime.isPlanned(x, z); } + public ProceduralStream getRawHeightStream() { + return heightStream; + } + + public ProceduralStream getHeightStream() { + return placementHeightStream; + } + + public Optional resolvedTerrainColumn(int blockX, int blockZ) { + if (transitionGenerationPlan == null || isNaturalTerrainContext() + || !transitionGenerationPlan.hasTransitionAtChunk(blockX >> 4, blockZ >> 4)) { + return Optional.empty(); + } + return Optional.of(resolvedTerrain.column(blockX, blockZ)); + } + + public OptionalInt resolvedTerrainHeight(int blockX, int blockZ, boolean ignoreFluid) { + if (transitionGenerationPlan == null || isNaturalTerrainContext() + || !transitionGenerationPlan.hasTransitionAtChunk(blockX >> 4, blockZ >> 4)) { + return OptionalInt.empty(); + } + return OptionalInt.of(resolvedTerrain.height(blockX, blockZ, ignoreFluid)); + } + + boolean isNaturalTerrainContext() { + IrisContext context = IrisContext.get(); + return context != null && context.getChunkContext() != null + && context.getChunkContext().getComplex() == this + && context.getChunkContext().isNaturalTerrain(); + } + + private double samplePlacementHeight(double x, double z) { + OptionalInt resolved = resolvedTerrainHeight(blockCoordinate(x), blockCoordinate(z), true); + return resolved.isPresent() ? resolved.getAsInt() : heightStream.getDouble(x, z); + } + public boolean isHistoricalChunk(int chunkX, int chunkZ) { return transitionGenerationPlan != null && transitionGenerationPlan.boundary().isHistoricalChunk(chunkX, chunkZ); @@ -1783,6 +1805,7 @@ public class IrisComplex implements DataProvider { } public void close() { + resolvedTerrain.clear(); if (hydrologyRuntime != null) { hydrologyRuntime.close(); } diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java index 7a3e0f085..ebcc171c1 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -37,6 +37,7 @@ import art.arcane.iris.engine.framework.GenerationSessionManager; import art.arcane.iris.engine.history.GenerationBoundarySignatureSampler; import art.arcane.iris.engine.history.GenerationHistory; import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter; +import art.arcane.iris.engine.history.ChunkGenerationSemantics; import art.arcane.iris.engine.history.GenerationKernelRegistry; import art.arcane.iris.engine.history.TransitionGenerationPlan; import art.arcane.iris.engine.framework.SeedManager; @@ -46,10 +47,7 @@ import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; import art.arcane.iris.engine.object.IrisEngineData; import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.core.compat.PackCompatReport; import art.arcane.iris.core.pack.PackValidationCache; -import art.arcane.iris.core.pack.PackValidationResult; -import art.arcane.iris.core.pack.PackValidationRegistry; import art.arcane.iris.spi.IrisLogging; import com.google.common.util.concurrent.AtomicDouble; import art.arcane.iris.spi.IrisPlatforms; @@ -78,20 +76,17 @@ import lombok.Data; import lombok.Getter; import lombok.Setter; -import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.Collections; import java.util.HashSet; import java.util.IdentityHashMap; -import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -118,6 +113,11 @@ public class IrisEngine implements Engine { private final EngineMetrics metrics; private final CompletableFuture generationCacheWarm; private final boolean studio; + @Setter(AccessLevel.NONE) + private volatile Path studioGenerationSource; + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + boolean studioGenerationTransition; private final AtomicRollingSequence wallClock; @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) @@ -163,6 +163,9 @@ public class IrisEngine implements Engine { @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) final EngineMetricsReport metricsReport = new EngineMetricsReport(this); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private final EngineDiagnostics diagnostics = new EngineDiagnostics(this); private final AtomicBoolean cleaning; private final ChronoLatch cleanLatch; private final SeedManager seedManager; @@ -267,7 +270,7 @@ public class IrisEngine implements Engine { this.initialKernelVersion = Objects.requireNonNull(kernelVersion, "generation kernel version"); this.initialTransitionPlan = transitionPlan; this.platformHooks = IrisServices.get(EnginePlatformHooks.class); - this.generationSessions = new GenerationSessionManager(); + this.generationSessions = new GenerationSessionManager(requiredMode.studio()); this.closing = new AtomicBoolean(true); this.nativeStructureVolumeQueriesEnabled = new AtomicBoolean(!requiredMode.studio()); this.lifecycleState = LifecycleState.INITIALIZING; @@ -313,7 +316,7 @@ public class IrisEngine implements Engine { String studioCacheIdentity = currentStudioCacheIdentity(); getData().loadPrefetch(this); IrisLogging.debug("[IrisEngine timing] loadPrefetch=" + (M.ms() - _t0) + "ms"); - logStudioInitializationPhase("load_prefetch", phaseStartedAt, false); + diagnostics.logStudioInitializationPhase("load_prefetch", phaseStartedAt, false); try { StructureIndexService.writeOnce(getData()); } catch (Throwable e) { @@ -326,8 +329,8 @@ public class IrisEngine implements Engine { enableStableStudioCache(initialRuntime, studioCacheIdentity); runtimeBuilder.publishRuntime(initialRuntime, null); IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms"); - logPackCompatSummary(); - logStudioInitializationPhase("build_runtime", phaseStartedAt, false); + diagnostics.logPackCompatSummary(); + diagnostics.logStudioInitializationPhase("build_runtime", phaseStartedAt, false); phaseStartedAt = System.nanoTime(); if (requiredMode.warmGenerationCaches()) { if (requiredMode.studio()) { @@ -337,7 +340,7 @@ public class IrisEngine implements Engine { } } else { generationCacheWarm.complete(null); - logStudioInitializationPhase("generation_cache_warm", phaseStartedAt, true); + diagnostics.logStudioInitializationPhase("generation_cache_warm", phaseStartedAt, true); } EngineTickRegistry.registerTicking(this); } catch (Throwable e) { @@ -347,38 +350,6 @@ public class IrisEngine implements Engine { IrisLogging.debug("Engine Initialized " + getCacheID()); } - /** - * One line per engine naming what this pack cannot generate on the running Minecraft version. The published - * validation result is complete (validation force-loads the whole pack), so it is preferred; an unvalidated pack - * falls back to what this engine has gated while building its runtime (dimension, regions, biomes). Never throws: - * a report failure must not take an otherwise working world down with it. - */ - private void logPackCompatSummary() { - try { - File folder = getData().getDataFolder(); - // A world engine reads its snapshot copy under /iris/pack, so the dimension key is the pack's name. - String pack = getDimension().getLoadKey(); - String world = target.getWorld().name(); - String version = IrisPlatforms.isBound() ? IrisPlatforms.get().minecraftVersion() : null; - PackValidationResult published = PackValidationRegistry.get(folder.toPath()); - if (published == null) { - published = PackValidationRegistry.get(folder.getName()); - } - PackCompatReport report = published != null && !published.getCompatFindings().isEmpty() - ? PackCompatReport.of(published.getCompatFindings()) - : getData().getCompatReport(); - if (!report.isEmpty()) { - IrisLogging.info("World '" + world + "' pack '" + pack + "' " + report.summaryLine(version)); - } - if (getDimension().isCompatExcluded()) { - IrisLogging.error("World '" + world + "' pack '" + pack + "' cannot generate on Minecraft " - + (version == null || version.isBlank() ? "unknown" : version)); - } - } catch (Throwable e) { - IrisLogging.debug("Pack compat summary failed: " + e.getMessage()); - } - } - public void awaitGenerationCacheWarm() { if (generationCacheWarm.isDone() && !generationCacheWarm.isCompletedExceptionally()) { return; @@ -394,18 +365,6 @@ public class IrisEngine implements Engine { return !generationCacheWarm.isDone(); } - private void logStudioInitializationPhase(String phase, long startedAtNanos, boolean skipped) { - if (!studio) { - return; - } - IrisLogging.info("[Studio engine timing] world=%s kind=%s phase=%s duration=%dms skipped=%s", - target.getWorld().name(), - initializationMode.name().toLowerCase(Locale.ROOT), - phase, - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAtNanos), - Boolean.toString(skipped)); - } - private String currentStudioCacheIdentity() { if (initializationMode != InitializationMode.STUDIO || !IrisPlatforms.isBound()) { return ""; @@ -469,7 +428,7 @@ public class IrisEngine implements Engine { throw new IllegalStateException("Generation cache warming failed.", failure); } finally { IrisLogging.debug("[IrisEngine timing] cache warm total=" + (M.ms() - startedAtMillis) + "ms"); - logStudioInitializationPhase("generation_cache_warm", phaseStartedAtNanos, false); + diagnostics.logStudioInitializationPhase("generation_cache_warm", phaseStartedAtNanos, false); } } @@ -575,6 +534,9 @@ public class IrisEngine implements Engine { @BlockCoordinates @Override public IrisBiome getCaveBiome(int x, int z) { + if (usesScopedNaturalTerrain()) { + return Engine.super.getCaveBiome(x, z); + } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a cave biome")) { return Engine.super.getCaveBiome(x, z); @@ -595,6 +557,9 @@ public class IrisEngine implements Engine { int z, IrisDimensionCarvingResolver.State state ) { + if (usesScopedNaturalTerrain()) { + return Engine.super.getCaveBiome(x, y, z, state); + } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a vertical cave biome")) { return Engine.super.getCaveBiome(x, y, z, state); @@ -604,6 +569,9 @@ public class IrisEngine implements Engine { @BlockCoordinates @Override public IrisBiome getSurfaceBiome(int x, int z) { + if (usesScopedNaturalTerrain()) { + return Engine.super.getSurfaceBiome(x, z); + } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a surface biome")) { return Engine.super.getSurfaceBiome(x, z); @@ -637,6 +605,10 @@ public class IrisEngine implements Engine { @ChunkCoordinates @Override public Set getObjectsAt(int x, int z) { + Optional recorded = recordedChunkSemantics(x, z); + if (recorded.isPresent()) { + return recorded.get().objectKeys(); + } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = openGenerationHistoryCoordinateScopeUnchecked(x << 4, z << 4, "resolve chunk objects")) { return getMantle().getObjectComponent().guess(x, z); @@ -646,6 +618,15 @@ public class IrisEngine implements Engine { @ChunkCoordinates @Override public Set> getPOIsAt(int chunkX, int chunkZ) { + Optional recorded = recordedChunkSemantics(chunkX, chunkZ); + if (recorded.isPresent()) { + Set> points = new HashSet<>(); + for (ChunkGenerationSemantics.PointOfInterest point : recorded.get().pointsOfInterest()) { + ChunkGenerationSemantics.BlockPosition position = point.position(); + points.add(new Pair<>(point.key(), new BlockPos(position.x(), position.y(), position.z()))); + } + return points; + } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = openGenerationHistoryCoordinateScopeUnchecked( chunkX << 4, chunkZ << 4, "resolve chunk points of interest")) { @@ -654,11 +635,19 @@ public class IrisEngine implements Engine { chunkX, chunkZ, MatterStructurePOI.class, - (x, y, z, data) -> pois.add(new Pair<>(data.getType(), new BlockPos(x, y, z)))); + (x, y, z, data) -> pois.add(new Pair<>(data.getType(), new BlockPos( + Math.addExact(Math.multiplyExact(chunkX, 16), x), y, + Math.addExact(Math.multiplyExact(chunkZ, 16), z))))); return pois; } } + private Optional recordedChunkSemantics(int chunkX, int chunkZ) { + GenerationHistoryRuntimeRouter router = getGenerationHistoryRuntimeRouter().orElse(null); + return router == null ? Optional.empty() + : router.history().semantics(chunkX, chunkZ).filter(ChunkGenerationSemantics::sealed); + } + private void warmupChunk(int x, int z) { for (int i = 0; i < 16; i++) { for (int j = 0; j < 16; j++) { @@ -675,6 +664,30 @@ public class IrisEngine implements Engine { hotloader.hotload(); } + public void configureStudioGenerationSource(Path source) { + if (!studio) { + throw new IllegalStateException("An authoring pack source requires a Studio engine."); + } + Path required = Objects.requireNonNull(source, "Studio generation source").toAbsolutePath().normalize(); + synchronized (lifecycleLock) { + requireRunning("configure the Studio authoring pack"); + if (studioGenerationSource != null && !studioGenerationSource.equals(required)) { + throw new IllegalStateException("The Studio authoring pack is already configured."); + } + studioGenerationSource = required; + } + } + + public Path getStudioGenerationSource() { + return Objects.requireNonNull(studioGenerationSource, "Studio authoring pack is not configured"); + } + + @Override + public Path getPackSource() { + Path source = studioGenerationSource; + return source == null ? getData().getDataFolder().toPath() : source; + } + public void hotloadComplex() { hotloader.hotloadComplex(); } @@ -827,6 +840,10 @@ public class IrisEngine implements Engine { return generationRuntimeScopes.current() != null; } + private boolean usesScopedNaturalTerrain() { + return hasGenerationRuntimeScope() && getComplex().isNaturalTerrainContext(); + } + private GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScopeUnchecked( int blockX, int blockZ, @@ -956,7 +973,7 @@ public class IrisEngine implements Engine { EngineTarget requiredTarget = Objects.requireNonNull(runtimeTarget, "detached generation target"); Path requiredMantleStorageDirectory = IrisEngineMantle.normalizeStorageDirectory(mantleStorageDirectory); synchronized (lifecycleLock) { - requireRunning("build a detached generation runtime"); + requireGenerationRuntimeMutation("build a detached generation runtime"); GenerationRuntime active = runtime.generation(); if (requiredTarget.getWorld() != active.target().getWorld()) { throw new IllegalArgumentException("Detached generation runtime must target this Iris world."); @@ -1066,7 +1083,7 @@ public class IrisEngine implements Engine { public void setDefaultGenerationRuntime(GenerationRuntimeBinding binding) { GenerationRuntimeBinding required = Objects.requireNonNull(binding, "generation runtime binding"); synchronized (lifecycleLock) { - requireRunning("select the default generation runtime"); + requireGenerationRuntimeMutation("select the default generation runtime"); if (required.engine != this) { throw new IllegalArgumentException("Generation runtime binding belongs to a different Iris engine."); } @@ -1439,6 +1456,14 @@ public class IrisEngine implements Engine { } } + private void requireGenerationRuntimeMutation(String operation) { + if (!closed && studio && studioGenerationTransition && lifecycleState == LifecycleState.HOTLOADING + && generationHistoryRuntimeRouter != null) { + return; + } + requireRunning(operation); + } + private boolean EngineSafe() { // Todo: this has potential if done right int EngineMCVersion = getEngineData().getStatistics().getMCVersion(); diff --git a/core/src/main/java/art/arcane/iris/engine/ResolvedTerrainProvider.java b/core/src/main/java/art/arcane/iris/engine/ResolvedTerrainProvider.java new file mode 100644 index 000000000..9c6553b23 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/ResolvedTerrainProvider.java @@ -0,0 +1,240 @@ +package art.arcane.iris.engine; + +import art.arcane.iris.engine.actuator.IrisTransitionGeometryActuator; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineMode; +import art.arcane.iris.engine.history.BoundaryColumnGeometry; +import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter; +import art.arcane.iris.engine.history.SavedTerrainChunk; +import art.arcane.iris.engine.history.TerrainBoundarySignature; +import art.arcane.iris.engine.history.TransitionGenerationPlan; +import art.arcane.iris.engine.history.TransitionFluidContainment; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.iris.util.project.hunk.Hunk; + +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +public final class ResolvedTerrainProvider { + private static final int MAXIMUM_CACHED_CHUNKS = 64; + private static final int MAXIMUM_HEIGHT_CHUNKS = 4_096; + + private final Engine engine; + private final LinkedHashMap completed = new LinkedHashMap<>(64, 0.75F, true); + private final ConcurrentHashMap> pending = new ConcurrentHashMap<>(); + private final LinkedHashMap rawCompleted = new LinkedHashMap<>(64, 0.75F, true); + private final ConcurrentHashMap> rawPending = new ConcurrentHashMap<>(); + private final LinkedHashMap heights = new LinkedHashMap<>(64, 0.75F, true); + + public ResolvedTerrainProvider(Engine engine) { + this.engine = engine; + } + + public TerrainBoundarySignature column(int blockX, int blockZ) { + return resolve(blockX >> 4, blockZ >> 4).column(blockX, blockZ); + } + + public int height(int blockX, int blockZ, boolean ignoreFluid) { + long key = chunkKey(blockX >> 4, blockZ >> 4); + synchronized (heights) { + int[] cached = heights.get(key); + if (cached != null) { + return cached[(ignoreFluid ? 256 : 0) + (blockX & 15) * 16 + (blockZ & 15)]; + } + } + TerrainBoundarySignature column = resolve(blockX >> 4, blockZ >> 4).column(blockX, blockZ); + return ignoreFluid ? column.oceanFloorHeight() : column.surfaceHeight(); + } + + public void generate(EngineMode mode, int x, int z, Hunk blocks, + Hunk biomes, boolean multicore, ChunkContext context) { + SavedTerrainChunk terrain; + if (context.getComplex().getTransitionGenerationPlan() != null + && context.getComplex().getTransitionGenerationPlan().hasTransitionAtChunk(x >> 4, z >> 4)) { + terrain = resolve(x >> 4, z >> 4); + copy(terrain, blocks, biomes, context); + } else { + mode.generateTerrain(x, z, blocks, biomes, multicore, context); + terrain = capture(x, z, blocks, biomes, context, true); + } + if (engine instanceof IrisEngine irisEngine) { + GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null); + if (router != null) { + router.recordNaturalTerrain(terrain.boundaryOnly()); + } + } + } + + public void clear() { + synchronized (completed) { + completed.clear(); + } + synchronized (rawCompleted) { + rawCompleted.clear(); + } + synchronized (heights) { + heights.clear(); + } + } + + private SavedTerrainChunk resolve(int chunkX, int chunkZ) { + return resolve(chunkX, chunkZ, false); + } + + private SavedTerrainChunk resolve(int chunkX, int chunkZ, boolean raw) { + LinkedHashMap cache = raw ? rawCompleted : completed; + ConcurrentHashMap> loads = raw ? rawPending : pending; + long key = chunkKey(chunkX, chunkZ); + synchronized (cache) { + SavedTerrainChunk cached = cache.get(key); + if (cached != null) { + return cached; + } + } + CompletableFuture result = new CompletableFuture<>(); + CompletableFuture existing = loads.putIfAbsent(key, result); + if (existing != null) { + return existing.join(); + } + try { + synchronized (cache) { + SavedTerrainChunk published = cache.get(key); + if (published != null) { + result.complete(published); + return published; + } + } + SavedTerrainChunk terrain = raw ? compute(chunkX, chunkZ) : contain(chunkX, chunkZ); + if (!raw) { + cacheHeights(key, terrain); + } + synchronized (cache) { + cache.put(key, terrain); + while (cache.size() > MAXIMUM_CACHED_CHUNKS) { + cache.pollFirstEntry(); + } + } + result.complete(terrain); + return terrain; + } catch (RuntimeException | Error failure) { + result.completeExceptionally(failure); + throw failure; + } finally { + loads.remove(key, result); + } + } + + private static long chunkKey(int chunkX, int chunkZ) { + return ((long) chunkX << 32) | (chunkZ & 0xffffffffL); + } + + private void cacheHeights(long key, SavedTerrainChunk terrain) { + int[] values = new int[512]; + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + TerrainBoundarySignature column = terrain.column((terrain.chunkX() << 4) + x, + (terrain.chunkZ() << 4) + z); + int index = x * 16 + z; + values[index] = column.surfaceHeight(); + values[256 + index] = column.oceanFloorHeight(); + } + } + synchronized (heights) { + heights.put(key, values); + while (heights.size() > MAXIMUM_HEIGHT_CHUNKS) { + heights.pollFirstEntry(); + } + } + } + + private SavedTerrainChunk contain(int chunkX, int chunkZ) { + SavedTerrainChunk raw = resolve(chunkX, chunkZ, true); + TransitionGenerationPlan plan = engine.getComplex().getTransitionGenerationPlan(); + if (plan == null || !plan.hasTransitionAtChunk(chunkX, chunkZ)) { + return raw; + } + try { + return TransitionFluidContainment.contain(raw, plan, (x, z) -> rawColumn(plan, raw, x, z)); + } catch (IOException failure) { + throw new IllegalStateException("Unable to contain transition fluids at " + chunkX + "," + chunkZ, failure); + } + } + + private BoundaryColumnGeometry rawColumn(TransitionGenerationPlan plan, SavedTerrainChunk current, int x, int z) + throws IOException { + if (plan.isHistoricalBlock(x, z)) { + BoundaryColumnGeometry geometry = plan.terrainSignatures().signatureAt(x, z) + .orElseThrow(() -> new IOException("Missing saved terrain boundary column at " + x + "," + z)) + .geometry(); + if (geometry.minimumY() != engine.getMinHeight() || geometry.height() != engine.getHeight()) { + throw new IOException("Saved terrain boundary column has the wrong vertical layout at " + x + "," + z); + } + return geometry; + } + return (x >> 4) == current.chunkX() && (z >> 4) == current.chunkZ() + ? current.column(x, z).geometry() : resolve(x >> 4, z >> 4, true).column(x, z).geometry(); + } + + private SavedTerrainChunk compute(int chunkX, int chunkZ) { + int x = Math.multiplyExact(chunkX, 16); + int z = Math.multiplyExact(chunkZ, 16); + try (GenerationHistoryRuntimeRouter.CoordinateScope runtimeScope = engine instanceof IrisEngine irisEngine + ? irisEngine.openGenerationHistoryCoordinateScope(x, z) : null) { + IrisContext active = IrisContext.get(); + long sessionId = active == null ? 0L : active.getGenerationSessionId(); + ChunkContext context = new ChunkContext(x, z, engine.getComplex(), sessionId, true, + ChunkContext.PrefillPlan.NATURAL_TERRAIN, engine.getMetrics(), engine.getDimensionStackContext()); + context.beginSpeculativeTerrain(); + Hunk blocks = Hunk.newArrayHunk(16, engine.getHeight(), 16); + Hunk biomes = Hunk.newArrayHunk(16, engine.getHeight(), 16); + try (IrisContext.Scope ignored = IrisContext.open(engine, sessionId, context)) { + engine.getMode().generateTerrain(x, z, blocks, biomes, false, context); + return capture(x, z, blocks, biomes, context, false); + } + } catch (IOException failure) { + throw new IllegalStateException("Unable to scope natural terrain at " + chunkX + "," + chunkZ, failure); + } + } + + private SavedTerrainChunk capture(int x, int z, Hunk blocks, + Hunk biomes, ChunkContext context, boolean boundaryOnly) { + try { + return IrisTransitionGeometryActuator.capture(x, z, blocks, biomes, engine.getMinHeight(), context, boundaryOnly); + } catch (IOException failure) { + throw new IllegalStateException("Unable to capture natural terrain at " + (x >> 4) + "," + (z >> 4), failure); + } + } + + private static void copy(SavedTerrainChunk terrain, Hunk blocks, + Hunk biomes, ChunkContext context) { + PlatformRegistries registries = IrisPlatforms.get().registries(); + Map states = new HashMap<>(); + Map physicalBiomes = new HashMap<>(); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + TerrainBoundarySignature column = terrain.column((terrain.chunkX() << 4) + x, (terrain.chunkZ() << 4) + z); + BoundaryColumnGeometry geometry = column.geometry(); + for (int offset = 0; offset < blocks.getHeight(); offset++) { + String stateKey = geometry.voxelAt(geometry.minimumY() + offset).stateKey(); + PlatformBlockState state = states.computeIfAbsent(stateKey, registries::blockOrNull); + if (state == null) { + throw new IllegalStateException("Resolved terrain requires unavailable block state " + stateKey); + } + blocks.setRaw(x, offset, z, state); + String biomeKey = column.biomeAtSample(offset / 4); + biomes.setRaw(x, offset, z, physicalBiomes.computeIfAbsent(biomeKey, registries::biome)); + } + context.setTerrainHeight(x, z, column.oceanFloorHeight()); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/StudioGenerationUpdate.java b/core/src/main/java/art/arcane/iris/engine/StudioGenerationUpdate.java new file mode 100644 index 000000000..4929787e8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/StudioGenerationUpdate.java @@ -0,0 +1,88 @@ +package art.arcane.iris.engine; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.pack.PackValidationResult; +import art.arcane.iris.core.pack.PackValidator; +import art.arcane.iris.core.pack.AtomicDirectoryPublisher; +import art.arcane.iris.engine.history.GenerationEpoch; +import art.arcane.iris.engine.history.GenerationEpochContractFactory; +import art.arcane.iris.engine.history.GenerationHistory; +import art.arcane.iris.engine.history.GenerationPackFingerprint; +import art.arcane.iris.engine.history.GenerationPackRepository; +import art.arcane.iris.engine.history.GenerationRegistryContract; +import art.arcane.iris.engine.history.GenerationRegistryContractFactory; +import art.arcane.iris.engine.object.IrisDimension; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Files; +import java.util.List; + +record StudioGenerationUpdate( + Path stagingRoot, + Path source, + String fingerprint, + GenerationEpoch.DimensionContract dimensionContract, + GenerationRegistryContract registryContract +) implements AutoCloseable { + static StudioGenerationUpdate prepare(IrisEngine engine, GenerationHistory history) throws IOException { + Path source = engine.getStudioGenerationSource(); + IrisData.invalidateLoadedAuthoringResources(source.toFile()); + String fingerprint = GenerationPackFingerprint.compute(source, GenerationPackFingerprint.CURRENT_VERSION); + GenerationEpoch active = history.activeEpoch(); + if (active.packFingerprintVersion() == GenerationPackFingerprint.CURRENT_VERSION + && fingerprint.equals(active.packFingerprint())) { + return null; + } + Path stagingRoot = Files.createTempDirectory(history.paths().generationRoot(), ".studio-"); + Path snapshot = stagingRoot.resolve(source.getFileName()); + try { + GenerationPackRepository.copyPackTree(source, snapshot); + if (!fingerprint.equals(GenerationPackFingerprint.compute(snapshot, GenerationPackFingerprint.CURRENT_VERSION))) { + throw new IOException("Studio pack changed while its update snapshot was being captured."); + } + return prepareSnapshot(engine, history, stagingRoot, snapshot, fingerprint); + } catch (Throwable failure) { + try { + AtomicDirectoryPublisher.deleteTree(stagingRoot); + } catch (IOException cleanupFailure) { + failure.addSuppressed(cleanupFailure); + } + throw failure; + } + } + + private static StudioGenerationUpdate prepareSnapshot(IrisEngine engine, GenerationHistory history, + Path stagingRoot, Path source, String fingerprint) + throws IOException { + GenerationEpoch active = history.activeEpoch(); + PackValidationResult validation = PackValidator.validate(source.toFile()); + if (!validation.isLoadable()) { + throw new IOException("Studio pack update is invalid: " + String.join("; ", validation.getBlockingErrors())); + } + IrisData data = IrisData.openRuntime(source.toFile()); + try { + IrisDimension replacement = data.getDimensionLoader().load(active.dimensionContract().dimensionKey()); + if (replacement == null) { + throw new IOException("Studio pack update is missing dimension '" + + active.dimensionContract().dimensionKey() + "'."); + } + engine.getPlatformHooks().validateDimensionHotload(engine, replacement); + GenerationEpoch.DimensionContract contract = GenerationEpochContractFactory.create( + replacement, replacement.getLoadKey(), active.dimensionContract().dimensionTypeKey()); + if (!active.dimensionContract().hasSameLayout(contract)) { + throw new IOException("Studio pack update changes the world's immutable dimension layout."); + } + GenerationRegistryContract registry = GenerationRegistryContractFactory.create(data, replacement, fingerprint); + registry.requireDefinitionsAvailableIn(GenerationRegistryContractFactory.captureRequiredDefinitions(List.of(registry))); + return new StudioGenerationUpdate(stagingRoot, source, fingerprint, contract, registry); + } finally { + data.close(); + } + } + + @Override + public void close() throws IOException { + AtomicDirectoryPublisher.deleteTree(stagingRoot); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java b/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java index a4f5e209f..6790131b7 100644 --- a/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java +++ b/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java @@ -20,8 +20,6 @@ package art.arcane.iris.engine; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.history.GenerationBlend; -import art.arcane.iris.engine.history.TransitionGenerationPlan; import art.arcane.iris.engine.image.IrisImageMapRuntime; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisDimension; @@ -33,16 +31,12 @@ import art.arcane.iris.util.project.stream.ProceduralStream; public class UpperDimensionContext implements DataProvider { private final DimensionTerrainContext terrainContext; private final ProceduralStream lowerHeightStream; - private final ProceduralStream unblendedLowerHeightStream; - private final TransitionGenerationPlan transitionPlan; private final CeilingLayout ceilingLayout; private UpperDimensionContext(DimensionTerrainContext terrainContext, Engine engine) { this.terrainContext = terrainContext; IrisComplex complex = engine.getComplex(); lowerHeightStream = complex.getHeightStream(); - unblendedLowerHeightStream = complex.getUnblendedNaturalHeightStream(); - transitionPlan = complex.getTransitionGenerationPlan(); ceilingLayout = new CeilingLayout(engine.getHeight(), engine.getDimension().getUpperDimensionGap()); } @@ -103,18 +97,6 @@ public class UpperDimensionContext implements DataProvider { 0D, Math.min(ceilingLayout.height() - 1D, terrainContext.getNormalTerrainHeight(x, z)) ); - if (transitionPlan != null) { - TransitionGenerationPlan.TerrainSample sample = transitionPlan.terrainSampleAt(x, z); - if (sample.newEpochWeight() < 1D && sample.hasHistoricalSignature()) { - int targetLowerSurfaceY = (int) Math.min( - ceilingLayout.height(), - Math.round(unblendedLowerHeightStream.getDouble(x, z)) - ); - int targetSurfaceY = effectiveSurfaceY(depth, ceilingLayout, targetLowerSurfaceY); - double targetDepth = Math.max(0D, ceilingLayout.height() - 1D - targetSurfaceY); - depth = blendCeilingDepth(targetDepth, sample); - } - } int lowerSurfaceY = (int) Math.min( ceilingLayout.height(), Math.round(lowerHeightStream.getDouble(x, z)) @@ -122,14 +104,6 @@ public class UpperDimensionContext implements DataProvider { return effectiveSurfaceY(depth, ceilingLayout, lowerSurfaceY); } - static double blendCeilingDepth(double newDepth, TransitionGenerationPlan.TerrainSample sample) { - if (sample.newEpochWeight() == 1D || !sample.hasHistoricalSignature()) { - return newDepth; - } - return GenerationBlend.interpolate( - sample.historicalUpperCeilingDepth(), newDepth, sample.newEpochWeight()); - } - static int effectiveSurfaceY(double depth, CeilingLayout layout, int lowerSurfaceY) { long mirroredSurface = layout.height() - 1L - Math.round(depth); long gapSurface = (long) lowerSurfaceY + layout.minimumGap(); diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisBiomeActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisBiomeActuator.java index 28c2524b0..9ec321a06 100644 --- a/core/src/main/java/art/arcane/iris/engine/actuator/IrisBiomeActuator.java +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisBiomeActuator.java @@ -36,6 +36,7 @@ import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterBiomeInject; @@ -65,7 +66,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator { int depth = h.getDepth(); int height = h.getHeight(); Engine engine = getEngine(); - Mantle mantle = engine.getMantle().getMantle(); + Mantle mantle = context.isSpeculativeTerrain() ? null : engine.getMantle().getMantle(); ChunkedDataCache biomeCache = context.getBiome(); IrisComplex complex = context.getComplex(); DimensionStackContext dimensionStackContext = engine.getDimensionStackContext(); @@ -74,21 +75,6 @@ public class IrisBiomeActuator extends EngineAssignedActuator { for (int zf = 0; zf < depth; zf++) { int worldX = x + xf; int worldZ = z + zf; - if (!complex.allowsNewDiscreteContentAt(worldX, worldZ)) { - writeHistoricalColumn( - h, - mantle, - xf, - zf, - worldX, - worldZ, - height, - engine, - complex - ); - continue; - } - IrisBiome biome = biomeCache.get(xf, zf); ResolvedBiome resolved = resolve(biomeKey( biome, @@ -117,11 +103,39 @@ public class IrisBiomeActuator extends EngineAssignedActuator { context.getDimensionStackLayout(xf, zf) ); } + writeHistoricalColumn(h, mantle, xf, zf, worldX, worldZ, height, engine, complex); } } engine.getMetrics().getBiome().put(p.getMilliseconds()); } + public static void publishNaturalMetadata(Engine engine, int x, int z, Hunk biomes, + ChunkContext context) { + if (context.isSpeculativeTerrain() || !context.getComplex().allowsMantleChunkWrite(x >> 4, z >> 4)) { + return; + } + Mantle mantle = engine.getMantle().getMantle(); + MantleChunk chunk = mantle.getChunk(x >> 4, z >> 4).use(); + try { + synchronized (chunk) { + IrisDimensionStackActuator.clearNaturalMetadata(chunk, context, biomes.getHeight()); + chunk.deleteSlices(MatterBiomeInject.class); + for (int localX = 0; localX < biomes.getWidth(); localX += 4) { + for (int localZ = 0; localZ < biomes.getDepth(); localZ += 4) { + for (int y = 0; y < biomes.getHeight(); y += 4) { + PlatformBiome biome = biomes.getRaw(localX, y, localZ); + if (biome != null) { + mantle.set(x + localX, y, z + localZ, BiomeInjectMatter.get(biome.key())); + } + } + } + } + } + } finally { + chunk.release(); + } + } + private void writeHistoricalColumn( Hunk output, Mantle mantle, @@ -137,14 +151,17 @@ public class IrisBiomeActuator extends EngineAssignedActuator { if (transitionPlan == null) { return; } - TransitionGenerationPlan.TerrainSample sample = transitionPlan.terrainSampleAt(worldX, worldZ); + TransitionGenerationPlan.TerrainSample terrainSample = transitionPlan.terrainSampleAt(worldX, worldZ); + if (terrainSample.newEpochWeight() == 1D) { + return; + } int minimumWorldY = engine.getMinHeight(); - String activeKey = sample.historicalPhysicalBiomeKeyAt(minimumWorldY).orElse(null); + String activeKey = transitionPlan.historicalPhysicalBiomeKeyAt(worldX, minimumWorldY, worldZ, terrainSample).orElse(null); int rangeStart = 0; for (int internalY = 1; internalY <= height; internalY++) { String nextKey = internalY == height ? null - : sample.historicalPhysicalBiomeKeyAt(minimumWorldY + internalY).orElse(null); + : transitionPlan.historicalPhysicalBiomeKeyAt(worldX, minimumWorldY + internalY, worldZ, terrainSample).orElse(null); if (Objects.equals(activeKey, nextKey)) { continue; } @@ -179,7 +196,9 @@ public class IrisBiomeActuator extends EngineAssignedActuator { if (resolved.biome() != null) { output.set(localX, 0, localZ, localX, height - 1, localZ, resolved.biome()); } - mantle.set(worldX, 0, worldZ, resolved.matter()); + if (mantle != null) { + mantle.set(worldX, 0, worldZ, resolved.matter()); + } } private void applyDimensionStackBiomes( @@ -256,7 +275,9 @@ public class IrisBiomeActuator extends EngineAssignedActuator { if (minimumY > maximumY) { return; } - clearBiomeMatterRange(mantle, worldX, worldZ, minimumY, maximumY); + if (mantle != null) { + clearBiomeMatterRange(mantle, worldX, worldZ, minimumY, maximumY); + } if (resolved.biome() != null) { output.set( localX, @@ -268,7 +289,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator { resolved.biome() ); } - if (resolved.matter() != null) { + if (mantle != null && resolved.matter() != null) { mantle.set(worldX, minimumY, worldZ, resolved.matter()); } } diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisDimensionStackActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisDimensionStackActuator.java index 1f46714b1..f6ab73312 100644 --- a/core/src/main/java/art/arcane/iris/engine/actuator/IrisDimensionStackActuator.java +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisDimensionStackActuator.java @@ -33,6 +33,7 @@ import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.project.matter.TileWrapper; +import art.arcane.iris.util.project.matter.PreObjectMatterCell; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; @@ -81,8 +82,8 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator mantleChunk = getEngine().getMantle().getMantle() - .getChunk(x >> 4, z >> 4).use(); + MantleChunk mantleChunk = context.isSpeculativeTerrain() ? null + : getEngine().getMantle().getMantle().getChunk(x >> 4, z >> 4).use(); try { MetadataCleaner metadata = new MetadataCleaner(mantleChunk, blocks.getHeight()); for (int localX = 0; localX < blocks.getWidth(); localX++) { @@ -100,7 +101,26 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator chunk, ChunkContext context, int height) { + MetadataCleaner metadata = new MetadataCleaner(chunk, height); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + DimensionStackLayout layout = context.getDimensionStackLayout(x, z); + if (layout == null) { + continue; + } + for (int y = 0; y < height; y++) { + if (layout.isHostFeatureProtectedY(y)) { + metadata.clear(x, y, z); + } + } + } } } @@ -229,17 +249,46 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator> 4; - List> slices = slicesBySection[sectionIndex]; - if (slices == null) { - slices = resolveSlices(sectionIndex); - slicesBySection[sectionIndex] = slices; + if (chunk == null) { + return; } - for (MatterSlice slice : slices) { - slice.set(x, y & 15, z, null); + synchronized (chunk) { + int sectionIndex = y >> 4; + captureNaturalFacts(sectionIndex, x, y & 15, z); + List> slices = slicesBySection[sectionIndex]; + if (slices == null) { + slices = resolveSlices(sectionIndex); + slicesBySection[sectionIndex] = slices; + } + for (MatterSlice slice : slices) { + slice.set(x, y & 15, z, null); + } } } + private void captureNaturalFacts(int sectionIndex, int x, int y, int z) { + Matter section = chunk.get(sectionIndex); + if (section == null) { + return; + } + MatterSlice caverns = section.getSlice(MatterCavern.class); + MatterSlice hydrology = section.getSlice(HydrologyCaveCell.class); + MatterCavern cavern = caverns == null ? null : caverns.get(x, y, z); + HydrologyCaveCell cell = hydrology == null ? null : hydrology.get(x, y, z); + if (cavern == null && cell == null) { + return; + } + MatterSlice journal = section.slice(PreObjectMatterCell.class); + PreObjectMatterCell previous = journal.get(x, y, z); + if (cavern != null) { + previous = previous == null ? PreObjectMatterCell.cavern(cavern) : previous.captureCavern(cavern); + } + if (cell != null) { + previous = previous == null ? PreObjectMatterCell.hydrology(cell) : previous.captureHydrology(cell); + } + journal.set(x, y, z, previous); + } + private List> resolveSlices(int sectionIndex) { Matter section = chunk.get(sectionIndex); if (section == null) { diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisTransitionGeometryActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisTransitionGeometryActuator.java new file mode 100644 index 000000000..b0be83ec5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisTransitionGeometryActuator.java @@ -0,0 +1,157 @@ +package art.arcane.iris.engine.actuator; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineAssignedComponent; +import art.arcane.iris.engine.framework.EngineStage; +import art.arcane.iris.engine.history.BoundaryColumnGeometry; +import art.arcane.iris.engine.history.BoundaryGeometryInfluence; +import art.arcane.iris.engine.history.GenerationBlend; +import art.arcane.iris.engine.history.SavedTerrainChunk; +import art.arcane.iris.engine.history.TransitionGenerationPlan; +import art.arcane.iris.engine.history.TransitionGeometryBlender; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.hunk.Hunk; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; + +public final class IrisTransitionGeometryActuator extends EngineAssignedComponent implements EngineStage { + public IrisTransitionGeometryActuator(Engine engine) { + super(engine, "Terrain Transition"); + } + + @Override + public void close() { + super.close(); + } + + @Override + public void generate(int x, int z, Hunk blocks, Hunk biomes, + boolean multicore, ChunkContext context) { + TransitionGenerationPlan plan = context.getComplex().getTransitionGenerationPlan(); + if (plan == null || !plan.hasTransitionAtChunk(x >> 4, z >> 4)) { + return; + } + GeometrySource source = new GeometrySource(blocks, biomes, getEngine().getMinHeight(), context); + if (plan != null && plan.hasTransitionAtChunk(x >> 4, z >> 4)) { + blendGeometry(x, z, blocks, context, source, plan); + } + } + + public static SavedTerrainChunk capture(int x, int z, Hunk blocks, + Hunk biomes, int minimumY, + ChunkContext context, boolean boundaryOnly) throws IOException { + GeometrySource source = new GeometrySource(blocks, biomes, minimumY, context); + return boundaryOnly + ? SavedTerrainChunk.captureBoundary(x >> 4, z >> 4, minimumY, blocks.getHeight(), "minecraft:noise", source) + : SavedTerrainChunk.capture(x >> 4, z >> 4, minimumY, blocks.getHeight(), "minecraft:noise", source); + } + + private void blendGeometry(int x, int z, Hunk blocks, ChunkContext context, + GeometrySource source, TransitionGenerationPlan plan) { + PlatformRegistries registries = IrisPlatforms.get().registries(); + Map resolved = new HashMap<>(); + for (int localX = 0; localX < blocks.getWidth(); localX++) { + for (int localZ = 0; localZ < blocks.getDepth(); localZ++) { + BoundaryGeometryInfluence influence = plan.geometryAt(x + localX, z + localZ); + if (influence.newTerrainWeight() == 1D || influence.contributions().isEmpty()) { + continue; + } + BoundaryColumnGeometry current = source.column(localX, localZ); + BoundaryColumnGeometry blended = TransitionGeometryBlender.blendColumn( + influence, x + localX, z + localZ, current); + if (blended == current) { + continue; + } + double expectedFloor = GenerationBlend.interpolate( + plan.terrainSampleAt(x + localX, z + localZ).historicalOceanFloorHeight(), + context.getRoundedHeight(localX, localZ), influence.newTerrainWeight()); + int floor = blended.surfaceOffsetNear(expectedFloor); + List voxels = blended.voxels(); + for (int offset = 0; offset < voxels.size(); offset++) { + BoundaryColumnGeometry.Voxel voxel = voxels.get(offset); + PlatformBlockState existing = blocks.getRaw(localX, offset, localZ); + if (existing != null && existing.key().equals(voxel.stateKey())) { + continue; + } + PlatformBlockState replacement = resolved.computeIfAbsent(voxel.stateKey(), registries::blockOrNull); + if (replacement == null) { + throw new IllegalStateException("Terrain boundary requires unavailable block state " + voxel.stateKey()); + } + blocks.setRaw(localX, offset, localZ, replacement); + } + context.setTerrainHeight(localX, localZ, floor); + } + } + } + + private static final class GeometrySource implements SavedTerrainChunk.VoxelSource { + private final Hunk blocks; + private final Hunk biomes; + private final int minimumY; + private final ChunkContext context; + private final Map voxels = new IdentityHashMap<>(); + + private GeometrySource(Hunk blocks, Hunk biomes, int minimumY, ChunkContext context) { + this.blocks = blocks; + this.biomes = biomes; + this.minimumY = minimumY; + this.context = context; + } + + @Override + public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) { + PlatformBlockState state = blocks.getRaw(localX, worldY - minimumY, localZ); + if (state == null) { + state = IrisPlatforms.get().registries().air(); + } + return voxels.computeIfAbsent(state, GeometrySource::encode); + } + + @Override + public OptionalInt groundSurface(int localX, int localZ) { + return OptionalInt.of(Math.max(0, Math.min(blocks.getHeight() - 1, context.getRoundedHeight(localX, localZ)))); + } + + @Override + public String biome(int localX, int worldY, int localZ) throws IOException { + PlatformBiome biome = biomes.getRaw(localX, worldY - minimumY, localZ); + if (biome == null) { + throw new IOException("Natural terrain has no physical biome at " + localX + "," + worldY + "," + localZ); + } + return biome.key(); + } + + private BoundaryColumnGeometry column(int localX, int localZ) { + ArrayList values = new ArrayList<>(blocks.getHeight()); + for (int offset = 0; offset < blocks.getHeight(); offset++) { + values.add(voxel(localX, minimumY + offset, localZ)); + } + return BoundaryColumnGeometry.fromVoxels(minimumY, values); + } + + private static BoundaryColumnGeometry.Voxel encode(PlatformBlockState state) { + boolean custom = state.isCustom(); + PlatformBlockState nativeState = state.placementBaseState(); + if (nativeState != null) { + state = nativeState; + } + BoundaryColumnGeometry.Phase phase = state.isAir() ? BoundaryColumnGeometry.Phase.AIR + : state.isFluid() ? BoundaryColumnGeometry.Phase.FLUID : BoundaryColumnGeometry.Phase.SOLID; + String fluidKey = state.isFluid() ? state.key() + : state.isWaterLogged() ? "minecraft:water[level=0]" : ""; + boolean protectedContent = custom || state.isDecorant() || state.isFoliage() || state.isTreeBlock() + || state.hasTileEntity(); + return new BoundaryColumnGeometry.Voxel(state.key(), phase, fluidKey, protectedContent); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java index 48297bf8b..7b66e1f3c 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java @@ -40,17 +40,16 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator { @Override public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max) { - double localFluidHeight = getComplex().getRiverWaterSurfaceStream().get(realX, realZ); + double localFluidHeight = getEngine().getMantle().getFluidHeight(realX, realZ); if (height != Math.round(localFluidHeight)) { return; } ProceduralStream heightStream = getComplex().getHeightStream(); - ProceduralStream fluidStream = getComplex().getRiverWaterSurfaceStream(); - if (Math.round(heightStream.get(realX1, realZ)) >= Math.round(fluidStream.get(realX1, realZ)) - && Math.round(heightStream.get(realX_1, realZ)) >= Math.round(fluidStream.get(realX_1, realZ)) - && Math.round(heightStream.get(realX, realZ1)) >= Math.round(fluidStream.get(realX, realZ1)) - && Math.round(heightStream.get(realX, realZ_1)) >= Math.round(fluidStream.get(realX, realZ_1))) { + if (Math.round(heightStream.get(realX1, realZ)) >= getEngine().getMantle().getFluidHeight(realX1, realZ) + && Math.round(heightStream.get(realX_1, realZ)) >= getEngine().getMantle().getFluidHeight(realX_1, realZ) + && Math.round(heightStream.get(realX, realZ1)) >= getEngine().getMantle().getFluidHeight(realX, realZ1) + && Math.round(heightStream.get(realX, realZ_1)) >= getEngine().getMantle().getFluidHeight(realX, realZ_1)) { return; } diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java index f1406219c..f7284c2c2 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java @@ -60,7 +60,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator { @BlockCoordinates public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, InferredType inferredType, int height, int max) { - int fluidHeight = (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(realX, realZ)); + int fluidHeight = getEngine().getMantle().getFluidHeight(realX, realZ); if (inferredType == InferredType.SHORE && height < fluidHeight) { return; } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java index 40c865ec6..03808b1db 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java @@ -19,6 +19,7 @@ package art.arcane.iris.engine.framework; + import art.arcane.iris.core.IrisSettings; import art.arcane.iris.engine.framework.render.RenderType; import art.arcane.iris.engine.framework.render.Renderer; @@ -75,6 +76,9 @@ import art.arcane.iris.util.project.stream.ProceduralStream; import org.jetbrains.annotations.Nullable; import java.awt.Color; +import java.nio.file.Path; +import java.util.Optional; +import java.util.OptionalInt; import java.util.Locale; import java.util.Set; import java.util.UUID; @@ -166,12 +170,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, if (context != null && context.getEngine() == this && context.getGenerationSessionId() != 0L) { return generationSessions.continueSession(operation, context.getGenerationSessionId()); } - if (isClosing() || isClosed()) { - throw new GenerationSessionException("Generation session rejected new work for " + operation - + " while the Iris engine is closing.", isClosed()); - } - - return generationSessions.acquire(operation); + return generationSessions.acquireForEngine(this, operation); } default long getGenerationSessionId() { @@ -238,6 +237,8 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, return getTarget().getData(); } + Path getPackSource(); + default IrisWorld getWorld() { return getTarget().getWorld(); } @@ -448,6 +449,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, @BlockCoordinates default int getHeight(int x, int z, boolean ignoreFluid) { + OptionalInt resolved = getComplex().resolvedTerrainHeight(x, z, ignoreFluid); + if (resolved.isPresent()) { + return resolved.getAsInt(); + } DimensionStackContext dimensionStackContext = getDimensionStackContext(); if (dimensionStackContext != null) { return ignoreFluid diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineLifecycleTasks.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineLifecycleTasks.java index d358c8bd3..50bf68c60 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/EngineLifecycleTasks.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineLifecycleTasks.java @@ -3,6 +3,7 @@ package art.arcane.iris.engine.framework; import art.arcane.iris.util.project.context.IrisContext; import java.util.Objects; +import java.util.Optional; import java.util.function.Supplier; public final class EngineLifecycleTasks { @@ -13,10 +14,16 @@ public final class EngineLifecycleTasks { Objects.requireNonNull(engine); Objects.requireNonNull(operation); Objects.requireNonNull(task); - try (GenerationSessionLease lease = engine.acquireGenerationLease(operation); - IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { - task.run(); - return true; + try { + Optional admitted = tryAcquire(engine, operation); + if (admitted.isEmpty()) { + return false; + } + try (GenerationSessionLease lease = admitted.get(); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + task.run(); + return true; + } } catch (GenerationSessionException exception) { if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) { return false; @@ -27,9 +34,17 @@ public final class EngineLifecycleTasks { public static T call(Engine engine, String operation, Supplier task, T unavailable) { Objects.requireNonNull(task); - try (GenerationSessionLease lease = engine.acquireGenerationLease(operation); - IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { - return task.get(); + Objects.requireNonNull(engine); + Objects.requireNonNull(operation); + try { + Optional admitted = tryAcquire(engine, operation); + if (admitted.isEmpty()) { + return unavailable; + } + try (GenerationSessionLease lease = admitted.get(); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + return task.get(); + } } catch (GenerationSessionException exception) { if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) { return unavailable; @@ -37,4 +52,11 @@ public final class EngineLifecycleTasks { throw new IllegalStateException("Iris lifecycle rejected " + operation + ".", exception); } } + + private static Optional tryAcquire(Engine engine, String operation) + throws GenerationSessionException { + GenerationSessionManager sessions = engine.getGenerationSessions(); + return sessions == null ? Optional.of(GenerationSessionLease.noop()) + : sessions.tryAcquireForEngine(engine, operation); + } } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java index 0584865e4..ca33bd01e 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java @@ -19,6 +19,8 @@ package art.arcane.iris.engine.framework; import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.actuator.IrisBiomeActuator; +import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.engine.IrisEngine; import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.util.project.context.ChunkContext; @@ -31,6 +33,21 @@ import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.spi.PlatformBlockState; public interface EngineMode extends Staged { + KList getTerrainStages(); + + void registerTerrainStage(EngineStage stage); + + EngineStage getTransitionStage(); + + default void generateTerrain(int x, int z, Hunk blocks, Hunk biomes, + boolean multicore, ChunkContext context) { + context.setTerrainBiomeOutput(biomes); + for (EngineStage stage : getTerrainStages()) { + stage.generate(x, z, blocks, biomes, multicore, context); + } + getTransitionStage().generate(x, z, blocks, biomes, multicore, context); + } + void close(); Engine getEngine(); @@ -159,8 +176,12 @@ public interface EngineMode extends Staged { return getEngine().getMantle(); } - default void generateMatter(int x, int z, boolean multicore, ChunkContext context) { - getMantle().generateMatter(x, z, multicore, context); + default void generateTerrainMatter(int x, int z, boolean multicore, ChunkContext context) { + getMantle().generateTerrainMatter(x, z, multicore, context); + } + + default void generateContentMatter(int x, int z, boolean multicore, ChunkContext context) { + getMantle().generateContentMatter(x, z, multicore, context); } @BlockCoordinates @@ -180,6 +201,9 @@ public interface EngineMode extends Staged { EngineStage[] stages = getStages().toArray(new EngineStage[0]); try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) { + getComplex().getResolvedTerrain().generate(this, x, z, blocks, biomes, multicore, ctx); + IrisBiomeActuator.publishNaturalMetadata(getEngine(), x, z, biomes, ctx); + ctx.beginContent(); for (EngineStage i : stages) { i.generate(x, z, blocks, biomes, multicore, ctx); } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EnginePlatformHooks.java b/core/src/main/java/art/arcane/iris/engine/framework/EnginePlatformHooks.java index 6b6c613d9..31e3b73c5 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/EnginePlatformHooks.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EnginePlatformHooks.java @@ -19,10 +19,23 @@ package art.arcane.iris.engine.framework; import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.history.SavedTerrainChunk; + +import java.util.concurrent.CompletableFuture; import art.arcane.iris.engine.mantle.MantleComponent; import art.arcane.volmlib.util.collection.KList; public interface EnginePlatformHooks { + default CompletableFuture flushSavedTerrainCapture(Engine engine) { + return CompletableFuture.failedFuture(new UnsupportedOperationException( + "This platform does not support saved terrain checkpoints.")); + } + + default CompletableFuture captureSavedTerrainChunk(Engine engine, int chunkX, int chunkZ) { + return CompletableFuture.failedFuture(new UnsupportedOperationException( + "This platform does not support saved terrain boundary capture.")); + } + /** * World-space piece bounds of every native structure that will generate inside the given XZ rect. Platforms * without native structures return no volumes, which keeps the object veto free on those platforms. diff --git a/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionLease.java b/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionLease.java index 75e45132f..b9efe626e 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionLease.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionLease.java @@ -1,17 +1,20 @@ package art.arcane.iris.engine.framework; public final class GenerationSessionLease implements AutoCloseable { - private static final GenerationSessionLease NOOP = new GenerationSessionLease(null, null, 0L); + private static final GenerationSessionLease NOOP = new GenerationSessionLease(null, null, 0L, null); private final GenerationSessionManager manager; private final GenerationSessionManager.GenerationSessionState state; private final long sessionId; + private final GenerationTransitionGate.Participation participation; private boolean released; - GenerationSessionLease(GenerationSessionManager manager, GenerationSessionManager.GenerationSessionState state, long sessionId) { + GenerationSessionLease(GenerationSessionManager manager, GenerationSessionManager.GenerationSessionState state, + long sessionId, GenerationTransitionGate.Participation participation) { this.manager = manager; this.state = state; this.sessionId = sessionId; + this.participation = participation; this.released = false; } @@ -23,6 +26,12 @@ public final class GenerationSessionLease implements AutoCloseable { return sessionId; } + public void detachThread() { + if (participation != null) { + participation.detachThread(); + } + } + @Override public void close() { if (released || state == null) { @@ -30,6 +39,10 @@ public final class GenerationSessionLease implements AutoCloseable { } released = true; - manager.releaseLease(state); + try { + manager.releaseLease(state); + } finally { + participation.close(); + } } } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionManager.java b/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionManager.java index d571a8f4e..1579e1f16 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionManager.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionManager.java @@ -1,22 +1,94 @@ package art.arcane.iris.engine.framework; +import art.arcane.iris.util.project.context.IrisContext; + +import java.util.Optional; + import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.CancellationException; public final class GenerationSessionManager { private final AtomicLong sessionSequence; private final AtomicReference current; private final Object drainMonitor; + private final GenerationTransitionGate transitionGate; public GenerationSessionManager() { + this(false); + } + + public GenerationSessionManager(boolean studioTransitions) { + this.transitionGate = new GenerationTransitionGate(studioTransitions); this.sessionSequence = new AtomicLong(0L); this.current = new AtomicReference<>(new GenerationSessionState(nextSessionId(), new AtomicBoolean(true), new AtomicInteger(0), new AtomicBoolean(false), new AtomicReference<>(null))); this.drainMonitor = new Object(); } public GenerationSessionLease acquire(String operation) throws GenerationSessionException { + GenerationTransitionGate.Participation participation = transitionGate.enter(); + try { + return acquireAdmitted(operation, participation); + } catch (Throwable failure) { + participation.close(); + throw failure; + } + } + + public GenerationTransitionGate transitionGate() { + return transitionGate; + } + + public GenerationSessionLease acquireForEngine(Engine engine, String operation) throws GenerationSessionException { + GenerationTransitionGate.Participation participation; + try { + participation = transitionGate.enter(); + } catch (CancellationException cancellation) { + GenerationSessionException failure = new GenerationSessionException( + "Generation session cancelled while waiting to run " + operation + ".", + engine.isClosing() || engine.isClosed()); + failure.initCause(cancellation); + throw failure; + } + try { + if (engine.isClosing() || engine.isClosed()) { + throw new GenerationSessionException("Generation session rejected new work for " + operation + + " while the Iris engine is closing.", engine.isClosed()); + } + return acquireAdmitted(operation, participation); + } catch (Throwable failure) { + participation.close(); + throw failure; + } + } + + public Optional tryAcquireForEngine(Engine engine, String operation) + throws GenerationSessionException { + Optional entered = transitionGate.tryEnter(); + if (entered.isEmpty()) { + return Optional.empty(); + } + GenerationTransitionGate.Participation participation = entered.get(); + try { + if (engine.isClosing() || engine.isClosed()) { + throw new GenerationSessionException("Generation session rejected new work for " + operation + + " while the Iris engine is closing.", engine.isClosed()); + } + IrisContext context = IrisContext.get(); + return Optional.of(context != null && context.getEngine() == engine && context.getGenerationSessionId() != 0L + ? continueAdmittedSession(operation, context.getGenerationSessionId(), participation) + : acquireAdmitted(operation, participation)); + } catch (Throwable failure) { + participation.close(); + throw failure; + } + } + + private GenerationSessionLease acquireAdmitted(String operation, + GenerationTransitionGate.Participation participation) + throws GenerationSessionException { while (true) { GenerationSessionState state = current.get(); if (state == null || !state.accepting().get()) { @@ -34,11 +106,23 @@ public final class GenerationSessionManager { throw rejected(operation, state); } - return new GenerationSessionLease(this, state, state.sessionId()); + return new GenerationSessionLease(this, state, state.sessionId(), participation); } } public GenerationSessionLease continueSession(String operation, long sessionId) throws GenerationSessionException { + GenerationTransitionGate.Participation participation = transitionGate.continueAdmittedWork(); + try { + return continueAdmittedSession(operation, sessionId, participation); + } catch (Throwable failure) { + participation.close(); + throw failure; + } + } + + private GenerationSessionLease continueAdmittedSession(String operation, long sessionId, + GenerationTransitionGate.Participation participation) + throws GenerationSessionException { while (true) { GenerationSessionState state = current.get(); if (state == null || state.sessionId() != sessionId) { @@ -51,7 +135,7 @@ public final class GenerationSessionManager { continue; } - return new GenerationSessionLease(this, state, state.sessionId()); + return new GenerationSessionLease(this, state, state.sessionId(), participation); } } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/GenerationTransitionGate.java b/core/src/main/java/art/arcane/iris/engine/framework/GenerationTransitionGate.java new file mode 100644 index 000000000..9441ab3bc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/GenerationTransitionGate.java @@ -0,0 +1,200 @@ +package art.arcane.iris.engine.framework; + +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CancellationException; +import java.util.concurrent.TimeUnit; + +public final class GenerationTransitionGate { + private final boolean enabled; + private final Participation noop = new Participation(null); + private final Map participants = new IdentityHashMap<>(); + private int active; + private int waitingTransitions; + private Thread transitionOwner; + + public GenerationTransitionGate(boolean enabled) { + this.enabled = enabled; + } + + public Participation enter() { + if (!enabled) { + return noop; + } + synchronized (this) { + return enter(false); + } + } + + public Optional tryEnter() { + if (!enabled) { + return Optional.of(noop); + } + synchronized (this) { + Thread owner = Thread.currentThread(); + return blocked(owner, false) ? Optional.empty() : Optional.of(admit(owner)); + } + } + + Participation continueAdmittedWork() { + if (!enabled) { + return noop; + } + synchronized (this) { + return enter(true); + } + } + + private Participation enter(boolean continuation) { + Thread owner = Thread.currentThread(); + while (blocked(owner, continuation)) { + awaitChange(); + } + return admit(owner); + } + + private boolean blocked(Thread owner, boolean continuation) { + return transitionOwner != null && transitionOwner != owner + || waitingTransitions > 0 && transitionOwner != owner + && !participants.containsKey(owner) && !(continuation && active > 0); + } + + private Participation admit(Thread owner) { + active++; + participants.merge(owner, 1, Integer::sum); + return new Participation(owner); + } + + public synchronized Transition beginTransition(long timeoutMillis) { + if (!enabled) { + throw new IllegalStateException("Live generation transitions are disabled for this engine."); + } + if (timeoutMillis < 1L) { + throw new IllegalArgumentException("Generation transition timeout must be positive."); + } + Thread owner = Thread.currentThread(); + if (participants.containsKey(owner) || transitionOwner == owner) { + throw new IllegalStateException("Cannot reload generation from inside an active generation operation."); + } + waitingTransitions++; + long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis); + try { + while (transitionOwner != null || active > 0) { + long remaining = deadline - System.nanoTime(); + if (remaining <= 0L) { + throw new IllegalStateException("Timed out draining generation for a Studio update after " + + timeoutMillis + "ms; active operations=" + active + "."); + } + try { + TimeUnit.NANOSECONDS.timedWait(this, remaining); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while draining generation for a Studio update.", failure); + } + } + transitionOwner = owner; + return new Transition(owner); + } finally { + waitingTransitions--; + notifyAll(); + } + } + + private void awaitChange() { + try { + wait(); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + CancellationException cancellation = new CancellationException( + "Interrupted while waiting for generation admission."); + cancellation.initCause(failure); + throw cancellation; + } + } + + public final class Participation implements AutoCloseable { + private final Thread owner; + private boolean closed; + private boolean attached = true; + + private Participation(Thread owner) { + this.owner = owner; + } + + public Participation attach() { + if (owner == null) { + return this; + } + synchronized (GenerationTransitionGate.this) { + if (closed) { + throw new IllegalStateException("Generation participation has already ended."); + } + return enter(true); + } + } + + public void detachThread() { + if (owner == null) { + return; + } + synchronized (GenerationTransitionGate.this) { + if (!closed && attached) { + detach(); + } + } + } + + private void detach() { + int depth = participants.get(owner); + if (depth == 1) { + participants.remove(owner); + } else { + participants.put(owner, depth - 1); + } + attached = false; + } + + @Override + public void close() { + if (owner == null) { + return; + } + synchronized (GenerationTransitionGate.this) { + if (closed) { + return; + } + closed = true; + if (attached) { + detach(); + } + active--; + GenerationTransitionGate.this.notifyAll(); + } + } + } + + public final class Transition implements AutoCloseable { + private final Thread owner; + private boolean closed; + + private Transition(Thread owner) { + this.owner = owner; + } + + @Override + public void close() { + synchronized (GenerationTransitionGate.this) { + if (closed) { + return; + } + if (Thread.currentThread() != owner || transitionOwner != owner) { + throw new IllegalStateException("Generation transition belongs to another thread."); + } + closed = true; + transitionOwner = null; + GenerationTransitionGate.this.notifyAll(); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/IrisEngineMode.java b/core/src/main/java/art/arcane/iris/engine/framework/IrisEngineMode.java index dd5aca0d2..9112f589f 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/IrisEngineMode.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/IrisEngineMode.java @@ -19,15 +19,20 @@ package art.arcane.iris.engine.framework; import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.engine.actuator.IrisTransitionGeometryActuator; public abstract class IrisEngineMode implements EngineMode { private final Engine engine; + private final EngineStage transitionStage; private final KList stages; + private final KList terrainStages; private boolean closed; public IrisEngineMode(Engine engine) { this.engine = engine; + this.transitionStage = new IrisTransitionGeometryActuator(engine); this.stages = new KList<>(); + this.terrainStages = new KList<>(); this.closed = false; } @@ -39,6 +44,9 @@ public abstract class IrisEngineMode implements EngineMode { closed = true; dump(); + terrainStages.forEach(EngineStage::close); + terrainStages.clear(); + transitionStage.close(); } @Override @@ -51,6 +59,21 @@ public abstract class IrisEngineMode implements EngineMode { return stages; } + @Override + public EngineStage getTransitionStage() { + return transitionStage; + } + + @Override + public KList getTerrainStages() { + return terrainStages; + } + + @Override + public void registerTerrainStage(EngineStage stage) { + terrainStages.add(stage); + } + @Override public void registerStage(EngineStage stage) { stages.add(stage); diff --git a/core/src/main/java/art/arcane/iris/engine/history/BoundaryColumnGeometry.java b/core/src/main/java/art/arcane/iris/engine/history/BoundaryColumnGeometry.java new file mode 100644 index 000000000..8f13e97fd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/history/BoundaryColumnGeometry.java @@ -0,0 +1,258 @@ +package art.arcane.iris.engine.history; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeSet; + +public final class BoundaryColumnGeometry { + public static final int MAXIMUM_HEIGHT = 65_536; + private static final Comparator VOXEL_ORDER = Comparator.comparing(Voxel::stateKey) + .thenComparing(Voxel::phase) + .thenComparing(Voxel::fluidStateKey) + .thenComparing(Voxel::protectedContent); + private static final Voxel AIR = new Voxel("minecraft:air", Phase.AIR, "", false); + private static final BoundaryColumnGeometry EMPTY = new BoundaryColumnGeometry(0, List.of(), new int[0], new short[0]); + + private final int minimumY; + private final List palette; + private final int[] runEnds; + private final short[] paletteIndices; + + public BoundaryColumnGeometry(int minimumY, List palette, int[] runEnds, short[] paletteIndices) { + this.minimumY = minimumY; + this.palette = List.copyOf(Objects.requireNonNull(palette, "Geometry palette")); + this.runEnds = Objects.requireNonNull(runEnds, "Geometry run ends").clone(); + this.paletteIndices = Objects.requireNonNull(paletteIndices, "Geometry palette indices").clone(); + validate(); + } + + public static BoundaryColumnGeometry empty() { + return EMPTY; + } + + public static BoundaryColumnGeometry fromVoxels(int minimumY, List voxels) { + Objects.requireNonNull(voxels, "Geometry voxels"); + if (voxels.size() > MAXIMUM_HEIGHT) { + throw new IllegalArgumentException("Geometry column exceeds maximum height"); + } + TreeSet used = new TreeSet<>(VOXEL_ORDER); + Voxel previous = null; + for (Voxel voxel : voxels) { + if (previous == null || !previous.equals(voxel)) { + used.add(voxel); + previous = voxel; + } + } + List palette = List.copyOf(used); + Map indices = new HashMap<>(palette.size()); + for (int index = 0; index < palette.size(); index++) { + if (index > Short.MAX_VALUE) { + throw new IllegalArgumentException("Geometry palette exceeds compact index capacity"); + } + indices.put(palette.get(index), (short) index); + } + int[] ends = new int[voxels.size()]; + short[] values = new short[voxels.size()]; + int runCount = 0; + previous = null; + for (int offset = 0; offset < voxels.size(); offset++) { + Voxel voxel = voxels.get(offset); + if (previous == null || !previous.equals(voxel)) { + values[runCount++] = indices.get(voxel); + previous = voxel; + } + ends[runCount - 1] = offset + 1; + } + return new BoundaryColumnGeometry(minimumY, palette, + Arrays.copyOf(ends, runCount), Arrays.copyOf(values, runCount)); + } + + public int minimumY() { + return minimumY; + } + + public int height() { + return runEnds.length == 0 ? 0 : runEnds[runEnds.length - 1]; + } + + public List palette() { + return palette; + } + + public int[] runEnds() { + return runEnds.clone(); + } + + public short[] paletteIndices() { + return paletteIndices.clone(); + } + + public Voxel voxelAt(int worldY) { + long offset = (long) worldY - minimumY; + if (offset < 0 || offset >= height()) { + return AIR; + } + return palette.get(paletteIndices[runAt((int) offset)]); + } + + public boolean isEnclosedOpenAt(int worldY) { + long offset = (long) worldY - minimumY; + if (offset < 0 || offset >= height() || voxelAt(worldY).phase() == Phase.SOLID) { + return false; + } + return hasSolidAbove(worldY); + } + + public boolean hasSolidAbove(int worldY) { + long offset = (long) worldY - minimumY; + if (offset >= height() - 1L) { + return false; + } + int startRun = offset < 0 ? 0 : runAt((int) offset); + for (int run = startRun; run < runEnds.length; run++) { + if (runEnds[run] <= offset + 1L) { + continue; + } + if (palette.get(paletteIndices[run]).phase() == Phase.SOLID) { + return true; + } + } + return false; + } + + public List voxels() { + ArrayList values = new ArrayList<>(height()); + int start = 0; + for (int run = 0; run < runEnds.length; run++) { + Voxel value = palette.get(paletteIndices[run]); + for (int offset = start; offset < runEnds[run]; offset++) { + values.add(value); + } + start = runEnds[run]; + } + return List.copyOf(values); + } + + public int surfaceOffsetNear(double expectedOffset) { + if (!Double.isFinite(expectedOffset) || height() == 0) { + throw new IllegalArgumentException("A surface reference requires finite height and nonempty geometry"); + } + int nearest = -1; + double nearestDistance = Double.POSITIVE_INFINITY; + for (int offset = 0; offset + 1 < height(); offset++) { + Voxel current = voxelAt(minimumY + offset); + Voxel above = voxelAt(minimumY + offset + 1); + if (current.phase() != Phase.SOLID || current.protectedContent() + || above.phase() == Phase.SOLID && !above.protectedContent()) { + continue; + } + double distance = Math.abs(offset - expectedOffset); + if (distance < nearestDistance) { + nearest = offset; + nearestDistance = distance; + } + } + if (nearest >= 0) { + return nearest; + } + for (int offset = height() - 1; offset >= 0; offset--) { + Voxel current = voxelAt(minimumY + offset); + if (current.phase() == Phase.SOLID && !current.protectedContent()) { + return offset; + } + } + return 0; + } + + public double[] solidDistances() { + return phaseDistances(Phase.SOLID); + } + + public double[] fluidDistances() { + return phaseDistances(Phase.FLUID); + } + + private double[] phaseDistances(Phase phase) { + int height = height(); + double[] distances = new double[height]; + boolean[] solid = new boolean[height]; + int start = 0; + for (int run = 0; run < runEnds.length; run++) { + Voxel value = palette.get(paletteIndices[run]); + boolean occupied = value.phase() == phase && !value.protectedContent(); + Arrays.fill(solid, start, runEnds[run], occupied); + start = runEnds[run]; + } + double distance = height + 0.5D; + for (int offset = 0; offset < height; offset++) { + distance = offset > 0 && solid[offset] != solid[offset - 1] ? 0.5D : distance + 1D; + distances[offset] = distance; + } + distance = height + 0.5D; + for (int offset = height - 1; offset >= 0; offset--) { + distance = offset + 1 < height && solid[offset] != solid[offset + 1] ? 0.5D : distance + 1D; + distances[offset] = Math.min(distances[offset], distance) * (solid[offset] ? 1D : -1D); + } + return distances; + } + + @Override + public boolean equals(Object compared) { + return this == compared || compared instanceof BoundaryColumnGeometry other + && minimumY == other.minimumY && palette.equals(other.palette) + && Arrays.equals(runEnds, other.runEnds) && Arrays.equals(paletteIndices, other.paletteIndices); + } + + @Override + public int hashCode() { + return Objects.hash(minimumY, palette, Arrays.hashCode(runEnds), Arrays.hashCode(paletteIndices)); + } + + private int runAt(int offset) { + int index = Arrays.binarySearch(runEnds, offset + 1); + return index >= 0 ? index : -index - 1; + } + + private void validate() { + if (palette.size() > Short.MAX_VALUE + 1 || runEnds.length > MAXIMUM_HEIGHT + || runEnds.length != paletteIndices.length) { + throw new IllegalArgumentException("Invalid compact geometry sizes"); + } + int previousEnd = 0; + for (int run = 0; run < runEnds.length; run++) { + if (runEnds[run] <= previousEnd || runEnds[run] > MAXIMUM_HEIGHT + || paletteIndices[run] < 0 || paletteIndices[run] >= palette.size()) { + throw new IllegalArgumentException("Invalid compact geometry run"); + } + previousEnd = runEnds[run]; + } + if (height() > 0) { + Math.toIntExact((long) minimumY + height() - 1L); + } + } + + public enum Phase { + AIR, + SOLID, + FLUID + } + + public record Voxel(String stateKey, Phase phase, String fluidStateKey, boolean protectedContent) { + public Voxel { + Objects.requireNonNull(stateKey, "Block state key"); + Objects.requireNonNull(phase, "Voxel phase"); + Objects.requireNonNull(fluidStateKey, "Fluid state key"); + if (stateKey.isBlank()) { + throw new IllegalArgumentException("Block state key cannot be blank"); + } + if (phase == Phase.FLUID && fluidStateKey.isBlank()) { + throw new IllegalArgumentException("Fluid voxels require a fluid state key"); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/history/BoundaryGeometryInfluence.java b/core/src/main/java/art/arcane/iris/engine/history/BoundaryGeometryInfluence.java new file mode 100644 index 000000000..89ea4038f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/history/BoundaryGeometryInfluence.java @@ -0,0 +1,41 @@ +package art.arcane.iris.engine.history; + +import java.util.List; +import java.util.Objects; + +public record BoundaryGeometryInfluence(double newTerrainWeight, double openingWeight, List contributions) { + private static final BoundaryGeometryInfluence NONE = new BoundaryGeometryInfluence(1D, 1D, List.of()); + + public BoundaryGeometryInfluence { + if (!Double.isFinite(newTerrainWeight) || newTerrainWeight < 0D || newTerrainWeight > 1D) { + throw new IllegalArgumentException("Geometry blend weight must be between zero and one"); + } + if (!Double.isFinite(openingWeight) || openingWeight < 0D || openingWeight > 1D) { + throw new IllegalArgumentException("Opening blend weight must be between zero and one"); + } + contributions = List.copyOf(contributions); + double total = 0D; + for (Contribution contribution : contributions) { + total += contribution.weight(); + } + if (!contributions.isEmpty() && Math.abs(total - 1D) > 1E-9D) { + throw new IllegalArgumentException("Geometry contribution weights must sum to one"); + } + } + + public static BoundaryGeometryInfluence none() { + return NONE; + } + + public record Contribution(BoundaryColumnGeometry geometry, double weight) { + public Contribution { + Objects.requireNonNull(geometry, "Historical geometry"); + if (geometry.height() == 0) { + throw new IllegalArgumentException("Historical boundary geometry is missing"); + } + if (!Double.isFinite(weight) || weight <= 0D || weight > 1D) { + throw new IllegalArgumentException("Geometry contribution weight must be positive and at most one"); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/history/ChunkGenerationOwnership.java b/core/src/main/java/art/arcane/iris/engine/history/ChunkGenerationOwnership.java index fc4425dfd..e7c3e2ac9 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/ChunkGenerationOwnership.java +++ b/core/src/main/java/art/arcane/iris/engine/history/ChunkGenerationOwnership.java @@ -8,6 +8,7 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; +import java.util.Set; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -184,6 +185,34 @@ public final class ChunkGenerationOwnership { } } + public int discardUnstoredClaims(WorldChunkInventory stored, Set activationIds) throws IOException { + Objects.requireNonNull(stored, "stored chunks"); + Set selected = Set.copyOf(activationIds); + for (long activation : selected) { + requireActivation(activation); + } + lock.writeLock().lock(); + try { + int removed = 0; + for (long regionKey : orderedRegionKeys()) { + RegionGenerationOwnership region = loadExistingRegion(regionX(regionKey), regionZ(regionKey)); + if (region == null) { + continue; + } + int count = region.discardUnstoredClaims(stored, selected); + if (count > 0) { + region.persist(directory); + explicitChunkCount -= count; + regionAssignmentCounts.put(regionKey, region.assignmentCount()); + removed += count; + } + } + return removed; + } finally { + lock.writeLock().unlock(); + } + } + public int persist() throws IOException { lock.writeLock().lock(); try { diff --git a/core/src/main/java/art/arcane/iris/engine/history/ChunkGenerationSemantics.java b/core/src/main/java/art/arcane/iris/engine/history/ChunkGenerationSemantics.java index 176317ccd..30c4393ed 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/ChunkGenerationSemantics.java +++ b/core/src/main/java/art/arcane/iris/engine/history/ChunkGenerationSemantics.java @@ -6,11 +6,9 @@ import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Collections; import java.util.Comparator; -import java.util.Locale; import java.util.Objects; import java.util.Set; import java.util.TreeSet; -import java.util.regex.Pattern; public final class ChunkGenerationSemantics { static final int MAX_KEY_BYTES = 1_024; @@ -18,12 +16,15 @@ public final class ChunkGenerationSemantics { static final int MAX_RIVER_FEATURES = 16_384; static final int MAX_STRUCTURES = 4_096; - private static final Pattern RESOURCE_KEY_PATTERN = Pattern.compile("[a-z0-9_][a-z0-9._:/#@-]*"); private static final Comparator STRUCTURE_COMPARATOR = Comparator .comparing(StructureOccurrence::key) .thenComparingInt(occurrence -> occurrence.position().x()) .thenComparingInt(occurrence -> occurrence.position().z()) .thenComparingInt(occurrence -> occurrence.position().y()); + private static final Comparator POI_COMPARATOR = Comparator.comparing(PointOfInterest::key) + .thenComparingInt(point -> point.position().x()) + .thenComparingInt(point -> point.position().z()) + .thenComparingInt(point -> point.position().y()); private static final Comparator RIVER_FEATURE_COMPARATOR = Comparator .comparing(RiverFeatureOccurrence::profileKey) .thenComparing(RiverFeatureOccurrence::type) @@ -43,6 +44,7 @@ public final class ChunkGenerationSemantics { private final Set objectKeys; private final Set riverFeatures; private final Set structures; + private final Set pointsOfInterest; private ChunkGenerationSemantics(Builder builder) { chunkX = builder.chunkX; @@ -56,6 +58,7 @@ public final class ChunkGenerationSemantics { objectKeys = immutableKeys(builder.objectKeys, "object"); riverFeatures = immutableRiverFeatures(builder.riverFeatures); structures = immutableStructures(builder.structures); + pointsOfInterest = immutablePoints(builder.pointsOfInterest); } public static Builder builder(int chunkX, int chunkZ, long activationId) { @@ -102,6 +105,10 @@ public final class ChunkGenerationSemantics { return riverFeatures; } + public Set pointsOfInterest() { + return pointsOfInterest; + } + public Set structures() { return structures; } @@ -129,7 +136,8 @@ public final class ChunkGenerationSemantics { || !riverProfileKeys.containsAll(requiredUpdate.riverProfileKeys) || !objectKeys.containsAll(requiredUpdate.objectKeys) || !riverFeatures.containsAll(requiredUpdate.riverFeatures) - || !structures.containsAll(requiredUpdate.structures); + || !structures.containsAll(requiredUpdate.structures) + || !pointsOfInterest.containsAll(requiredUpdate.pointsOfInterest); if (sealed && addsFacts) { throw new IllegalStateException( "Chunk " + chunkX + "," + chunkZ + " has sealed generation semantics" @@ -153,7 +161,9 @@ public final class ChunkGenerationSemantics { .addRiverFeatures(riverFeatures) .addRiverFeatures(requiredUpdate.riverFeatures) .addStructures(structures) - .addStructures(requiredUpdate.structures); + .addStructures(requiredUpdate.structures) + .addPointsOfInterest(pointsOfInterest) + .addPointsOfInterest(requiredUpdate.pointsOfInterest); if (sealed || requiredUpdate.sealed) { merged.seal(); } @@ -178,7 +188,8 @@ public final class ChunkGenerationSemantics { && riverProfileKeys.equals(semantics.riverProfileKeys) && objectKeys.equals(semantics.objectKeys) && riverFeatures.equals(semantics.riverFeatures) - && structures.equals(semantics.structures); + && structures.equals(semantics.structures) + && pointsOfInterest.equals(semantics.pointsOfInterest); } @Override @@ -194,7 +205,8 @@ public final class ChunkGenerationSemantics { riverProfileKeys, objectKeys, riverFeatures, - structures + structures, + pointsOfInterest ); } @@ -230,11 +242,11 @@ public final class ChunkGenerationSemantics { if (!requiredKey.equals(requiredKey.trim())) { throw new IllegalArgumentException("Generation semantic resource keys must not contain surrounding whitespace: " + requiredKey); } - if (!requiredKey.equals(requiredKey.toLowerCase(Locale.ROOT))) { - throw new IllegalArgumentException("Generation semantic resource keys must be lowercase: " + requiredKey); - } - if (!RESOURCE_KEY_PATTERN.matcher(requiredKey).matches()) { - throw new IllegalArgumentException("Generation semantic resource key is not normalized: " + requiredKey); + for (int index = 0; index < requiredKey.length(); index++) { + char character = requiredKey.charAt(index); + if (character == '\\' || Character.isISOControl(character)) { + throw new IllegalArgumentException("Generation semantic resource key contains an invalid character"); + } } if (requiredKey.getBytes(StandardCharsets.UTF_8).length > MAX_KEY_BYTES) { throw new IllegalArgumentException("Generation semantic resource key is too long"); @@ -283,6 +295,28 @@ public final class ChunkGenerationSemantics { return Collections.unmodifiableSet(validated); } + private static Set immutablePoints(Collection points) { + if (points.size() > MAX_STRUCTURES) { + throw new IllegalArgumentException("A chunk contains too many points of interest"); + } + TreeSet copy = new TreeSet<>(POI_COMPARATOR); + for (PointOfInterest point : points) { + copy.add(Objects.requireNonNull(point, "point of interest")); + } + return Collections.unmodifiableSet(copy); + } + + static Comparator pointComparator() { + return POI_COMPARATOR; + } + + public record PointOfInterest(String key, BlockPosition position) { + public PointOfInterest { + key = requireResourceKey(key); + position = Objects.requireNonNull(position, "position"); + } + } + public record BlockPosition(int x, int y, int z) { } @@ -317,6 +351,7 @@ public final class ChunkGenerationSemantics { private final Set objectKeys; private final Set riverFeatures; private final Set structures; + private final Set pointsOfInterest; private boolean sealed; private Builder(int chunkX, int chunkZ, long activationId) { @@ -333,6 +368,7 @@ public final class ChunkGenerationSemantics { objectKeys = new TreeSet<>(); riverFeatures = new TreeSet<>(RIVER_FEATURE_COMPARATOR); structures = new TreeSet<>(STRUCTURE_COMPARATOR); + pointsOfInterest = new TreeSet<>(POI_COMPARATOR); } public Builder addSurfaceBiome(String key) { @@ -450,6 +486,22 @@ public final class ChunkGenerationSemantics { return this; } + public Builder addPointOfInterest(PointOfInterest point) { + Objects.requireNonNull(point, "point of interest"); + if (pointsOfInterest.size() >= MAX_STRUCTURES && !pointsOfInterest.contains(point)) { + throw new IllegalArgumentException("A chunk contains too many points of interest"); + } + pointsOfInterest.add(point); + return this; + } + + public Builder addPointsOfInterest(Collection points) { + for (PointOfInterest point : points) { + addPointOfInterest(point); + } + return this; + } + public Builder seal() { sealed = true; return this; diff --git a/core/src/main/java/art/arcane/iris/engine/history/DiskBoundaryCapture.java b/core/src/main/java/art/arcane/iris/engine/history/DiskBoundaryCapture.java new file mode 100644 index 000000000..ea821e09d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/history/DiskBoundaryCapture.java @@ -0,0 +1,44 @@ +package art.arcane.iris.engine.history; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +public final class DiskBoundaryCapture implements TerrainBoundarySignatureStore.SignatureSampler { + private static final int MAXIMUM_CACHED_CHUNKS = 32; + + private final Path dimensionRoot; + private final int minimumY; + private final int height; + private final LinkedHashMap chunks = new LinkedHashMap<>(32, 0.75F, true); + + public DiskBoundaryCapture(Path dimensionRoot, int minimumY, int height) { + this.dimensionRoot = Objects.requireNonNull(dimensionRoot, "dimension root"); + this.minimumY = minimumY; + this.height = height; + } + + @Override + public TerrainBoundarySignature sample(int blockX, int blockZ) throws IOException { + int chunkX = Math.floorDiv(blockX, 16); + int chunkZ = Math.floorDiv(blockZ, 16); + long key = ChunkGenerationOwnership.packChunk(chunkX, chunkZ); + SavedTerrainChunk chunk = chunks.get(key); + if (chunk == null) { + chunk = SavedTerrainChunk.read(dimensionRoot, chunkX, chunkZ, minimumY, height); + chunks.put(key, chunk); + if (chunks.size() > MAXIMUM_CACHED_CHUNKS) { + Map.Entry oldest = chunks.firstEntry(); + chunks.remove(oldest.getKey()); + } + } + return chunk.column(blockX, blockZ); + } + + @Override + public void close() { + chunks.clear(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationBlend.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationBlend.java index ed5a8f531..be8ae1cf3 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationBlend.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationBlend.java @@ -58,6 +58,23 @@ public final class GenerationBlend { return (int) Math.round(historicalHeight + ((double) newHeight - historicalHeight) * newEpochWeight); } + public static boolean usesHistoricalMaterial(int blockX, int blockY, int blockZ, double newWeight) { + validateWeight(newWeight); + if (newWeight == 0D) { + return true; + } + if (newWeight == 1D) { + return false; + } + long hash = Math.floorDiv(blockX, 8) * 341873128712L + ^ Math.floorDiv(blockY, 8) * 132897987541L + ^ Math.floorDiv(blockZ, 8) * 42317861L; + hash = (hash ^ (hash >>> 30)) * 0xbf58476d1ce4e5b9L; + hash = (hash ^ (hash >>> 27)) * 0x94d049bb133111ebL; + hash ^= hash >>> 31; + return newWeight < (hash >>> 11) * 0x1.0p-53; + } + private static void validateWeight(double weight) { if (!Double.isFinite(weight) || weight < 0D || weight > 1D) { throw new IllegalArgumentException("Blend weight must be finite and between zero and one"); diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationBoundarySignatureSampler.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationBoundarySignatureSampler.java index ee3dfc61e..587a64264 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationBoundarySignatureSampler.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationBoundarySignatureSampler.java @@ -7,4 +7,8 @@ import java.io.IOException; @FunctionalInterface public interface GenerationBoundarySignatureSampler { TerrainBoundarySignature sample(IrisEngine engine, int blockX, int blockZ) throws IOException; + + default TerrainBoundarySignatureStore.SignatureSampler open(IrisEngine engine) throws IOException { + return (blockX, blockZ) -> sample(engine, blockX, blockZ); + } } diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationBoundaryStore.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationBoundaryStore.java index 8afebe1ef..b3e20596b 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationBoundaryStore.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationBoundaryStore.java @@ -632,13 +632,16 @@ public final class GenerationBoundaryStore { } @Override - public synchronized boolean contains(int chunkX, int chunkZ) throws IOException { + public boolean contains(int chunkX, int chunkZ) throws IOException { long regionKey = packRegion(chunkX >> 5, chunkZ >> 5); String hash = shardHashes.get(regionKey); if (hash == null) { return false; } - byte[] mask = load(regionKey, hash); + byte[] mask; + synchronized (this) { + mask = load(regionKey, hash); + } int localIndex = (Math.floorMod(chunkZ, REGION_SIDE) << 5) | Math.floorMod(chunkX, REGION_SIDE); return (mask[localIndex >>> 3] & (1 << (localIndex & 7))) != 0; diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationKernelSourceSeal.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationBuildRevision.java similarity index 80% rename from core/src/main/java/art/arcane/iris/engine/history/GenerationKernelSourceSeal.java rename to core/src/main/java/art/arcane/iris/engine/history/GenerationBuildRevision.java index 81c34d481..00dfb73e0 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationKernelSourceSeal.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationBuildRevision.java @@ -11,31 +11,31 @@ import java.util.List; import java.util.Objects; import java.util.regex.Pattern; -final class GenerationKernelSourceSeal { +final class GenerationBuildRevision { private static final int MAXIMUM_BYTES = 8 * 1024 * 1024; private static final Pattern SHA256 = Pattern.compile("[0-9a-f]{64}"); - private GenerationKernelSourceSeal() { + private GenerationBuildRevision() { } static String requireFingerprint(int abi, Class factory) { Descriptor descriptor = load(abi); if (!descriptor.factoryClass().equals(factory.getName())) { - throw new IllegalStateException("Generation ABI " + abi + " is sealed for " + descriptor.factoryClass() + "."); + throw new IllegalStateException("Generation ABI " + abi + " declares factory " + descriptor.factoryClass() + "."); } return descriptor.fingerprint(); } static Descriptor load(int abi) { - String resource = "/META-INF/iris/generation-kernels/abi-" + abi + ".seal"; - try (InputStream input = GenerationKernelSourceSeal.class.getResourceAsStream(resource)) { + String resource = "/META-INF/iris/generation-kernels/abi-" + abi + ".revision"; + try (InputStream input = GenerationBuildRevision.class.getResourceAsStream(resource)) { if (input == null) { - throw new IOException("Missing generation kernel source seal " + resource + "."); + throw new IOException("Missing generation kernel build revision " + resource + "."); } byte[] bytes = input.readNBytes(MAXIMUM_BYTES + 1); String[] lines = new String(bytes, StandardCharsets.UTF_8).split("\n", -1); if (lines.length < 4 || !lines[2].startsWith("factory\t")) { - throw new IOException("Generation kernel seal has no factory identity."); + throw new IOException("Generation build revision has no factory identity."); } String factoryClass = lines[2].substring("factory\t".length()); if (!factoryClass.matches("[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)+")) { @@ -52,7 +52,7 @@ final class GenerationKernelSourceSeal { } return new Descriptor(abi, factoryClass, List.copyOf(algorithms), fingerprint); } catch (IOException failure) { - throw new IllegalStateException("Cannot load sealed Iris generation ABI " + abi + ".", failure); + throw new IllegalStateException("Cannot load Iris generation build revision for ABI " + abi + ".", failure); } } @@ -60,13 +60,13 @@ final class GenerationKernelSourceSeal { Objects.requireNonNull(factoryClass, "factoryClass"); Objects.requireNonNull(bytes, "bytes"); if (abi < 1 || bytes.length == 0 || bytes.length > MAXIMUM_BYTES) { - throw new IOException("Invalid generation kernel seal size or ABI."); + throw new IOException("Invalid generation build revision size or ABI."); } String source = new String(bytes, StandardCharsets.UTF_8); - String prefix = "iris-generation-kernel-seal-v1\nabi\t" + abi + "\nfactory\t" + factoryClass + "\n"; + String prefix = "iris-generation-build-revision-v1\nabi\t" + abi + "\nfactory\t" + factoryClass + "\n"; if (!source.startsWith(prefix) || !source.endsWith("\n") || source.indexOf('\r') >= 0 || !source.contains("\nsource\t") || !source.contains("\ndependency\t")) { - throw new IOException("Generation kernel seal does not match ABI " + abi + " and factory " + factoryClass + "."); + throw new IOException("Generation build revision does not match ABI " + abi + " and factory " + factoryClass + "."); } requireCanonicalEntries(source.substring(prefix.length())); try { @@ -79,7 +79,7 @@ final class GenerationKernelSourceSeal { private static void requireCanonicalEntries(String source) throws IOException { String[] entries = source.split("\n", -1); if (entries.length > 30_000) { - throw new IOException("Generation kernel seal has too many entries."); + throw new IOException("Generation build revision has too many entries."); } int previousType = -1; String previousKey = ""; @@ -92,7 +92,7 @@ final class GenerationKernelSourceSeal { if (fields[0].equals("algorithm")) { if (previousType != -1 || fields.length != 3 || !fields[1].matches("[1-9][0-9]{0,8}") || !fields[2].matches("[1-9][0-9]{0,8}")) { - throw new IOException("Generation kernel seal contains an invalid algorithm version."); + throw new IOException("Generation build revision contains an invalid algorithm version."); } int rng = Integer.parseInt(fields[1]); int seed = Integer.parseInt(fields[2]); @@ -116,14 +116,14 @@ final class GenerationKernelSourceSeal { || fields[1].chars().anyMatch(Character::isWhitespace) || (type == previousType && fields[1].compareTo(previousKey) <= 0) || (type >= 2 && !SHA256.matcher(fields[2]).matches())) { - throw new IOException("Generation kernel seal contains an invalid or noncanonical entry."); + throw new IOException("Generation build revision contains an invalid or noncanonical entry."); } previousType = type; previousKey = fields[1]; hasScope |= type == 0; } if (!hasScope || !hasAlgorithm) { - throw new IOException("Generation kernel seal has no source scope or algorithm version."); + throw new IOException("Generation build revision has no source scope or algorithm version."); } } diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationEpoch.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationEpoch.java index 04245be6c..5d409125c 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationEpoch.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationEpoch.java @@ -346,15 +346,10 @@ public final class GenerationEpoch { return dimensionKey.equals(other.dimensionKey) && dimensionTypeKey.equals(other.dimensionTypeKey) && environment.equals(other.environment) - && generationMode.equals(other.generationMode) - && internalFluidHeight == other.internalFluidHeight && minHeight == other.minHeight && height == other.height && logicalHeight == other.logicalHeight && Double.compare(coordinateScale, other.coordinateScale) == 0 - && upperTerrainEnabled == other.upperTerrainEnabled - && upperDimensionKey.equals(other.upperDimensionKey) - && upperDimensionGap == other.upperDimensionGap && dimensionTypeFingerprintSchema == other.dimensionTypeFingerprintSchema && dimensionTypeFingerprint.equals(other.dimensionTypeFingerprint); } diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationFindCatalog.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationFindCatalog.java index 790cfab7b..15d868c39 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationFindCatalog.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationFindCatalog.java @@ -1,23 +1,16 @@ package art.arcane.iris.engine.history; -import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.engine.IrisEngine; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.iris.engine.object.IrisObjectPlacement; import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.engine.object.IrisStructurePlacement; import art.arcane.volmlib.util.collection.KList; import java.io.IOException; import java.io.UncheckedIOException; -import java.nio.file.Path; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; -import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; @@ -55,7 +48,7 @@ public final class GenerationFindCatalog { } public static boolean hasObjectPlacement(Engine engine, String key) { - return engine.hasObjectPlacement(key) || retained(engine).objects.contains(normalizeObjectKey(key)); + return engine.hasObjectPlacement(key) || containsKey(retained(engine).objects, normalizeObjectKey(key)); } public static KList retainedStructureKeys(Engine engine) { @@ -63,7 +56,7 @@ public final class GenerationFindCatalog { } public static boolean hasRetainedStructurePlacement(Engine engine, String key) { - return retained(engine).structures.contains(normalizeKey(key)); + return containsKey(retained(engine).structures, normalizeKey(key)); } private static RetainedCatalog retained(Engine engine) { @@ -90,87 +83,48 @@ public final class GenerationFindCatalog { private static RetainedCatalog loadRetained(GenerationHistory history, GenerationManifest manifest) { RetainedCatalog catalog = new RetainedCatalog(manifest.activeActivation().activationId()); - Set loadedPacks = new HashSet<>(); - Long activationId = manifest.activeActivation().parentActivationId(); - while (activationId != null) { - GenerationActivation activation = manifest.activation(activationId).orElseThrow(); - GenerationEpoch epoch = manifest.epoch(activation.epochId()).orElseThrow(); - String dimensionKey = epoch.dimensionContract().dimensionKey(); - if (loadedPacks.add(epoch.packFingerprint() + ":" + dimensionKey)) { - try { - loadPack(catalog, history.packRoot(activationId), dimensionKey); - } catch (IOException error) { - throw new UncheckedIOException("Unable to read retained Iris find catalog for activation " - + activationId, error); - } - } - activationId = activation.parentActivationId(); + try { + history.forEachRecordedSemantic(semantics -> addRecorded(catalog, semantics)); + } catch (IOException error) { + throw new UncheckedIOException("Unable to read recorded Iris find catalog", error); } return catalog; } - private static void loadPack(RetainedCatalog catalog, Path packRoot, String dimensionKey) { - IrisData data = IrisData.openDatapackCompiler(packRoot.toFile()); - try { - IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false); - if (dimension == null) { - throw new IllegalStateException("Retained Iris dimension is absent: " + dimensionKey); + private static void addRecorded(RetainedCatalog catalog, ChunkGenerationSemantics semantics) { + if (!semantics.sealed() || semantics.activationId() == catalog.activeActivationId) { + return; + } + for (String key : semantics.surfaceBiomeKeys()) { + addBiome(catalog, key); + } + for (String key : semantics.caveBiomeKeys()) { + addBiome(catalog, key); + } + for (String key : semantics.regionKeys()) { + String normalized = normalizeKey(key); + if (!catalog.regions.containsKey(normalized)) { + IrisRegion region = new IrisRegion().setName(key); + region.setLoadKey(key); + catalog.regions.put(normalized, region); } - addStructures(catalog.structures, dimension.getStructures()); - for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) { - catalog.biomes.putIfAbsent(normalizeKey(biome.getLoadKey()), biome); - addObjects(catalog.objects, biome.getObjects()); - addStructures(catalog.structures, biome.getStructures()); - } - for (IrisRegion region : dimension.getAllRegions(() -> data)) { - catalog.regions.putIfAbsent(normalizeKey(region.getLoadKey()), region); - addObjects(catalog.objects, region.getObjects()); - addStructures(catalog.structures, region.getStructures()); - } - if (dimension.hasUpperDimension() && !dimensionKey.equals(dimension.getUpperDimension())) { - IrisDimension upper = data.getDimensionLoader().load(dimension.getUpperDimension(), false); - if (upper == null) { - throw new IllegalStateException("Retained Iris upper dimension is absent: " - + dimension.getUpperDimension()); - } - for (IrisBiome biome : upper.getReachableBiomes(() -> data)) { - addObjects(catalog.objects, biome.getSurfaceObjects()); - } - for (IrisRegion region : upper.getAllRegions(() -> data)) { - addObjects(catalog.objects, region.getSurfaceObjects()); - } - } - } finally { - data.close(); + } + for (String key : semantics.objectKeys()) { + catalog.objects.add(normalizeObjectKey(key)); + } + for (ChunkGenerationSemantics.StructureOccurrence occurrence : semantics.structures()) { + catalog.structures.add(normalizeKey(occurrence.key())); } } - private static void addStructures(Set structures, Iterable placements) { - for (IrisStructurePlacement placement : placements) { - if (placement == null) { - continue; - } - for (String key : placement.getStructures()) { - String normalized = normalizeKey(key); - if (!normalized.isEmpty()) { - structures.add(normalized); - } - } - } - } - - private static void addObjects(Set objects, Iterable placements) { - for (IrisObjectPlacement placement : placements) { - if (placement == null || placement.getPlace() == null) { - continue; - } - for (String key : placement.getPlace()) { - String normalized = normalizeObjectKey(key); - if (!normalized.isEmpty()) { - objects.add(normalized); - } - } + private static void addBiome(RetainedCatalog catalog, String key) { + String normalized = normalizeKey(key); + if (catalog.biomes.containsKey(normalized)) { + return; } + IrisBiome biome = new IrisBiome().setName(key); + biome.setLoadKey(key); + catalog.biomes.put(normalized, biome); } private static KList merge(Iterable active, Map historical) { @@ -184,16 +138,33 @@ public final class GenerationFindCatalog { private static T find(Iterable entries, String key) { String normalized = normalizeKey(key); + T caseInsensitiveMatch = null; for (T entry : entries) { - if (normalizeKey(entry.getLoadKey()).equals(normalized)) { + String entryKey = normalizeKey(entry.getLoadKey()); + if (entryKey.equals(normalized)) { return entry; } + if (caseInsensitiveMatch == null && entryKey.equalsIgnoreCase(normalized)) { + caseInsensitiveMatch = entry; + } } - return null; + return caseInsensitiveMatch; + } + + private static boolean containsKey(Set keys, String key) { + if (keys.contains(key)) { + return true; + } + for (String candidate : keys) { + if (candidate.equalsIgnoreCase(key)) { + return true; + } + } + return false; } private static String normalizeKey(String key) { - return key == null ? "" : key.trim().toLowerCase(Locale.ROOT); + return key == null ? "" : key.trim(); } private static String normalizeObjectKey(String key) { diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationHistory.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationHistory.java index 53e92fdb3..444e57bd0 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationHistory.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationHistory.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; public final class GenerationHistory { @@ -380,9 +381,44 @@ public final class GenerationHistory { } } + LiveCutover beginLiveCutover() { + return new LiveCutover(admission.beginCutover()); + } + + final class LiveCutover implements AutoCloseable { + private final GenerationAdmission.CutoverLease lease; + private final Thread owner = Thread.currentThread(); + private boolean closed; + + private LiveCutover(GenerationAdmission.CutoverLease lease) { + this.lease = lease; + } + + GenerationActivation promote(BoundarySignatureCapture signatureCapture) throws IOException { + requireOpen(); + synchronized (GenerationHistory.this) { + return promotePendingLocked(Objects.requireNonNull(signatureCapture, "signature capture")); + } + } + + private void requireOpen() { + if (closed || Thread.currentThread() != owner) { + throw new IllegalStateException("Generation cutover is closed or belongs to another thread."); + } + } + + @Override + public void close() { + requireOpen(); + closed = true; + lease.close(); + } + } + private GenerationActivation promotePendingLocked( BoundarySignatureCapture signatureCapture ) throws IOException { + WorldChunkInventory inventory = recoverUnstoredClaims(); validateReferencedState(); Optional pending = store.pendingActivation(); if (pending.isEmpty()) { @@ -390,16 +426,9 @@ public final class GenerationHistory { } long outgoingActivationId = store.activeActivation().activationId(); - semantics.compactJournals(); - WorldChunkInventory inventory = WorldChunkInventory.scan(paths.dimensionRoot()); ownership.assignUnassigned(inventory, outgoingActivationId); - semantics.forEachSealedClaim( - outgoingActivationId, - (chunkX, chunkZ) -> ownership.assign(chunkX, chunkZ, outgoingActivationId) - ); ownership.persist(); requireExplicitOwnership(inventory); - requireExplicitSemanticOwnership(outgoingActivationId); GenerationBoundary boundary = boundaries.publishOwnership( pending.get().activationId(), ownership @@ -440,6 +469,13 @@ public final class GenerationHistory { return kernels.current(); } + public synchronized boolean usesCurrentGenerator() throws IOException { + GenerationEpoch active = store.activeEpoch(); + return active.kernelVersion().equals(kernels.current()) + && active.kernelImplementationFingerprint().equals( + kernels.requireSupported(kernels.current()).implementationFingerprint()); + } + public synchronized GenerationActivation stageCurrentKernel( int transitionWidthBlocks ) throws IOException { @@ -455,6 +491,49 @@ public final class GenerationHistory { ); } + public void prepareCurrentGenerator(int transitionWidthBlocks) throws IOException { + try (GenerationAdmission.CutoverLease ignored = admission.beginStartupCutover()) { + synchronized (this) { + if (store.pendingActivation().isEmpty() && usesCurrentGenerator()) { + recoverUnstoredClaims(); + validateReferencedState(); + } else { + if (store.pendingActivation().isEmpty()) { + stageCurrentKernel(transitionWidthBlocks); + } + promoteSavedBoundary(); + if (!usesCurrentGenerator()) { + stageCurrentKernel(transitionWidthBlocks); + promoteSavedBoundary(); + } + } + packs.releaseArchivedPacks(store.manifest()); + } + } + } + + private void promoteSavedBoundary() throws IOException { + GenerationEpoch.DimensionContract contract = activeEpoch().dimensionContract(); + promotePendingLocked(boundary -> new DiskBoundaryCapture(paths.dimensionRoot(), contract.minHeight(), contract.height())); + } + + private WorldChunkInventory recoverUnstoredClaims() throws IOException { + long outgoing = store.activeActivation().activationId(); + Set selected = store.pendingActivation() + .map(pending -> Set.of(outgoing, pending.activationId())) + .orElseGet(() -> Set.of(outgoing)); + WorldChunkInventory inventory = WorldChunkInventory.scan(paths.dimensionRoot()).filter((chunkX, chunkZ) -> { + if (ownership.isExplicitlyAssigned(chunkX, chunkZ) + && !selected.contains(ownership.resolve(chunkX, chunkZ, outgoing))) { + return true; + } + return SavedTerrainChunk.hasTerrain(SavedTerrainChunk.readStatus(paths.dimensionRoot(), chunkX, chunkZ)); + }); + semantics.discardUnstoredClaims(inventory, selected); + ownership.discardUnstoredClaims(inventory, selected); + return inventory; + } + public synchronized Optional pendingActivation() { return store.pendingActivation(); } @@ -482,7 +561,7 @@ public final class GenerationHistory { GenerationAdmission.StageLease lease = admission.enterStage(); try { synchronized (this) { - GenerationActivation activation = resolveActivation(chunkX, chunkZ); + GenerationActivation activation = store.activeActivation(); GenerationEpoch epoch = requireEpoch(activation.epochId()); return new GenerationStage( this, @@ -526,6 +605,10 @@ public final class GenerationHistory { return semantics.claimAndPersist(requiredUpdate); } + public synchronized void forEachRecordedSemantic(GenerationSemanticIndex.RecordConsumer consumer) throws IOException { + semantics.forEachRecord(consumer); + } + public synchronized Optional semantics(int chunkX, int chunkZ) { return semantics.get(chunkX, chunkZ); } @@ -652,13 +735,11 @@ public final class GenerationHistory { private void validateReferencedState() throws IOException { validateRuntimeVersions(); - Collection epochs = store.manifest().epochs(); - for (GenerationEpoch epoch : epochs) { - packs.requireExactPack( - epoch.epochId(), - epoch.packFingerprint(), - epoch.packFingerprintVersion() - ); + GenerationEpoch active = store.activeEpoch(); + packs.requireExactPack(active.epochId(), active.packFingerprint(), active.packFingerprintVersion()); + if (store.pendingEpoch().isPresent()) { + GenerationEpoch pending = store.pendingEpoch().orElseThrow(); + packs.requireExactPack(pending.epochId(), pending.packFingerprint(), pending.packFingerprintVersion()); } for (GenerationActivation activation : store.manifest().activations()) { requireSafeDirectoryIfPresent(paths.activationRoot(activation.activationId())); @@ -698,14 +779,6 @@ public final class GenerationHistory { private void validateRuntimeVersions() throws IOException { for (GenerationEpoch epoch : store.manifest().epochs()) { - kernels.requireSupported( - new GenerationKernelRegistry.Version( - epoch.generatorAbi(), - epoch.rngVersion(), - epoch.seedDerivationVersion() - ), - epoch.kernelImplementationFingerprint() - ); GenerationPackFingerprint.requireSupported(epoch.packFingerprintVersion()); } } @@ -716,7 +789,6 @@ public final class GenerationHistory { GenerationBoundary expectedBoundary = active.isInitial() ? null : boundary(activeActivationId); boolean pendingCutover = store.pendingActivation().isPresent(); int[] assignedCount = new int[1]; - WorldChunkInventory[] inventory = new WorldChunkInventory[1]; ownership.forEachAssignment((chunkX, chunkZ, activationId) -> { assignedCount[0] = Math.addExact(assignedCount[0], 1); if (store.activation(activationId).isEmpty()) { @@ -739,14 +811,6 @@ public final class GenerationHistory { + chunkX + "," + chunkZ + ": expected activation " + activeActivationId + " but found " + activationId + "."); } - if (inventory[0] == null) { - inventory[0] = WorldChunkInventory.scan(paths.dimensionRoot()); - } - if (!inventory[0].contains(chunkX, chunkZ) - && !semantics.hasSealedClaim(chunkX, chunkZ, activationId)) { - throw new IOException("Pending generation cutover claims an ungenerated chunk " - + chunkX + "," + chunkZ + " without matching sealed semantics."); - } }); int expectedCount = expectedBoundary == null ? 0 : expectedBoundary.historicalChunkCount(); @@ -803,24 +867,6 @@ public final class GenerationHistory { + " was not durably assigned before generation activation promotion."); } - private void requireExplicitSemanticOwnership(long activationId) throws IOException { - boolean[] missing = new boolean[1]; - long[] firstMissing = new long[1]; - semantics.forEachSealedClaim(activationId, (chunkX, chunkZ) -> { - if (!missing[0] && !ownership.isExplicitlyAssigned(chunkX, chunkZ)) { - missing[0] = true; - firstMissing[0] = ChunkGenerationOwnership.packChunk(chunkX, chunkZ); - } - }); - if (!missing[0]) { - return; - } - long first = firstMissing[0]; - throw new IOException("Generated chunk " + ChunkGenerationOwnership.chunkX(first) - + "," + ChunkGenerationOwnership.chunkZ(first) - + " was not durably assigned before generation activation promotion."); - } - private GenerationActivation requireActivation(long activationId) { return store.activation(activationId).orElseThrow(() -> new IllegalStateException( "Generation ownership references missing activation " + activationId + "." diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouter.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouter.java index f2a8790c6..cf42e2c73 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouter.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouter.java @@ -3,6 +3,7 @@ package art.arcane.iris.engine.history; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.IrisEngine; import art.arcane.iris.engine.framework.EngineTarget; +import art.arcane.iris.engine.framework.GenerationTransitionGate; import art.arcane.iris.engine.object.IrisDimension; import java.io.IOException; @@ -19,7 +20,6 @@ import java.util.concurrent.locks.ReentrantLock; public final class GenerationHistoryRuntimeRouter implements AutoCloseable { public static final int DEFAULT_TRANSITION_WIDTH_BLOCKS = 256; - public static final int DEFAULT_RUNTIME_CACHE_CAPACITY = 4; private final IrisEngine engine; private final GenerationHistory history; @@ -27,7 +27,6 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { private final ActivationRuntimeFactory runtimeFactory; private final LinkedHashMap bindings; private final Map retiringBindings; - private final int runtimeCacheCapacity; private final ReentrantLock stateLock; private final Condition inactive; private final ThreadLocal operationDepth; @@ -43,18 +42,13 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { IrisEngine engine, GenerationHistory history, GenerationBoundarySignatureSampler signatureSampler, - ActivationRuntimeFactory runtimeFactory, - int runtimeCacheCapacity + ActivationRuntimeFactory runtimeFactory ) throws IOException { this.engine = Objects.requireNonNull(engine, "Iris engine"); this.history = Objects.requireNonNull(history, "generation history"); this.signatureSampler = Objects.requireNonNull(signatureSampler, "boundary signature sampler"); this.runtimeFactory = Objects.requireNonNull(runtimeFactory, "activation runtime factory"); - if (runtimeCacheCapacity < 1) { - throw new IllegalArgumentException("Generation runtime cache capacity must be positive."); - } - this.runtimeCacheCapacity = runtimeCacheCapacity; - this.bindings = new LinkedHashMap<>(runtimeCacheCapacity, 0.75F, true); + this.bindings = new LinkedHashMap<>(); this.retiringBindings = new LinkedHashMap<>(); this.stateLock = new ReentrantLock(true); this.inactive = stateLock.newCondition(); @@ -65,7 +59,9 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { GenerationEpoch epoch = requireEpoch(active); IrisEngine.GenerationRuntimeBinding base = engine.getActiveGenerationRuntimeBinding(); runtimeFactory.validateBase(engine, history, active, epoch, base); - requireBindingContract(active, epoch, base); + if (history.usesCurrentGenerator()) { + requireBindingContract(active, epoch, base); + } RuntimeCacheEntry baseEntry = new RuntimeCacheEntry(active.activationId()); baseEntry.binding = base; baseEntry.defaultRuntime = true; @@ -117,8 +113,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { history, signatureSampler, transitionWidthBlocks, - new DefaultActivationRuntimeFactory(), - DEFAULT_RUNTIME_CACHE_CAPACITY + new DefaultActivationRuntimeFactory() ); } @@ -128,31 +123,12 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { GenerationBoundarySignatureSampler signatureSampler, int transitionWidthBlocks, ActivationRuntimeFactory runtimeFactory - ) throws IOException { - return attachAndPromotePending( - engine, - history, - signatureSampler, - transitionWidthBlocks, - runtimeFactory, - DEFAULT_RUNTIME_CACHE_CAPACITY - ); - } - - static GenerationHistoryRuntimeRouter attachAndPromotePending( - IrisEngine engine, - GenerationHistory history, - GenerationBoundarySignatureSampler signatureSampler, - int transitionWidthBlocks, - ActivationRuntimeFactory runtimeFactory, - int runtimeCacheCapacity ) throws IOException { GenerationHistoryRuntimeRouter router = attach( engine, history, signatureSampler, - runtimeFactory, - runtimeCacheCapacity + runtimeFactory ); try { router.promotePendingAndReconcileCurrentKernel(transitionWidthBlocks); @@ -174,29 +150,12 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { GenerationHistory history, GenerationBoundarySignatureSampler signatureSampler, ActivationRuntimeFactory runtimeFactory - ) throws IOException { - return attach( - engine, - history, - signatureSampler, - runtimeFactory, - DEFAULT_RUNTIME_CACHE_CAPACITY - ); - } - - static GenerationHistoryRuntimeRouter attach( - IrisEngine engine, - GenerationHistory history, - GenerationBoundarySignatureSampler signatureSampler, - ActivationRuntimeFactory runtimeFactory, - int runtimeCacheCapacity ) throws IOException { return new GenerationHistoryRuntimeRouter( engine, history, signatureSampler, - runtimeFactory, - runtimeCacheCapacity + runtimeFactory ); } @@ -208,24 +167,94 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { return history; } + public StudioCutover beginStudioCutover(long timeoutMillis) { + if (!engine.isStudio()) { + throw new IllegalStateException("Live generation cutovers require a Studio world."); + } + if (scopedRoute.get() != null || operationDepth.get() > 0) { + throw new IllegalStateException("Cannot begin a Studio cutover inside another generation operation."); + } + enterOperation(); + GenerationTransitionGate.Transition transition = null; + try { + transition = engine.getGenerationSessions().transitionGate().beginTransition(timeoutMillis); + return new StudioCutover(history.beginLiveCutover(), transition); + } catch (Throwable failure) { + if (transition != null) { + transition.close(); + } + leaveOperation(); + throw failure; + } + } + + public final class StudioCutover implements AutoCloseable { + private final GenerationHistory.LiveCutover cutover; + private final GenerationTransitionGate.Transition transition; + private final Thread owner = Thread.currentThread(); + private boolean closed; + + private StudioCutover(GenerationHistory.LiveCutover cutover, GenerationTransitionGate.Transition transition) { + this.cutover = cutover; + this.transition = transition; + } + + public GenerationActivation promotePending() throws IOException { + requireOpen(); + GenerationActivation activated = cutover.promote(GenerationHistoryRuntimeRouter.this::captureBoundarySignatures); + try (RuntimeLease lease = acquireRuntime(activated, requireEpoch(activated))) { + engine.setDefaultGenerationRuntime(lease.binding()); + retireBindings(pinDefault(lease.entry)); + } + return activated; + } + + private void requireOpen() { + if (closed || Thread.currentThread() != owner) { + throw new IllegalStateException("Studio cutover is closed or belongs to another thread."); + } + } + + @Override + public void close() { + requireOpen(); + closed = true; + try { + cutover.close(); + } finally { + try { + transition.close(); + } finally { + leaveOperation(); + } + } + } + } + public Optional currentRuntimeOwnership() { RuntimeRoute current = scopedRoute.get(); return current == null ? Optional.empty() : Optional.of(current.ownership); } public RuntimeRoute openRoute(int chunkX, int chunkZ) throws IOException { - enterRouteOperation(); + GenerationTransitionGate.Participation participation = engine.getGenerationSessions().transitionGate().enter(); + boolean operation = false; try { + enterRouteOperation(); + operation = true; GenerationHistory.GenerationStage stage = history.openStage(chunkX, chunkZ); try { RuntimeLease lease = acquireRuntime(stage.activation(), stage.epoch()); - return new RuntimeRoute(this, stage, lease); + return new RuntimeRoute(this, stage, lease, participation); } catch (Throwable failure) { stage.close(); throw failure; } } catch (Throwable failure) { - leaveRouteOperation(); + if (operation) { + leaveRouteOperation(); + } + participation.close(); throw propagate(failure, "Unable to open an Iris generation-history stage."); } } @@ -259,6 +288,15 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { return new CoordinateScope(blockX, blockZ, stage.route, stage); } + public void recordNaturalTerrain(SavedTerrainChunk terrain) { + SavedTerrainChunk captured = Objects.requireNonNull(terrain, "natural terrain"); + RuntimeRoute route = scopedRoute.get(); + if (route == null || route.chunkX() != captured.chunkX() || route.chunkZ() != captured.chunkZ()) { + throw new IllegalStateException("Natural terrain must be captured inside its chunk generation route."); + } + route.naturalTerrain = captured; + } + public void preloadActiveRuntimes() throws IOException { enterOperation(); try { @@ -274,6 +312,9 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { enterOperation(); try { GenerationActivation activated = history.promotePending(this::captureBoundarySignatures); + if (!history.usesCurrentGenerator()) { + return activated; + } GenerationEpoch epoch = requireEpoch(activated); try (RuntimeLease lease = acquireRuntime(activated, epoch)) { engine.setDefaultGenerationRuntime(lease.binding()); @@ -288,8 +329,11 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { private GenerationActivation promotePendingAndReconcileCurrentKernel( int transitionWidthBlocks ) throws IOException { + if (history.pendingActivation().isEmpty() && !history.usesCurrentGenerator()) { + history.stageCurrentKernel(transitionWidthBlocks); + } GenerationActivation activated = promotePending(); - while (!history.activeEpoch().kernelVersion().equals(history.currentKernelVersion())) { + if (!history.usesCurrentGenerator()) { history.stageCurrentKernel(transitionWidthBlocks); activated = promotePending(); } @@ -364,8 +408,8 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { private TerrainBoundarySignatureStore.SignatureSampler captureBoundarySignatures( GenerationBoundary boundary - ) { - return new BoundaryCapture(); + ) throws IOException { + return signatureSampler.open(engine); } private RuntimeLease acquireRuntime( @@ -522,7 +566,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { private List collectEvictionsLocked() { ArrayList retired = new ArrayList<>(); - while (bindings.size() > runtimeCacheCapacity) { + while (bindings.size() > 1) { RuntimeCacheEntry candidate = null; Iterator> iterator = bindings.entrySet().iterator(); while (iterator.hasNext()) { @@ -576,6 +620,18 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { if (failure != null) { throw new IllegalStateException("Failed to retire cached generation runtimes.", failure); } + if (!retired.isEmpty()) { + stateLock.lock(); + try { + if (bindings.size() == 1 && retiringBindings.isEmpty()) { + new GenerationPackRepository(history.paths().dimensionRoot()).releaseArchivedPacks(history.manifest()); + } + } catch (IOException cleanupFailure) { + throw new IllegalStateException("Unable to release archived generation packs after runtime retirement.", cleanupFailure); + } finally { + stateLock.unlock(); + } + } } private void requireBindingContract( @@ -807,49 +863,6 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { } } - private final class BoundaryCapture implements TerrainBoundarySignatureStore.SignatureSampler { - private RuntimeLease lease; - private IrisEngine.GenerationRuntimeScope scope; - private long activationId; - - @Override - public TerrainBoundarySignature sample(int blockX, int blockZ) throws IOException { - GenerationActivation activation = history.resolveActivation( - Math.floorDiv(blockX, GenerationBoundary.CHUNK_SIZE), - Math.floorDiv(blockZ, GenerationBoundary.CHUNK_SIZE) - ); - if (lease == null || activationId != activation.activationId()) { - close(); - lease = acquireRuntime(activation, requireEpoch(activation)); - try { - scope = engine.openGenerationRuntimeScope(lease.binding()); - activationId = activation.activationId(); - } catch (Throwable failure) { - close(); - throw propagate(failure, "Unable to scope boundary capture."); - } - } - return signatureSampler.sample(engine, blockX, blockZ); - } - - @Override - public void close() { - try { - if (scope != null) { - IrisEngine.GenerationRuntimeScope closing = scope; - scope = null; - closing.close(); - } - } finally { - if (lease != null) { - RuntimeLease closing = lease; - lease = null; - closing.close(); - } - } - } - } - interface ActivationRuntimeFactory { void validateBase( IrisEngine engine, @@ -960,17 +973,21 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { private final RuntimeLease lease; private final IrisEngine.GenerationRuntimeBinding binding; private final RuntimeOwnership ownership; + private final GenerationTransitionGate.Participation participation; private int activeScopes; + private volatile SavedTerrainChunk naturalTerrain; private boolean closed; private RuntimeRoute( GenerationHistoryRuntimeRouter router, GenerationHistory.GenerationStage stage, - RuntimeLease lease + RuntimeLease lease, + GenerationTransitionGate.Participation participation ) { this.router = router; this.stage = stage; this.lease = lease; + this.participation = participation; this.binding = lease.binding(); this.ownership = new RuntimeOwnership(stage.activation().activationId(), binding); } @@ -979,6 +996,10 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { return stage; } + public Optional naturalTerrain() { + return Optional.ofNullable(naturalTerrain); + } + public int chunkX() { return stage.chunkX(); } @@ -999,15 +1020,29 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { return binding.transitionPlan(); } + public void detachThread() { + participation.detachThread(); + } + public boolean claimGeneratedSemantics() throws IOException { + return claimGeneratedSemantics((x, y, z) -> true); + } + + public boolean claimGeneratedSemantics(GenerationSemanticCapture.CaveSpace caveSpace) throws IOException { if (router.scopedRoute.get() != this) { throw new IllegalStateException( "Generation semantics must be captured inside this route's runtime scope." ); } + Optional recorded = router.history.semantics(chunkX(), chunkZ()); + if (recorded.isPresent() && recorded.get().sealed() + || router.history.resolveActivation(chunkX(), chunkZ()).activationId() != stage.activation().activationId()) { + return false; + } ChunkGenerationSemantics semantics = GenerationSemanticCapture.capture( router.engine, - stage + stage, + caveSpace ); return router.history.claimGeneratedSemantics(stage, semantics); } @@ -1021,10 +1056,12 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { } router.enterRuntimeScope(); RuntimeRoute previousRoute = router.installScopedRoute(this); + GenerationTransitionGate.Participation scopedParticipation = participation.attach(); try { IrisEngine.GenerationRuntimeScope opened = router.engine.openGenerationRuntimeScope(binding); - return new RuntimeScope(this, opened, Thread.currentThread(), previousRoute); + return new RuntimeScope(this, opened, Thread.currentThread(), previousRoute, scopedParticipation); } catch (Throwable failure) { + scopedParticipation.close(); try { router.restoreScopedRoute(this, previousRoute); } catch (Throwable closeFailure) { @@ -1077,6 +1114,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { failure.addSuppressed(closeFailure); } } + participation.close(); if (failure instanceof RuntimeException runtimeFailure) { throw runtimeFailure; } @@ -1100,18 +1138,21 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { private final IrisEngine.GenerationRuntimeScope runtimeScope; private final Thread owner; private final RuntimeRoute previousRoute; + private final GenerationTransitionGate.Participation participation; private boolean closed; private RuntimeScope( RuntimeRoute route, IrisEngine.GenerationRuntimeScope runtimeScope, Thread owner, - RuntimeRoute previousRoute + RuntimeRoute previousRoute, + GenerationTransitionGate.Participation participation ) { this.route = route; this.runtimeScope = runtimeScope; this.owner = owner; this.previousRoute = previousRoute; + this.participation = participation; } @Override @@ -1148,6 +1189,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { } catch (Throwable closeFailure) { failure = appendFailure(failure, closeFailure); } + participation.close(); if (failure instanceof RuntimeException runtimeFailure) { throw runtimeFailure; } diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationHistoryStore.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationHistoryStore.java index 675867e69..cf194565e 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationHistoryStore.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationHistoryStore.java @@ -3,6 +3,8 @@ package art.arcane.iris.engine.history; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import com.google.gson.Strictness; @@ -27,6 +29,8 @@ import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.time.Clock; import java.util.Objects; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; public final class GenerationHistoryStore { @@ -97,7 +101,7 @@ public final class GenerationHistoryStore { requiredClock.millis() ); DirectorySync requiredDirectorySync = Objects.requireNonNull(directorySync, "directorySync"); - writeManifestAtomically(directory, manifestPath, manifest, false, requiredDirectorySync); + writeManifestAtomically(directory, manifestPath, manifest, null, false, requiredDirectorySync); return new GenerationHistoryStore(directory, manifest, requiredClock, requiredDirectorySync); } } @@ -207,7 +211,7 @@ public final class GenerationHistoryStore { private void writeUpdatedManifest(GenerationManifest updated) throws IOException { try { - writeManifestAtomically(generationDirectory, manifestPath, updated, true, directorySync); + writeManifestAtomically(generationDirectory, manifestPath, updated, manifest, true, directorySync); } catch (IOException error) { reconcileAfterWriteFailure(error); throw error; @@ -233,7 +237,16 @@ public final class GenerationHistoryStore { if (reader.peek() != JsonToken.END_DOCUMENT) { throw new IllegalArgumentException("Generation manifest contains trailing content."); } - return GenerationManifest.fromJson(parsed.getAsJsonObject()); + JsonObject index = parsed.getAsJsonObject(); + JsonArray references = GenerationManifest.JsonSchema.requireArray(index, "epochs", "generation manifest"); + List epochs = new ArrayList<>(references.size()); + for (JsonElement reference : references) { + if (!reference.isJsonPrimitive() || !reference.getAsJsonPrimitive().isString()) { + throw new IOException("Generation epoch reference must be a digest."); + } + epochs.add(readEpoch(manifestPath.getParent(), reference.getAsString())); + } + return GenerationManifest.fromJson(index, epochs); } catch (JsonParseException | IllegalArgumentException | ArithmeticException exception) { throw new IOException("Invalid generation manifest: " + manifestPath, exception); } @@ -243,12 +256,21 @@ public final class GenerationHistoryStore { Path generationDirectory, Path manifestPath, GenerationManifest manifest, + GenerationManifest previous, boolean replaceExisting, DirectorySync directorySync ) throws IOException { requireSafeParent(generationDirectory); requireDirectory(generationDirectory); byte[] content = GSON.toJson(manifest.toJson()).getBytes(StandardCharsets.UTF_8); + if (content.length > MAX_MANIFEST_BYTES) { + throw new IOException("Generation manifest exceeds the supported size: " + manifestPath); + } + for (GenerationEpoch epoch : manifest.epochs()) { + if (previous == null || previous.epoch(epoch.epochId()).isEmpty()) { + publishEpoch(generationDirectory, epoch); + } + } Path temporary = Files.createTempFile(generationDirectory, ".manifest-", ".tmp"); try { try (FileChannel channel = FileChannel.open( @@ -282,6 +304,74 @@ public final class GenerationHistoryStore { } } + private static GenerationEpoch readEpoch(Path generationDirectory, String epochId) throws IOException { + Path epochDirectory = epochDirectory(generationDirectory, epochId); + requireDirectory(generationDirectory.resolve("epochs")); + requireDirectory(epochDirectory); + Path metadata = epochDirectory.resolve("epoch.json"); + requireRegularFile(metadata); + if (Files.size(metadata) > MAX_MANIFEST_BYTES) { + throw new IOException("Generation epoch metadata exceeds the supported size: " + metadata); + } + try (JsonReader reader = new JsonReader(Files.newBufferedReader(metadata, StandardCharsets.UTF_8))) { + reader.setStrictness(Strictness.STRICT); + JsonElement parsed = JsonParser.parseReader(reader); + if (!parsed.isJsonObject() || reader.peek() != JsonToken.END_DOCUMENT) { + throw new IOException("Invalid generation epoch metadata: " + metadata); + } + GenerationEpoch epoch = GenerationEpoch.fromJson(parsed.getAsJsonObject()); + if (!epoch.epochId().equals(epochId)) { + throw new IOException("Generation epoch metadata identity differs from its reference: " + metadata); + } + return epoch; + } catch (JsonParseException | IllegalArgumentException exception) { + throw new IOException("Invalid generation epoch metadata: " + metadata, exception); + } + } + + private static void publishEpoch(Path generationDirectory, GenerationEpoch epoch) throws IOException { + Path epochsDirectory = generationDirectory.resolve("epochs"); + ensureDirectory(epochsDirectory); + Path epochDirectory = epochDirectory(generationDirectory, epoch.epochId()); + ensureDirectory(epochDirectory); + Path metadata = epochDirectory.resolve("epoch.json"); + if (Files.exists(metadata, LinkOption.NOFOLLOW_LINKS)) { + if (!readEpoch(generationDirectory, epoch.epochId()).equals(epoch)) { + throw new IOException("Generation epoch metadata is immutable: " + metadata); + } + return; + } + byte[] content = GSON.toJson(epoch.toJson()).getBytes(StandardCharsets.UTF_8); + if (content.length > MAX_MANIFEST_BYTES) { + throw new IOException("Generation epoch metadata exceeds the supported size: " + metadata); + } + Path temporary = Files.createTempFile(epochDirectory, ".epoch-", ".tmp"); + try { + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.WRITE)) { + ByteBuffer buffer = ByteBuffer.wrap(content); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + try { + Files.move(temporary, metadata, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException exception) { + throw new IOException("Generation history requires atomic epoch publication.", exception); + } + forceDirectory(epochDirectory); + } finally { + Files.deleteIfExists(temporary); + } + } + + private static Path epochDirectory(Path generationDirectory, String epochId) throws IOException { + if (!epochId.matches("[0-9a-f]{64}")) { + throw new IOException("Generation epoch reference must be a digest."); + } + return generationDirectory.resolve("epochs").resolve(epochId); + } + private static Path normalizeDirectory(Path directory) { return Objects.requireNonNull(directory, "generationDirectory").toAbsolutePath().normalize(); } diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationKernelCatalog.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationKernelCatalog.java index f51831eff..12bed0b53 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationKernelCatalog.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationKernelCatalog.java @@ -23,19 +23,19 @@ final class GenerationKernelCatalog { Catalog catalog = parse(input.readNBytes(MAXIMUM_BYTES + 1)); List kernels = new ArrayList<>(); for (int abi : catalog.abis()) { - GenerationKernelSourceSeal.Descriptor seal = GenerationKernelSourceSeal.load(abi); + GenerationBuildRevision.Descriptor revision = GenerationBuildRevision.load(abi); Class factoryType = Class.forName( - seal.factoryClass(), true, GenerationKernelCatalog.class.getClassLoader()) + revision.factoryClass(), true, GenerationKernelCatalog.class.getClassLoader()) .asSubclass(GenerationKernelRegistry.RuntimeFactory.class); Map factories = new LinkedHashMap<>(); - for (GenerationKernelRegistry.AlgorithmVersion algorithm : seal.algorithms()) { + for (GenerationKernelRegistry.AlgorithmVersion algorithm : revision.algorithms()) { factories.put(algorithm, factoryType.getDeclaredConstructor().newInstance()); } - kernels.add(new GenerationKernelRegistry.Kernel(abi, seal.fingerprint(), factories)); + kernels.add(new GenerationKernelRegistry.Kernel(abi, revision.fingerprint(), factories)); } return new GenerationKernelRegistry(catalog.current(), kernels); } catch (IOException | ReflectiveOperationException | IllegalArgumentException failure) { - throw new IllegalStateException("Cannot load the sealed Iris generation kernel catalog.", failure); + throw new IllegalStateException("Cannot load the Iris generation kernel catalog.", failure); } } diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationKernelV1.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationKernelV1.java index 22640bcf5..eaabb2f5a 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationKernelV1.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationKernelV1.java @@ -20,7 +20,7 @@ import java.nio.file.Path; final class GenerationKernelV1 implements GenerationKernelRegistry.RuntimeFactory { static final String IMPLEMENTATION_FINGERPRINT = - GenerationKernelSourceSeal.requireFingerprint(1, GenerationKernelV1.class); + GenerationBuildRevision.requireFingerprint(1, GenerationKernelV1.class); @Override public SeedManager createSeedManager(long worldSeed) { @@ -125,7 +125,7 @@ final class GenerationKernelV1 implements GenerationKernelRegistry.RuntimeFactor return; } IrisDimensionStackActuator dimensionStack = new IrisDimensionStackActuator(engine); - mode.registerStage((x, z, blocks, biomes, multicore, chunkContext) -> + mode.registerTerrainStage((x, z, blocks, biomes, multicore, chunkContext) -> dimensionStack.actuate(x, z, blocks, multicore, chunkContext)); IrisCustomModifier stackCustom = new IrisCustomModifier(engine); mode.registerStage((x, z, blocks, biomes, multicore, chunkContext) -> diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationManifest.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationManifest.java index b1e11f1fe..b6be3b48d 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationManifest.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationManifest.java @@ -20,7 +20,7 @@ import java.util.Set; import java.util.TreeMap; public final class GenerationManifest { - public static final int CURRENT_SCHEMA_VERSION = 1; + public static final int CURRENT_SCHEMA_VERSION = 2; private final int schemaVersion; private final NavigableMap epochs; @@ -189,7 +189,7 @@ public final class GenerationManifest { JsonArray epochArray = new JsonArray(epochs.size()); for (GenerationEpoch epoch : epochs.values()) { - epochArray.add(epoch.toJson()); + epochArray.add(epoch.epochId()); } json.add("epochs", epochArray); @@ -207,7 +207,7 @@ public final class GenerationManifest { return json; } - static GenerationManifest fromJson(JsonObject json) { + static GenerationManifest fromJson(JsonObject json, List epochRecords) { JsonSchema.requireFields( json, "generation manifest", @@ -231,10 +231,17 @@ public final class GenerationManifest { "generation manifest" ); + if (epochArray.size() != epochRecords.size()) { + throw new IllegalArgumentException("Generation manifest epoch references do not match metadata."); + } TreeMap epochs = new TreeMap<>(); for (int index = 0; index < epochArray.size(); index++) { - JsonObject epochJson = JsonSchema.requireObject(epochArray.get(index), "epochs[" + index + "]"); - GenerationEpoch epoch = GenerationEpoch.fromJson(epochJson); + GenerationEpoch epoch = epochRecords.get(index); + if (!epochArray.get(index).isJsonPrimitive() + || !epochArray.get(index).getAsJsonPrimitive().isString() + || !epoch.epochId().equals(epochArray.get(index).getAsString())) { + throw new IllegalArgumentException("Generation manifest epoch reference does not match metadata."); + } if (epochs.putIfAbsent(epoch.epochId(), epoch) != null) { throw new IllegalArgumentException("Generation manifest contains duplicate epoch IDs."); } diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationPackRepository.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationPackRepository.java index 315965a0c..e83523761 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationPackRepository.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationPackRepository.java @@ -70,35 +70,55 @@ public final class GenerationPackRepository { int packFingerprintVersion, Path source ) throws IOException { - Path target = epochsRoot.resolve(epochId); + Path epochDirectory = ensureChildDirectory(epochsRoot, epochId); + Path target = epochDirectory.resolve("pack"); if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) { - requireSafeDirectory(target, "Generation epoch path is not a safe directory"); return requireExactPack(epochId, packFingerprint, packFingerprintVersion); } - Path stage = Files.createTempDirectory(epochsRoot, ".epoch-" + epochId.substring(0, 12) + "-"); - boolean published = false; + Path stage = Files.createTempDirectory(epochDirectory, ".pack-"); try { - Path stagedPack = stage.resolve("pack"); - copyPackTree(source, stagedPack); - requireFingerprint(stagedPack, packFingerprint, packFingerprintVersion); + Files.delete(stage); + copyPackTree(source, stage); + requireFingerprint(stage, packFingerprint, packFingerprintVersion); try (AtomicDirectoryPublisher.Publication publication = AtomicDirectoryPublisher.publishAbsent(stage, target)) { publication.commit(); } catch (FileAlreadyExistsException race) { - requireSafeDirectory(target, "Generation epoch path is not a safe directory"); return requireExactPack(epochId, packFingerprint, packFingerprintVersion); } - forceDirectory(epochsRoot); - published = true; + forceDirectory(epochDirectory); return requireExactPack(epochId, packFingerprint, packFingerprintVersion); } finally { - if (!published && Files.exists(stage, LinkOption.NOFOLLOW_LINKS)) { + if (Files.exists(stage, LinkOption.NOFOLLOW_LINKS)) { AtomicDirectoryPublisher.deleteTree(stage); } } } + public synchronized void releaseArchivedPacks(GenerationManifest manifest) throws IOException { + GenerationManifest retained = Objects.requireNonNull(manifest, "manifest"); + String activeEpochId = retained.activeEpoch().epochId(); + String pendingEpochId = retained.pendingEpoch().map(GenerationEpoch::epochId).orElse(null); + for (GenerationEpoch epoch : retained.epochs()) { + String epochId = epoch.epochId(); + if (epochId.equals(activeEpochId) || epochId.equals(pendingEpochId)) { + continue; + } + try (GenerationPublicationLock ignored = GenerationPublicationLock.acquire( + epochsRoot, ".epoch-" + epochId + ".lock")) { + validateExistingAncestors(epochId); + Path pack = packRoot(epochId); + if (!Files.exists(pack, LinkOption.NOFOLLOW_LINKS)) { + continue; + } + requireSafeDirectory(pack, "Generation epoch pack is unsafe"); + AtomicDirectoryPublisher.deleteTree(pack); + forceDirectory(epochRoot(epochId)); + } + } + } + public Path requireExactPack( String epochId, String packFingerprint, @@ -182,7 +202,7 @@ public final class GenerationPackRepository { } } - private static void copyPackTree(Path source, Path target) throws IOException { + public static void copyPackTree(Path source, Path target) throws IOException { Path sourcePath = Objects.requireNonNull(source, "source").toAbsolutePath().normalize(); if (Files.isSymbolicLink(sourcePath)) { throw new IOException("Pack source is a symbolic link: " + sourcePath); diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationRegistryContract.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationRegistryContract.java index 7a6b793d8..d6f84c104 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationRegistryContract.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationRegistryContract.java @@ -11,6 +11,10 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Collections; +import java.util.Collection; +import java.util.List; +import java.util.ArrayList; +import java.util.TreeSet; import java.util.HexFormat; import java.util.LinkedHashMap; import java.util.Map; @@ -36,19 +40,22 @@ public final class GenerationRegistryContract { private final String fingerprint; private final NavigableMap definitions; private final NavigableMap generatedSources; + private final NavigableMap> biomeTags; private GenerationRegistryContract( int fingerprintSchema, String fingerprint, NavigableMap definitions, - NavigableMap generatedSources + NavigableMap generatedSources, + Map> biomeTags ) { requireSupportedFingerprintSchema(fingerprintSchema); this.fingerprintSchema = fingerprintSchema; this.definitions = immutableDefinitions(definitions); this.generatedSources = immutableGeneratedSources(generatedSources, this.definitions); + this.biomeTags = canonicalBiomeTags(biomeTags, this.definitions); this.fingerprint = requireSha256(fingerprint, "Registry contract fingerprint"); - String expectedFingerprint = fingerprint(fingerprintSchema, this.definitions, this.generatedSources); + String expectedFingerprint = fingerprint(fingerprintSchema, this.definitions, this.generatedSources, this.biomeTags); if (!this.fingerprint.equals(expectedFingerprint)) { throw new IllegalArgumentException("Registry contract fingerprint does not match its definitions."); } @@ -93,12 +100,24 @@ public final class GenerationRegistryContract { ); return new GenerationRegistryContract( fingerprintSchema, - fingerprint(fingerprintSchema, canonicalDefinitions, canonicalSources), + fingerprint(fingerprintSchema, canonicalDefinitions, canonicalSources, new TreeMap<>()), canonicalDefinitions, - canonicalSources + canonicalSources, + Map.of() ); } + public GenerationRegistryContract withBiomeTags(Map> tags) { + NavigableMap> canonical = canonicalBiomeTags(tags, definitions); + return new GenerationRegistryContract(fingerprintSchema, + fingerprint(fingerprintSchema, definitions, generatedSources, canonical), + definitions, generatedSources, canonical); + } + + public NavigableMap> biomeTags() { + return biomeTags; + } + public int fingerprintSchema() { return fingerprintSchema; } @@ -160,6 +179,13 @@ public final class GenerationRegistryContract { generatedSourceArray.add(sourceJson); } json.add("generatedSources", generatedSourceArray); + JsonObject tags = new JsonObject(); + for (Map.Entry> entry : biomeTags.entrySet()) { + JsonArray values = new JsonArray(); + entry.getValue().forEach(values::add); + tags.add(entry.getKey(), values); + } + json.add("biomeTags", tags); return json; } @@ -170,7 +196,8 @@ public final class GenerationRegistryContract { "fingerprintSchema", "fingerprint", "definitions", - "generatedSources" + "generatedSources", + "biomeTags" ); int fingerprintSchema = GenerationManifest.JsonSchema.requireInt( json, @@ -197,12 +224,20 @@ public final class GenerationRegistryContract { generatedSourceArray, definitions ); - return new GenerationRegistryContract( - fingerprintSchema, - fingerprint, - definitions, - generatedSources - ); + JsonObject encodedTags = GenerationManifest.JsonSchema.requireObject(json.get("biomeTags"), "registry biome tags"); + Map> tags = new TreeMap<>(); + for (Map.Entry entry : encodedTags.entrySet()) { + JsonArray values = GenerationManifest.JsonSchema.requireArray(encodedTags, entry.getKey(), "registry biome tags"); + List names = new ArrayList<>(); + for (JsonElement value : values) { + if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) { + throw new IllegalArgumentException("Registry biome tags must contain resource keys."); + } + names.add(value.getAsString()); + } + tags.put(entry.getKey(), names); + } + return new GenerationRegistryContract(fingerprintSchema, fingerprint, definitions, generatedSources, tags); } @Override @@ -216,12 +251,13 @@ public final class GenerationRegistryContract { return fingerprintSchema == contract.fingerprintSchema && fingerprint.equals(contract.fingerprint) && definitions.equals(contract.definitions) - && generatedSources.equals(contract.generatedSources); + && generatedSources.equals(contract.generatedSources) + && biomeTags.equals(contract.biomeTags); } @Override public int hashCode() { - return Objects.hash(fingerprintSchema, fingerprint, definitions, generatedSources); + return Objects.hash(fingerprintSchema, fingerprint, definitions, generatedSources, biomeTags); } @Override @@ -442,13 +478,39 @@ public final class GenerationRegistryContract { ); } + private static NavigableMap> canonicalBiomeTags( + Map> values, + Map definitions + ) { + if (values.size() > MAX_DEFINITIONS) { + throw new IllegalArgumentException("Registry biome tags contain too many biomes."); + } + TreeMap> normalized = new TreeMap<>(); + for (Map.Entry> entry : values.entrySet()) { + PhysicalResourceKey biome = new PhysicalResourceKey("minecraft:worldgen/biome", entry.getKey()); + if (!definitions.containsKey(biome)) { + throw new IllegalArgumentException("Registry biome tags have no matching biome definition: " + entry.getKey()); + } + TreeSet tags = new TreeSet<>(); + for (String tag : entry.getValue()) { + tags.add(new PhysicalResourceKey("minecraft:worldgen/biome", tag).resourceKey()); + } + if (tags.size() > MAX_DEFINITIONS) { + throw new IllegalArgumentException("Registry biome contains too many tags."); + } + normalized.put(biome.resourceKey(), List.copyOf(tags)); + } + return Collections.unmodifiableNavigableMap(normalized); + } + private static String fingerprint( int fingerprintSchema, NavigableMap definitions, - NavigableMap generatedSources + NavigableMap generatedSources, + NavigableMap> biomeTags ) { return switch (fingerprintSchema) { - case FINGERPRINT_SCHEMA_VERSION_ONE -> fingerprintVersionOne(definitions, generatedSources); + case FINGERPRINT_SCHEMA_VERSION_ONE -> fingerprintVersionOne(definitions, generatedSources, biomeTags); default -> throw new IllegalArgumentException( "Unsupported generation registry fingerprint schema " + fingerprintSchema + "." ); @@ -457,7 +519,8 @@ public final class GenerationRegistryContract { private static String fingerprintVersionOne( NavigableMap definitions, - NavigableMap generatedSources + NavigableMap generatedSources, + NavigableMap> biomeTags ) { MessageDigest digest = sha256(); updateString(digest, FINGERPRINT_DOMAIN); @@ -479,6 +542,14 @@ public final class GenerationRegistryContract { updateString(digest, entry.getValue().renderedDefinitionSha256()); updateString(digest, entry.getValue().sourceJson()); } + updateInt(digest, biomeTags.size()); + for (Map.Entry> entry : biomeTags.entrySet()) { + updateString(digest, entry.getKey()); + updateInt(digest, entry.getValue().size()); + for (String tag : entry.getValue()) { + updateString(digest, tag); + } + } return HexFormat.of().formatHex(digest.digest()); } diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationRegistryContractFactory.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationRegistryContractFactory.java index 9e2f17cee..613b6213d 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationRegistryContractFactory.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationRegistryContractFactory.java @@ -169,6 +169,7 @@ public final class GenerationRegistryContractFactory { new LinkedHashMap<>(); Map generatedSources = new LinkedHashMap<>(); + Map> biomeTags = new TreeMap<>(); String packName = requiredData.getDataFolder().getName(); List reachableBiomes = new ArrayList<>(requiredDimension.getReachableBiomes(() -> requiredData)); for (IrisDimension registryDimension : registryDimensions(requiredData, requiredDimension)) { @@ -196,7 +197,8 @@ public final class GenerationRegistryContractFactory { requiredData, requiredFixer, requiredGenerationRegistry, - requiredAliasPolicy + requiredAliasPolicy, + biomeTags ); } addNativeStructures( @@ -209,7 +211,7 @@ public final class GenerationRegistryContractFactory { ReferencedPlatformKeys referenced = referencedPlatformKeys(packRoot, requiredRegistries); addPlatformDefinitions(definitions, BLOCK_REGISTRY, referenced.blockKeys(), requiredGenerationRegistry); addPlatformDefinitions(definitions, ENTITY_TYPE_REGISTRY, referenced.entityKeys(), requiredGenerationRegistry); - return contract(definitions, generatedSources, requiredGenerationRegistry); + return contract(definitions, generatedSources, requiredGenerationRegistry).withBiomeTags(biomeTags); } public static CustomBiomeDefinition customBiomeDefinition( @@ -798,7 +800,8 @@ public final class GenerationRegistryContractFactory { IrisData data, IDataFixer fixer, PlatformGenerationRegistry registry, - CustomBiomeAliasPolicy aliasPolicy + CustomBiomeAliasPolicy aliasPolicy, + Map> biomeTags ) throws IOException { TreeSet platformBiomeKeys = new TreeSet<>(); Map customResourceKeys = new LinkedHashMap<>(); @@ -833,6 +836,19 @@ public final class GenerationRegistryContractFactory { registry.generatedDefinitionRendererIdentity(), generated.definition() ); + Set tags = biomeTags.computeIfAbsent(generated.physicalKey().resourceKey(), ignored -> new TreeSet<>()); + for (String rawTag : customBiome.getEffectiveTags(biome.getVanillaDerivativeKey())) { + if (rawTag == null || rawTag.isBlank()) { + continue; + } + String tag = rawTag.trim().toLowerCase(Locale.ROOT); + if (tag.indexOf(':') < 0) { + tag = "minecraft:" + tag; + } + if (tag.matches("[a-z0-9_.-]+:[a-z0-9/._-]+")) { + tags.add(tag); + } + } String logicalKey = dimensionKey.toLowerCase(Locale.ROOT) + ":" + customBiomeId.toLowerCase(Locale.ROOT); customResourceKeys.merge( @@ -858,6 +874,7 @@ public final class GenerationRegistryContractFactory { } for (Map.Entry entry : legacyDefinitions.entrySet()) { CustomBiomeDefinition generated = entry.getValue(); + biomeTags.put(entry.getKey(), new TreeSet<>(biomeTags.get(generated.physicalKey().resourceKey()))); putGeneratedDefinition( definitions, generatedSources, diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationSemanticCapture.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationSemanticCapture.java index 4d2fb1b78..388523e1e 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationSemanticCapture.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationSemanticCapture.java @@ -15,6 +15,7 @@ import art.arcane.volmlib.util.mantle.flag.ReservedFlag; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterStructurePOI; import java.util.HashSet; import java.util.Objects; @@ -29,6 +30,14 @@ public final class GenerationSemanticCapture { public static ChunkGenerationSemantics capture( Engine engine, GenerationHistory.GenerationStage stage + ) { + return capture(engine, stage, (x, y, z) -> true); + } + + public static ChunkGenerationSemantics capture( + Engine engine, + GenerationHistory.GenerationStage stage, + CaveSpace caveSpace ) { Engine requiredEngine = Objects.requireNonNull(engine, "engine"); GenerationHistory.GenerationStage requiredStage = Objects.requireNonNull(stage, "stage"); @@ -36,7 +45,8 @@ public final class GenerationSemanticCapture { requiredEngine, requiredStage.chunkX(), requiredStage.chunkZ(), - requiredStage.activation().activationId() + requiredStage.activation().activationId(), + caveSpace ); } @@ -46,6 +56,17 @@ public final class GenerationSemanticCapture { int chunkZ, long activationId ) { + return capture(engine, chunkX, chunkZ, activationId, (x, y, z) -> true); + } + + private static ChunkGenerationSemantics capture( + Engine engine, + int chunkX, + int chunkZ, + long activationId, + CaveSpace caveSpace + ) { + Objects.requireNonNull(caveSpace, "cave space"); Engine requiredEngine = Objects.requireNonNull(engine, "engine"); ChunkGenerationSemantics.Builder semantics = ChunkGenerationSemantics.builder( chunkX, @@ -53,7 +74,7 @@ public final class GenerationSemanticCapture { activationId ); captureColumns(requiredEngine, chunkX, chunkZ, semantics); - captureMantleFacts(requiredEngine, chunkX, chunkZ, semantics); + captureMantleFacts(requiredEngine, chunkX, chunkZ, semantics, caveSpace); captureStructures(requiredEngine, chunkX, chunkZ, semantics); return semantics.seal().build(); } @@ -110,13 +131,17 @@ public final class GenerationSemanticCapture { Engine engine, int chunkX, int chunkZ, - ChunkGenerationSemantics.Builder semantics + ChunkGenerationSemantics.Builder semantics, + CaveSpace caveSpace ) { Mantle mantle = engine.getMantle().getMantle(); int minimumX = Math.multiplyExact(chunkX, CHUNK_SIZE); int minimumZ = Math.multiplyExact(chunkZ, CHUNK_SIZE); Set resolvedCaves = new HashSet<>(); mantle.iterateChunk(chunkX, chunkZ, MatterCavern.class, (localX, y, localZ, cavern) -> { + if (!caveSpace.isOpen(localX, y, localZ)) { + return; + } captureCave( engine, semantics, @@ -128,6 +153,9 @@ public final class GenerationSemanticCapture { ); }); mantle.iterateChunk(chunkX, chunkZ, HydrologyCaveCell.class, (localX, y, localZ, cell) -> { + if (!caveSpace.isOpen(localX, y, localZ)) { + return; + } int blockX = minimumX + localX; int blockZ = minimumZ + localZ; if (!hasFullHydrologyWeight(engine.getComplex(), blockX, blockZ)) { @@ -146,6 +174,13 @@ public final class GenerationSemanticCapture { semantics.addRiverProfile(cell.fluidProfileKey()); } }); + mantle.iterateChunk(chunkX, chunkZ, MatterStructurePOI.class, (localX, y, localZ, point) -> { + if (point != null) { + semantics.addPointOfInterest(new ChunkGenerationSemantics.PointOfInterest(point.getType(), + new ChunkGenerationSemantics.BlockPosition(Math.addExact(minimumX, localX), y, + Math.addExact(minimumZ, localZ)))); + } + }); mantle.iterateChunk(chunkX, chunkZ, String.class, (localX, y, localZ, marker) -> { StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker); if (decoded != null) { @@ -219,6 +254,11 @@ public final class GenerationSemanticCapture { } } + @FunctionalInterface + public interface CaveSpace { + boolean isOpen(int localX, int internalY, int localZ); + } + private record CavePosition(int x, int y, int z) { } } diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationSemanticIndex.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationSemanticIndex.java index 7e86834b0..4f0a1aa8d 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationSemanticIndex.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationSemanticIndex.java @@ -53,7 +53,7 @@ public final class GenerationSemanticIndex { private static final String JOURNAL_SUFFIX = ".iswal"; private static final String CATALOG_FILE_NAME = "index.isix"; private static final int MAGIC = 0x4953454D; - private static final int FORMAT_VERSION = 5; + private static final int FORMAT_VERSION = 6; private static final int CATALOG_MAGIC = 0x49534958; private static final int CATALOG_FORMAT_VERSION = 1; private static final int CATALOG_FIXED_BODY_BYTES = 12; @@ -204,6 +204,54 @@ public final class GenerationSemanticIndex { } } + public int discardUnstoredClaims(WorldChunkInventory stored, Set activationIds) throws IOException { + Objects.requireNonNull(stored, "stored chunks"); + Set selected = Set.copyOf(activationIds); + for (long activation : selected) { + if (activation <= 0L) { + throw new IllegalArgumentException("Generation activation IDs must be positive."); + } + } + lock.writeLock().lock(); + try { + compactJournals(); + int removed = 0; + for (long regionKey : allRegionKeys()) { + RegionSummary summary = loadSummaryLocked(regionKey); + boolean relevant = false; + for (long activation : selected) { + if (summary.activations.contains(activation)) { + relevant = true; + break; + } + } + if (!relevant) { + continue; + } + int regionX = regionX(regionKey); + int regionZ = regionZ(regionKey); + RegionShard previous = loadRegionLocked(regionKey, regionX, regionZ); + RegionShard replacement = previous.retainingStoredClaims(stored, selected); + int count = previous.recordCount() - replacement.recordCount(); + if (count == 0) { + continue; + } + byte[] encoded = replacement.encode(); + String hash = sha256(encoded); + publisher.publish(directory, regionX, regionZ, encoded); + pointerPublisher.publish(directory, regionX, regionZ, hash); + String previousHash = shardHashes.put(regionKey, hash); + regionRecordCounts.put(regionKey, replacement.recordCount()); + cacheRegion(regionKey, replacement); + removed += count; + deleteUnreferencedShard(regionX, regionZ, previousHash, hash); + } + return removed; + } finally { + lock.writeLock().unlock(); + } + } + public void compactJournals() throws IOException { lock.writeLock().lock(); try { @@ -2095,7 +2143,7 @@ public final class GenerationSemanticIndex { validateRegionCoordinate(file, regionX); validateRegionCoordinate(file, regionZ); int recordCount = input.readInt(); - if (recordCount <= 0 || recordCount > CHUNKS_PER_REGION) { + if (recordCount < 0 || recordCount > CHUNKS_PER_REGION) { throw invalid(file, "invalid record count " + recordCount); } int paletteSize = input.readInt(); @@ -2138,7 +2186,7 @@ public final class GenerationSemanticIndex { validateRegionCoordinate(file, regionX); validateRegionCoordinate(file, regionZ); int recordCount = input.readInt(); - if (recordCount <= 0 || recordCount > CHUNKS_PER_REGION) { + if (recordCount < 0 || recordCount > CHUNKS_PER_REGION) { throw invalid(file, "invalid record count " + recordCount); } int paletteSize = input.readInt(); @@ -2191,7 +2239,7 @@ public final class GenerationSemanticIndex { validateRegionCoordinate(source, regionX); validateRegionCoordinate(source, regionZ); int recordCount = input.readInt(); - if (recordCount <= 0 || recordCount > CHUNKS_PER_REGION) { + if (recordCount < 0 || recordCount > CHUNKS_PER_REGION) { throw invalid(source, "invalid record count " + recordCount); } int paletteSize = input.readInt(); @@ -2237,6 +2285,7 @@ public final class GenerationSemanticIndex { readKeys(source, input, palette, paletteUsed, builder, SemanticKind.OBJECT); readRiverFeatures(source, input, palette, paletteUsed, builder); readStructures(source, input, palette, paletteUsed, builder); + readPointsOfInterest(source, input, palette, paletteUsed, builder); if ((recordFlags & 1) != 0) { builder.seal(); } @@ -2280,6 +2329,19 @@ public final class GenerationSemanticIndex { return records[localIndex(chunkX, chunkZ)]; } + private RegionShard retainingStoredClaims(WorldChunkInventory stored, Set activationIds) { + RegionShard replacement = new RegionShard(regionX, regionZ); + for (int index = 0; index < records.length; index++) { + ChunkGenerationSemantics record = records[index]; + if (record != null && (!activationIds.contains(record.activationId()) + || stored.contains(record.chunkX(), record.chunkZ()))) { + replacement.records[index] = record; + replacement.recordCount++; + } + } + return replacement; + } + private RegionShard withRecord(ChunkGenerationSemantics semantics) { if ((semantics.chunkX() >> 5) != regionX || (semantics.chunkZ() >> 5) != regionZ) { throw new IllegalArgumentException("Chunk does not belong to generation semantic shard"); @@ -2362,6 +2424,9 @@ public final class GenerationSemanticIndex { for (ChunkGenerationSemantics.StructureOccurrence occurrence : semantics.structures()) { addPaletteKey(paletteKeys, occurrence.key()); } + for (ChunkGenerationSemantics.PointOfInterest point : semantics.pointsOfInterest()) { + addPaletteKey(paletteKeys, point.key()); + } } List palette = new ArrayList<>(paletteKeys); Map paletteIndexes = new HashMap<>(palette.size()); @@ -2418,6 +2483,13 @@ public final class GenerationSemanticIndex { output.writeInt(occurrence.position().y()); output.writeInt(occurrence.position().z()); } + output.writeShort(semantics.pointsOfInterest().size()); + for (ChunkGenerationSemantics.PointOfInterest point : semantics.pointsOfInterest()) { + output.writeShort(paletteIndexes.get(point.key())); + output.writeInt(point.position().x()); + output.writeInt(point.position().y()); + output.writeInt(point.position().z()); + } if (bodyBytes.size() > MAX_FILE_BYTES - CHECKSUM_BYTES) { throw new IOException("Generation semantic shard exceeds its size limit"); } @@ -2552,6 +2624,42 @@ public final class GenerationSemanticIndex { } } + private static void readPointsOfInterest( + Path file, + DataInputStream input, + String[] palette, + boolean[] paletteUsed, + ChunkGenerationSemantics.Builder builder + ) throws IOException { + int count = input.readUnsignedShort(); + if (count > ChunkGenerationSemantics.MAX_STRUCTURES) { + throw invalid(file, "too many points of interest"); + } + ChunkGenerationSemantics.PointOfInterest previous = null; + for (int index = 0; index < count; index++) { + int paletteIndex = input.readUnsignedShort(); + if (paletteIndex >= palette.length) { + throw invalid(file, "point of interest key palette index is out of range"); + } + paletteUsed[paletteIndex] = true; + ChunkGenerationSemantics.PointOfInterest occurrence = + new ChunkGenerationSemantics.PointOfInterest( + palette[paletteIndex], + new ChunkGenerationSemantics.BlockPosition( + input.readInt(), + input.readInt(), + input.readInt() + ) + ); + if (previous != null + && ChunkGenerationSemantics.pointComparator().compare(previous, occurrence) >= 0) { + throw invalid(file, "points of interest are not in canonical order"); + } + builder.addPointOfInterest(occurrence); + previous = occurrence; + } + } + private static void readRiverFeatures( Path file, DataInputStream input, diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationSemanticQueries.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationSemanticQueries.java index 6022aab1c..546d93bd1 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationSemanticQueries.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationSemanticQueries.java @@ -9,7 +9,6 @@ import art.arcane.iris.engine.hydrology.HydrologyFeatureType; import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime; import java.math.BigInteger; -import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.OptionalLong; @@ -29,7 +28,7 @@ public final class GenerationSemanticQueries { ) { Engine requiredEngine = Objects.requireNonNull(engine, "engine"); String requiredKey = ChunkGenerationSemantics.requireResourceKey( - Objects.requireNonNull(key, "key").trim().toLowerCase(Locale.ROOT)); + Objects.requireNonNull(key, "key").trim()); int radius = Math.max(0, Math.min(maximumChunkRadius, 2048)); GenerationHistory history = history(requiredEngine).orElse(null); if (history == null) { diff --git a/core/src/main/java/art/arcane/iris/engine/history/IrisBoundarySignatureSampler.java b/core/src/main/java/art/arcane/iris/engine/history/IrisBoundarySignatureSampler.java index 09bd471f2..466e884a3 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/IrisBoundarySignatureSampler.java +++ b/core/src/main/java/art/arcane/iris/engine/history/IrisBoundarySignatureSampler.java @@ -1,170 +1,94 @@ package art.arcane.iris.engine.history; -import art.arcane.iris.engine.IrisComplex; -import art.arcane.iris.engine.DimensionStackContext; -import art.arcane.iris.engine.DimensionStackLayout; -import art.arcane.iris.engine.UpperDimensionContext; import art.arcane.iris.engine.IrisEngine; -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisBiomeCustom; -import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.volmlib.util.math.RNG; import java.io.IOException; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.OptionalInt; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; public final class IrisBoundarySignatureSampler implements GenerationBoundarySignatureSampler { public static final IrisBoundarySignatureSampler INSTANCE = new IrisBoundarySignatureSampler(); + private static final int MAXIMUM_CACHED_CHUNKS = 32; - static final int VERTICAL_SAMPLE_STEP = 4; private IrisBoundarySignatureSampler() { } + public static void checkpoint(IrisEngine engine) throws IOException { + if (engine.getPlatformHooks().isMainThread()) { + throw new IOException("Terrain save checkpoints must run outside the platform tick thread."); + } + try { + engine.getPlatformHooks().flushSavedTerrainCapture(engine).get(30, TimeUnit.SECONDS); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while saving the terrain boundary.", failure); + } catch (ExecutionException | TimeoutException failure) { + throw new IOException("Unable to save the terrain boundary.", failure); + } + } + @Override public TerrainBoundarySignature sample(IrisEngine engine, int blockX, int blockZ) throws IOException { - IrisEngine requiredEngine = Objects.requireNonNull(engine, "Iris engine"); - try { - IrisComplex complex = Objects.requireNonNull(requiredEngine.getComplex(), "Iris complex"); - int height = requiredEngine.getHeight(); - if (height <= 0) { - throw new IOException("Iris boundary sampling requires a positive world height."); + try (TerrainBoundarySignatureStore.SignatureSampler capture = open(engine)) { + return capture.sample(blockX, blockZ); + } + } + + @Override + public TerrainBoundarySignatureStore.SignatureSampler open(IrisEngine engine) throws IOException { + if (engine.getPlatformHooks().isMainThread()) { + throw new IOException("Terrain boundary capture must run outside the platform tick thread."); + } + return new LiveCapture(engine); + } + + private static final class LiveCapture implements TerrainBoundarySignatureStore.SignatureSampler { + private final IrisEngine engine; + private final LinkedHashMap chunks = new LinkedHashMap<>(32, 0.75F, true); + + private LiveCapture(IrisEngine engine) { + this.engine = engine; + } + + @Override + public TerrainBoundarySignature sample(int blockX, int blockZ) throws IOException { + int chunkX = Math.floorDiv(blockX, 16); + int chunkZ = Math.floorDiv(blockZ, 16); + long key = ChunkGenerationOwnership.packChunk(chunkX, chunkZ); + SavedTerrainChunk terrain = chunks.get(key); + if (terrain == null) { + terrain = captureChunk(chunkX, chunkZ); + chunks.put(key, terrain); + if (chunks.size() > MAXIMUM_CACHED_CHUNKS) { + Map.Entry oldest = chunks.firstEntry(); + chunks.remove(oldest.getKey()); + } } + return terrain.column(blockX, blockZ); + } - int minimumY = requiredEngine.getMinHeight(); - int terrainHeight = clampHeight( - (int) Math.round(complex.getHeightStream().getDouble(blockX, blockZ)), - height - ); - int fluidHeight = clampHeight( - (int) Math.round(complex.getRiverWaterSurfaceStream().getDouble(blockX, blockZ)), - height - ); - OptionalInt fluid = fluidHeight > terrainHeight - ? OptionalInt.of(fluidHeight) - : OptionalInt.empty(); - int sampleCount = Math.ceilDiv(height, VERTICAL_SAMPLE_STEP); - TerrainBoundarySignature.VerticalLayout layout = new TerrainBoundarySignature.VerticalLayout( - minimumY, - VERTICAL_SAMPLE_STEP, - sampleCount - ); - short[] biomeIndices = new short[sampleCount]; - LinkedHashMap paletteIndices = new LinkedHashMap<>(); - - for (int sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++) { - int worldY = layout.sampleY(sampleIndex); - int internalY = worldY - minimumY; - String biomeKey = physicalBiomeKey( - requiredEngine, - complex, - blockX, - worldY, - blockZ, - internalY, - terrainHeight - ); - biomeIndices[sampleIndex] = paletteIndex(paletteIndices, biomeKey); + private SavedTerrainChunk captureChunk(int chunkX, int chunkZ) throws IOException { + try { + return engine.getPlatformHooks().captureSavedTerrainChunk(engine, chunkX, chunkZ) + .get(30, TimeUnit.SECONDS); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while capturing terrain at " + chunkX + "," + chunkZ, failure); + } catch (ExecutionException | TimeoutException failure) { + throw new IOException("Unable to capture terrain at " + chunkX + "," + chunkZ, failure); } - - int visibleSurfaceHeight = Math.max(terrainHeight, fluidHeight); - UpperDimensionContext upperContext = requiredEngine.getUpperContext(); - int upperDepth = upperContext == null ? 0 - : height - 1 - upperContext.getEffectiveSurfaceY(blockX, blockZ); - return new TerrainBoundarySignature( - new TerrainBoundarySignature.Column( - blockX, - blockZ, - visibleSurfaceHeight, - terrainHeight, - fluid, - upperDepth > 0 ? OptionalInt.of(upperDepth) : OptionalInt.empty() - ), - new TerrainBoundarySignature.Samples( - layout, - new TerrainBoundarySignature.BiomeEncoding( - List.copyOf(paletteIndices.keySet()), - biomeIndices - ) - ) - ); - } catch (IOException exception) { - throw exception; - } catch (RuntimeException exception) { - throw new IOException("Unable to sample Iris terrain boundary at " - + blockX + "," + blockZ + ".", exception); - } - } - - private static int clampHeight(int value, int height) { - return Math.max(0, Math.min(height - 1, value)); - } - - private static short paletteIndex(Map indices, String biomeKey) { - Short existing = indices.get(biomeKey); - if (existing != null) { - return existing; - } - int next = indices.size(); - if (next > Short.MAX_VALUE) { - throw new IllegalStateException("Iris boundary biome palette exceeds compact index capacity."); - } - short index = (short) next; - indices.put(biomeKey, index); - return index; - } - - private static String physicalBiomeKey( - IrisEngine engine, - IrisComplex complex, - int blockX, - int worldY, - int blockZ, - int internalY, - int terrainHeight - ) { - String historicalKey = complex.historicalPhysicalBiomeKeyAt(blockX, worldY, blockZ).orElse(null); - if (historicalKey != null) { - return historicalKey; - } - int caveSwitchY = Math.max(-8 - engine.getMinHeight(), 40); - boolean underground = internalY <= caveSwitchY && internalY <= terrainHeight - 8; - IrisDimension biomeDimension = engine.getDimension(); - IrisBiome biome; - DimensionStackContext stackContext = engine.getDimensionStackContext(); - if (stackContext == null) { - biome = underground - ? engine.getCaveBiome(blockX, internalY, blockZ) - : complex.getTrueBiomeStream().get(blockX, blockZ); - } else { - DimensionStackLayout.Layer layer = stackContext.getLayout(blockX, blockZ).layerAt(internalY); - biome = layer.biome(); - biomeDimension = layer.terrainContext().getDimension(); - underground = false; - } - if (biome == null && underground) { - biome = complex.getTrueBiomeStream().get(blockX, blockZ); - } - if (biome == null) { - return "minecraft:plains"; } - RNG rng = new RNG(engine.getWorld().getRawWorldSeed() - ^ ((long) blockX * 341873128712L) - ^ ((long) worldY * 132897987541L) - ^ ((long) blockZ * 42317861L)); - if (biome.isCustom()) { - IrisBiomeCustom custom = biome.getCustomBiome(rng, engine, blockX, worldY, blockZ); - return custom == null - ? "minecraft:plains" - : engine.getData().customBiomeResourceKey(biomeDimension, custom); + @Override + public void close() throws IOException { + try { + checkpoint(engine); + } finally { + chunks.clear(); + } } - String key = underground - ? biome.getGroundBiomeKey(rng, engine, blockX, worldY, blockZ) - : biome.getSkyBiomeKey(rng, engine, blockX, worldY, blockZ); - return key == null || key.isBlank() ? "minecraft:plains" : key; } } diff --git a/core/src/main/java/art/arcane/iris/engine/history/NativeTerrainReceipt.java b/core/src/main/java/art/arcane/iris/engine/history/NativeTerrainReceipt.java new file mode 100644 index 000000000..fc518e4c3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/history/NativeTerrainReceipt.java @@ -0,0 +1,196 @@ +package art.arcane.iris.engine.history; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +public final class NativeTerrainReceipt { + public static final String STRUCTURE_ACTIVATION_KEY = "iris:structure_activation"; + public static final String NBT_KEY = "iris:natural_terrain"; + private static final int MAGIC = 0x4952544E; + private static final int VERSION = 1; + private static final int MAXIMUM_BYTES = 64 * 1024 * 1024; + + private NativeTerrainReceipt() { + } + + public static byte[] encode(SavedTerrainChunk chunk, long activationId, String epochId) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (DataOutputStream output = new DataOutputStream(new GZIPOutputStream(bytes))) { + output.writeInt(MAGIC); + output.writeInt(VERSION); + output.writeInt(chunk.chunkX()); + output.writeInt(chunk.chunkZ()); + output.writeLong(activationId); + output.writeUTF(epochId); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + output.writeBoolean(chunk.hasColumn(x, z)); + if (!chunk.hasColumn(x, z)) { + continue; + } + writeColumn(output, chunk.column((chunk.chunkX() << 4) + x, (chunk.chunkZ() << 4) + z)); + } + } + } + return bytes.toByteArray(); + } + + public static SavedTerrainChunk restore(byte[] bytes, String nativeStatus, int chunkX, int chunkZ, + int minimumY, int height) throws IOException { + SavedTerrainChunk terrain = decode(bytes, nativeStatus).terrain(); + if (terrain.chunkX() != chunkX || terrain.chunkZ() != chunkZ) { + throw new IOException("Native terrain receipt coordinates do not match its chunk"); + } + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + if (!terrain.hasColumn(x, z)) { + if (x == 0 || x == 15 || z == 0 || z == 15) { + throw new IOException("Native terrain receipt is missing an edge column"); + } + continue; + } + BoundaryColumnGeometry geometry = terrain.column((chunkX << 4) + x, (chunkZ << 4) + z).geometry(); + if (geometry.minimumY() != minimumY || geometry.height() != height) { + throw new IOException("Native terrain receipt height does not match its dimension"); + } + } + } + return terrain; + } + + public static Decoded decode(byte[] bytes, String nativeStatus) throws IOException { + if (bytes.length > MAXIMUM_BYTES) { + throw new IOException("Native terrain receipt exceeds its size limit"); + } + byte[] decoded; + try (GZIPInputStream compressed = new GZIPInputStream(new ByteArrayInputStream(bytes))) { + decoded = compressed.readNBytes(MAXIMUM_BYTES + 1); + } + if (decoded.length > MAXIMUM_BYTES) { + throw new IOException("Native terrain receipt expands beyond its size limit"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(decoded))) { + if (input.readInt() != MAGIC || input.readInt() != VERSION) { + throw new IOException("Invalid native terrain receipt format"); + } + int chunkX = input.readInt(); + int chunkZ = input.readInt(); + long activation = input.readLong(); + String epoch = input.readUTF(); + if (activation < 0 || epoch.isBlank()) { + throw new IOException("Invalid native terrain receipt identity"); + } + List columns = new ArrayList<>(256); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + if (!input.readBoolean()) { + columns.add(null); + continue; + } + columns.add(readColumn(input, Math.addExact(Math.multiplyExact(chunkX, 16), x), + Math.addExact(Math.multiplyExact(chunkZ, 16), z))); + } + } + if (input.read() != -1) { + throw new IOException("Trailing native terrain receipt data"); + } + return new Decoded(new SavedTerrainChunk(chunkX, chunkZ, nativeStatus, columns), activation, epoch); + } catch (IllegalArgumentException | ArithmeticException exception) { + throw new IOException("Invalid native terrain receipt", exception); + } + } + + private static void writeColumn(DataOutputStream output, TerrainBoundarySignature signature) throws IOException { + output.writeInt(signature.surfaceHeight()); + output.writeInt(signature.oceanFloorHeight()); + output.writeInt(signature.fluidHeight().orElse(-1)); + output.writeInt(signature.upperCeilingDepth().orElse(-1)); + TerrainBoundarySignature.VerticalLayout layout = signature.samples().layout(); + output.writeInt(layout.minimumY()); + output.writeInt(layout.sampleStep()); + output.writeInt(layout.sampleCount()); + for (int index = 0; index < layout.sampleCount(); index++) { + output.writeUTF(signature.biomeAtSample(index)); + } + BoundaryColumnGeometry geometry = signature.geometry(); + output.writeInt(geometry.minimumY()); + output.writeInt(geometry.palette().size()); + for (BoundaryColumnGeometry.Voxel voxel : geometry.palette()) { + output.writeUTF(voxel.stateKey()); + output.writeByte(voxel.phase().ordinal()); + output.writeUTF(voxel.fluidStateKey()); + output.writeBoolean(voxel.protectedContent()); + } + int[] ends = geometry.runEnds(); + short[] indices = geometry.paletteIndices(); + output.writeInt(ends.length); + for (int index = 0; index < ends.length; index++) { + output.writeInt(ends[index]); + output.writeShort(indices[index]); + } + } + + private static TerrainBoundarySignature readColumn(DataInputStream input, int x, int z) throws IOException { + int surface = input.readInt(); + int floor = input.readInt(); + int fluid = input.readInt(); + int ceiling = input.readInt(); + int biomeMinimum = input.readInt(); + int step = input.readInt(); + int count = boundedCount(input.readInt()); + List biomes = new ArrayList<>(); + short[] biomeIndices = new short[count]; + for (int index = 0; index < count; index++) { + String biome = input.readUTF(); + int paletteIndex = biomes.indexOf(biome); + if (paletteIndex < 0) { + paletteIndex = biomes.size(); + biomes.add(biome); + } + biomeIndices[index] = (short) paletteIndex; + } + int minimumY = input.readInt(); + int paletteCount = boundedCount(input.readInt()); + List palette = new ArrayList<>(paletteCount); + for (int index = 0; index < paletteCount; index++) { + String state = input.readUTF(); + int phase = input.readUnsignedByte(); + if (phase >= BoundaryColumnGeometry.Phase.values().length) { + throw new IOException("Invalid terrain receipt voxel phase"); + } + palette.add(new BoundaryColumnGeometry.Voxel(state, BoundaryColumnGeometry.Phase.values()[phase], + input.readUTF(), input.readBoolean())); + } + int runs = boundedCount(input.readInt()); + int[] ends = new int[runs]; + short[] indices = new short[runs]; + for (int index = 0; index < runs; index++) { + ends[index] = input.readInt(); + indices[index] = input.readShort(); + } + return new TerrainBoundarySignature(new TerrainBoundarySignature.Column(x, z, surface, floor, + fluid < 0 ? OptionalInt.empty() : OptionalInt.of(fluid), + ceiling < 0 ? OptionalInt.empty() : OptionalInt.of(ceiling)), + new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(biomeMinimum, step, count), + new TerrainBoundarySignature.BiomeEncoding(biomes, biomeIndices)), + new BoundaryColumnGeometry(minimumY, palette, ends, indices)); + } + + private static int boundedCount(int count) throws IOException { + if (count < 0 || count > 4096) { + throw new IOException("Invalid native terrain receipt collection size"); + } + return count; + } + + public record Decoded(SavedTerrainChunk terrain, long activationId, String epochId) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/history/RegionGenerationOwnership.java b/core/src/main/java/art/arcane/iris/engine/history/RegionGenerationOwnership.java index 7a3aa7d3c..5831ccb41 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/RegionGenerationOwnership.java +++ b/core/src/main/java/art/arcane/iris/engine/history/RegionGenerationOwnership.java @@ -20,6 +20,7 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.util.zip.CRC32; +import java.util.Set; final class RegionGenerationOwnership { static final int CHUNKS_PER_SIDE = 32; @@ -202,6 +203,27 @@ final class RegionGenerationOwnership { return true; } + synchronized int discardUnstoredClaims(WorldChunkInventory stored, Set activationIds) { + int removed = 0; + for (int localZ = 0; localZ < CHUNKS_PER_SIDE; localZ++) { + for (int localX = 0; localX < CHUNKS_PER_SIDE; localX++) { + int index = chunkIndex(localX, localZ); + if (activations[index] == 0L || !activationIds.contains(activations[index])) { + continue; + } + int chunkX = (regionX << 5) + localX; + int chunkZ = (regionZ << 5) + localZ; + if (!stored.contains(chunkX, chunkZ)) { + activations[index] = 0L; + assignmentCount--; + removed++; + } + } + } + dirty |= removed > 0; + return removed; + } + synchronized boolean persist(Path directory) throws IOException { if (!dirty) { return false; diff --git a/core/src/main/java/art/arcane/iris/engine/history/SavedTerrainChunk.java b/core/src/main/java/art/arcane/iris/engine/history/SavedTerrainChunk.java new file mode 100644 index 000000000..1387f1e97 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/history/SavedTerrainChunk.java @@ -0,0 +1,208 @@ +package art.arcane.iris.engine.history; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.OptionalInt; + +public final class SavedTerrainChunk { + private final int chunkX; + private final int chunkZ; + private final String nativeStatus; + private final List columns; + + SavedTerrainChunk(int chunkX, int chunkZ, String nativeStatus, + List columns) { + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.nativeStatus = Objects.requireNonNull(nativeStatus, "native status"); + this.columns = Collections.unmodifiableList(new ArrayList<>(columns)); + } + + public static SavedTerrainChunk read(Path dimensionRoot, int chunkX, int chunkZ, + int minimumY, int height) throws IOException { + return SavedTerrainChunkReader.read(dimensionRoot, chunkX, chunkZ, minimumY, height); + } + + public static SavedTerrainChunk readNbt(byte[] bytes, int chunkX, int chunkZ, + int minimumY, int height) throws IOException { + return SavedTerrainChunkReader.readNbt(bytes, chunkX, chunkZ, minimumY, height); + } + + public static void verifyCheckpoint(Path dimensionRoot, int chunkX, int chunkZ, String status, byte[] receipt, long structureActivation) + throws IOException { + SavedTerrainChunkReader.verifyCheckpoint(dimensionRoot, chunkX, chunkZ, status, receipt, structureActivation); + } + + public static byte[] readReceipt(Path dimensionRoot, int chunkX, int chunkZ) throws IOException { + return SavedTerrainChunkReader.readReceipt(dimensionRoot, chunkX, chunkZ); + } + + public static String readStatus(Path dimensionRoot, int chunkX, int chunkZ) throws IOException { + return SavedTerrainChunkReader.readStatus(dimensionRoot, chunkX, chunkZ); + } + + public static SavedTerrainChunk capture(int chunkX, int chunkZ, int minimumY, int height, + String nativeStatus, VoxelSource source) throws IOException { + return capture(chunkX, chunkZ, minimumY, height, nativeStatus, source, false); + } + + public static SavedTerrainChunk captureBoundary(int chunkX, int chunkZ, int minimumY, int height, + String nativeStatus, VoxelSource source) throws IOException { + return capture(chunkX, chunkZ, minimumY, height, nativeStatus, source, true); + } + + private static SavedTerrainChunk capture(int chunkX, int chunkZ, int minimumY, int height, + String nativeStatus, VoxelSource source, boolean boundaryOnly) throws IOException { + Objects.requireNonNull(source, "voxel source"); + if (height <= 0 || height > 4096 || minimumY % 16 != 0 || height % 16 != 0) { + throw new IOException("Invalid saved terrain height: " + minimumY + "/" + height); + } + Math.addExact(minimumY, height); + List columns = new ArrayList<>(256); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + if (boundaryOnly && x != 0 && x != 15 && z != 0 && z != 15) { + columns.add(null); + continue; + } + List voxels = new ArrayList<>(height); + Map biomePalette = new LinkedHashMap<>(); + short[] biomeIndices = new short[height / 4]; + int surface = 0; + int floor = 0; + int fluid = -1; + for (int offset = 0; offset < height; offset++) { + int y = minimumY + offset; + BoundaryColumnGeometry.Voxel voxel = Objects.requireNonNull(source.voxel(x, y, z), "voxel"); + voxels.add(voxel); + if (voxel.phase() != BoundaryColumnGeometry.Phase.AIR) { + surface = offset; + } + if (voxel.phase() == BoundaryColumnGeometry.Phase.SOLID) { + floor = offset; + } + if (!voxel.fluidStateKey().isEmpty()) { + fluid = offset; + } + if (offset % 4 == 0) { + String biome = Objects.requireNonNull(source.biome(x, y, z), "physical biome"); + Short index = biomePalette.get(biome); + if (index == null) { + index = (short) biomePalette.size(); + biomePalette.put(biome, index); + } + biomeIndices[offset / 4] = index; + } + } + BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(minimumY, voxels); + OptionalInt groundSurface = source.groundSurface(x, z); + if (groundSurface.isPresent()) { + floor = geometry.surfaceOffsetNear(groundSurface.getAsInt()); + if (floor < 0 || floor >= height) { + throw new IOException("Natural terrain ground surface is outside the chunk height"); + } + surface = Math.max(floor, fluid); + } + columns.add(new TerrainBoundarySignature( + new TerrainBoundarySignature.Column(Math.addExact(Math.multiplyExact(chunkX, 16), x), + Math.addExact(Math.multiplyExact(chunkZ, 16), z), surface, floor, + fluid > floor ? OptionalInt.of(fluid) : OptionalInt.empty(), OptionalInt.empty()), + new TerrainBoundarySignature.Samples( + new TerrainBoundarySignature.VerticalLayout(minimumY, 4, biomeIndices.length), + new TerrainBoundarySignature.BiomeEncoding(List.copyOf(biomePalette.keySet()), biomeIndices)), + geometry)); + } + } + return new SavedTerrainChunk(chunkX, chunkZ, nativeStatus, columns); + } + + public SavedTerrainChunk boundaryOnly() { + if (!hasColumn(1, 1)) { + return this; + } + List boundary = new ArrayList<>(columns); + for (int x = 1; x < 15; x++) { + for (int z = 1; z < 15; z++) { + boundary.set(x * 16 + z, null); + } + } + return new SavedTerrainChunk(chunkX, chunkZ, nativeStatus, boundary); + } + + public TerrainBoundarySignature column(int blockX, int blockZ) { + if (Math.floorDiv(blockX, 16) != chunkX || Math.floorDiv(blockZ, 16) != chunkZ) { + throw new IllegalArgumentException("Boundary column is outside the captured chunk"); + } + return Objects.requireNonNull(columns.get(Math.floorMod(blockX, 16) * 16 + Math.floorMod(blockZ, 16)), + "Interior columns are not retained in a boundary receipt"); + } + + public boolean hasColumn(int localX, int localZ) { + return columns.get(localX * 16 + localZ) != null; + } + + public int chunkX() { + return chunkX; + } + + public int chunkZ() { + return chunkZ; + } + + public String nativeStatus() { + return nativeStatus; + } + + public static int statusRank(String status) { + if (status == null) { + return -1; + } + String key = status.startsWith("minecraft:") ? status.substring(10) : status; + return switch (key) { + case "empty" -> 0; + case "structure_starts" -> 1; + case "structure_references" -> 2; + case "biomes" -> 3; + case "noise" -> 4; + case "surface" -> 5; + case "carvers" -> 6; + case "features" -> 7; + case "initialize_light" -> 8; + case "light" -> 9; + case "spawn" -> 10; + case "full" -> 11; + default -> -1; + }; + } + + public static boolean hasTerrain(String status) { + return statusRank(status) >= 4; + } + + public static boolean isComplete(String status) { + return "minecraft:full".equals(status) || "full".equals(status); + } + + public static void requireComplete(String status, int chunkX, int chunkZ) throws IOException { + if (!isComplete(status)) { + throw new IOException("Saved chunk " + chunkX + "," + chunkZ + " has unfinished native generation status " + + status + "; finish its generation before changing the generator."); + } + } + + public interface VoxelSource { + default OptionalInt groundSurface(int localX, int localZ) { + return OptionalInt.empty(); + } + + BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) throws IOException; + + String biome(int localX, int worldY, int localZ) throws IOException; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/history/SavedTerrainChunkReader.java b/core/src/main/java/art/arcane/iris/engine/history/SavedTerrainChunkReader.java new file mode 100644 index 000000000..97ccab3c8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/history/SavedTerrainChunkReader.java @@ -0,0 +1,368 @@ +package art.arcane.iris.engine.history; + +import art.arcane.volmlib.util.nbt.io.NBTUtil; +import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import art.arcane.volmlib.util.nbt.tag.ByteArrayTag; +import art.arcane.volmlib.util.nbt.tag.ListTag; +import art.arcane.volmlib.util.nbt.tag.LongArrayTag; +import art.arcane.volmlib.util.nbt.tag.LongTag; +import art.arcane.volmlib.util.nbt.tag.NumberTag; +import art.arcane.volmlib.util.nbt.tag.StringTag; +import art.arcane.volmlib.util.nbt.tag.Tag; +import net.jpountz.lz4.LZ4BlockInputStream; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.zip.GZIPInputStream; +import java.util.zip.InflaterInputStream; + +final class SavedTerrainChunkReader { + private static final int SECTOR_BYTES = 4096; + private static final int MAX_CHUNK_BYTES = 64 * 1024 * 1024; + private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel( + "minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false); + + private SavedTerrainChunkReader() { + } + + static SavedTerrainChunk read(Path dimensionRoot, int chunkX, int chunkZ, int minimumY, int height) + throws IOException { + return readRoot(readRoot(dimensionRoot, chunkX, chunkZ), chunkX, chunkZ, minimumY, height); + } + + static SavedTerrainChunk readNbt(byte[] bytes, int chunkX, int chunkZ, int minimumY, int height) throws IOException { + if (bytes.length > MAX_CHUNK_BYTES) { + throw new IOException("Native chunk exceeds its size limit"); + } + Tag parsed = NBTUtil.read(new ByteArrayInputStream(bytes), false).getTag(); + if (!(parsed instanceof CompoundTag root) || number(root, "xPos") != chunkX || number(root, "zPos") != chunkZ) { + throw new IOException("Native chunk identity differs from requested coordinates"); + } + return readRoot(root, chunkX, chunkZ, minimumY, height); + } + + private static SavedTerrainChunk readRoot(CompoundTag root, int chunkX, int chunkZ, int minimumY, int height) + throws IOException { + String status = string(root, "Status"); + if (!SavedTerrainChunk.hasTerrain(status)) { + throw new IOException("Saved chunk " + chunkX + "," + chunkZ + " has no terrain at native status " + status); + } + Tag receiptTag = root.get(NativeTerrainReceipt.NBT_KEY); + if (root.get("ChunkBukkitValues") instanceof CompoundTag bukkitValues) { + receiptTag = bukkitValues.get(NativeTerrainReceipt.NBT_KEY); + } + if (receiptTag != null) { + if (!(receiptTag instanceof ByteArrayTag bytes)) { + throw new IOException("Native terrain receipt is not a byte array"); + } + return NativeTerrainReceipt.restore(bytes.getValue(), status, chunkX, chunkZ, minimumY, height); + } + Tag sectionTag = root.get("sections"); + if (!(sectionTag instanceof ListTag sectionList)) { + throw new IOException("Saved chunk has no current-format sections: " + chunkX + "," + chunkZ); + } + Map sections = new HashMap<>(); + for (Tag entry : sectionList) { + if (!(entry instanceof CompoundTag section)) { + throw new IOException("Saved chunk section is not a compound"); + } + int sectionY = number(section, "Y"); + if (sectionY * 16 < minimumY || sectionY * 16 >= (long) minimumY + height) { + continue; + } + if (sections.put(sectionY, decodeSection(section)) != null) { + throw new IOException("Saved chunk contains duplicate section " + sectionY); + } + } + return SavedTerrainChunk.captureBoundary(chunkX, chunkZ, minimumY, height, status, new SavedTerrainChunk.VoxelSource() { + @Override + public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) throws IOException { + Section section = requireSection(sections, y); + return section.blocks().get((Math.floorMod(y, 16) << 8) | (z << 4) | x); + } + + @Override + public String biome(int x, int y, int z) throws IOException { + Section section = requireSection(sections, y); + return section.biomes().get(((Math.floorMod(y, 16) >> 2) << 4) | ((z >> 2) << 2) | (x >> 2)); + } + }); + } + + static void verifyCheckpoint(Path dimensionRoot, int chunkX, int chunkZ, String status, byte[] expectedReceipt, long expectedStructureActivation) + throws IOException { + CompoundTag root = readRoot(dimensionRoot, chunkX, chunkZ); + if (SavedTerrainChunk.statusRank(string(root, "Status")) < SavedTerrainChunk.statusRank(status)) { + throw new IOException("Native terrain checkpoint did not persist stage " + status + " for " + chunkX + "," + chunkZ); + } + if (expectedStructureActivation > 0 && structureActivation(root) != expectedStructureActivation) { + throw new IOException("Native structure checkpoint differs for " + chunkX + "," + chunkZ); + } + if (expectedReceipt != null && !Arrays.equals(expectedReceipt, receipt(root))) { + throw new IOException("Native terrain receipt checkpoint differs for " + chunkX + "," + chunkZ); + } + } + + static byte[] readReceipt(Path dimensionRoot, int chunkX, int chunkZ) throws IOException { + return receipt(readRoot(dimensionRoot, chunkX, chunkZ)); + } + + private static long structureActivation(CompoundTag root) throws IOException { + Tag activation = root.get(NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY); + if (root.get("ChunkBukkitValues") instanceof CompoundTag values) { + activation = values.get(NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY); + } + if (activation == null) { + return 0; + } + if (!(activation instanceof LongTag value) || value.getValue() <= 0) { + throw new IOException("Invalid native structure activation"); + } + return value.getValue(); + } + + private static byte[] receipt(CompoundTag root) throws IOException { + Tag receipt = root.get(NativeTerrainReceipt.NBT_KEY); + if (root.get("ChunkBukkitValues") instanceof CompoundTag values) { + receipt = values.get(NativeTerrainReceipt.NBT_KEY); + } + if (receipt == null) { + return null; + } + if (!(receipt instanceof ByteArrayTag bytes)) { + throw new IOException("Native terrain receipt is not a byte array"); + } + return bytes.getValue(); + } + + static String readStatus(Path dimensionRoot, int chunkX, int chunkZ) throws IOException { + return string(readRoot(dimensionRoot, chunkX, chunkZ), "Status"); + } + + private static Section requireSection(Map sections, int y) throws IOException { + Section section = sections.get(Math.floorDiv(y, 16)); + if (section == null) { + throw new IOException("Saved terrain is missing section " + Math.floorDiv(y, 16)); + } + return section; + } + + private static Section decodeSection(CompoundTag section) 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"); + } + decodedBlocks.add(decodeBlock(state)); + } + ListTag biomePalette = list(biomes, "palette"); + List decodedBiomes = new ArrayList<>(biomePalette.size()); + for (Tag entry : biomePalette) { + if (!(entry instanceof StringTag biome) || biome.getValue().isBlank()) { + throw new IOException("Saved biome palette entry is not a resource key"); + } + decodedBiomes.add(biome.getValue()); + } + return new Section(new Palette<>(decodedBlocks, packedData(blocks), 4, 4096), + new Palette<>(decodedBiomes, packedData(biomes), 1, 64)); + } + + private static BoundaryColumnGeometry.Voxel decodeBlock(CompoundTag state) throws IOException { + String name = string(state, "Name"); + TreeMap properties = new TreeMap<>(); + if (state.get("Properties") instanceof CompoundTag values) { + for (Map.Entry> property : values) { + if (!(property.getValue() instanceof StringTag value)) { + throw new IOException("Saved block property is not a string"); + } + properties.put(property.getKey(), value.getValue()); + } + } + 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 + : new BoundaryColumnGeometry.Voxel(stateKey, BoundaryColumnGeometry.Phase.AIR, "", false); + } + boolean liquid = name.equals("minecraft:water") || name.equals("minecraft:lava"); + String fluidKey = liquid ? stateKey : "true".equals(properties.get("waterlogged")) + ? "minecraft:water[level=0]" : ""; + return new BoundaryColumnGeometry.Voxel(stateKey, + liquid ? BoundaryColumnGeometry.Phase.FLUID : BoundaryColumnGeometry.Phase.SOLID, fluidKey, false); + } + + private static String stateKey(String name, Map properties) { + if (properties.isEmpty()) { + return name; + } + StringBuilder encoded = new StringBuilder(name).append('['); + for (Map.Entry property : properties.entrySet()) { + if (encoded.charAt(encoded.length() - 1) != '[') { + encoded.append(','); + } + encoded.append(property.getKey()).append('=').append(property.getValue()); + } + return encoded.append(']').toString(); + } + + private static CompoundTag readRoot(Path dimensionRoot, int chunkX, int chunkZ) throws IOException { + Path regionDirectory = dimensionRoot.toAbsolutePath().normalize().resolve("region"); + Path region = regionDirectory.resolve("r." + Math.floorDiv(chunkX, 32) + "." + + Math.floorDiv(chunkZ, 32) + ".mca"); + if (!Files.isRegularFile(region, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Saved chunk region is unavailable: " + region); + } + byte[] compressed; + int compression; + try (RandomAccessFile input = new RandomAccessFile(region.toFile(), "r")) { + int slot = Math.floorMod(chunkZ, 32) * 32 + Math.floorMod(chunkX, 32); + input.seek((long) slot * 4); + int allocation = input.readInt(); + long offset = (long) (allocation >>> 8) * SECTOR_BYTES; + int sectors = allocation & 255; + if (offset < 2L * SECTOR_BYTES || sectors == 0 || offset + 5 > input.length()) { + throw new IOException("Saved chunk allocation is unavailable: " + chunkX + "," + chunkZ); + } + input.seek(offset); + int length = input.readInt(); + compression = input.readUnsignedByte(); + if ((compression & 128) != 0) { + if (length != 1) { + throw new IOException("External saved chunk has an invalid allocation length"); + } + Path external = regionDirectory.resolve("c." + chunkX + "." + chunkZ + ".mcc"); + if (!Files.isRegularFile(external, LinkOption.NOFOLLOW_LINKS) + || Files.size(external) > MAX_CHUNK_BYTES) { + throw new IOException("External saved chunk is unavailable or too large: " + external); + } + compressed = Files.readAllBytes(external); + compression &= 127; + } else { + if (length < 1 || length > MAX_CHUNK_BYTES || (long) length + 4 > (long) sectors * SECTOR_BYTES + || offset + 4L + length > input.length()) { + throw new IOException("Saved chunk payload is truncated or too large: " + chunkX + "," + chunkZ); + } + compressed = new byte[length - 1]; + input.readFully(compressed); + } + input.seek((long) slot * 4); + if (input.readInt() != allocation) { + throw new IOException("Saved chunk allocation changed during boundary capture: " + chunkX + "," + chunkZ); + } + } + try (InputStream decoded = decompress(compressed, compression)) { + byte[] bytes = decoded.readNBytes(MAX_CHUNK_BYTES + 1); + if (bytes.length > MAX_CHUNK_BYTES) { + throw new IOException("Saved chunk decompressed payload exceeds the size limit"); + } + Tag tag = NBTUtil.read(new ByteArrayInputStream(bytes), false).getTag(); + if (!(tag instanceof CompoundTag root)) { + throw new IOException("Saved chunk root is not a compound"); + } + if (number(root, "xPos") != chunkX || number(root, "zPos") != chunkZ) { + throw new IOException("Saved chunk coordinates do not match its region allocation"); + } + return root; + } catch (IllegalArgumentException | ClassCastException exception) { + throw new IOException("Invalid saved chunk data at " + chunkX + "," + chunkZ, exception); + } + } + + private static InputStream decompress(byte[] encoded, int compression) throws IOException { + InputStream input = new ByteArrayInputStream(encoded); + return switch (compression) { + case 1 -> new GZIPInputStream(input); + case 2 -> new InflaterInputStream(input); + case 3 -> input; + case 4 -> new LZ4BlockInputStream(input); + default -> throw new IOException("Unsupported saved chunk compression " + compression); + }; + } + + private static long[] packedData(CompoundTag tag) throws IOException { + Tag data = tag.get("data"); + if (data == null) { + return new long[0]; + } + if (!(data instanceof LongArrayTag longs)) { + throw new IOException("Saved palette data is not a long array"); + } + return longs.getValue(); + } + + private static CompoundTag compound(CompoundTag tag, String key) throws IOException { + if (!(tag.get(key) instanceof CompoundTag value)) { + throw new IOException("Saved chunk field " + key + " is not a compound"); + } + return value; + } + + private static ListTag list(CompoundTag tag, String key) throws IOException { + if (!(tag.get(key) instanceof ListTag value)) { + throw new IOException("Saved chunk field " + key + " is not a list"); + } + return value; + } + + private static String string(CompoundTag tag, String key) throws IOException { + if (!(tag.get(key) instanceof StringTag value) || value.getValue().isBlank()) { + throw new IOException("Saved chunk field " + key + " is not a nonempty string"); + } + return value.getValue(); + } + + private static int number(CompoundTag tag, String key) throws IOException { + if (!(tag.get(key) instanceof NumberTag value)) { + throw new IOException("Saved chunk field " + key + " is not a number"); + } + return value.asInt(); + } + + private record Section(Palette blocks, Palette biomes) { + } + + private static final class Palette { + private final List values; + private final long[] data; + private final int bits; + private final int valuesPerLong; + private final long mask; + + private Palette(List values, long[] data, int minimumBits, int count) throws IOException { + if (values.isEmpty() || values.size() > count) { + throw new IOException("Saved palette size is invalid"); + } + this.values = List.copyOf(values); + this.data = data; + bits = Math.max(minimumBits, 32 - Integer.numberOfLeadingZeros(values.size() - 1)); + valuesPerLong = 64 / bits; + mask = (1L << bits) - 1; + int expected = values.size() == 1 ? 0 : Math.ceilDiv(count, valuesPerLong); + if (data.length != expected) { + throw new IOException("Saved palette data length " + data.length + " differs from " + expected); + } + } + + private T get(int index) throws IOException { + int paletteIndex = values.size() == 1 ? 0 + : (int) ((data[index / valuesPerLong] >>> ((index % valuesPerLong) * bits)) & mask); + if (paletteIndex >= values.size()) { + throw new IOException("Saved block or biome palette index is outside its palette"); + } + return values.get(paletteIndex); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/history/TerrainBoundarySignature.java b/core/src/main/java/art/arcane/iris/engine/history/TerrainBoundarySignature.java index 83f5ea070..e13085071 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/TerrainBoundarySignature.java +++ b/core/src/main/java/art/arcane/iris/engine/history/TerrainBoundarySignature.java @@ -8,10 +8,16 @@ import java.util.OptionalInt; public final class TerrainBoundarySignature { private final Column column; private final Samples samples; + private final BoundaryColumnGeometry geometry; - public TerrainBoundarySignature(Column column, Samples samples) { + public TerrainBoundarySignature(Column column, Samples samples, BoundaryColumnGeometry geometry) { this.column = Objects.requireNonNull(column, "Boundary column"); this.samples = Objects.requireNonNull(samples, "Boundary samples"); + this.geometry = Objects.requireNonNull(geometry, "Boundary geometry"); + } + + public BoundaryColumnGeometry geometry() { + return geometry; } public Column column() { diff --git a/core/src/main/java/art/arcane/iris/engine/history/TerrainBoundarySignatureStore.java b/core/src/main/java/art/arcane/iris/engine/history/TerrainBoundarySignatureStore.java index ae66e762c..417adcf61 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/TerrainBoundarySignatureStore.java +++ b/core/src/main/java/art/arcane/iris/engine/history/TerrainBoundarySignatureStore.java @@ -53,9 +53,9 @@ public final class TerrainBoundarySignatureStore { private static final String SHARD_FILE_SUFFIX = ".irtm"; private static final int MAGIC = 0x49525453; - private static final int SCHEMA_VERSION = 3; + private static final int SCHEMA_VERSION = 4; private static final int SHARD_MAGIC = 0x4952544D; - private static final int SHARD_SCHEMA_VERSION = 2; + private static final int SHARD_SCHEMA_VERSION = 3; private static final int IDENTITY_BYTES = 32; private static final int CATALOG_FIXED_BODY_BYTES = 56; private static final int CATALOG_ENTRY_BYTES = 44; @@ -76,7 +76,7 @@ public final class TerrainBoundarySignatureStore { + CHECKSUM_BYTES; private static final byte[] IDENTITY_DOMAIN = { 0x49, 0x72, 0x69, 0x73, 0x54, 0x65, 0x72, 0x72, - 0x61, 0x69, 0x6E, 0x53, 0x69, 0x67, 0x03 + 0x61, 0x69, 0x6E, 0x53, 0x69, 0x67, 0x04 }; private final Path dimensionRoot; @@ -376,7 +376,8 @@ public final class TerrainBoundarySignatureStore { new TerrainBoundarySignature.Samples( signature.samples().layout(), new TerrainBoundarySignature.BiomeEncoding(palette, paletteIndices) - ) + ), + BoundaryColumnGeometry.fromVoxels(signature.geometry().minimumY(), signature.geometry().voxels()) ); validateSignature(canonical); return canonical; @@ -389,6 +390,10 @@ public final class TerrainBoundarySignatureStore { if (signature.samples().biomes().palette().size() > MAX_PALETTE_COUNT) { throw new IllegalArgumentException("Terrain boundary biome palette exceeds compact index capacity"); } + for (BoundaryColumnGeometry.Voxel voxel : signature.geometry().palette()) { + encodeBiome(voxel.stateKey()); + encodeBiome(voxel.fluidStateKey()); + } for (String biome : signature.samples().biomes().palette()) { encodeBiome(Objects.requireNonNull(biome, "Terrain boundary biome")); } @@ -614,7 +619,8 @@ public final class TerrainBoundarySignatureStore { signature.fluidHeight(), signature.upperCeilingDepth() ), - signature.samples() + signature.samples(), + signature.geometry() )); } return List.copyOf(relocated); @@ -713,7 +719,7 @@ public final class TerrainBoundarySignatureStore { TerrainBoundarySignature sample(int blockX, int blockZ) throws IOException; @Override - default void close() { + default void close() throws IOException { } } @@ -1275,6 +1281,77 @@ public final class TerrainBoundarySignatureStore { for (short paletteIndex : biomes.paletteIndices()) { output.writeShort(paletteIndex); } + writeGeometry(output, signature.geometry()); + } + + private static void writeGeometry(DataOutputStream output, BoundaryColumnGeometry geometry) + throws IOException { + output.writeInt(geometry.minimumY()); + output.writeInt(geometry.palette().size()); + for (BoundaryColumnGeometry.Voxel voxel : geometry.palette()) { + writeGeometryKey(output, voxel.stateKey()); + output.writeByte(voxel.phase().ordinal()); + writeGeometryKey(output, voxel.fluidStateKey()); + output.writeBoolean(voxel.protectedContent()); + } + int[] runEnds = geometry.runEnds(); + short[] indices = geometry.paletteIndices(); + output.writeInt(runEnds.length); + for (int run = 0; run < runEnds.length; run++) { + output.writeInt(runEnds[run]); + output.writeShort(indices[run]); + } + } + + private static BoundaryColumnGeometry readGeometry(Path source, DataInputStream input) + throws IOException { + int minimumY = input.readInt(); + int paletteCount = input.readInt(); + if (paletteCount < 0 || paletteCount > MAX_PALETTE_COUNT) { + throw invalid(source, "invalid geometry palette count " + paletteCount); + } + ArrayList palette = new ArrayList<>(paletteCount); + BoundaryColumnGeometry.Phase[] phases = BoundaryColumnGeometry.Phase.values(); + for (int index = 0; index < paletteCount; index++) { + String stateKey = readGeometryKey(source, input); + int phase = input.readUnsignedByte(); + if (phase >= phases.length) { + throw invalid(source, "invalid geometry phase " + phase); + } + String fluidKey = readGeometryKey(source, input); + int protectedFlag = input.readUnsignedByte(); + if (protectedFlag > 1) { + throw invalid(source, "invalid geometry protection flag " + protectedFlag); + } + palette.add(new BoundaryColumnGeometry.Voxel(stateKey, phases[phase], fluidKey, + protectedFlag == 1)); + } + int runCount = input.readInt(); + if (runCount < 0 || runCount > BoundaryColumnGeometry.MAXIMUM_HEIGHT + || (long) runCount * (Integer.BYTES + Short.BYTES) > input.available()) { + throw invalid(source, "invalid geometry run count " + runCount); + } + int[] runEnds = new int[runCount]; + short[] indices = new short[runCount]; + for (int run = 0; run < runCount; run++) { + runEnds[run] = input.readInt(); + indices[run] = input.readShort(); + } + return new BoundaryColumnGeometry(minimumY, palette, runEnds, indices); + } + + private static void writeGeometryKey(DataOutputStream output, String key) throws IOException { + byte[] encoded = encodeBiome(key); + output.writeInt(encoded.length); + output.write(encoded); + } + + private static String readGeometryKey(Path source, DataInputStream input) throws IOException { + int encodedLength = input.readInt(); + if (encodedLength < 0 || encodedLength > MAX_BIOME_BYTES || encodedLength > input.available()) { + throw invalid(source, "invalid geometry state key length " + encodedLength); + } + return decodeBiome(source, input.readNBytes(encodedLength)); } private static TerrainBoundarySignature readSignature( @@ -1344,7 +1421,8 @@ public final class TerrainBoundarySignatureStore { new TerrainBoundarySignature.Samples( new TerrainBoundarySignature.VerticalLayout(minimumY, sampleStep, sampleCount), new TerrainBoundarySignature.BiomeEncoding(palette, paletteIndices) - ) + ), + readGeometry(source, input) ); } } diff --git a/core/src/main/java/art/arcane/iris/engine/history/TransitionBoundarySampler.java b/core/src/main/java/art/arcane/iris/engine/history/TransitionBoundarySampler.java index 43a3119b1..a4f807c38 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/TransitionBoundarySampler.java +++ b/core/src/main/java/art/arcane/iris/engine/history/TransitionBoundarySampler.java @@ -1,5 +1,6 @@ package art.arcane.iris.engine.history; +import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Iterator; @@ -27,7 +28,7 @@ final class TransitionBoundarySampler { throw new IllegalArgumentException("Terrain transition width must be positive"); } this.terrainWidth = terrainWidth; - this.searchWidth = Math.multiplyExact(terrainWidth, 2); + this.searchWidth = terrainWidth; this.signatures = Objects.requireNonNull(signatures, "Terrain boundary signatures"); this.chunkCandidates = new LinkedHashMap<>(MAXIMUM_CACHED_CHUNKS, 0.75F, true); this.cache = ThreadLocal.withInitial(ChunkCache::new); @@ -56,6 +57,34 @@ final class TransitionBoundarySampler { return sampled; } + BoundaryGeometryInfluence geometryAt(int blockX, int blockZ) { + CandidateIndex candidates = candidatesForChunk( + Math.floorDiv(blockX, GenerationBoundary.CHUNK_SIZE), + Math.floorDiv(blockZ, GenerationBoundary.CHUNK_SIZE)); + Nearest nearest = candidates.nearest(blockX, blockZ, square(terrainWidth)); + if (nearest.count == 0 || nearest.distancesSquared[0] >= square(terrainWidth)) { + return BoundaryGeometryInfluence.none(); + } + double distance = Math.sqrt(nearest.distancesSquared[0]); + double weight = GenerationBlend.newEpochWeight(Math.max(0D, distance - 1D), + Math.max(1, terrainWidth - 1)); + int count = nearest.distancesSquared[0] <= 1D ? 1 : nearest.count; + double total = 0D; + for (int index = 0; index < count; index++) { + total += 1D / Math.max(1D, nearest.distancesSquared[index]); + } + ArrayList contributions = new ArrayList<>(count); + for (int index = 0; index < count; index++) { + contributions.add(new BoundaryGeometryInfluence.Contribution( + nearest.signatures[index].geometry(), + (1D / Math.max(1D, nearest.distancesSquared[index])) / total)); + } + int openingDepth = Math.min(8, Math.max(1, terrainWidth / 4)); + double openingWeight = GenerationBlend.newEpochWeight(Math.max(0D, distance - openingDepth), + Math.max(1, terrainWidth - openingDepth)); + return new BoundaryGeometryInfluence(weight, openingWeight, contributions); + } + boolean intersectsTerrainBand(int minimumX, int minimumZ, int maximumX, int maximumZ) { return signatures.intersectsTerrainBand( new TerrainBoundarySignatureStore.BlockBounds(minimumX, minimumZ, maximumX, maximumZ), @@ -89,11 +118,7 @@ final class TransitionBoundarySampler { Math.min(distance, terrainWidth), terrainWidth ); - double hydrologyDistance = Math.max(0D, distance - terrainWidth); - double hydrologyWeight = GenerationBlend.newEpochWeight( - Math.min(hydrologyDistance, terrainWidth), - terrainWidth - ); + double hydrologyWeight = newEpochWeight; return new TransitionGenerationPlan.TerrainSample( Math.min(distance, searchWidth), newEpochWeight, diff --git a/core/src/main/java/art/arcane/iris/engine/history/TransitionFluidContainment.java b/core/src/main/java/art/arcane/iris/engine/history/TransitionFluidContainment.java new file mode 100644 index 000000000..d0fa7045d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/history/TransitionFluidContainment.java @@ -0,0 +1,149 @@ +package art.arcane.iris.engine.history; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; +import java.util.Set; + +public final class TransitionFluidContainment { + private static final Set BANK_MATERIALS = Set.of( + "minecraft:stone", "minecraft:deepslate", "minecraft:granite", "minecraft:diorite", + "minecraft:andesite", "minecraft:tuff", "minecraft:calcite", "minecraft:bedrock", + "minecraft:dirt", "minecraft:coarse_dirt", "minecraft:grass_block", "minecraft:podzol", + "minecraft:mycelium", "minecraft:clay", "minecraft:terracotta", "minecraft:netherrack", + "minecraft:basalt", "minecraft:smooth_basalt", "minecraft:blackstone", "minecraft:end_stone", + "minecraft:obsidian", "minecraft:crying_obsidian"); + private static final BoundaryColumnGeometry.Voxel STONE = new BoundaryColumnGeometry.Voxel( + "minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false); + + private TransitionFluidContainment() { + } + + public static SavedTerrainChunk contain(SavedTerrainChunk raw, TransitionGenerationPlan plan, ColumnSource source) + throws IOException { + List columns = new ArrayList<>(256); + boolean changed = false; + for (int localX = 0; localX < 16; localX++) { + for (int localZ = 0; localZ < 16; localZ++) { + int x = Math.addExact(Math.multiplyExact(raw.chunkX(), 16), localX); + int z = Math.addExact(Math.multiplyExact(raw.chunkZ(), 16), localZ); + BoundaryColumnGeometry original = raw.column(x, z).geometry(); + BoundaryColumnGeometry corrected = plan.isHistoricalBlock(x, z) || plan.newEpochWeightAt(x, z) == 1D + ? original : containColumn(x, z, original, source); + columns.add(corrected); + changed |= corrected != original; + } + } + if (!changed) { + return raw; + } + BoundaryColumnGeometry first = columns.getFirst(); + return SavedTerrainChunk.capture(raw.chunkX(), raw.chunkZ(), first.minimumY(), first.height(), + raw.nativeStatus(), new ContainedSource(raw, columns)); + } + + private static BoundaryColumnGeometry containColumn(int x, int z, BoundaryColumnGeometry original, + ColumnSource source) throws IOException { + List voxels = original.voxels(); + List corrected = null; + BoundaryColumnGeometry.Voxel[] banks = null; + BoundaryColumnGeometry[] neighbors = null; + for (int offset = 0; offset < voxels.size(); offset++) { + BoundaryColumnGeometry.Voxel voxel = voxels.get(offset); + if (voxel.phase() != BoundaryColumnGeometry.Phase.FLUID || voxel.protectedContent()) { + continue; + } + if (neighbors == null) { + neighbors = new BoundaryColumnGeometry[]{ + source.column(Math.subtractExact(x, 1), z), source.column(Math.addExact(x, 1), z), + source.column(x, Math.subtractExact(z, 1)), source.column(x, Math.addExact(z, 1))}; + } + int worldY = Math.addExact(original.minimumY(), offset); + if (!exposed(original, neighbors, worldY)) { + continue; + } + if (corrected == null) { + corrected = new ArrayList<>(voxels); + banks = nearestDrySolids(voxels); + } + corrected.set(offset, banks[offset]); + } + return corrected == null ? original : BoundaryColumnGeometry.fromVoxels(original.minimumY(), corrected); + } + + private static boolean exposed(BoundaryColumnGeometry original, BoundaryColumnGeometry[] neighbors, int worldY) { + if (original.voxelAt(Math.subtractExact(worldY, 1)).phase() == BoundaryColumnGeometry.Phase.AIR) { + return true; + } + for (BoundaryColumnGeometry neighbor : neighbors) { + if (neighbor.voxelAt(worldY).phase() == BoundaryColumnGeometry.Phase.AIR) { + return true; + } + } + return false; + } + + private static BoundaryColumnGeometry.Voxel[] nearestDrySolids(List voxels) { + BoundaryColumnGeometry.Voxel[] materials = new BoundaryColumnGeometry.Voxel[voxels.size()]; + int[] distances = new int[voxels.size()]; + BoundaryColumnGeometry.Voxel nearest = STONE; + int last = -voxels.size() - 1; + for (int offset = 0; offset < voxels.size(); offset++) { + if (drySolid(voxels.get(offset))) { + nearest = voxels.get(offset); + last = offset; + } + materials[offset] = nearest; + distances[offset] = offset - last; + } + last = voxels.size() * 2; + nearest = null; + for (int offset = voxels.size() - 1; offset >= 0; offset--) { + if (drySolid(voxels.get(offset))) { + nearest = voxels.get(offset); + last = offset; + } + if (nearest != null && last - offset < distances[offset]) { + materials[offset] = nearest; + } + } + return materials; + } + + private static boolean drySolid(BoundaryColumnGeometry.Voxel voxel) { + String state = voxel.stateKey(); + int properties = state.indexOf('['); + String key = properties < 0 ? state : state.substring(0, properties); + return voxel.phase() == BoundaryColumnGeometry.Phase.SOLID + && voxel.fluidStateKey().isEmpty() && !voxel.protectedContent() && BANK_MATERIALS.contains(key); + } + + @FunctionalInterface + public interface ColumnSource { + BoundaryColumnGeometry column(int blockX, int blockZ) throws IOException; + } + + private record ContainedSource(SavedTerrainChunk raw, List columns) + implements SavedTerrainChunk.VoxelSource { + @Override + public OptionalInt groundSurface(int localX, int localZ) { + return OptionalInt.of(signature(localX, localZ).oceanFloorHeight()); + } + + @Override + public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) { + return columns.get(localX * 16 + localZ).voxelAt(worldY); + } + + @Override + public String biome(int localX, int worldY, int localZ) { + TerrainBoundarySignature signature = signature(localX, localZ); + return signature.biomeAtSample((worldY - signature.samples().layout().minimumY()) / 4); + } + + private TerrainBoundarySignature signature(int x, int z) { + return raw.column((raw.chunkX() << 4) + x, (raw.chunkZ() << 4) + z); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/history/TransitionGenerationPlan.java b/core/src/main/java/art/arcane/iris/engine/history/TransitionGenerationPlan.java index 0dc14c9ba..32c986579 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/TransitionGenerationPlan.java +++ b/core/src/main/java/art/arcane/iris/engine/history/TransitionGenerationPlan.java @@ -89,7 +89,25 @@ public final class TransitionGenerationPlan { } public boolean allowsNewDiscreteContentAt(int blockX, int blockZ) { - return terrainSampleAt(blockX, blockZ).newEpochWeight() == 1D; + return !boundary.isHistoricalBlock(blockX, blockZ); + } + + public boolean hasTransitionAtChunk(int chunkX, int chunkZ) { + if (boundary.isHistoricalChunk(chunkX, chunkZ)) { + return false; + } + int minimumX = Math.multiplyExact(chunkX, GenerationBoundary.CHUNK_SIZE); + int minimumZ = Math.multiplyExact(chunkZ, GenerationBoundary.CHUNK_SIZE); + return terrainSampler.intersectsTerrainBand(minimumX, minimumZ, + Math.addExact(minimumX, GenerationBoundary.CHUNK_SIZE - 1), + Math.addExact(minimumZ, GenerationBoundary.CHUNK_SIZE - 1)); + } + + public BoundaryGeometryInfluence geometryAt(int blockX, int blockZ) { + if (boundary.isHistoricalBlock(blockX, blockZ)) { + return BoundaryGeometryInfluence.none(); + } + return terrainSampler.geometryAt(blockX, blockZ); } public TerrainSample terrainSampleAt(int blockX, int blockZ) { @@ -100,19 +118,30 @@ public final class TransitionGenerationPlan { } public Optional historicalPhysicalBiomeKeyAt(int blockX, int blockY, int blockZ) { - TerrainSample sample = terrainSampleAt(blockX, blockZ); + return historicalPhysicalBiomeKeyAt(blockX, blockY, blockZ, terrainSampleAt(blockX, blockZ)); + } + + public Optional historicalPhysicalBiomeKeyAt( + int blockX, + int blockY, + int blockZ, + TerrainSample sample + ) { + Objects.requireNonNull(sample, "Terrain sample"); if (sample.newEpochWeight() == 1D) { return Optional.empty(); } - return sample.historicalPhysicalBiomeKeyAt(blockY); + double weight = GenerationBlend.newEpochWeight( + Math.max(0D, sample.distanceToHistoricalTerrain() - 1D), Math.max(1, widthBlocks() - 1)); + return GenerationBlend.usesHistoricalMaterial(blockX, blockY, blockZ, weight) + ? sample.historicalPhysicalBiomeKeyAt(blockY) : Optional.empty(); } public boolean allowsNewFootprint(int minimumX, int minimumZ, int maximumX, int maximumZ) { if (minimumX > maximumX || minimumZ > maximumZ) { throw new IllegalArgumentException("Generation footprint bounds are inverted"); } - return !boundary.intersectsHistoricalBlocks(minimumX, minimumZ, maximumX, maximumZ) - && !terrainSampler.intersectsTerrainBand(minimumX, minimumZ, maximumX, maximumZ); + return !boundary.intersectsHistoricalBlocks(minimumX, minimumZ, maximumX, maximumZ); } long terrainCatalogProbeCount() { diff --git a/core/src/main/java/art/arcane/iris/engine/history/TransitionGeometryBlender.java b/core/src/main/java/art/arcane/iris/engine/history/TransitionGeometryBlender.java new file mode 100644 index 000000000..9e42e6a7f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/history/TransitionGeometryBlender.java @@ -0,0 +1,205 @@ +package art.arcane.iris.engine.history; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public final class TransitionGeometryBlender { + private static final int MAXIMUM_OPENING_HEIGHT = 64; + private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel( + "minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false); + private static final BoundaryColumnGeometry.Voxel FLUID_BARRIER = new BoundaryColumnGeometry.Voxel( + "minecraft:obsidian", BoundaryColumnGeometry.Phase.SOLID, "", false); + + private TransitionGeometryBlender() { + } + + public static BoundaryColumnGeometry blendColumn( + TransitionGenerationPlan plan, + int blockX, + int blockZ, + BoundaryColumnGeometry currentGeometry + ) { + Objects.requireNonNull(plan, "Transition plan"); + return blendColumn(plan.geometryAt(blockX, blockZ), blockX, blockZ, currentGeometry); + } + + public static BoundaryColumnGeometry blendColumn( + BoundaryGeometryInfluence influence, + int blockX, + int blockZ, + BoundaryColumnGeometry currentGeometry + ) { + Objects.requireNonNull(influence, "Boundary influence"); + Objects.requireNonNull(currentGeometry, "Current geometry"); + if (influence.newTerrainWeight() == 1D || influence.contributions().isEmpty() + || currentGeometry.height() == 0) { + return currentGeometry; + } + ColumnProfile current = new ColumnProfile(currentGeometry); + ArrayList historical = new ArrayList<>(influence.contributions().size()); + for (BoundaryGeometryInfluence.Contribution contribution : influence.contributions()) { + BoundaryColumnGeometry geometry = contribution.geometry(); + if (geometry.minimumY() != currentGeometry.minimumY() || geometry.height() != currentGeometry.height()) { + throw new IllegalArgumentException("Boundary geometry does not match the current vertical layout"); + } + historical.add(new WeightedProfile(new ColumnProfile(geometry), contribution.weight())); + } + ArrayList result = new ArrayList<>(currentGeometry.height()); + boolean changed = false; + for (int offset = 0; offset < currentGeometry.height(); offset++) { + BoundaryColumnGeometry.Voxel value = blendVoxel(current, historical, offset, + influence.newTerrainWeight(), influence.openingWeight(), GenerationBlend.usesHistoricalMaterial(blockX, + currentGeometry.minimumY() + offset, blockZ, influence.newTerrainWeight())); + result.add(value); + changed |= !value.equals(current.voxels.get(offset)); + } + return changed ? BoundaryColumnGeometry.fromVoxels(currentGeometry.minimumY(), result) : currentGeometry; + } + + private static BoundaryColumnGeometry.Voxel blendVoxel( + ColumnProfile current, + List historical, + int offset, + double currentWeight, + double openingWeight, + boolean historicalMaterial + ) { + BoundaryColumnGeometry.Voxel currentVoxel = current.voxels.get(offset); + if (currentVoxel.protectedContent()) { + return currentVoxel; + } + boolean historicalOpening = false; + double solid = 0D; + double fluid = 0D; + double total = 0D; + double solidMaterialWeight = -1D; + double fluidMaterialWeight = -1D; + BoundaryColumnGeometry.Voxel oldSolid = null; + BoundaryColumnGeometry.Voxel oldFluid = null; + for (WeightedProfile contribution : historical) { + ColumnProfile profile = contribution.profile(); + if (profile.voxels.get(offset).protectedContent()) { + continue; + } + historicalOpening |= profile.enclosedOpenings[offset]; + double weight = contribution.weight(); + total += weight; + solid += profile.solidDistances[offset] * weight; + fluid += profile.fluidDistances[offset] * weight; + if (profile.solidMaterials[offset] != null && weight > solidMaterialWeight) { + oldSolid = profile.solidMaterials[offset]; + solidMaterialWeight = weight; + } + if (profile.fluidMaterials[offset] != null && weight > fluidMaterialWeight) { + oldFluid = profile.fluidMaterials[offset]; + fluidMaterialWeight = weight; + } + } + if (total == 0D) { + return currentVoxel; + } + double blendedSolid = GenerationBlend.interpolate(solid / total, + current.solidDistances[offset], historicalOpening ? openingWeight : currentWeight); + if (blendedSolid > 0D) { + return selectMaterial(oldSolid, current.solidMaterials[offset], historicalMaterial); + } + double blendedFluid = GenerationBlend.interpolate(fluid / total, + current.fluidDistances[offset], historicalOpening ? openingWeight : currentWeight); + if (blendedFluid > 0D) { + BoundaryColumnGeometry.Voxel newFluid = current.fluidMaterials[offset]; + if (oldFluid != null && newFluid != null && !fluidFamily(oldFluid).equals(fluidFamily(newFluid))) { + return FLUID_BARRIER; + } + return selectMaterial(oldFluid, newFluid, currentWeight <= 0.5D); + } + return AIR; + } + + private static String fluidFamily(BoundaryColumnGeometry.Voxel voxel) { + String key = voxel.fluidStateKey(); + int properties = key.indexOf('['); + return properties < 0 ? key : key.substring(0, properties); + } + + private static BoundaryColumnGeometry.Voxel selectMaterial( + BoundaryColumnGeometry.Voxel historical, + BoundaryColumnGeometry.Voxel current, + boolean historicalMaterial + ) { + if (historical == null) { + return current == null ? AIR : current; + } + return current == null || historicalMaterial ? historical : current; + } + + private static BoundaryColumnGeometry.Voxel[] nearestMaterials( + List voxels, + BoundaryColumnGeometry.Phase phase + ) { + BoundaryColumnGeometry.Voxel[] materials = new BoundaryColumnGeometry.Voxel[voxels.size()]; + int[] distances = new int[voxels.size()]; + int last = -voxels.size() - 1; + BoundaryColumnGeometry.Voxel nearest = null; + for (int index = 0; index < voxels.size(); index++) { + BoundaryColumnGeometry.Voxel voxel = voxels.get(index); + if (voxel.phase() == phase && !voxel.protectedContent()) { + nearest = voxel; + last = index; + } + materials[index] = nearest; + distances[index] = index - last; + } + last = voxels.size() * 2; + nearest = null; + for (int index = voxels.size() - 1; index >= 0; index--) { + BoundaryColumnGeometry.Voxel voxel = voxels.get(index); + if (voxel.phase() == phase && !voxel.protectedContent()) { + nearest = voxel; + last = index; + } + if (nearest != null && last - index < distances[index]) { + materials[index] = nearest; + } + } + return materials; + } + + private static boolean[] enclosedOpenings(List voxels) { + boolean[] openings = new boolean[voxels.size()]; + int lowerSolid = -1; + for (int index = 0; index < voxels.size(); index++) { + BoundaryColumnGeometry.Voxel voxel = voxels.get(index); + if (voxel.phase() == BoundaryColumnGeometry.Phase.SOLID && !voxel.protectedContent()) { + if (lowerSolid >= 0 && index - lowerSolid - 1 <= MAXIMUM_OPENING_HEIGHT) { + for (int opening = lowerSolid + 1; opening < index; opening++) { + openings[opening] = !voxels.get(opening).protectedContent(); + } + } + lowerSolid = index; + } + } + return openings; + } + + private static final class ColumnProfile { + private final List voxels; + private final boolean[] enclosedOpenings; + private final double[] solidDistances; + private final double[] fluidDistances; + private final BoundaryColumnGeometry.Voxel[] solidMaterials; + private final BoundaryColumnGeometry.Voxel[] fluidMaterials; + + private ColumnProfile(BoundaryColumnGeometry geometry) { + voxels = geometry.voxels(); + enclosedOpenings = enclosedOpenings(voxels); + solidDistances = geometry.solidDistances(); + fluidDistances = geometry.fluidDistances(); + solidMaterials = nearestMaterials(voxels, BoundaryColumnGeometry.Phase.SOLID); + fluidMaterials = nearestMaterials(voxels, BoundaryColumnGeometry.Phase.FLUID); + } + } + + private record WeightedProfile(ColumnProfile profile, double weight) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/history/WorldChunkInventory.java b/core/src/main/java/art/arcane/iris/engine/history/WorldChunkInventory.java index 1f2b25b23..fa62fd4a1 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/WorldChunkInventory.java +++ b/core/src/main/java/art/arcane/iris/engine/history/WorldChunkInventory.java @@ -6,6 +6,7 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import java.io.IOException; import java.io.RandomAccessFile; +import java.nio.ByteBuffer; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.LinkOption; @@ -61,6 +62,31 @@ public final class WorldChunkInventory { return scanRegionDirectory(worldDirectory.resolve("region")); } + public WorldChunkInventory filter(ChunkPredicate predicate) throws IOException { + Objects.requireNonNull(predicate, "chunk predicate"); + Long2ObjectOpenHashMap selected = new Long2ObjectOpenHashMap<>(regionMasks.size()); + for (long regionKey : regionKeys) { + int baseX = MCAUtil.regionToChunk(ChunkGenerationOwnership.chunkX(regionKey)); + int baseZ = MCAUtil.regionToChunk(ChunkGenerationOwnership.chunkZ(regionKey)); + BitSet source = regionMasks.get(regionKey); + BitSet accepted = new BitSet(REGION_SIDE * REGION_SIDE); + for (int index = source.nextSetBit(0); index >= 0; index = source.nextSetBit(index + 1)) { + if (predicate.test(baseX + (index & 31), baseZ + (index >> 5))) { + accepted.set(index); + } + } + if (!accepted.isEmpty()) { + selected.put(regionKey, accepted); + } + } + return new WorldChunkInventory(selected); + } + + @FunctionalInterface + public interface ChunkPredicate { + boolean test(int chunkX, int chunkZ) throws IOException; + } + public static boolean isDurablyAllocated(Path worldDirectory, int chunkX, int chunkZ) throws IOException { Path regionDirectory = Objects.requireNonNull(worldDirectory, "worldDirectory") .toAbsolutePath() @@ -175,24 +201,36 @@ public final class WorldChunkInventory { private static BitSet scanAllocationTable(Path file) throws IOException { try (RandomAccessFile input = new RandomAccessFile(file.toFile(), "r")) { long length = input.length(); - if (length < HEADER_BYTES || length % SECTOR_BYTES != 0L) { + if (length == 0L) { + return new BitSet(REGION_SIDE * REGION_SIDE); + } + if (length < HEADER_BYTES) { throw new IOException("Truncated world region file: " + file); } - long sectorCapacity = length / SECTOR_BYTES; + byte[] header = new byte[SECTOR_BYTES]; + input.readFully(header); + ByteBuffer locations = ByteBuffer.wrap(header); + length = input.length(); + if (length < HEADER_BYTES) { + throw new IOException("Truncated world region file: " + file); + } + long sectorCapacity = length / SECTOR_BYTES + (length % SECTOR_BYTES == 0L ? 0L : 1L); BitSet claimedSectors = new BitSet(Math.toIntExact(sectorCapacity)); claimedSectors.set(0, HEADER_SECTORS); BitSet mask = new BitSet(REGION_SIDE * REGION_SIDE); for (int localZ = 0; localZ < REGION_SIDE; localZ++) { for (int localX = 0; localX < REGION_SIDE; localX++) { int index = MCAFile.getChunkIndex(localX, localZ); - input.seek((long) index * Integer.BYTES); - int location = input.readInt(); + int location = locations.getInt(index * Integer.BYTES); if (location == 0) { continue; } int sectorOffset = location >>> Byte.SIZE; int sectorCount = location & 0xFF; validateAllocation(file, sectorOffset, sectorCount, sectorCapacity, claimedSectors); + if (((long) sectorOffset + sectorCount) * SECTOR_BYTES > length) { + validateUnpaddedAllocation(file, input, sectorOffset, sectorCount, length); + } mask.set(index); } } @@ -202,6 +240,26 @@ public final class WorldChunkInventory { } } + private static void validateUnpaddedAllocation( + Path file, + RandomAccessFile input, + int sectorOffset, + int sectorCount, + long fileLength + ) throws IOException { + long chunkStart = (long) sectorOffset * SECTOR_BYTES; + if (chunkStart + Integer.BYTES >= fileLength) { + throw new IOException("Truncated chunk payload in world region file: " + file); + } + input.seek(chunkStart); + int payloadLength = input.readInt(); + long storedLength = Integer.BYTES + (long) payloadLength; + if (payloadLength < 1 || storedLength > (long) sectorCount * SECTOR_BYTES + || chunkStart + storedLength > fileLength) { + throw new IOException("Truncated chunk payload in world region file: " + file); + } + } + private static void validateAllocation( Path file, int sectorOffset, diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/EngineMantle.java b/core/src/main/java/art/arcane/iris/engine/mantle/EngineMantle.java index 487a66cd6..91e5817a5 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/EngineMantle.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/EngineMantle.java @@ -21,6 +21,8 @@ package art.arcane.iris.engine.mantle; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.history.TerrainBoundarySignature; +import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.engine.DimensionStackContext; import art.arcane.iris.engine.DimensionStackLayout; import art.arcane.iris.engine.UpperDimensionContext; @@ -54,6 +56,8 @@ import org.jetbrains.annotations.UnmodifiableView; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; import java.util.concurrent.TimeUnit; public interface EngineMantle extends MatterGenerator { @@ -109,6 +113,10 @@ public interface EngineMantle extends MatterGenerator { } default int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { + OptionalInt resolved = getComplex().resolvedTerrainHeight(x, z, ignoreFluid); + if (resolved.isPresent()) { + return resolved.getAsInt(); + } return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getFluidHeight(x, z)); } @@ -117,6 +125,21 @@ public interface EngineMantle extends MatterGenerator { } default boolean isCarved(int x, int h, int z) { + Optional resolved = getComplex().resolvedTerrainColumn(x, z); + if (resolved.isPresent()) { + PreObjectMatterCell cell = getMantle().get(x, h, z, PreObjectMatterCell.class); + if (cell != null && cell.blockCaptured()) { + PlatformBlockState block = getMantle().get(x, h, z, PlatformBlockState.class); + if (block != null) { + return (block.isAir() || block.isFluid()) + && resolved.get().geometry().hasSolidAbove(h + getEngine().getMinHeight()); + } + } + if (cell != null && cell.cavernCaptured()) { + return getMantle().get(x, h, z, MatterCavern.class) != null; + } + return resolved.get().geometry().isEnclosedOpenAt(h + getEngine().getMinHeight()); + } HydrologyCaveCell hydrology = HydrologyCaveStorage.getIfPresent(getMantle(), x, h, z); if (hydrology != null) { return hydrology.carves(); @@ -126,9 +149,19 @@ public interface EngineMantle extends MatterGenerator { default PlatformBlockState get(int x, int y, int z) { PlatformBlockState block = getMantle().get(x, y, z, PlatformBlockState.class); - if (block == null) - return AIR; - return block; + Optional resolved = getComplex().resolvedTerrainColumn(x, z); + if (resolved.isPresent()) { + PreObjectMatterCell cell = getMantle().get(x, y, z, PreObjectMatterCell.class); + if (cell == null || !cell.blockCaptured()) { + String stateKey = resolved.get().geometry().voxelAt(y + getEngine().getMinHeight()).stateKey(); + PlatformBlockState natural = IrisPlatforms.get().registries().blockOrNull(stateKey); + if (natural == null) { + throw new IllegalStateException("Saved terrain state is unavailable: " + stateKey); + } + return natural; + } + } + return block == null ? AIR : block; } default boolean isPreventingDecay() { @@ -144,6 +177,10 @@ public interface EngineMantle extends MatterGenerator { } default int getFluidHeight(int x, int z) { + Optional resolved = getComplex().resolvedTerrainColumn(x, z); + if (resolved.isPresent()) { + return resolved.get().fluidHeight().orElse(-1); + } return (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z)); } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MantleComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/MantleComponent.java index e7c262958..ecb5537bd 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/MantleComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MantleComponent.java @@ -32,6 +32,10 @@ import org.jetbrains.annotations.NotNull; public interface MantleComponent extends Comparable { int getPriority(); + default MatterGenerationPhase getGenerationPhase() { + return MatterGenerationPhase.CONTENT; + } + int getRadius(); default int getOutputRadius() { diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java index 3953b5528..791612192 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java @@ -30,6 +30,9 @@ import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.history.TransitionGenerationPlan; +import art.arcane.iris.engine.history.TerrainBoundarySignature; +import art.arcane.iris.engine.history.BoundaryColumnGeometry; +import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.engine.object.IrisGeneratorStyle; import art.arcane.iris.engine.object.IrisPosition; import art.arcane.iris.engine.object.TileData; @@ -59,6 +62,7 @@ import org.bukkit.util.Vector; import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReferenceArray; @@ -463,12 +467,34 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { } } + private Optional resolvedColumn(int x, int z) { + IrisComplex complex = engineMantle.getComplex(); + return complex == null ? Optional.empty() : complex.resolvedTerrainColumn(x, z); + } + + private PlatformBlockState resolvedBlock(TerrainBoundarySignature column, int y) { + String stateKey = column.geometry().voxelAt(y + engineMantle.getEngine().getMinHeight()).stateKey(); + PlatformBlockState state = IrisPlatforms.get().registries().blockOrNull(stateKey); + if (state == null) { + throw new IllegalStateException("Saved terrain state is unavailable: " + stateKey); + } + return state; + } + public PlatformBlockState getPrerequisiteBlock(int x, int y, int z) { + Optional resolved = resolvedColumn(x, z); + if (resolved.isPresent()) { + return resolvedBlock(resolved.get(), y); + } PlatformBlockState block = getPrerequisiteDataIfPresent(x, y, z, PlatformBlockState.class); return block == null ? AIR : block; } public boolean isPrerequisiteCarved(int x, int y, int z) { + Optional resolved = resolvedColumn(x, z); + if (resolved.isPresent()) { + return resolved.get().geometry().isEnclosedOpenAt(y + engineMantle.getEngine().getMinHeight()); + } HydrologyCaveCell hydrology = getPrerequisiteDataIfPresent(x, y, z, HydrologyCaveCell.class); if (hydrology != null) { return hydrology.carves(); @@ -477,6 +503,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { } public byte[] getPrerequisiteCarvedColumn(int x, int z, int height) { + Optional resolved = resolvedColumn(x, z); + if (resolved.isPresent()) { + return resolvedCarvedColumn(resolved.get(), height); + } int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight()); byte[] carvedColumn = new byte[cappedHeight]; if (cappedHeight <= 0) { @@ -818,6 +848,13 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { // Read-only probe: getDataIfPresent returns the identical answer without materializing // a 16^3 section + slice on a miss the way getData's getOrCreate path does. PlatformBlockState block = getDataIfPresent(x, y, z, PlatformBlockState.class); + Optional resolved = resolvedColumn(x, z); + if (resolved.isPresent()) { + PreObjectMatterCell cell = getDataIfPresent(x, y, z, PreObjectMatterCell.class); + if (cell == null || !cell.blockCaptured()) { + return resolvedBlock(resolved.get(), y); + } + } if (block == null) return AIR; return block; @@ -830,6 +867,20 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { @Override public boolean isCarved(int x, int y, int z) { + Optional resolved = resolvedColumn(x, z); + if (resolved.isPresent()) { + PreObjectMatterCell cell = getDataIfPresent(x, y, z, PreObjectMatterCell.class); + if (cell != null && cell.blockCaptured()) { + PlatformBlockState block = getDataIfPresent(x, y, z, PlatformBlockState.class); + if (block != null) { + return (block.isAir() || block.isFluid()) + && resolved.get().geometry().hasSolidAbove(y + engineMantle.getEngine().getMinHeight()); + } + } + return cell != null && cell.cavernCaptured() + ? getDataIfPresent(x, y, z, MatterCavern.class) != null + : resolved.get().geometry().isEnclosedOpenAt(y + engineMantle.getEngine().getMinHeight()); + } HydrologyCaveCell hydrology = getDataIfPresent(x, y, z, HydrologyCaveCell.class); if (hydrology != null) { return hydrology.carves(); @@ -837,7 +888,45 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { return getDataIfPresent(x, y, z, MatterCavern.class) != null; } + private byte[] resolvedCarvedColumn(TerrainBoundarySignature column, int height) { + int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight()); + byte[] carved = new byte[cappedHeight]; + int minimumY = engineMantle.getEngine().getMinHeight(); + BoundaryColumnGeometry geometry = column.geometry(); + boolean ceiling = false; + for (int worldY = geometry.minimumY() + geometry.height() - 1; worldY >= minimumY; worldY--) { + BoundaryColumnGeometry.Voxel voxel = geometry.voxelAt(worldY); + int internalY = worldY - minimumY; + if (voxel.phase() == BoundaryColumnGeometry.Phase.SOLID) { + ceiling = true; + } else if (ceiling && internalY < cappedHeight) { + carved[internalY] = 1; + } + } + return carved; + } + public byte[] getCarvedColumn(int x, int z, int height) { + Optional resolved = resolvedColumn(x, z); + if (resolved.isPresent()) { + byte[] carved = resolvedCarvedColumn(resolved.get(), height); + for (int y = 0; y < carved.length; y++) { + PreObjectMatterCell cell = getDataIfPresent(x, y, z, PreObjectMatterCell.class); + if (cell != null && cell.blockCaptured()) { + PlatformBlockState block = getDataIfPresent(x, y, z, PlatformBlockState.class); + if (block != null) { + carved[y] = (block.isAir() || block.isFluid()) + && resolved.get().geometry().hasSolidAbove(y + engineMantle.getEngine().getMinHeight()) + ? (byte) 1 : 0; + continue; + } + } + if (cell != null && cell.cavernCaptured()) { + carved[y] = getDataIfPresent(x, y, z, MatterCavern.class) != null ? (byte) 1 : 0; + } + } + return carved; + } int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight()); byte[] carvedColumn = new byte[cappedHeight]; if (cappedHeight <= 0) { @@ -888,6 +977,13 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { return carvedColumn; } + @Override + public boolean isSurfaceSolid(int x, int y, int z) { + Optional resolved = resolvedColumn(x, z); + return resolved.isEmpty() || resolved.get().geometry() + .voxelAt(y + engineMantle.getEngine().getMinHeight()).phase() == BoundaryColumnGeometry.Phase.SOLID; + } + @Override public boolean isSolid(int x, int y, int z) { return B.isSolid(get(x, y, z)); diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerationPhase.java b/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerationPhase.java new file mode 100644 index 000000000..eb69da005 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerationPhase.java @@ -0,0 +1,11 @@ +package art.arcane.iris.engine.mantle; + +public enum MatterGenerationPhase { + ALL, + TERRAIN, + CONTENT; + + public boolean includes(MantleComponent component) { + return this == ALL || component.getGenerationPhase() == this; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java b/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java index 045af2ce5..0ae2d9bdf 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java @@ -43,6 +43,21 @@ public interface MatterGenerator { @ChunkCoordinates default void generateMatter(int x, int z, boolean multicore, ChunkContext context) { + generateMatterPhase(x, z, multicore, context, MatterGenerationPhase.ALL); + } + + @ChunkCoordinates + default void generateTerrainMatter(int x, int z, boolean multicore, ChunkContext context) { + generateMatterPhase(x, z, multicore, context, MatterGenerationPhase.TERRAIN); + } + + @ChunkCoordinates + default void generateContentMatter(int x, int z, boolean multicore, ChunkContext context) { + generateMatterPhase(x, z, multicore, context, MatterGenerationPhase.CONTENT); + } + + private void generateMatterPhase(int x, int z, boolean multicore, ChunkContext context, + MatterGenerationPhase phase) { if (!getEngine().getDimension().isUseMantle()) { return; } @@ -53,7 +68,7 @@ public interface MatterGenerator { if (complex != null && !complex.allowsMantleChunkWrite(x, z)) { return; } - generateMatterWindow(x, z, multicore, context, complex); + generateMatterWindow(x, z, multicore, context, complex, phase); } private void generateMatterWindow( @@ -61,16 +76,19 @@ public interface MatterGenerator { int z, boolean multicore, ChunkContext context, - IrisComplex complex + IrisComplex complex, + MatterGenerationPhase phase ) { - MatterGenerationPlan generationPlan = resolveGenerationPlan(x, z, context); + MatterGenerationPlan generationPlan = resolveGenerationPlan(x, z, context, phase); MatterPassPlan[] passPlans = generationPlan.passPlans(); if (passPlans.length == 0) { return; } int prefetchRadius = passPlans[0].passChunkRadius(); LongOpenHashSet partialChunks = new LongOpenHashSet(); + List requiredComponents = enabledComponents(); + List terrainFlags = terrainFlags(requiredComponents); try (MantleWriter writer = new MantleWriter( getEngine().getMantle(), @@ -94,7 +112,7 @@ public interface MatterGenerator { int[] componentPassRadii = new int[passComponents.size()]; int enabledComponentCount = 0; for (MantleComponent component : passComponents) { - if (shouldGenerateComponent(component)) { + if (phase.includes(component) && shouldGenerateComponent(component)) { // A component must cover its own reach plus every later pass' reach, or a // later pass reads this component's data from chunks it never wrote. int componentReach = component.getOutputRadius() + passPlan.downstreamBlockRadius(); @@ -162,6 +180,9 @@ public interface MatterGenerator { continue; } + if (phase == MatterGenerationPhase.CONTENT) { + requireTerrainPhase(chunk, passX, passZ, terrainFlags); + } int eligibleComponentCount = 0; for (int componentIndex = 0; componentIndex < enabledComponentCount; componentIndex++) { MantleComponent component = enabledComponents[componentIndex]; @@ -217,6 +238,9 @@ public interface MatterGenerator { } } + if (phase == MatterGenerationPhase.TERRAIN) { + return; + } int realRadius = passPlans[passPlans.length - 1].passChunkRadius(); for (int i = -realRadius; i <= realRadius; i++) { for (int j = -realRadius; j <= realRadius; j++) { @@ -229,7 +253,10 @@ public interface MatterGenerator { if (complex != null && !complex.allowsMantleChunkWrite(realX, realZ)) { continue; } - writer.acquireChunk(realX, realZ).flag(MantleFlag.PLANNED, true); + MantleChunk chunk = writer.acquireChunk(realX, realZ); + if (hasCompletedComponents(chunk, requiredComponents)) { + chunk.flag(MantleFlag.PLANNED, true); + } } } } finally { @@ -242,7 +269,7 @@ public interface MatterGenerator { return (((long) x) << 32) ^ (z & 0xffffffffL); } - private MatterGenerationPlan resolveGenerationPlan(int x, int z, ChunkContext context) { + private MatterGenerationPlan resolveGenerationPlan(int x, int z, ChunkContext context, MatterGenerationPhase phase) { List passes = getComponents(); MatterPassPlan[] plans = new MatterPassPlan[passes.size()]; int accessDownstreamBlockRadius = 0; @@ -278,7 +305,48 @@ public interface MatterGenerator { int writerChunkRadius = accessDownstreamBlockRadius > 0 ? Math.ceilDiv(accessDownstreamBlockRadius, 16) : 0; - return new MatterGenerationPlan(plans, writerChunkRadius * 2); + return new MatterGenerationPlan(plans, writerChunkRadius * 2, phase); + } + + private List enabledComponents() { + List components = new ArrayList<>(); + for (MantlePass pass : getComponents()) { + for (MantleComponent component : pass.components()) { + if (shouldGenerateComponent(component)) { + components.add(component); + } + } + } + return List.copyOf(components); + } + + private static List terrainFlags(List components) { + List flags = new ArrayList<>(); + for (MantleComponent component : components) { + if (component.getGenerationPhase() == MatterGenerationPhase.TERRAIN) { + flags.add(component.getFlag()); + } + } + return List.copyOf(flags); + } + + private static void requireTerrainPhase(MantleChunk chunk, int chunkX, int chunkZ, + List flags) { + for (MantleFlag flag : flags) { + if (!chunk.isFlagged(flag)) { + throw new IllegalStateException("Content generation at " + chunkX + "," + chunkZ + + " requires completed terrain component " + flag.name()); + } + } + } + + private static boolean hasCompletedComponents(MantleChunk chunk, List components) { + for (MantleComponent component : components) { + if (!chunk.isFlagged(component.getFlag())) { + return false; + } + } + return true; } private boolean shouldGenerateComponent(MantleComponent component) { @@ -535,7 +603,7 @@ public interface MatterGenerator { record MatterPassPlan(MantlePass pass, int passChunkRadius, int downstreamBlockRadius) { } - record MatterGenerationPlan(MatterPassPlan[] passPlans, int writerAccessRadius) { + record MatterGenerationPlan(MatterPassPlan[] passPlans, int writerAccessRadius, MatterGenerationPhase phase) { } final class MatterTaskKey { diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/TerrainMatterView.java b/core/src/main/java/art/arcane/iris/engine/mantle/TerrainMatterView.java new file mode 100644 index 000000000..d350ba442 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/TerrainMatterView.java @@ -0,0 +1,94 @@ +package art.arcane.iris.engine.mantle; + +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell; +import art.arcane.iris.util.project.matter.PreObjectMatterCell; +import art.arcane.volmlib.util.function.Consumer4; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; + +import java.util.Objects; +import java.util.ArrayList; +import java.util.List; + +public final class TerrainMatterView { + private TerrainMatterView() { + } + + public static T get(Mantle mantle, int x, int y, int z, Class type) { + if (!hasJournal(type)) { + return mantle.get(x, y, z, type); + } + MantleChunk chunk = mantle.getChunk(x >> 4, z >> 4); + synchronized (chunk) { + PreObjectMatterCell cell = mantle.get(x, y, z, PreObjectMatterCell.class); + return cell != null && cell.captures(type) ? cell.original(type) : mantle.get(x, y, z, type); + } + } + + public static T get(MantleChunk chunk, int x, int y, int z, Class type) { + Objects.requireNonNull(type, "Terrain matter type"); + if (chunk == null || y < 0) { + return null; + } + if (!hasJournal(type)) { + return raw(chunk, x, y, z, type); + } + synchronized (chunk) { + PreObjectMatterCell cell = raw(chunk, x, y, z, PreObjectMatterCell.class); + return cell != null && cell.captures(type) ? cell.original(type) : raw(chunk, x, y, z, type); + } + } + + public static void iterate(MantleChunk chunk, Class type, + Consumer4 consumer) { + Objects.requireNonNull(chunk, "Terrain mantle chunk"); + Objects.requireNonNull(type, "Terrain matter type"); + Objects.requireNonNull(consumer, "Terrain matter consumer"); + if (!hasJournal(type)) { + chunk.iterate(type, consumer); + return; + } + List> entries = new ArrayList<>(); + synchronized (chunk) { + chunk.iterate(type, (x, y, z, value) -> { + T original = get(chunk, x, y, z, type); + if (original != null) { + entries.add(new TerrainEntry<>(x, y, z, original)); + } + }); + chunk.iterate(PreObjectMatterCell.class, (x, y, z, cell) -> { + if (cell == null || !cell.captures(type) || raw(chunk, x, y, z, type) != null) { + return; + } + T original = cell.original(type); + if (original != null) { + entries.add(new TerrainEntry<>(x, y, z, original)); + } + }); + } + for (TerrainEntry entry : entries) { + consumer.accept(entry.x(), entry.y(), entry.z(), entry.value()); + } + } + + private record TerrainEntry(int x, int y, int z, T value) { + } + + private static boolean hasJournal(Class type) { + return type == MatterCavern.class || type == PlatformBlockState.class || type == String.class + || type == HydrologyCaveCell.class; + } + + private static T raw(MantleChunk chunk, int x, int y, int z, Class type) { + int section = y >> 4; + if (!chunk.exists(section)) { + return null; + } + Matter matter = chunk.get(section); + return matter == null || !matter.hasSlice(type) ? null + : matter.getSlice(type).get(x & 15, y & 15, z & 15); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java index 9fe4eb392..808ebb13d 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java @@ -20,6 +20,7 @@ package art.arcane.iris.engine.mantle.components; import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.UpperDimensionContext; +import art.arcane.iris.engine.mantle.MatterGenerationPhase; import art.arcane.iris.engine.mantle.ComponentFlag; import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.mantle.IrisMantleComponent; @@ -84,6 +85,11 @@ public class MantleCarvingComponent extends IrisMantleComponent { super(engineMantle, ReservedFlag.CARVED, 0); } + @Override + public MatterGenerationPhase getGenerationPhase() { + return MatterGenerationPhase.TERRAIN; + } + @Override public void hotload() { super.hotload(); @@ -98,7 +104,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { @Override public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { IrisComplex complex = context.getComplex(); - if (!complex.allowsNewGenerationChunk(x, z)) { + if (!complex.allowsMantleChunkWrite(x, z)) { return; } IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleHydrologyCaveVoxelView.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleHydrologyCaveVoxelView.java index d8254bcd0..c40bfb92b 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleHydrologyCaveVoxelView.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleHydrologyCaveVoxelView.java @@ -11,6 +11,7 @@ import art.arcane.iris.engine.hydrology.cave.CaveVoxelView; import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.mantle.MantleComponent; import art.arcane.iris.engine.mantle.MantleWriter; +import art.arcane.iris.engine.mantle.TerrainMatterView; import art.arcane.iris.engine.object.IrisProceduralBlocks; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.project.context.ChunkContext; @@ -203,7 +204,7 @@ public final class MantleHydrologyCaveVoxelView implements CaveVoxelView { if (loadedChunks.add(chunkKey)) { chunkLoader.accept(chunkX, chunkZ); } - return mantle.get(position.x(), position.y(), position.z(), type); + return TerrainMatterView.get(mantle, position.x(), position.y(), position.z(), type); } private static int plannedSurfaceHeight( diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponent.java index cf51f3dcd..6b2fa8065 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponent.java @@ -12,6 +12,7 @@ import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell; import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan; import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime; import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.mantle.MatterGenerationPhase; import art.arcane.iris.engine.mantle.ComponentFlag; import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.mantle.IrisMantleComponent; @@ -60,6 +61,11 @@ public final class MantleHydrologyComponent extends IrisMantleComponent { super(engineMantle, ReservedFlag.RIVER_HYDROLOGY, PRIORITY); } + @Override + public MatterGenerationPhase getGenerationPhase() { + return MatterGenerationPhase.TERRAIN; + } + @Override public MantleFlag[] getPrerequisiteFlags() { return PREREQUISITES; @@ -74,7 +80,7 @@ public final class MantleHydrologyComponent extends IrisMantleComponent { public void generateLayer(MantleWriter writer, int chunkX, int chunkZ, ChunkContext context) { IrisComplex complex = context.getComplex(); IrisHydrologyRuntime runtime = complex.getHydrologyRuntime(); - if (runtime == null || !complex.allowsNewGenerationChunk(chunkX, chunkZ)) { + if (runtime == null || !complex.allowsMantleChunkWrite(chunkX, chunkZ)) { return; } int minimumX = chunkX << 4; @@ -218,6 +224,7 @@ public final class MantleHydrologyComponent extends IrisMantleComponent { addAcceptedGuards( caveCells, cavePlans, + samples, publishedCaveCells, minimumX, minimumZ, @@ -529,6 +536,7 @@ public final class MantleHydrologyComponent extends IrisMantleComponent { private static void addAcceptedGuards( Map caveCells, Collection cavePlans, + Map samples, Map published, int minimumX, int minimumZ, @@ -550,6 +558,9 @@ public final class MantleHydrologyComponent extends IrisMantleComponent { for (CavePosition position : guardPositions) { PlannedCell owner = guardOwner(position, plan, caveCells); if (owner == null) { + if (guardOutsidePublication(position, plan, samples)) { + continue; + } throw new IllegalStateException("Accepted containment guard has no adjacent cave volume at " + position); } @@ -592,6 +603,34 @@ public final class MantleHydrologyComponent extends IrisMantleComponent { return null; } + private static boolean guardOutsidePublication( + CavePosition guard, + HydrologyCavePlan plan, + Map samples + ) { + boolean hasPlannedVolume = false; + for (int[] offset : NEIGHBORS) { + CavePosition position = guard.offset(-offset[0], -offset[1], -offset[2]); + HydrologyCaveAction action = plan.actions().get(position); + if (action == null || action == HydrologyCaveAction.SEAL_GUARD) { + continue; + } + hasPlannedVolume = true; + HydrologyColumnSample sample = samples.get(pack(position.x(), position.z())); + if (sample == null) { + continue; + } + for (HydrologyColumnLayer layer : sample.layers()) { + if (layer.feature().courseId() == plan.source().sourceId() + && layer.channel() && layer.terrainOwned() && !layer.oceanApron() + && isCaveLayer(layer)) { + return false; + } + } + } + return hasPlannedVolume; + } + private static boolean preconditionsHold( Collection cavePlans, CaveVoxelView caveView, diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java index 8ad7d812e..a5037414c 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java @@ -23,6 +23,9 @@ import art.arcane.iris.core.IrisSettings; import art.arcane.iris.util.common.data.B; import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.history.TerrainBoundarySignature; +import art.arcane.iris.engine.history.BoundaryColumnGeometry; +import java.util.Optional; import art.arcane.iris.engine.UpperDimensionContext; import art.arcane.iris.engine.mantle.ComponentFlag; import art.arcane.iris.engine.mantle.EngineMantle; @@ -771,12 +774,7 @@ public class MantleObjectComponent extends IrisMantleComponent { : writer.getDataIfPresent(candidateX, candidateY, candidateZ, MatterCavern.class); if (candidateY < 0 || caveAnchorBiomeConflicts(candidateX, candidateY, candidateZ, expectedCaveBiomeKey) - || !acceptsCaveAnchorFluid( - underwater, - hydrology == null ? cavern : hydrology.asCavern(), - hydrology, - candidateY, - getDimension().getCaveLavaHeight())) { + || !acceptsCaveAnchorAt(candidateX, candidateY, candidateZ, underwater, cavern, hydrology)) { continue; } return new CavePlacementAnchor(candidateX, candidateY, candidateZ); @@ -784,6 +782,22 @@ public class MantleObjectComponent extends IrisMantleComponent { return null; } + private boolean acceptsCaveAnchorAt(int x, int y, int z, boolean underwater, + MatterCavern cavern, HydrologyCaveCell hydrology) { + Optional resolved = getEngineMantle().getComplex().resolvedTerrainColumn(x, z); + if (resolved.isPresent()) { + return acceptsResolvedCaveAnchor(underwater, + resolved.get().geometry().voxelAt(y + getEngineMantle().getEngine().getMinHeight())); + } + return acceptsCaveAnchorFluid(underwater, hydrology == null ? cavern : hydrology.asCavern(), + hydrology, y, getDimension().getCaveLavaHeight()); + } + + static boolean acceptsResolvedCaveAnchor(boolean underwater, BoundaryColumnGeometry.Voxel voxel) { + return !voxel.protectedContent() && (voxel.phase() == BoundaryColumnGeometry.Phase.AIR + || underwater && voxel.phase() == BoundaryColumnGeometry.Phase.FLUID); + } + static boolean acceptsCaveAnchorFluid(boolean underwater, MatterCavern cavern, int y, int lavaHeight) { return acceptsCaveAnchorFluid(underwater, cavern, null, y, lavaHeight); } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/ObjectDestinationTransaction.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/ObjectDestinationTransaction.java index 86674bc38..c3941583b 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/ObjectDestinationTransaction.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/ObjectDestinationTransaction.java @@ -140,12 +140,16 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { @Override public boolean isCarved(int x, int y, int z) { - HydrologyCaveCell hydrology = getDataIfPresent(x, y, z, HydrologyCaveCell.class); - if (hydrology != null) { + Object block = overlay.get(new DataKey(x, y, z, PlatformBlockState.class)); + if (block instanceof PlatformBlockState state && !state.isAir() && !state.isFluid()) { + return false; + } + Object hydrologyValue = overlay.get(new DataKey(x, y, z, HydrologyCaveCell.class)); + if (hydrologyValue instanceof HydrologyCaveCell hydrology) { return hydrology.carves(); } - MatterCavern cavern = getDataIfPresent(x, y, z, MatterCavern.class); - return cavern != null || writer.isPrerequisiteCarved(x, y, z); + Object cavern = overlay.get(new DataKey(x, y, z, MatterCavern.class)); + return cavern == null ? writer.isPrerequisiteCarved(x, y, z) : cavern != CLEARED; } @Override @@ -225,6 +229,11 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { carved = carved.clone(); } for (int y = 0; y < cappedHeight; y++) { + Object block = overlay.get(new DataKey(x, y, z, PlatformBlockState.class)); + if (block instanceof PlatformBlockState state && !state.isAir() && !state.isFluid()) { + carved[y] = 0; + continue; + } DataKey hydrologyKey = new DataKey(x, y, z, HydrologyCaveCell.class); Object hydrologyValue = overlay.get(hydrologyKey); if (hydrologyValue instanceof HydrologyCaveCell hydrology) { diff --git a/core/src/main/java/art/arcane/iris/engine/mode/ModeEnclosure.java b/core/src/main/java/art/arcane/iris/engine/mode/ModeEnclosure.java index a6f12a5f1..2edb04a1a 100644 --- a/core/src/main/java/art/arcane/iris/engine/mode/ModeEnclosure.java +++ b/core/src/main/java/art/arcane/iris/engine/mode/ModeEnclosure.java @@ -30,7 +30,7 @@ public class ModeEnclosure extends IrisEngineMode implements EngineMode { IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine()); IrisBiomeActuator biome = new IrisBiomeActuator(getEngine()); - registerStage(burst( + registerTerrainStage(burst( (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c), (x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c) )); diff --git a/core/src/main/java/art/arcane/iris/engine/mode/ModeIslands.java b/core/src/main/java/art/arcane/iris/engine/mode/ModeIslands.java index 6485f9b96..8146ef6e4 100644 --- a/core/src/main/java/art/arcane/iris/engine/mode/ModeIslands.java +++ b/core/src/main/java/art/arcane/iris/engine/mode/ModeIslands.java @@ -30,7 +30,7 @@ public class ModeIslands extends IrisEngineMode implements EngineMode { IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine()); IrisBiomeActuator biome = new IrisBiomeActuator(getEngine()); - registerStage(burst( + registerTerrainStage(burst( (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c), (x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c) )); diff --git a/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java b/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java index b9a4d8615..1a885e67c 100644 --- a/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java +++ b/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java @@ -57,7 +57,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode { if (shouldBypassMantleStages()) { return; } - generateMatter( + generateTerrainMatter( x >> 4, z >> 4, m || getEngine().isStudio(), @@ -101,18 +101,24 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode { // Matter runs on the calling thread so its window fans out across the burst pool (a pool // thread would run every chunk of the window inline) while biome and terrain run alongside. - registerStage(burstAround( + registerTerrainStage(burstAround( sGenMatter, sBiome, sTerrain )); - registerStage(sCave); - registerStage(sPost); - registerStage(sFloatingTerrainSolid); + registerTerrainStage(sCave); + registerTerrainStage(sPost); + registerTerrainStage(sFloatingTerrainSolid); + registerStage((x, z, k, p, m, c) -> cave.decorateNaturalCaves(x, z, k)); // Never burst these three: all of them write the same block hunk (and sDecorant reads // the surface sInsertMatter writes), so parallel order is scheduler-dependent. The // production path already runs them inline in this order; sequential registration // makes studio (the only multicore path) match production and the goldenhash baseline. + registerStage((x, z, k, p, m, c) -> { + if (!shouldBypassMantleStages()) { + generateContentMatter(x >> 4, z >> 4, m || getEngine().isStudio(), c); + } + }); registerStage(sDeposit); registerStage(sInsertMatter); registerStage(sDecorant); diff --git a/core/src/main/java/art/arcane/iris/engine/mode/ModeSuperFlat.java b/core/src/main/java/art/arcane/iris/engine/mode/ModeSuperFlat.java index 62a8f212d..36c2208bd 100644 --- a/core/src/main/java/art/arcane/iris/engine/mode/ModeSuperFlat.java +++ b/core/src/main/java/art/arcane/iris/engine/mode/ModeSuperFlat.java @@ -30,7 +30,7 @@ public class ModeSuperFlat extends IrisEngineMode implements EngineMode { IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine()); IrisBiomeActuator biome = new IrisBiomeActuator(getEngine()); - registerStage(burst( + registerTerrainStage(burst( (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c), (x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c) )); diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java index 147dcebd1..444296d6b 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java @@ -25,6 +25,7 @@ import art.arcane.iris.engine.UpperDimensionContext; import art.arcane.iris.engine.actuator.IrisDecorantActuator; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineAssignedModifier; +import art.arcane.iris.engine.mantle.TerrainMatterView; import art.arcane.iris.engine.object.InferredType; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisDecorationPart; @@ -55,6 +56,7 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import lombok.Data; import art.arcane.iris.spi.PlatformBlockState; +import java.util.HashMap; import java.util.Map; public class IrisCarveModifier extends EngineAssignedModifier { @@ -129,14 +131,14 @@ public class IrisCarveModifier extends EngineAssignedModifier carveResolver.apply( + TerrainMatterView.iterate(mantleChunk, MatterCavern.class, (xx, yy, zz, cavern) -> carveResolver.apply( xx, yy, zz, cavern, dataIfPresent(mantleChunk, xx, yy, zz, HydrologyCaveCell.class) )); - mantleChunk.iterate(HydrologyCaveCell.class, (xx, yy, zz, hydrology) -> { + TerrainMatterView.iterate(mantleChunk, HydrologyCaveCell.class, (xx, yy, zz, hydrology) -> { if (dataIfPresent(mantleChunk, xx, yy, zz, MatterCavern.class) == null) { carveResolver.apply(xx, yy, zz, null, hydrology); } @@ -570,15 +572,7 @@ public class IrisCarveModifier extends EngineAssignedModifier T dataIfPresent(MantleChunk mantleChunk, int x, int y, int z, Class type) { - int section = y >> 4; - if (y < 0 || !mantleChunk.exists(section)) { - return null; - } - Matter matter = mantleChunk.get(section); - if (matter == null || !matter.hasSlice(type)) { - return null; - } - return matter.getSlice(type).get(x & 15, y & 15, z & 15); + return TerrainMatterView.get(mantleChunk, x, y, z, type); } private void processColumnFromMask( @@ -802,17 +796,6 @@ public class IrisCarveModifier extends EngineAssignedModifier output) { + Mantle mantle = getEngine().getMantle().getMantle(); + MantleChunk chunk = mantle.getChunk(blockX >> 4, blockZ >> 4).use(); + IrisDimensionCarvingResolver.State resolver = new IrisDimensionCarvingResolver.State(); + Long2ObjectOpenHashMap caveBiomes = new Long2ObjectOpenHashMap<>(256); + Map customBiomes = new HashMap<>(); + try { + for (int localX = 0; localX < output.getWidth(); localX++) { + for (int localZ = 0; localZ < output.getDepth(); localZ++) { + int worldX = blockX + localX; + int worldZ = blockZ + localZ; + int floor = -1; + for (int y = 1; y < output.getHeight(); y++) { + PlatformBlockState state = output.getRaw(localX, y, localZ); + if (B.isSolid(state)) { + if (floor >= 0) { + CaveZone zone = new CaveZone(); + zone.setFloor(floor); + zone.setCeiling(y - 1); + if (zone.isValid(getEngine())) { + if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) { + mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING); + } + if (markerRoll(worldX, zone.floor, worldZ, 0xC2B2AE3D27D4EB4FL)) { + mantle.set(worldX, zone.floor, worldZ, MarkerMatter.CAVE_FLOOR); + } + IrisBiome floorBiome = resolveCaveBoundaryBiome(chunk, localX, floor, localZ, + worldX, worldZ, resolver, caveBiomes, customBiomes); + IrisBiome ceilingBiome = resolveCaveBoundaryBiome(chunk, localX, y - 1, localZ, + worldX, worldZ, resolver, caveBiomes, customBiomes); + decorateZone(output, zone, localX, localZ, worldX, worldZ, + floorBiome, ceilingBiome); + } + floor = -1; + } + } else if (floor < 0 && B.isSolid(output.getRaw(localX, y - 1, localZ))) { + floor = y; + } + } + } + } + } finally { + chunk.release(); + } + } + + private void decorateZone(Hunk output, CaveZone zone, + int rx, int rz, int xx, int zz, IrisBiome floorBiome, IrisBiome ceilingBiome) { + int maxY = output.getHeight(); IrisDecorator[] surfaceDecorators = floorBiome == null ? new IrisDecorator[0] : floorBiome.getDecoratorBucket(IrisDecorationPart.NONE); @@ -896,7 +931,6 @@ public class IrisCarveModifier extends EngineAssignedModifier generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context)); + burst.queue(scopedDepositTask( + () -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context)); } for (IrisDepositGenerator k : region.getDeposits()) { long finalSeed = seed * ++mask; - burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context)); + burst.queue(scopedDepositTask( + () -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context)); } for (IrisDepositGenerator k : biome.getDeposits()) { long finalSeed = seed * ++mask; - burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context)); + burst.queue(scopedDepositTask( + () -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context)); } } finally { // complete() must run before release() even when queueing throws — already @@ -91,6 +96,20 @@ public class IrisDepositModifier extends EngineAssignedModifier { + try (IrisEngine.GenerationRuntimeScope runtimeScope = generationEngine == null + ? null : generationEngine.openGenerationRuntimeScope(binding); + IrisContext.Scope chunkScope = IrisContext.open(getEngine(), context.getGenerationSessionId(), context)) { + task.run(); + } + }; + } + public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk data, RNG rng, int cx, int cz, boolean safe, ChunkContext context) { generate(k, chunk, data, rng, cx, cz, safe, null, context); } diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisFloatingChildBiomeModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisFloatingChildBiomeModifier.java index ef3424183..3b349d4f5 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisFloatingChildBiomeModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisFloatingChildBiomeModifier.java @@ -44,11 +44,13 @@ import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.project.noise.CNG; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; -import art.arcane.volmlib.util.matter.MatterBiomeInject; import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.engine.history.TransitionGenerationPlan; +import java.util.Optional; import java.util.IdentityHashMap; @@ -294,7 +296,8 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier matterByEntry = new IdentityHashMap<>(); + IdentityHashMap matterByEntry = new IdentityHashMap<>(); for (int k = 0; k <= sample.topIdx; k++) { if (!sample.solidMask[k]) { continue; @@ -404,16 +408,27 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier historical = transition.historicalPhysicalBiomeKeyAt(wx, y + getEngine().getMinHeight(), wz); + if (historical.isPresent()) { + selected = IrisPlatforms.get().registries().biome(historical.get()); + } + } + context.setNaturalBiome(wx & 15, y, wz & 15, selected); + if (!context.isSpeculativeTerrain()) { + getEngine().getMantle().getMantle().set(wx, y, wz, BiomeInjectMatter.get(selected.key())); + } } } catch (Throwable e) { IrisLogging.reportError(e); @@ -424,16 +439,16 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier resolved = engine.getComplex().resolvedTerrainColumn(x, z); + if (resolved.isPresent()) { + return resolved.get().fluidHeight().isPresent() + ? coordinateShift + resolved.get().fluidHeight().getAsInt() : -1; + } return coordinateShift + (int) Math.round( engine.getComplex().getRiverWaterSurfaceStream().get(x, z) ); diff --git a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java index 01fe75b41..168de2f38 100644 --- a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java @@ -29,7 +29,6 @@ import art.arcane.iris.spi.IrisServices; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisWorlds; -import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.runtime.ObjectStudioActivation; @@ -128,7 +127,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun private final boolean studio; private final AtomicBoolean studioEntryBootstrapActive; private final AtomicInteger a = new AtomicInteger(0); - private volatile long lastChunkGenTime = 0L; private final CompletableFuture spawnChunks = new CompletableFuture<>(); private final CompletableFuture initialSpawnReady = new CompletableFuture<>(); private final AtomicCache targetCache = new AtomicCache<>(); @@ -168,7 +166,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun this.folder = new ReactiveFolder( dataLocation, (_a, _b, _c) -> hotloadFromWatcher(), - new KList<>(".iob", ".json"), + new KList<>(".iob", ".json", ".png"), new KList<>(".iris"), new KList<>() ); @@ -390,6 +388,14 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun private void setupEngine() { lastMode = StudioMode.NORMAL; lastJigsawStudioRequestId = null; + if (generationHistory != null) { + try { + generationHistory.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()); + targetCache.reset(); + } catch (IOException failure) { + throw new IllegalStateException("Unable to prepare saved Iris terrain for the current generator.", failure); + } + } EngineTarget engineTarget = getTarget(); String packKey = engineTarget.getDimension().getLoadKey(); JigsawStudioActivation.Request request = studio @@ -483,12 +489,16 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun } catch (IOException failure) { throw new IllegalStateException("Unable to load the active Iris generation transition.", failure); } - return new IrisEngine( + IrisEngine createdEngine = new IrisEngine( engineTarget, mode, generationHistory.paths().activationMantleRoot(active.activationId()), epoch.kernelVersion(), plan); + if (studio) { + createdEngine.configureStudioGenerationSource(dataLocation.toPath()); + } + return createdEngine; } private static RuntimeException propagateEngineSetupFailure(Throwable failure) { @@ -520,6 +530,13 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun if (engine != null) return engine.getTarget(); return targetCache.aquireOrThrow(() -> { + if (generationHistory != null) { + try { + return loadActiveGenerationHistoryTarget(); + } catch (IOException failure) { + throw new IllegalStateException("Unable to load the active Iris generation pack.", failure); + } + } IrisData data = IrisData.openRuntime(dataLocation); data.dump(); data.clearLists(); @@ -577,7 +594,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun && irisEngine.isGenerationCacheWarmPending()) { return HOTLOAD_LOOP_DELAY_MS; } - if (shouldThrottleHotload()) { + if (isMaintenanceActive()) { return HOTLOAD_MAINTENANCE_DELAY_MS; } @@ -978,7 +995,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun try { Engine engine = getEngine(world); - lastChunkGenTime = System.currentTimeMillis(); computeStudioGenerator(); TerrainChunk tc = TerrainChunk.create(d); if (studioGenerator != null) { @@ -1070,20 +1086,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun } } - private boolean shouldThrottleHotload() { - if (isMaintenanceActive()) { - return true; - } - - if (System.currentTimeMillis() - lastChunkGenTime < 2000L) { - return true; - } - - World realWorld = BukkitWorldBinding.world(this.world); - PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); - return realWorld != null && pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(WorldIdentity.serialize(realWorld)); - } - @Override public int getBaseHeight(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull HeightMap heightMap) { Engine currentEngine = getEngine(worldInfo); diff --git a/core/src/main/java/art/arcane/iris/util/common/director/DirectorExecutor.java b/core/src/main/java/art/arcane/iris/util/common/director/DirectorExecutor.java index 754e3c263..dca53e89c 100644 --- a/core/src/main/java/art/arcane/iris/util/common/director/DirectorExecutor.java +++ b/core/src/main/java/art/arcane/iris/util/common/director/DirectorExecutor.java @@ -20,12 +20,17 @@ package art.arcane.iris.util.common.director; import art.arcane.volmlib.util.director.DirectorExecutorBase; import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.pack.PackDirectoryResolver; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.util.common.plugin.VolmitSender; import org.bukkit.entity.Player; +import java.io.File; +import java.util.Arrays; + public interface DirectorExecutor extends DirectorExecutorBase { default VolmitSender sender() { return DirectorContext.get(); @@ -42,11 +47,32 @@ public interface DirectorExecutor extends DirectorExecutorBase { } default IrisData data() { - var access = access(); - if (access != null) { - return access.getData(); + return authoringData(engine()); + } + + static IrisData authoringData(Engine engine) { + if (engine == null) { + return null; } - return null; + if (engine.isStudio()) { + return IrisData.get(engine.getPackSource().toFile()); + } + if (engine.getDimension() == null) { + return null; + } + String dimensionKey = engine.getDimension().getLoadKey(); + IrisData matching = null; + for (File pack : PackDirectoryResolver.listVisiblePackDirectories(IrisPlatforms.get().packsFolder())) { + IrisData candidate = IrisData.get(pack); + if (!Arrays.asList(candidate.getDimensionLoader().getPossibleKeys()).contains(dimensionKey)) { + continue; + } + if (matching != null) { + return null; + } + matching = candidate; + } + return matching; } default Engine engine() { diff --git a/core/src/main/java/art/arcane/iris/util/common/director/context/BiomeContextHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/context/BiomeContextHandler.java index d5d90fd32..a8fa25c56 100644 --- a/core/src/main/java/art/arcane/iris/util/common/director/context/BiomeContextHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/director/context/BiomeContextHandler.java @@ -19,9 +19,12 @@ package art.arcane.iris.util.common.director.context; import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.platform.EngineBukkitOps; import art.arcane.iris.util.common.director.DirectorContextHandler; +import art.arcane.iris.util.common.director.DirectorExecutor; import art.arcane.iris.util.common.plugin.VolmitSender; public class BiomeContextHandler implements DirectorContextHandler { @@ -33,7 +36,13 @@ public class BiomeContextHandler implements DirectorContextHandler { if (sender.isPlayer() && IrisToolbelt.isIrisWorld(sender.player().getWorld()) && IrisToolbelt.access(sender.player().getWorld()).getEngine() != null) { - return EngineBukkitOps.getBiomeOrMantle(IrisToolbelt.access(sender.player().getWorld()).getEngine(), sender.player().getLocation()); + Engine engine = IrisToolbelt.access(sender.player().getWorld()).getEngine(); + IrisData source = DirectorExecutor.authoringData(engine); + if (source == null) { + return null; + } + IrisBiome biome = EngineBukkitOps.getBiomeOrMantle(engine, sender.player().getLocation()); + return biome == null ? null : source.getBiomeLoader().load(biome.getLoadKey()); } return null; diff --git a/core/src/main/java/art/arcane/iris/util/common/director/context/DimensionContextHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/context/DimensionContextHandler.java index cb6ab3b81..6d2d3e99c 100644 --- a/core/src/main/java/art/arcane/iris/util/common/director/context/DimensionContextHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/director/context/DimensionContextHandler.java @@ -19,8 +19,11 @@ package art.arcane.iris.util.common.director.context; import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.util.common.director.DirectorContextHandler; +import art.arcane.iris.util.common.director.DirectorExecutor; import art.arcane.iris.util.common.plugin.VolmitSender; public class DimensionContextHandler implements DirectorContextHandler { @@ -32,7 +35,9 @@ public class DimensionContextHandler implements DirectorContextHandler { @@ -37,7 +39,9 @@ public class GeneratorContextHandler implements DirectorContextHandler { @@ -33,7 +36,13 @@ public class RegionContextHandler implements DirectorContextHandler if (sender.isPlayer() && IrisToolbelt.isIrisWorld(sender.player().getWorld()) && IrisToolbelt.access(sender.player().getWorld()).getEngine() != null) { - return EngineBukkitOps.getRegion(IrisToolbelt.access(sender.player().getWorld()).getEngine(), sender.player().getLocation()); + Engine engine = IrisToolbelt.access(sender.player().getWorld()).getEngine(); + IrisData source = DirectorExecutor.authoringData(engine); + if (source == null) { + return null; + } + IrisRegion region = EngineBukkitOps.getRegion(engine, sender.player().getLocation()); + return region == null ? null : source.getRegionLoader().load(region.getLoadKey()); } return null; diff --git a/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java b/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java index 216d4d572..e6bc1bf5f 100644 --- a/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java +++ b/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java @@ -10,6 +10,8 @@ import art.arcane.iris.engine.object.FloatingIslandBoundarySampler; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.common.parallel.MultiBurst; import java.util.ArrayList; @@ -17,6 +19,9 @@ import java.util.List; import java.util.concurrent.CompletableFuture; public class ChunkContext { + private boolean naturalTerrain = true; + private boolean speculativeTerrain; + private Hunk terrainBiomes; private final int x; private final int z; private final IrisComplex complex; @@ -66,7 +71,7 @@ public class ChunkContext { this.z = z; this.complex = complex; this.generationSessionId = generationSessionId; - this.height = new ChunkedDoubleDataCache(complex.getHeightStream(), x, z, cache); + this.height = new ChunkedDoubleDataCache(complex.getRawHeightStream(), x, z, cache); this.roundedHeight = new int[cache ? 256 : 0]; this.biome = new ChunkedDataCache<>(complex.getTrueBiomeStream(), x, z, cache); this.cave = new ChunkedDataCache<>(complex.getCaveBiomeStream(), x, z, cache); @@ -105,7 +110,7 @@ public class ChunkContext { fillTasks.add(new PrefillFillTask(cave)); } - if (!shouldPrefillAsync(fillTasks.size())) { + if (!shouldPrefillAsync(resolvedPlan, fillTasks.size())) { for (Runnable fillTask : fillTasks) { fillTask.run(); } @@ -124,14 +129,40 @@ public class ChunkContext { } } - static boolean shouldPrefillAsync(int fillTaskCount) { - if (fillTaskCount <= 1 || !IrisPlatforms.isBound()) { + static boolean shouldPrefillAsync(PrefillPlan prefillPlan, int fillTaskCount) { + if (prefillPlan == PrefillPlan.NATURAL_TERRAIN || fillTaskCount <= 1 || !IrisPlatforms.isBound()) { return false; } return !MultiBurst.burst.ownsCurrentThread(); } + public void setTerrainBiomeOutput(Hunk terrainBiomes) { + this.terrainBiomes = terrainBiomes; + } + + public void setNaturalBiome(int localX, int y, int localZ, PlatformBiome biome) { + if (terrainBiomes != null) { + terrainBiomes.setRaw(localX, y, localZ, biome); + } + } + + public boolean isSpeculativeTerrain() { + return speculativeTerrain; + } + + public void beginSpeculativeTerrain() { + speculativeTerrain = true; + } + + public boolean isNaturalTerrain() { + return naturalTerrain; + } + + public void beginContent() { + naturalTerrain = false; + } + public int getX() { return x; } @@ -156,6 +187,13 @@ public class ChunkContext { return height; } + public void setTerrainHeight(int localX, int localZ, int terrainHeight) { + height.setDouble(localX, localZ, terrainHeight); + if (roundedHeight.length > 0) { + roundedHeight[(localZ << 4) + localX] = terrainHeight; + } + } + public int getRoundedHeight(int x, int z) { if (roundedHeight.length == 0) { return (int) Math.round(height.getDouble(x, z)); @@ -200,6 +238,7 @@ public class ChunkContext { public enum PrefillPlan { ALL(true, true, true, true, true, true), NO_CAVE(true, true, false, true, true, true), + NATURAL_TERRAIN(true, true, false, true, true, true), NONE(false, false, false, false, false, false); private final boolean height; diff --git a/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDoubleDataCache.java b/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDoubleDataCache.java index 2f5a2c9a5..833633eba 100644 --- a/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDoubleDataCache.java +++ b/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDoubleDataCache.java @@ -14,6 +14,7 @@ public class ChunkedDoubleDataCache { private final ProceduralStream stream; private final boolean cache; private final double[] data; + private double[] overrides; @BlockCoordinates public ChunkedDoubleDataCache(ProceduralStream stream, int x, int z) { @@ -75,8 +76,26 @@ public class ChunkedDoubleDataCache { } } + public void setDouble(int localX, int localZ, double value) { + if (localX < 0 || localX >= 16 || localZ < 0 || localZ >= 16 || !Double.isFinite(value)) { + throw new IllegalArgumentException("Invalid chunk height override"); + } + if (cache) { + data[(localZ << 4) + localX] = value; + return; + } + if (overrides == null) { + overrides = new double[256]; + Arrays.fill(overrides, Double.NaN); + } + overrides[(localZ << 4) + localX] = value; + } + @BlockCoordinates public double getDouble(int x, int z) { + if (overrides != null && !Double.isNaN(overrides[(z << 4) + x])) { + return overrides[(z << 4) + x]; + } if (!cache) { return stream.getDouble(this.x + x, this.z + z); } diff --git a/core/src/main/java/art/arcane/iris/util/project/matter/PreObjectMatterCell.java b/core/src/main/java/art/arcane/iris/util/project/matter/PreObjectMatterCell.java index 2972c7413..ca7b40b79 100644 --- a/core/src/main/java/art/arcane/iris/util/project/matter/PreObjectMatterCell.java +++ b/core/src/main/java/art/arcane/iris/util/project/matter/PreObjectMatterCell.java @@ -1,6 +1,7 @@ package art.arcane.iris.util.project.matter; import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell; import art.arcane.volmlib.util.matter.MatterCavern; public record PreObjectMatterCell( @@ -9,7 +10,9 @@ public record PreObjectMatterCell( boolean stringCaptured, String string, boolean cavernCaptured, - MatterCavern cavern + MatterCavern cavern, + boolean hydrologyCaptured, + HydrologyCaveCell hydrology ) { public PreObjectMatterCell { if (!blockCaptured && block != null) { @@ -21,45 +24,60 @@ public record PreObjectMatterCell( if (!cavernCaptured && cavern != null) { throw new IllegalArgumentException("An uncaptured cavern cannot have a value"); } - if (!blockCaptured && !stringCaptured && !cavernCaptured) { + if (!hydrologyCaptured && hydrology != null) { + throw new IllegalArgumentException("An uncaptured hydrology cell cannot have a value"); + } + if (!blockCaptured && !stringCaptured && !cavernCaptured && !hydrologyCaptured) { throw new IllegalArgumentException("A pre-object cell must capture at least one value"); } } public static PreObjectMatterCell block(PlatformBlockState value) { - return new PreObjectMatterCell(true, value, false, null, false, null); + return new PreObjectMatterCell(true, value, false, null, false, null, false, null); } public static PreObjectMatterCell string(String value) { - return new PreObjectMatterCell(false, null, true, value, false, null); + return new PreObjectMatterCell(false, null, true, value, false, null, false, null); } public static PreObjectMatterCell cavern(MatterCavern value) { - return new PreObjectMatterCell(false, null, false, null, true, value); + return new PreObjectMatterCell(false, null, false, null, true, value, false, null); } public PreObjectMatterCell captureBlock(PlatformBlockState value) { if (blockCaptured) { return this; } - return new PreObjectMatterCell(true, value, stringCaptured, string, cavernCaptured, cavern); + return new PreObjectMatterCell(true, value, stringCaptured, string, cavernCaptured, cavern, hydrologyCaptured, hydrology); } public PreObjectMatterCell captureString(String value) { if (stringCaptured) { return this; } - return new PreObjectMatterCell(blockCaptured, block, true, value, cavernCaptured, cavern); + return new PreObjectMatterCell(blockCaptured, block, true, value, cavernCaptured, cavern, hydrologyCaptured, hydrology); } public PreObjectMatterCell captureCavern(MatterCavern value) { if (cavernCaptured) { return this; } - return new PreObjectMatterCell(blockCaptured, block, stringCaptured, string, true, value); + return new PreObjectMatterCell(blockCaptured, block, stringCaptured, string, true, value, hydrologyCaptured, hydrology); + } + + public static PreObjectMatterCell hydrology(HydrologyCaveCell value) { + return new PreObjectMatterCell(false, null, false, null, false, null, true, value); + } + + public PreObjectMatterCell captureHydrology(HydrologyCaveCell value) { + return hydrologyCaptured ? this : new PreObjectMatterCell(blockCaptured, block, + stringCaptured, string, cavernCaptured, cavern, true, value); } public boolean captures(Class type) { + if (type == HydrologyCaveCell.class) { + return hydrologyCaptured; + } if (type == PlatformBlockState.class) { return blockCaptured; } @@ -83,6 +101,6 @@ public record PreObjectMatterCell( if (type == String.class) { return (T) string; } - return (T) cavern; + return type == HydrologyCaveCell.class ? (T) hydrology : (T) cavern; } } diff --git a/core/src/main/java/art/arcane/iris/util/project/matter/slices/PreObjectMatter.java b/core/src/main/java/art/arcane/iris/util/project/matter/slices/PreObjectMatter.java index 4b25e18c2..f305aa607 100644 --- a/core/src/main/java/art/arcane/iris/util/project/matter/slices/PreObjectMatter.java +++ b/core/src/main/java/art/arcane/iris/util/project/matter/slices/PreObjectMatter.java @@ -1,6 +1,7 @@ package art.arcane.iris.util.project.matter.slices; import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell; import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.project.matter.PreObjectMatterCell; import art.arcane.volmlib.util.data.palette.Palette; @@ -24,6 +25,9 @@ public final class PreObjectMatter extends MappedHunk private static final int STRING_PRESENT = 1 << 3; private static final int CAVERN_CAPTURED = 1 << 4; private static final int CAVERN_PRESENT = 1 << 5; + private static final int HYDROLOGY_CAPTURED = 1 << 6; + private static final int HYDROLOGY_PRESENT = 1 << 7; + private static final HydrologyCaveMatter HYDROLOGY_CODEC = new HydrologyCaveMatter(); public PreObjectMatter() { this(1, 1, 1); @@ -68,6 +72,9 @@ public final class PreObjectMatter extends MappedHunk output.writeUTF(cell.cavern().getCustomBiome()); output.writeByte(cell.cavern().getLiquid()); } + if ((flags & HYDROLOGY_PRESENT) != 0) { + HYDROLOGY_CODEC.writeNode(cell.hydrology(), output); + } } @Override @@ -79,13 +86,16 @@ public final class PreObjectMatter extends MappedHunk MatterCavern cavern = (flags & CAVERN_PRESENT) == 0 ? null : new MatterCavern(input.readBoolean(), input.readUTF(), input.readByte()); + HydrologyCaveCell hydrology = (flags & HYDROLOGY_PRESENT) == 0 ? null : HYDROLOGY_CODEC.readNode(input); return new PreObjectMatterCell( (flags & BLOCK_CAPTURED) != 0, block, (flags & STRING_CAPTURED) != 0, string, (flags & CAVERN_CAPTURED) != 0, - cavern + cavern, + (flags & HYDROLOGY_CAPTURED) != 0, + hydrology ); } @@ -109,16 +119,22 @@ public final class PreObjectMatter extends MappedHunk flags |= CAVERN_PRESENT; } } + if (cell.hydrologyCaptured()) { + flags |= HYDROLOGY_CAPTURED; + if (cell.hydrology() != null) { + flags |= HYDROLOGY_PRESENT; + } + } return flags; } private void validateFlags(int flags) throws IOException { int knownFlags = BLOCK_CAPTURED | BLOCK_PRESENT | STRING_CAPTURED | STRING_PRESENT - | CAVERN_CAPTURED | CAVERN_PRESENT; + | CAVERN_CAPTURED | CAVERN_PRESENT | HYDROLOGY_CAPTURED | HYDROLOGY_PRESENT; if ((flags & ~knownFlags) != 0) { throw new IOException("Unknown pre-object matter flags " + flags); } - if ((flags & (BLOCK_CAPTURED | STRING_CAPTURED | CAVERN_CAPTURED)) == 0) { + if ((flags & (BLOCK_CAPTURED | STRING_CAPTURED | CAVERN_CAPTURED | HYDROLOGY_CAPTURED)) == 0) { throw new IOException("Pre-object matter cell captures no values"); } if ((flags & BLOCK_PRESENT) != 0 && (flags & BLOCK_CAPTURED) == 0) { @@ -127,6 +143,9 @@ public final class PreObjectMatter extends MappedHunk if ((flags & STRING_PRESENT) != 0 && (flags & STRING_CAPTURED) == 0) { throw new IOException("Pre-object string value is not captured"); } + if ((flags & HYDROLOGY_PRESENT) != 0 && (flags & HYDROLOGY_CAPTURED) == 0) { + throw new IOException("Pre-object hydrology value is not captured"); + } if ((flags & CAVERN_PRESENT) != 0 && (flags & CAVERN_CAPTURED) == 0) { throw new IOException("Pre-object cavern value is not captured"); } diff --git a/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream2D.java b/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream2D.java index 4c074919e..db2fe06b0 100644 --- a/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream2D.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream2D.java @@ -31,14 +31,11 @@ public class CachedStream2D extends BasicStream implements ProceduralStrea private final ProceduralStream stream; private final WorldCache2D cache; private final Engine engine; - private final boolean fastCache; - private final boolean chunked = true; public CachedStream2D(String name, Engine engine, ProceduralStream stream, int size) { super(); this.stream = stream; this.engine = engine; - this.fastCache = Boolean.getBoolean("iris.cache.fast"); cache = WorldCache2D.ofInts(stream::get, size, () -> new ChunkCache2D<>("iris")); IrisServices.get(PreservationRegistry.class).registerCache(this); } @@ -78,10 +75,6 @@ public class CachedStream2D extends BasicStream implements ProceduralStrea return cache.getMaxSize(); } - public boolean usesFastCache() { - return fastCache; - } - @Override public boolean isClosed() { return engine.isClosed(); diff --git a/core/src/test/java/art/arcane/iris/core/IrisSafeguardTest.java b/core/src/test/java/art/arcane/iris/core/IrisSafeguardTest.java new file mode 100644 index 000000000..c05aa7f30 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/IrisSafeguardTest.java @@ -0,0 +1,59 @@ +package art.arcane.iris.core; + +import art.arcane.iris.core.safeguard.IrisSafeguard; +import art.arcane.iris.core.safeguard.Mode; +import art.arcane.iris.core.safeguard.task.Task; +import art.arcane.iris.core.safeguard.task.Tasks; +import art.arcane.iris.spi.IrisLogging; +import org.junit.After; +import org.junit.Test; +import org.mockito.MockedStatic; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mockStatic; + +public class IrisSafeguardTest { + @After + public void resetValidationAndSafeguard() { + IrisStartupValidation.disable(); + try (MockedStatic tasks = mockStatic(Tasks.class)) { + tasks.when(Tasks::getTasks).thenReturn(List.of()); + IrisSafeguard.execute(); + } + } + + @Test + public void injectionExceptionRevokesReadinessAndReportsUnstableRuntime() { + IllegalStateException injectionFailure = new IllegalStateException("Java agent unavailable"); + Task injection = Task.of("injection", () -> { + throw injectionFailure; + }); + IrisStartupValidation.begin(); + IrisStartupValidation.markDatapacksReady(); + IrisStartupValidation.markPacksReady(); + IrisStartupValidation.beginRuntimeValidation(); + IrisStartupValidation.markRuntimeReady(); + assertTrue(IrisStartupValidation.isReady()); + + try (MockedStatic tasks = mockStatic(Tasks.class); + MockedStatic logging = mockStatic(IrisLogging.class)) { + tasks.when(Tasks::getTasks).thenReturn(List.of(injection)); + + IrisSafeguard.execute(); + + assertFalse(IrisStartupValidation.isReady()); + assertEquals(IrisStartupValidation.ValidationState.INVALID, IrisStartupValidation.snapshot().runtime()); + assertEquals(Mode.UNSTABLE, IrisSafeguard.mode()); + assertEquals("unstable", IrisSafeguard.asContext().get("injection")); + assertEquals("Iris runtime injection failed. Resolve the startup errors and restart the server.", + IrisStartupValidation.denialReason().orElseThrow()); + assertThrows(IllegalStateException.class, IrisStartupValidation::requireWorldCreationReady); + logging.verify(() -> IrisLogging.reportError(injectionFailure)); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/core/IrisStartupValidationTest.java b/core/src/test/java/art/arcane/iris/core/IrisStartupValidationTest.java index c900b0449..873bde608 100644 --- a/core/src/test/java/art/arcane/iris/core/IrisStartupValidationTest.java +++ b/core/src/test/java/art/arcane/iris/core/IrisStartupValidationTest.java @@ -129,4 +129,88 @@ public class IrisStartupValidationTest { assertEquals("pack registry unavailable", IrisStartupValidation.denialReason().orElseThrow()); } + + @Test + public void runtimeValidationMustCompleteBeforeCreation() { + IrisStartupValidation.begin(); + IrisStartupValidation.markDatapacksReady(); + IrisStartupValidation.markPacksReady(); + IrisStartupValidation.beginRuntimeValidation(); + + assertFalse(IrisStartupValidation.isReady()); + assertEquals("Iris is still validating runtime injection.", IrisStartupValidation.denialReason().orElseThrow()); + assertThrows(IllegalStateException.class, IrisStartupValidation::requireWorldCreationReady); + + IrisStartupValidation.markRuntimeReady(); + + assertTrue(IrisStartupValidation.isReady()); + IrisStartupValidation.requireWorldCreationReady(); + } + + @Test + public void packRevalidationCannotClearRuntimeFailure() { + IrisStartupValidation.begin(); + IrisStartupValidation.beginRuntimeValidation(); + IrisStartupValidation.markRuntimeInvalid("Java agent unavailable"); + IrisStartupValidation.markDatapacksReady(); + IrisStartupValidation.markPacksReady(); + IrisStartupValidation.beginDatapackValidation(); + IrisStartupValidation.markDatapacksInvalid("temporary datapack failure"); + IrisStartupValidation.markDatapacksReady(); + IrisStartupValidation.markPacksInvalid(List.of("temporary pack failure")); + IrisStartupValidation.markPacksReady(); + + assertFalse(IrisStartupValidation.isReady()); + assertEquals("Java agent unavailable", IrisStartupValidation.denialReason().orElseThrow()); + } + + @Test + public void forcedStudioAndReplacementCannotBypassRuntimeFailure() { + IrisStartupValidation.begin(); + IrisStartupValidation.markDatapacksReady(); + IrisStartupValidation.markPacksReady(); + IrisStartupValidation.requireRestart("restart boundary"); + IrisStartupValidation.beginRuntimeValidation(); + + assertFalse(IrisStartupValidation.studioDenialReason(true).isEmpty()); + assertThrows(IllegalStateException.class, IrisStartupValidation::requireWorldReplacementStagingReady); + + IrisStartupValidation.markRuntimeInvalid("Java agent unavailable"); + + assertEquals("Java agent unavailable", IrisStartupValidation.studioDenialReason(true).orElseThrow()); + assertThrows(IllegalStateException.class, IrisStartupValidation::requireWorldReplacementStagingReady); + + IrisStartupValidation.markRuntimeReady(); + + assertTrue(IrisStartupValidation.studioDenialReason(true).isEmpty()); + IrisStartupValidation.requireWorldReplacementStagingReady(); + assertThrows(IllegalStateException.class, IrisStartupValidation::requireWorldCreationReady); + } + + @Test + public void repeatedRuntimeValidationRevokesPreviousReadiness() { + IrisStartupValidation.begin(); + IrisStartupValidation.markDatapacksReady(); + IrisStartupValidation.markPacksReady(); + IrisStartupValidation.beginRuntimeValidation(); + IrisStartupValidation.markRuntimeReady(); + IrisStartupValidation.beginRuntimeValidation(); + + assertFalse(IrisStartupValidation.isReady()); + assertThrows(IllegalStateException.class, IrisStartupValidation::requireWorldCreationReady); + + IrisStartupValidation.markRuntimeInvalid("injection retry failed"); + + assertEquals("injection retry failed", IrisStartupValidation.denialReason().orElseThrow()); + } + + @Test + public void runtimeFailureCannotEnableDisabledValidation() { + IrisStartupValidation.disable(); + IrisStartupValidation.beginRuntimeValidation(); + IrisStartupValidation.markRuntimeInvalid("Java agent unavailable"); + + assertTrue(IrisStartupValidation.isReady()); + IrisStartupValidation.requireWorldCreationReady(); + } } diff --git a/core/src/test/java/art/arcane/iris/core/gui/ImageMapStudioGUITest.java b/core/src/test/java/art/arcane/iris/core/gui/ImageMapStudioGUITest.java index ad6da8c33..8f9e7df04 100644 --- a/core/src/test/java/art/arcane/iris/core/gui/ImageMapStudioGUITest.java +++ b/core/src/test/java/art/arcane/iris/core/gui/ImageMapStudioGUITest.java @@ -1,30 +1,20 @@ package art.arcane.iris.core.gui; -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.loader.ResourceLoader; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.object.IrisImageMap; import org.junit.Test; -import org.mockito.InOrder; -import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; public class ImageMapStudioGUITest { @Test - public void invalidatesPresetKeysBeforeHotloadingTheActiveEngine() { + public void exportRequestsHotloadWithoutMutatingTheActiveGenerationData() { Engine engine = mock(Engine.class); - IrisData data = mock(IrisData.class); - @SuppressWarnings("unchecked") - ResourceLoader loader = mock(ResourceLoader.class); - when(engine.getData()).thenReturn(data); - when(data.getImageMapLoader()).thenReturn(loader); ImageMapStudioGUI.reloadActiveEngine(engine); - InOrder order = inOrder(loader, engine); - order.verify(loader).clearCache(); - order.verify(engine).hotloadSilently(); + verify(engine).hotloadSilently(); + verifyNoMoreInteractions(engine); } } diff --git a/core/src/test/java/art/arcane/iris/core/lifecycle/BukkitPublicBackendTest.java b/core/src/test/java/art/arcane/iris/core/lifecycle/BukkitPublicBackendTest.java new file mode 100644 index 000000000..ff491676c --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/lifecycle/BukkitPublicBackendTest.java @@ -0,0 +1,71 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.nms.INMSBinding; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.WorldType; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.ChunkGenerator; +import org.junit.Test; +import org.mockito.MockedStatic; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +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.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.verify; + +public class BukkitPublicBackendTest { + @Test + public void injectionFailureCompletesExceptionallyAndClearsStaging() { + String worldName = "injection_failure"; + NamespacedKey worldKey = new NamespacedKey("iris", worldName); + ChunkGenerator generator = mock(ChunkGenerator.class); + BiomeProvider biomeProvider = mock(BiomeProvider.class); + INMSBinding binding = mock(INMSBinding.class); + IllegalStateException injectionFailure = new IllegalStateException("injection failed"); + WorldLifecycleRequest request = new WorldLifecycleRequest( + worldName, worldKey, World.Environment.NORMAL, generator, biomeProvider, + WorldType.NORMAL, true, false, 123L, false, false, WorldLifecycleCaller.CREATE); + BukkitPublicBackend backend = new BukkitPublicBackend( + CapabilitySnapshot.forTesting(ServerFamily.PAPER, false, false, false)); + + doAnswer(invocation -> { + assertSame(generator, WorldLifecycleStaging.peekStemGenerator(worldName)); + throw injectionFailure; + }).when(binding).ensureServerLevelInjection(); + + try (MockedStatic identity = mockStatic(WorldIdentity.class); + MockedStatic nms = mockStatic(INMS.class); + MockedStatic bukkit = mockStatic(Bukkit.class)) { + identity.when(() -> WorldIdentity.resolve(worldKey)).thenReturn(Optional.empty()); + nms.when(INMS::get).thenReturn(binding); + + CompletableFuture result = backend.create(request); + + assertTrue(result.isCompletedExceptionally()); + CompletionException failure = assertThrows(CompletionException.class, result::join); + assertSame(injectionFailure, failure.getCause()); + verify(binding).ensureServerLevelInjection(); + bukkit.verify(() -> Bukkit.createWorld(any(WorldCreator.class)), never()); + assertNull(WorldLifecycleStaging.consumeGenerator(worldName)); + assertNull(WorldLifecycleStaging.consumeBiomeProvider(worldName)); + assertNull(WorldLifecycleStaging.consumeStemGenerator(worldName)); + } finally { + WorldLifecycleStaging.clearAll(worldName); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/core/loader/IrisDataAuthoringCacheTest.java b/core/src/test/java/art/arcane/iris/core/loader/IrisDataAuthoringCacheTest.java new file mode 100644 index 000000000..0c8320558 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/loader/IrisDataAuthoringCacheTest.java @@ -0,0 +1,75 @@ +package art.arcane.iris.core.loader; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.engine.object.IrisGenerator; +import art.arcane.iris.spi.IrisServices; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +public class IrisDataAuthoringCacheTest { + @Rule + public final TemporaryFolder temporary = new TemporaryFolder(); + + private IrisData source; + private IrisData epoch; + private IrisSettings previousSettings; + + @Before + public void registerServices() { + previousSettings = IrisSettings.settings; + IrisSettings.settings = new IrisSettings(); + IrisServices.register(PreservationRegistry.class, mock(PreservationRegistry.class)); + } + + @After + public void closeData() { + if (source != null) { + source.close(); + } + if (epoch != null) { + epoch.close(); + } + IrisServices.clear(); + IrisSettings.settings = previousSettings; + } + + @Test + public void externalEditsRefreshOnlyTheEditableSourceAndKeyLists() throws Exception { + Path sourceRoot = temporary.newFolder("source").toPath(); + Path epochRoot = temporary.newFolder("epoch").toPath(); + Files.createDirectories(sourceRoot.resolve("generators")); + Files.createDirectories(epochRoot.resolve("generators")); + Files.writeString(sourceRoot.resolve("generators/sample.json"), "{\"zoom\":2.0}"); + Files.writeString(epochRoot.resolve("generators/sample.json"), "{\"zoom\":3.0}"); + source = IrisData.get(new File(sourceRoot.toFile(), ".")); + epoch = IrisData.openRuntime(epochRoot.toFile()); + assertEquals(2.0, source.getGeneratorLoader().load("sample").getZoom(), 0.0); + IrisGenerator retained = epoch.getGeneratorLoader().load("sample"); + assertFalse(Arrays.asList(source.getGeneratorLoader().getPossibleKeys()).contains("added")); + Files.writeString(sourceRoot.resolve("generators/sample.json"), "{\"zoom\":4.0}"); + Files.writeString(sourceRoot.resolve("generators/added.json"), "{\"zoom\":5.0}"); + + IrisData.invalidateLoadedAuthoringResources(sourceRoot.toFile()); + + assertEquals(4.0, source.getGeneratorLoader().load("sample").getZoom(), 0.0); + assertTrue(Arrays.asList(source.getGeneratorLoader().getPossibleKeys()).contains("added")); + assertSame(retained, epoch.getGeneratorLoader().load("sample")); + assertEquals(3.0, retained.getZoom(), 0.0); + assertEquals("{\"zoom\":3.0}", Files.readString(epochRoot.resolve("generators/sample.json"))); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorStructureTerrainBackendTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorStructureTerrainBackendTest.java index bbfa2e4af..b64fb0859 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorStructureTerrainBackendTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorStructureTerrainBackendTest.java @@ -16,12 +16,100 @@ import java.util.Locale; import java.util.Set; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class PackValidatorStructureTerrainBackendTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Test + public void rejectsNativeAnchorsInNestedResourcesAcrossEveryHost() throws Exception { + File pack = temporaryFolder.newFolder("native-anchors"); + List expected = new ArrayList<>(); + for (String folder : List.of("dimensions", "regions", "biomes")) { + JSONArray placements = new JSONArray(); + for (String anchor : List.of("SURFACE", "HEIGHT_BAND", "CAVE_FLOOR", "CAVE_CEILING")) { + placements.put(new JSONObject() + .put("placementId", folder + "-" + anchor) + .put("anchor", anchor) + .put("nativeStructures", new JSONArray().put( + new JSONObject().put("structure", "minecraft:village_plains")))); + String host = switch (folder) { + case "dimensions" -> "Dimension"; + case "regions" -> "Region"; + default -> "Biome"; + }; + expected.add(host + " 'nested/host' structures[" + (placements.length() - 1) + + "].anchor must be omitted, null, or LEGACY for nativeStructures."); + } + write(pack, folder + "/nested/host.json", new JSONObject().put("structures", placements).toString()); + } + List errors = new ArrayList<>(); + + PackStructurePlacementValidator.validateStructurePlacements(pack, Set.of(), false, errors); + + assertEquals(expected, errors); + PackValidationResult result = PackValidator.validateForPackaging(pack); + assertFalse(result.isLoadable()); + assertTrue(result.getBlockingErrors().toString(), result.getBlockingErrors().containsAll(expected)); + } + + @Test + public void rejectsMixedAndEmptyBackendsAcrossEveryHost() throws Exception { + File pack = temporaryFolder.newFolder("invalid-backends"); + List expected = new ArrayList<>(); + for (String folder : List.of("dimensions", "regions", "biomes")) { + JSONArray placements = new JSONArray() + .put(new JSONObject().put("placementId", folder + "-mixed") + .put("structures", new JSONArray().put("tower")) + .put("nativeStructures", new JSONArray().put( + new JSONObject().put("structure", "minecraft:village_plains")))) + .put(new JSONObject().put("placementId", folder + "-empty") + .put("structures", new JSONArray()).put("nativeStructures", new JSONArray())); + write(pack, folder + "/host.json", new JSONObject().put("structures", placements).toString()); + String host = switch (folder) { + case "dimensions" -> "Dimension"; + case "regions" -> "Region"; + default -> "Biome"; + }; + for (int index = 0; index < 2; index++) { + expected.add(host + " 'host' structures[" + index + + "] must declare exactly one non-empty backend: structures or nativeStructures."); + } + } + List errors = new ArrayList<>(); + + PackStructurePlacementValidator.validateStructurePlacements(pack, Set.of("tower"), false, errors); + + assertEquals(expected, errors); + } + + @Test + public void acceptsNativeDefaultAndLegacyAnchorsAndEditableSurfaceAnchor() throws Exception { + File pack = temporaryFolder.newFolder("supported-anchors"); + JSONArray placements = new JSONArray(); + for (int index = 0; index < 3; index++) { + JSONObject placement = new JSONObject().put("placementId", "native-" + index) + .put("nativeStructures", new JSONArray().put( + new JSONObject().put("structure", "minecraft:village_plains"))); + if (index == 1) { + placement.put("anchor", "LEGACY"); + } else if (index == 2) { + placement.put("anchor", JSONObject.NULL); + } + placements.put(placement); + } + placements.put(new JSONObject().put("placementId", "editable") + .put("anchor", "SURFACE").put("structures", new JSONArray().put("tower"))); + write(pack, "dimensions/main.json", new JSONObject().put("structures", placements).toString()); + List errors = new ArrayList<>(); + + PackStructurePlacementValidator.validateStructurePlacements(pack, Set.of("tower"), false, errors); + + assertTrue(errors.toString(), errors.isEmpty()); + } + @Test public void rejectsNativeOnlyTerrainModesForEditablePlacementsAcrossEveryHost() throws Exception { for (String mode : List.of("VACUUM", "ENCASE")) { diff --git a/core/src/test/java/art/arcane/iris/core/pregenerator/PregenPerformanceProfileTest.java b/core/src/test/java/art/arcane/iris/core/pregenerator/PregenPerformanceProfileTest.java index 3e43c9d0b..2ea3869fe 100644 --- a/core/src/test/java/art/arcane/iris/core/pregenerator/PregenPerformanceProfileTest.java +++ b/core/src/test/java/art/arcane/iris/core/pregenerator/PregenPerformanceProfileTest.java @@ -3,149 +3,94 @@ package art.arcane.iris.core.pregenerator; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D; -import art.arcane.iris.util.project.stream.utility.CachedStream2D; +import org.junit.After; +import org.junit.Before; import org.junit.Test; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doThrow; 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.when; public class PregenPerformanceProfileTest { - @Test - public void liveGeneratorUsesItsPlatformHotloadBoundary() { - IrisSettings previousSettings = IrisSettings.settings; - String previousFastCache = System.getProperty("iris.cache.fast"); - IrisSettings.settings = new IrisSettings(); - System.clearProperty("iris.cache.fast"); - Engine engine = engineWithProfile(1_024, false); - PlatformChunkGenerator generator = generatorFor(engine); - try { - PregenPerformanceProfile.applyToGenerator(generator); + private IrisSettings previousSettings; + private Engine engine; + private IrisComplex complex; + private CachedDoubleStream2D natural; + private CachedDoubleStream2D raw; - assertEquals(4_096, IrisSettings.get().getPerformance().getNoiseCacheSize()); - assertTrue(Boolean.getBoolean("iris.cache.fast")); - verify(generator).hotloadComplexAsync(30L, TimeUnit.SECONDS); - verify(engine, never()).hotloadComplex(); - } finally { - restore(previousSettings, previousFastCache); - } + @Before + public void setUp() { + previousSettings = IrisSettings.settings; + IrisSettings.settings = new IrisSettings(); + IrisSettings.get().getPerformance().setNoiseCacheSize(1_024); + engine = mock(Engine.class); + complex = mock(IrisComplex.class); + natural = mock(CachedDoubleStream2D.class); + raw = mock(CachedDoubleStream2D.class); + when(engine.getComplex()).thenReturn(complex); + when(complex.getNaturalHeightStream()).thenReturn(natural); + when(complex.getRawHeightStream()).thenReturn(raw); + when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 47D)); + doThrow(new IllegalStateException("Immutable history cannot rebuild its runtime")) + .when(engine).hotloadComplex(); + } + + @After + public void tearDown() { + IrisSettings.settings = previousSettings; } @Test - public void eachExistingEngineRefreshesAgainstTheAppliedProfile() { - IrisSettings previousSettings = IrisSettings.settings; - String previousFastCache = System.getProperty("iris.cache.fast"); - IrisSettings.settings = new IrisSettings(); - IrisSettings.get().getPerformance().setNoiseCacheSize(4_096); - System.clearProperty("iris.cache.fast"); - Engine overworldEngine = engineWithProfile(4_096, false); - Engine netherEngine = engineWithProfile(4_096, false); - PlatformChunkGenerator overworldGenerator = generatorFor(overworldEngine); - PlatformChunkGenerator netherGenerator = generatorFor(netherEngine); - try { - PregenPerformanceProfile.applyToGenerator(overworldGenerator); - PregenPerformanceProfile.applyToGenerator(netherGenerator); + public void directEngineProfileResizesActualTerrainCachesWithoutRebuildingHistory() { + PregenPerformanceProfile.apply(engine); - verify(overworldGenerator).hotloadComplexAsync(30L, TimeUnit.SECONDS); - verify(netherGenerator).hotloadComplexAsync(30L, TimeUnit.SECONDS); - } finally { - restore(previousSettings, previousFastCache); - } + assertEquals(4_096, IrisSettings.get().getPerformance().getNoiseCacheSize()); + verify(natural).setMaximumChunks(4_096); + verify(raw).setMaximumChunks(4_096); + verify(complex, never()).getHeightStream(); + verify(engine, never()).hotloadComplex(); } @Test - public void profileAppliedBeforeWorldCreationAvoidsLiveHotload() { - IrisSettings previousSettings = IrisSettings.settings; - String previousFastCache = System.getProperty("iris.cache.fast"); - IrisSettings.settings = new IrisSettings(); - System.clearProperty("iris.cache.fast"); - Engine engine = engineWithProfile(4_096, true); + public void platformProfileDoesNotHotloadOrPublishAStudioActivation() { PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class); when(generator.getEngine()).thenReturn(engine); - try { - PregenPerformanceProfile.apply(); - PregenPerformanceProfile.applyToGenerator(generator); - verify(generator, never()).hotloadComplexAsync(30L, TimeUnit.SECONDS); - verify(engine, never()).hotloadComplex(); - } finally { - restore(previousSettings, previousFastCache); - } + PregenPerformanceProfile.applyToGenerator(generator); + + verify(natural).setMaximumChunks(4_096); + verify(raw).setMaximumChunks(4_096); + verify(generator, never()).hotloadComplexAsync(30L, TimeUnit.SECONDS); + verify(engine, never()).hotloadComplex(); } @Test - public void liveHeightCacheGrowsWithoutRebuildingTheEngine() { - IrisSettings previousSettings = IrisSettings.settings; - String previousFastCache = System.getProperty("iris.cache.fast"); - IrisSettings.settings = new IrisSettings(); - System.setProperty("iris.cache.fast", "true"); - Engine engine = mock(Engine.class); - IrisComplex complex = mock(IrisComplex.class); - CachedDoubleStream2D heightStream = mock(CachedDoubleStream2D.class); - CachedDoubleStream2D naturalHeightStream = mock(CachedDoubleStream2D.class); - CachedStream2D caveBiomeStream = mock(CachedStream2D.class); - PlatformChunkGenerator generator = generatorFor(engine); - when(engine.getComplex()).thenReturn(complex); - when(complex.getHeightStream()).thenReturn(heightStream); - when(complex.getNaturalHeightStream()).thenReturn(naturalHeightStream); - when(complex.getCaveBiomeStream()).thenReturn(caveBiomeStream); - when(caveBiomeStream.usesFastCache()).thenReturn(true); - try { - PregenPerformanceProfile.applyToGenerator(generator); + public void largerConfiguredCachesRemainLarger() { + IrisSettings.get().getPerformance().setNoiseCacheSize(8_192); - verify(heightStream, times(1)).setMaximumChunks(4_096); - verify(naturalHeightStream, times(1)).setMaximumChunks(4_096); - verify(generator, never()).hotloadComplexAsync(30L, TimeUnit.SECONDS); - verify(engine, never()).hotloadComplex(); - } finally { - restore(previousSettings, previousFastCache); - } + PregenPerformanceProfile.apply(engine); + + assertEquals(8_192, IrisSettings.get().getPerformance().getNoiseCacheSize()); + verify(natural).setMaximumChunks(8_192); + verify(raw).setMaximumChunks(8_192); } - @SuppressWarnings("unchecked") - private static Engine engineWithProfile(int cacheSize, boolean fastCache) { - Engine engine = mock(Engine.class); - IrisComplex complex = mock(IrisComplex.class); - CachedDoubleStream2D heightStream = mock(CachedDoubleStream2D.class); - CachedDoubleStream2D naturalHeightStream = mock(CachedDoubleStream2D.class); - CachedStream2D caveBiomeStream = mock(CachedStream2D.class); - when(heightStream.getMaxSize()).thenReturn((long) cacheSize * 256L); - when(naturalHeightStream.getMaxSize()).thenReturn((long) cacheSize * 256L); - when(caveBiomeStream.usesFastCache()).thenReturn(fastCache); - when(complex.getHeightStream()).thenReturn(heightStream); - when(complex.getNaturalHeightStream()).thenReturn(naturalHeightStream); - when(complex.getCaveBiomeStream()).thenReturn(caveBiomeStream); - when(engine.getComplex()).thenReturn(complex); - return engine; - } + @Test + public void profileBeforeWorldCreationStillAppliesGlobalTuning() { + PregenPerformanceProfile.applyToGenerator(null); - private static PlatformChunkGenerator generatorFor(Engine engine) { - PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class); - when(generator.getEngine()).thenReturn(engine); - when(generator.hotloadComplexAsync(30L, TimeUnit.SECONDS)) - .thenReturn(CompletableFuture.completedFuture(null)); - return generator; - } - - private static void restore(IrisSettings settings, String fastCache) { - IrisSettings.settings = settings; - if (fastCache == null) { - System.clearProperty("iris.cache.fast"); - } else { - System.setProperty("iris.cache.fast", fastCache); - } + assertEquals(4_096, IrisSettings.get().getPerformance().getNoiseCacheSize()); } @Test diff --git a/core/src/test/java/art/arcane/iris/core/pregenerator/cache/PregenSavedChunkStatusTest.java b/core/src/test/java/art/arcane/iris/core/pregenerator/cache/PregenSavedChunkStatusTest.java new file mode 100644 index 000000000..7970e6374 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/pregenerator/cache/PregenSavedChunkStatusTest.java @@ -0,0 +1,93 @@ +package art.arcane.iris.core.pregenerator.cache; + +import art.arcane.volmlib.util.nbt.io.NBTUtil; +import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicInteger; + +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 PregenSavedChunkStatusTest { + @Rule + public TemporaryFolder temporary = new TemporaryFolder(); + + @Test + public void savedStatusRequiresFullAndMissingChunksAreUnfinished() throws IOException { + Path root = temporary.newFolder().toPath(); + assertFalse(PregenSavedChunkStatus.fromWorld(root).isFull(-1, -2)); + for (String stage : new String[]{"minecraft:noise", "minecraft:initialize_light", "minecraft:spawn", "minecraft:full"}) { + writeChunk(root, stage); + PregenSavedChunkStatus status = PregenSavedChunkStatus.fromWorld(root); + assertEquals(stage.equals("minecraft:full"), status.isFull(-1, -2)); + assertFalse(status.isFull(-2, -2)); + } + } + + @Test + public void malformedNativeStatusFailsValidation() throws IOException { + Path root = temporary.newFolder().toPath(); + writeChunk(root, null); + assertThrows(UncheckedIOException.class, () -> PregenSavedChunkStatus.fromWorld(root).isFull(-1, -2)); + } + + @Test + public void statusMasksReuseReadsAndEvictAtTheRegionBound() { + AtomicInteger reads = new AtomicInteger(); + PregenSavedChunkStatus status = new PregenSavedChunkStatus((x, z) -> { + reads.incrementAndGet(); + return x == -1 && z == -2; + }); + assertTrue(status.isFull(-1, -2)); + assertTrue(status.isFull(-1, -2)); + assertFalse(status.isFull(-2, -1)); + assertFalse(status.isFull(-2, -1)); + assertEquals(2, reads.get()); + for (int region = 0; region < 256; region++) { + status.isFull(region << 5, 0); + } + assertTrue(status.isFull(-1, -2)); + assertEquals(259, reads.get()); + } + + @Test + public void invalidSavedDataDoesNotBecomeACompletionHint() { + PregenSavedChunkStatus status = new PregenSavedChunkStatus((x, z) -> { + throw new IOException("corrupt native data"); + }); + assertThrows(UncheckedIOException.class, () -> status.isFull(1, 2)); + } + + private static void writeChunk(Path root, String stage) throws IOException { + CompoundTag data = new CompoundTag(); + data.putInt("xPos", -1); + data.putInt("zPos", -2); + if (stage != null) { + data.putString("Status", stage); + } + ByteArrayOutputStream output = new ByteArrayOutputStream(); + NBTUtil.write(data, output, false); + byte[] payload = output.toByteArray(); + Path region = Files.createDirectories(root.resolve("region")).resolve("r.-1.-1.mca"); + try (RandomAccessFile file = new RandomAccessFile(region.toFile(), "rw")) { + file.setLength(12288); + file.seek((30L * 32 + 31) * 4); + file.writeInt((2 << 8) | 1); + file.seek(8192); + file.writeInt(payload.length + 1); + file.writeByte(3); + file.write(payload); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/core/pregenerator/methods/CachedPregenMethodCompletionTest.java b/core/src/test/java/art/arcane/iris/core/pregenerator/methods/CachedPregenMethodCompletionTest.java index f079cc7d0..3e6224c1e 100644 --- a/core/src/test/java/art/arcane/iris/core/pregenerator/methods/CachedPregenMethodCompletionTest.java +++ b/core/src/test/java/art/arcane/iris/core/pregenerator/methods/CachedPregenMethodCompletionTest.java @@ -4,10 +4,12 @@ import art.arcane.iris.core.pregenerator.PregenListener; import art.arcane.iris.core.pregenerator.PregenTask; import art.arcane.iris.core.pregenerator.PregeneratorMethod; import art.arcane.iris.core.pregenerator.cache.PregenCache; +import art.arcane.iris.core.pregenerator.cache.PregenSavedChunkStatus; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.math.Position2; import org.junit.Before; import org.junit.Test; +import org.mockito.InOrder; import org.bukkit.World; import java.io.File; @@ -15,12 +17,17 @@ import java.lang.reflect.Constructor; import java.nio.file.Files; import java.util.ArrayList; import java.util.List; +import java.util.HashSet; +import java.util.Set; 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.assertTrue; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class CachedPregenMethodCompletionTest { private File directory; @@ -29,6 +36,7 @@ public class CachedPregenMethodCompletionTest { private PregenTask task; private CachedPregenMethod method; private RecordingListener listener; + private Set savedFull; @Before public void setUp() throws Exception { @@ -40,7 +48,9 @@ public class CachedPregenMethodCompletionTest { .radiusX(256) .radiusZ(256) .build(); - method = new CachedPregenMethod(underlying, cache, task); + savedFull = new HashSet<>(); + method = new CachedPregenMethod(new CachedPregenMethod.Configuration(underlying, cache, task, + new PregenSavedChunkStatus((x, z) -> savedFull.contains(x + "," + z)))); listener = new RecordingListener(); } @@ -73,6 +83,7 @@ public class CachedPregenMethodCompletionTest { underlying.capturedListener.get().onChunkGenerated(1, 2, false); assertEquals(1, underlying.generateChunkCalls.get()); + savedFull.add("1,2"); method.generateChunk(1, 2, listener); assertEquals(1, underlying.generateChunkCalls.get()); @@ -83,6 +94,11 @@ public class CachedPregenMethodCompletionTest { @Test public void generateRegionReplayRespectsTaskBounds() { cache.cacheRegion(0, 0); + for (int x = 0; x < 32; x++) { + for (int z = 0; z < 32; z++) { + savedFull.add(x + "," + z); + } + } List expected = new ArrayList<>(); task.iterateChunks(0, 0, (x, z) -> expected.add(new long[]{x, z})); assertTrue(expected.size() < 1024); @@ -104,7 +120,8 @@ public class CachedPregenMethodCompletionTest { .getDeclaredConstructor(World.class, PregeneratorMethod.class); hybridConstructor.setAccessible(true); HybridPregenMethod hybrid = hybridConstructor.newInstance(null, selector); - CachedPregenMethod wrapped = new CachedPregenMethod(hybrid, cache, task); + CachedPregenMethod wrapped = new CachedPregenMethod(new CachedPregenMethod.Configuration(hybrid, cache, task, + new PregenSavedChunkStatus((x, z) -> false))); wrapped.onPregenStart(123, -456); @@ -113,7 +130,57 @@ public class CachedPregenMethodCompletionTest { assertEquals(-456, underlying.centerBlockZ.get()); } + @Test + public void closePersistsCacheAfterTheNativeBackendCloses() { + PregenCache trackedCache = mock(PregenCache.class); + when(trackedCache.sync()).thenReturn(trackedCache); + PregeneratorMethod trackedMethod = mock(PregeneratorMethod.class); + CachedPregenMethod wrapped = new CachedPregenMethod(new CachedPregenMethod.Configuration( + trackedMethod, trackedCache, task, new PregenSavedChunkStatus((x, z) -> false))); + wrapped.close(); + InOrder order = inOrder(trackedMethod, trackedCache); + order.verify(trackedCache).sync(); + order.verify(trackedMethod).close(); + order.verify(trackedCache).write(); + order.verifyNoMoreInteractions(); + } + + @Test + public void staleChunkBitRequiresNativeCompletionAgain() { + cache.cacheChunk(126, 103); + method.generateChunk(126, 103, listener); + assertEquals(1, underlying.generateChunkCalls.get()); + assertEquals(0, listener.generated.get()); + underlying.capturedListener.get().onChunkGenerated(126, 103, false); + assertEquals(1, listener.generated.get()); + assertEquals(0, listener.generatedCached.get()); + } + + @Test + public void staleRegionBitCannotSkipPartialNativeChunks() { + cache.cacheRegion(0, 0); + assertFalse(method.supportsRegions(0, 0, listener)); + method.generateRegion(0, 0, listener); + AtomicInteger expected = new AtomicInteger(); + task.iterateChunks(0, 0, (x, z) -> expected.incrementAndGet()); + assertEquals(expected.get(), underlying.generateChunkCalls.get()); + assertEquals(0, underlying.generateRegionCalls.get()); + assertEquals(0, listener.generated.get()); + } + + @Test + public void regionSubmissionDoesNotPublishWholeRegionCompletion() { + underlying.regionSupport = true; + method.generateRegion(0, 0, listener); + assertFalse(cache.isRegionCached(0, 0)); + assertFalse(cache.isChunkCached(1, 2)); + underlying.capturedListener.get().onChunkGenerated(1, 2, false); + assertTrue(cache.isChunkCached(1, 2)); + assertFalse(cache.isChunkCached(31, 31)); + } + private static final class CapturingMethod implements PregeneratorMethod { + private boolean regionSupport; private final AtomicInteger generateChunkCalls = new AtomicInteger(); private final AtomicInteger generateRegionCalls = new AtomicInteger(); private final AtomicInteger pregenStartCalls = new AtomicInteger(); @@ -135,7 +202,7 @@ public class CachedPregenMethodCompletionTest { @Override public boolean supportsRegions(int x, int z, PregenListener listener) { - return false; + return regionSupport; } @Override @@ -146,6 +213,7 @@ public class CachedPregenMethodCompletionTest { @Override public void generateRegion(int x, int z, PregenListener listener) { generateRegionCalls.incrementAndGet(); + capturedListener.set(listener); } @Override diff --git a/core/src/test/java/art/arcane/iris/core/service/ObjectStudioSaveSourceTest.java b/core/src/test/java/art/arcane/iris/core/service/ObjectStudioSaveSourceTest.java new file mode 100644 index 000000000..855b58635 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/service/ObjectStudioSaveSourceTest.java @@ -0,0 +1,51 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ObjectStudioSaveSourceTest { + @Rule + public final TemporaryFolder temporary = new TemporaryFolder(); + + @Test + public void fallbackLayoutSavesToTheEditableSource() throws Exception { + Path source = temporary.newFolder("source").toPath(); + Path epoch = temporary.newFolder("epoch").toPath(); + Files.createDirectories(epoch.resolve("objects")); + Files.writeString(epoch.resolve("objects/tree.iob"), "retained"); + Engine engine = mock(Engine.class); + IrisData retained = mock(IrisData.class); + when(engine.getData()).thenReturn(retained); + when(engine.getPackSource()).thenReturn(source); + when(retained.getDataFolder()).thenReturn(epoch.toFile()); + File destination = ObjectStudioSaveService.resolveObjectsDir(retained, engine); + assertEquals(source.resolve("objects").toFile(), destination); + Files.writeString(destination.toPath().resolve("tree.iob"), "edited"); + assertEquals("retained", Files.readString(epoch.resolve("objects/tree.iob"))); + assertEquals("edited", Files.readString(source.resolve("objects/tree.iob"))); + } + + @Test + public void registeredMultiPackSourcesKeepTheirOwnDestination() throws Exception { + File source = temporary.newFolder("additional-pack"); + File epoch = temporary.newFolder("epoch"); + Engine engine = mock(Engine.class); + IrisData registered = mock(IrisData.class); + IrisData retained = mock(IrisData.class); + when(engine.getData()).thenReturn(retained); + when(retained.getDataFolder()).thenReturn(epoch); + when(registered.getDataFolder()).thenReturn(source); + assertEquals(new File(source, "objects"), ObjectStudioSaveService.resolveObjectsDir(registered, engine)); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/structure/StructureIndexServiceTest.java b/core/src/test/java/art/arcane/iris/core/structure/StructureIndexServiceTest.java index bb605f451..b796e3311 100644 --- a/core/src/test/java/art/arcane/iris/core/structure/StructureIndexServiceTest.java +++ b/core/src/test/java/art/arcane/iris/core/structure/StructureIndexServiceTest.java @@ -3,16 +3,15 @@ package art.arcane.iris.core.structure; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.ResourceLoader; import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.engine.history.GenerationPackFingerprint; import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.PlatformStructureHooks; -import art.arcane.volmlib.util.data.KCache; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import org.mockito.InOrder; import java.io.File; import java.io.InputStream; @@ -25,7 +24,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -61,73 +59,35 @@ public class StructureIndexServiceTest { } @Test - public void legacyIndexPathRemainsExplicitForCleanup() throws Exception { + public void writeOnceWaitsForBoundHooksAndPreservesGenerationInputs() throws Exception { File pack = temporaryFolder.newFolder("pack"); - - assertEquals(new File(pack, "structures/structure-index.json"), - StructureIndexService.legacyIndexFile(pack)); - } - - @Test - public void legacyIndexIsDeletedAndStructureKeysAreRefreshed() throws Exception { - File pack = temporaryFolder.newFolder("pack"); - File legacyIndex = StructureIndexService.legacyIndexFile(pack); - Files.createDirectories(legacyIndex.toPath().getParent()); - Files.writeString(legacyIndex.toPath(), "{}", StandardCharsets.UTF_8); + File structure = new File(pack, "structures/castle.json"); + Files.createDirectories(structure.toPath().getParent()); + Files.writeString(structure.toPath(), "{}", StandardCharsets.UTF_8); + String fingerprint = GenerationPackFingerprint.compute(pack.toPath(), GenerationPackFingerprint.CURRENT_VERSION); IrisData data = mock(IrisData.class); @SuppressWarnings("unchecked") ResourceLoader structureLoader = mock(ResourceLoader.class); - @SuppressWarnings("unchecked") - KCache loadCache = mock(KCache.class); - when(data.getDataFolder()).thenReturn(pack); - when(data.getStructureLoader()).thenReturn(structureLoader); - when(structureLoader.getLoadCache()).thenReturn(loadCache); - - StructureIndexService.removeLegacyIndex(data); - - assertFalse(legacyIndex.exists()); - verify(loadCache).invalidate("structure-index"); - verify(structureLoader).clearList(); - } - - @Test - public void writeOnceWaitsForBoundHooksThenCleansBeforeEnumerating() throws Exception { - File pack = temporaryFolder.newFolder("pack"); - File legacyIndex = StructureIndexService.legacyIndexFile(pack); - Files.createDirectories(legacyIndex.toPath().getParent()); - Files.writeString(legacyIndex.toPath(), "{}", StandardCharsets.UTF_8); - IrisData data = mock(IrisData.class); - @SuppressWarnings("unchecked") - ResourceLoader structureLoader = mock(ResourceLoader.class); - @SuppressWarnings("unchecked") - KCache loadCache = mock(KCache.class); IrisPlatform platform = mock(IrisPlatform.class); PlatformStructureHooks structureHooks = mock(PlatformStructureHooks.class); when(data.getDataFolder()).thenReturn(pack); when(data.getStructureLoader()).thenReturn(structureLoader); - when(structureLoader.getLoadCache()).thenReturn(loadCache); - when(structureLoader.getPossibleKeys()).thenReturn(new String[]{"castle", "structure-index"}); + when(structureLoader.getPossibleKeys()).thenReturn(new String[]{"castle"}); when(structureHooks.structureKeys()).thenReturn(List.of("minecraft:village", "example:castle")); when(structureHooks.structureSetKeys()).thenReturn(List.of("minecraft:villages")); assertNull(StructureIndexService.writeOnce(data)); - assertTrue(legacyIndex.exists()); IrisPlatforms.bind(platform); assertNull(StructureIndexService.writeOnce(data)); - assertTrue(legacyIndex.exists()); when(platform.structureHooks()).thenReturn(structureHooks); File index = StructureIndexService.writeOnce(data); assertEquals(StructureIndexService.indexFile(pack), index); assertTrue(index.isFile()); - assertFalse(legacyIndex.exists()); - InOrder order = inOrder(loadCache, structureLoader, structureHooks); - order.verify(loadCache).invalidate("structure-index"); - order.verify(structureLoader).clearList(); - order.verify(structureHooks).structureKeys(); - order.verify(structureHooks).structureSetKeys(); + assertEquals("{}", Files.readString(structure.toPath(), StandardCharsets.UTF_8)); + assertEquals(fingerprint, GenerationPackFingerprint.compute(pack.toPath(), GenerationPackFingerprint.CURRENT_VERSION)); assertNull(StructureIndexService.writeOnce(data)); verify(structureHooks, times(1)).structureKeys(); verify(structureHooks, times(1)).structureSetKeys(); diff --git a/core/src/test/java/art/arcane/iris/core/tools/IrisCreatorStudioGenerationHistoryContractTest.java b/core/src/test/java/art/arcane/iris/core/tools/IrisCreatorStudioGenerationHistoryContractTest.java new file mode 100644 index 000000000..dba990176 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/tools/IrisCreatorStudioGenerationHistoryContractTest.java @@ -0,0 +1,28 @@ +package art.arcane.iris.core.tools; + +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class IrisCreatorStudioGenerationHistoryContractTest { + @Test + public void studioPublishesHistoryBeforePreparingItsGeneratorAndKeepsTheAuthoringSource() throws Exception { + Path creatorPath = Path.of(System.getProperty("iris.irisCreatorSource")); + String creator = Files.readString(creatorPath); + String factory = Files.readString(creatorPath.resolveSibling("IrisWorldCreator.java")); + int snapshot = creator.indexOf("studioService.installIntoWorld(sender, resolvedDimension, dimensionRoot, seed)"); + int generator = creator.indexOf("new IrisWorldCreator()"); + + assertTrue(snapshot >= 0); + assertTrue(generator > snapshot); + assertFalse(creator.contains("if (!studio() || benchmark)")); + assertTrue(creator.contains(".dimension(installedDimension)")); + assertTrue(creator.contains(".studioPackSource(resolvedDimension.getLoader().getDataFolder())")); + assertTrue(factory.contains("GenerationHistory generationHistory = persistent || studio")); + assertTrue(factory.contains("File packRoot = studio\n ? Objects.requireNonNull(studioPackSource, \"Studio authoring source\")")); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/IrisComplexTransitionTest.java b/core/src/test/java/art/arcane/iris/engine/IrisComplexTransitionTest.java index 3bccc5bc9..5c4a32571 100644 --- a/core/src/test/java/art/arcane/iris/engine/IrisComplexTransitionTest.java +++ b/core/src/test/java/art/arcane/iris/engine/IrisComplexTransitionTest.java @@ -1,7 +1,5 @@ package art.arcane.iris.engine; -import art.arcane.iris.engine.history.TerrainBoundarySignature; -import art.arcane.iris.engine.history.TransitionGenerationPlan; import art.arcane.iris.engine.hydrology.HydrologyColumnLayer; import art.arcane.iris.engine.hydrology.HydrologyColumnSample; import art.arcane.iris.engine.hydrology.HydrologyFeatureRef; @@ -9,56 +7,15 @@ import art.arcane.iris.engine.hydrology.HydrologyFeatureType; import org.junit.Test; import java.util.List; -import java.util.OptionalInt; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; public class IrisComplexTransitionTest { @Test - public void blendsDryBoundaryTerrainFromTheFrozenSurface() { - TransitionGenerationPlan.TerrainSample sample = new TransitionGenerationPlan.TerrainSample( - 8D, - 0.5D, - 0D, - signature(OptionalInt.empty()), - 64D, - 55D, - 0D - ); - - assertEquals(82D, IrisComplex.blendNaturalTerrainHeight(100D, sample), 0D); - } - - @Test - public void blendsSubmergedBoundaryTerrainFromTheFrozenOceanFloor() { - TransitionGenerationPlan.TerrainSample sample = new TransitionGenerationPlan.TerrainSample( - 8D, - 0.5D, - 0D, - signature(OptionalInt.of(63)), - 64D, - 55D, - 0D - ); - - assertEquals(75D, IrisComplex.blendNaturalTerrainHeight(95D, sample), 0D); - } - - @Test - public void preservesNewTerrainAndHydrologyObjectsAtFullWeight() { - TransitionGenerationPlan.TerrainSample terrain = new TransitionGenerationPlan.TerrainSample( - 32D, - 1D, - 1D, - signature(OptionalInt.empty()), - 64D, - 55D, - 0D - ); + public void preservesHydrologyObjectsAtFullWeight() { HydrologyColumnSample hydrology = hydrologySample(); - assertEquals(101.25D, IrisComplex.blendNaturalTerrainHeight(101.25D, terrain), 0D); assertSame(hydrology, IrisComplex.taperHydrologySample(hydrology, 1D)); } @@ -74,20 +31,42 @@ public class IrisComplexTransitionTest { assertEquals("river", layer.profileKey()); } - private static TerrainBoundarySignature signature(OptionalInt fluidHeight) { - return new TerrainBoundarySignature( - new TerrainBoundarySignature.Column(15, 8, 64, 55, fluidHeight, OptionalInt.empty()), - new TerrainBoundarySignature.Samples( - new TerrainBoundarySignature.VerticalLayout(0, 1, 1), - new TerrainBoundarySignature.BiomeEncoding(List.of("iris:frozen"), new short[]{0}) - ) - ); + @Test + public void preservesCaveContainmentCoordinatesThroughoutTransition() { + for (HydrologyFeatureType type : HydrologyFeatureType.values()) { + if (!type.isUnderground() && !type.isDeepFluid()) { + continue; + } + HydrologyColumnSample sample = hydrologySample(type); + for (double weight : new double[]{0D, 0.01D, 0.25D, 0.5D, 0.99D, 1D}) { + HydrologyColumnSample tapered = IrisComplex.taperHydrologySample(sample, weight); + assertSame(type + " at " + weight, sample.layers().getFirst(), tapered.layers().getFirst()); + } + } + } + + @Test + public void tapersSurfaceLayerWithoutMovingOverlappingCave() { + HydrologyColumnLayer cave = hydrologySample(HydrologyFeatureType.UNDERGROUND_POOL).layers().getFirst(); + HydrologyColumnLayer surface = hydrologySample().layers().getFirst(); + HydrologyColumnSample sample = new HydrologyColumnSample(4, 6, 80, 63, false, "parent", List.of(surface, cave)); + + HydrologyColumnSample tapered = IrisComplex.taperHydrologySample(sample, 0.5D); + + assertSame(cave, tapered.layers().getFirst()); + assertEquals(73, tapered.layers().getLast().bedY()); + assertEquals(75, tapered.layers().getLast().fluidHeadY()); + assertEquals(75, tapered.layers().getLast().ceilingY()); } private static HydrologyColumnSample hydrologySample() { + return hydrologySample(HydrologyFeatureType.SURFACE_POOL); + } + + private static HydrologyColumnSample hydrologySample(HydrologyFeatureType type) { HydrologyFeatureRef feature = new HydrologyFeatureRef( 1L, - HydrologyFeatureType.SURFACE_POOL, + type, 2L, 3L, 4, diff --git a/core/src/test/java/art/arcane/iris/engine/IrisEngineGenerationRuntimeScopeTest.java b/core/src/test/java/art/arcane/iris/engine/IrisEngineGenerationRuntimeScopeTest.java index a47e48353..3007eac6f 100644 --- a/core/src/test/java/art/arcane/iris/engine/IrisEngineGenerationRuntimeScopeTest.java +++ b/core/src/test/java/art/arcane/iris/engine/IrisEngineGenerationRuntimeScopeTest.java @@ -14,6 +14,8 @@ import art.arcane.iris.engine.framework.NativeStructureVolumeMemo; import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter; import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; import art.arcane.iris.engine.object.IrisWorld; import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatforms; @@ -21,6 +23,8 @@ import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformRegistries; import art.arcane.iris.util.common.parallel.MultiBurst; import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.iris.util.project.stream.ProceduralStream; import org.junit.Test; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -47,9 +51,13 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doReturn; 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.when; @@ -70,6 +78,77 @@ public class IrisEngineGenerationRuntimeScopeTest { IrisPlatforms.unbind(); } + @Test + @SuppressWarnings("unchecked") + public void naturalCaveSamplesKeepSelectedRuntimeAcrossChunkEdges() throws Exception { + RuntimeFixture active = runtime(1, 1D, 1D, 1D); + RuntimeFixture selected = runtime(2, 2D, 2D, 2D); + IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class)); + IrisEngine.GenerationRuntimeBinding binding = detachedBinding(engine, selected.runtime); + GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class); + setField(engine, "generationHistoryRuntimeRouter", router); + doReturn(null).when(engine).getDimensionStackContext(); + doReturn(false).when(engine).answersFromNaturalTerrain(anyInt(), anyInt()); + doCallRealMethod().when(selected.complex).isNaturalTerrainContext(); + ChunkContext context = mock(ChunkContext.class); + when(context.getComplex()).thenReturn(selected.complex); + when(context.isNaturalTerrain()).thenReturn(true); + IrisBiome cave = mock(IrisBiome.class); + IrisBiome surface = mock(IrisBiome.class); + ProceduralStream caves = mock(ProceduralStream.class); + ProceduralStream surfaces = mock(ProceduralStream.class); + ProceduralStream height = mock(ProceduralStream.class); + when(selected.complex.getCaveBiomeStream()).thenReturn(caves); + when(selected.complex.getTrueBiomeStream()).thenReturn(surfaces); + when(selected.complex.getHeightStream()).thenReturn(height); + when(caves.get(19D, -3D)).thenReturn(cave); + when(surfaces.get(19D, -3D)).thenReturn(surface); + when(height.get(19D, -3D)).thenReturn(80D); + + try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(binding); + IrisContext.Scope contextScope = IrisContext.open(engine, 73L, context)) { + assertSame(cave, engine.getCaveBiome(19, -3)); + assertSame(cave, engine.getCaveBiome(19, 20, -3)); + assertSame(cave, engine.getCaveBiome(19, 20, -3, new IrisDimensionCarvingResolver.State())); + assertSame(surface, engine.getSurfaceBiome(19, -3)); + assertSame(selected.complex, engine.getComplex()); + verify(router, never()).openCoordinateScope(anyInt(), anyInt()); + when(context.isNaturalTerrain()).thenReturn(false); + assertSame(cave, engine.getCaveBiome(19, -3)); + verify(router, times(1)).openCoordinateScope(19, -3); + } + assertSame(active.complex, engine.getComplex()); + assertNull(IrisContext.get()); + } + + @Test + @SuppressWarnings("unchecked") + public void mismatchedAndUnboundNaturalContextsKeepCoordinateRouting() throws Exception { + RuntimeFixture active = runtime(1, 1D, 1D, 1D); + RuntimeFixture selected = runtime(2, 2D, 2D, 2D); + IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class)); + GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class); + setField(engine, "generationHistoryRuntimeRouter", router); + ProceduralStream caves = mock(ProceduralStream.class); + when(active.complex.getCaveBiomeStream()).thenReturn(caves); + when(selected.complex.getCaveBiomeStream()).thenReturn(caves); + doCallRealMethod().when(active.complex).isNaturalTerrainContext(); + doCallRealMethod().when(selected.complex).isNaturalTerrainContext(); + ChunkContext context = mock(ChunkContext.class); + when(context.getComplex()).thenReturn(active.complex); + when(context.isNaturalTerrain()).thenReturn(true); + + try (IrisContext.Scope ignored = IrisContext.open(engine, 73L, context)) { + engine.getCaveBiome(19, -3); + try (IrisEngine.GenerationRuntimeScope runtimeScope = engine.openGenerationRuntimeScope( + detachedBinding(engine, selected.runtime))) { + engine.getCaveBiome(19, -3); + } + } + engine.getCaveBiome(19, -3); + verify(router, times(3)).openCoordinateScope(19, -3); + } + @Test public void scopedGettersResolveDetachedGenerationStateAndWorldServicesStayActive() throws Exception { RuntimeFixture active = runtime(1, 1D, 2D, 3D); diff --git a/core/src/test/java/art/arcane/iris/engine/ResolvedTerrainProviderTest.java b/core/src/test/java/art/arcane/iris/engine/ResolvedTerrainProviderTest.java new file mode 100644 index 000000000..00a1fdcf8 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/ResolvedTerrainProviderTest.java @@ -0,0 +1,423 @@ +package art.arcane.iris.engine; + +import art.arcane.iris.engine.framework.EngineMode; +import art.arcane.iris.engine.framework.EnginePlatformHooks; +import art.arcane.iris.engine.framework.EngineStage; +import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter; +import art.arcane.iris.engine.history.BoundaryColumnGeometry; +import art.arcane.iris.engine.history.TerrainBoundarySignatureStore; +import art.arcane.iris.engine.history.TransitionGenerationPlan; +import art.arcane.iris.engine.history.SavedTerrainChunk; +import art.arcane.iris.engine.history.TerrainBoundarySignature; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.volmlib.util.collection.KList; +import org.junit.Test; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doCallRealMethod; +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.when; + +public final class ResolvedTerrainProviderTest { + @Test + public void columnRequestsAreDeterministicAcrossOrderAndNegativeCoordinates() { + Fixture first = new Fixture(); + Fixture second = new Fixture(); + int[][] columns = {{0, 0}, {-1, -17}, {31, 16}, {7, 13}}; + List expected = new ArrayList<>(); + for (int[] column : columns) { + expected.add(first.provider.column(column[0], column[1])); + } + for (int index = columns.length - 1; index >= 0; index--) { + int[] coordinate = columns[index]; + assertColumnEquals(expected.get(index), second.provider.column(coordinate[0], coordinate[1])); + } + assertEquals(3, first.computations.size()); + assertEquals(3, second.computations.size()); + assertNull(IrisContext.get()); + } + + @Test + public void evictedChunksRegenerateTheSameGeometryAndBiomes() { + Fixture fixture = new Fixture(); + TerrainBoundarySignature original = fixture.provider.column(0, 0); + assertSame(original, fixture.provider.column(0, 0)); + for (int chunkX = 1; chunkX <= 64; chunkX++) { + fixture.provider.column(chunkX * 16, 0); + } + + TerrainBoundarySignature regenerated = fixture.provider.column(0, 0); + + assertColumnEquals(original, regenerated); + assertEquals(Integer.valueOf(2), fixture.computations.get("0,0")); + fixture.provider.clear(); + assertColumnEquals(original, fixture.provider.column(0, 0)); + assertEquals(Integer.valueOf(3), fixture.computations.get("0,0")); + } + + @Test + public void scalarHeightsSurviveGeometryEvictionForEveryColumnAndClearWithProvider() { + Fixture fixture = new Fixture(); + int expected = fixture.provider.height(-1, -17, true); + for (int chunkX = 1; chunkX <= 64; chunkX++) { + fixture.provider.column(chunkX * 16, 0); + } + for (int localX = 0; localX < 16; localX++) { + for (int localZ = 0; localZ < 16; localZ++) { + int x = -16 + localX; + int z = -32 + localZ; + int height = 2 + Math.floorMod(x + z, 5); + assertEquals(height, fixture.provider.height(x, z, true)); + assertEquals(height, fixture.provider.height(x, z, false)); + } + } + assertEquals(Integer.valueOf(1), fixture.computations.get("-1,-2")); + assertEquals(expected, fixture.provider.column(-1, -17).oceanFloorHeight()); + assertEquals(Integer.valueOf(2), fixture.computations.get("-1,-2")); + fixture.provider.clear(); + assertEquals(expected, fixture.provider.height(-1, -17, true)); + assertEquals(Integer.valueOf(3), fixture.computations.get("-1,-2")); + verify(fixture.router, never()).recordNaturalTerrain(any()); + } + + @Test + public void scalarHeightLookupHonorsNaturalContextAndTransitionBand() throws Exception { + Fixture fixture = new Fixture(); + TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class); + when(plan.hasTransitionAtChunk(-1, -2)).thenReturn(true); + Field transitionField = IrisComplex.class.getDeclaredField("transitionGenerationPlan"); + transitionField.setAccessible(true); + transitionField.set(fixture.complex, plan); + Field providerField = IrisComplex.class.getDeclaredField("resolvedTerrain"); + providerField.setAccessible(true); + providerField.set(fixture.complex, fixture.provider); + doCallRealMethod().when(fixture.complex).resolvedTerrainHeight(anyInt(), anyInt(), anyBoolean()); + doCallRealMethod().when(fixture.complex).isNaturalTerrainContext(); + ChunkContext context = new ChunkContext(-16, -32, fixture.complex, false, + ChunkContext.PrefillPlan.NONE, null); + + assertTrue(fixture.complex.resolvedTerrainHeight(0, 0, true).isEmpty()); + try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 5L, context)) { + assertTrue(fixture.complex.resolvedTerrainHeight(-1, -17, true).isEmpty()); + assertTrue(fixture.computations.isEmpty()); + context.beginContent(); + assertEquals(OptionalInt.of(4), fixture.complex.resolvedTerrainHeight(-1, -17, true)); + } + transitionField.set(fixture.complex, null); + assertTrue(fixture.complex.resolvedTerrainHeight(-1, -17, true).isEmpty()); + } + + @Test + public void speculativeQueriesRestoreTheCallerContextAndNeverRecordTerrain() { + Fixture fixture = new Fixture(); + ChunkContext outer = new ChunkContext(64, 64, fixture.complex, false, ChunkContext.PrefillPlan.NONE, null); + outer.beginContent(); + try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 73L, outer)) { + IrisContext caller = IrisContext.require(); + fixture.provider.column(-1, -1); + assertSame(caller, IrisContext.require()); + assertSame(outer, IrisContext.require().getChunkContext()); + assertEquals(73L, fixture.lastSessionId); + } + verify(fixture.router, never()).recordNaturalTerrain(any()); + assertNull(IrisContext.get()); + } + + @Test + public void neighborRawTerrainKeepsOneRuntimeScopeThroughPrefillAndRestoresCaller() throws Exception { + Fixture fixture = new Fixture(); + ChunkContext outer = new ChunkContext(64, 64, fixture.complex, false, ChunkContext.PrefillPlan.NONE, null); + outer.beginContent(); + AtomicBoolean scoped = new AtomicBoolean(); + GenerationHistoryRuntimeRouter.CoordinateScope runtimeScope = mock(GenerationHistoryRuntimeRouter.CoordinateScope.class); + when(fixture.engine.openGenerationHistoryCoordinateScope(-16, -16)).thenAnswer(invocation -> { + assertFalse(scoped.getAndSet(true)); + return runtimeScope; + }); + ProceduralStream height = fixture.complex.getRawHeightStream(); + when(fixture.complex.getRawHeightStream()).thenAnswer(invocation -> { + assertTrue(scoped.get()); + return height; + }); + doAnswer(invocation -> { + assertTrue(scoped.get()); + assertSame(outer, IrisContext.require().getChunkContext()); + scoped.set(false); + return null; + }).when(runtimeScope).close(); + doAnswer(invocation -> { + assertTrue(scoped.get()); + fixture.fill(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2), + invocation.getArgument(3), invocation.getArgument(5)); + return null; + }).when(fixture.mode).generateTerrain(anyInt(), anyInt(), any(), any(), anyBoolean(), any()); + + try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 73L, outer)) { + TerrainBoundarySignature result = fixture.provider.column(-1, -1); + assertSame(result, fixture.provider.column(-1, -1)); + assertSame(outer, IrisContext.require().getChunkContext()); + assertFalse(scoped.get()); + } + verify(fixture.engine, times(1)).openGenerationHistoryCoordinateScope(-16, -16); + verify(runtimeScope, times(1)).close(); + verify(fixture.router, never()).recordNaturalTerrain(any()); + } + + @Test + public void failedNeighborTerrainClosesItsRuntimeScopeAndRestoresCaller() throws Exception { + Fixture fixture = new Fixture(); + ChunkContext outer = new ChunkContext(64, 64, fixture.complex, false, ChunkContext.PrefillPlan.NONE, null); + GenerationHistoryRuntimeRouter.CoordinateScope runtimeScope = mock(GenerationHistoryRuntimeRouter.CoordinateScope.class); + when(fixture.engine.openGenerationHistoryCoordinateScope(16, 0)).thenReturn(runtimeScope); + IllegalStateException expected = new IllegalStateException("terrain failed"); + fixture.terrainOverride = (x, z, blocks, biomes, multicore, context) -> { + throw expected; + }; + + try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 73L, outer)) { + assertSame(expected, assertThrows(IllegalStateException.class, () -> fixture.provider.column(16, 0))); + assertSame(outer, IrisContext.require().getChunkContext()); + } + verify(runtimeScope, times(1)).close(); + assertNull(IrisContext.get()); + } + + @Test + public void actualGenerationRecordsNaturalTerrainBeforeContentMutatesBlocks() { + Fixture fixture = new Fixture(); + AtomicReference recorded = new AtomicReference<>(); + doAnswer(invocation -> { + assertEquals(List.of("terrain"), fixture.events); + assertTrue(IrisContext.require().getChunkContext().isNaturalTerrain()); + recorded.set(invocation.getArgument(0)); + fixture.events.add("receipt"); + return null; + }).when(fixture.router).recordNaturalTerrain(any()); + EngineStage content = (x, z, blocks, biomes, multicore, context) -> { + assertFalse(context.isNaturalTerrain()); + assertEquals(List.of("terrain", "receipt"), fixture.events); + assertNotNull(recorded.get()); + assertEquals("minecraft:stone", recorded.get().column(x, z).geometry().voxelAt(0).stateKey()); + blocks.setRaw(0, 0, 0, fixture.air); + fixture.events.add("content"); + }; + when(fixture.mode.getStages()).thenReturn(new KList<>(content)); + doCallRealMethod().when(fixture.mode).generate(anyInt(), anyInt(), any(), any(), anyBoolean(), anyLong()); + Hunk blocks = Hunk.newArrayHunk(16, 16, 16); + Hunk biomes = Hunk.newArrayHunk(16, 16, 16); + + fixture.mode.generate(0, 0, blocks, biomes, false, 41L); + + assertEquals(List.of("terrain", "receipt", "content"), fixture.events); + assertSame(fixture.air, blocks.getRaw(0, 0, 0)); + assertEquals("minecraft:stone", recorded.get().column(0, 0).geometry().voxelAt(0).stateKey()); + assertFalse(recorded.get().hasColumn(1, 1)); + assertNull(IrisContext.get()); + } + + @Test + public void naturalContextGuardAppliesOnlyToItsOwnComplexUntilContentBegins() { + Fixture fixture = new Fixture(); + IrisComplex other = mock(IrisComplex.class); + doCallRealMethod().when(fixture.complex).isNaturalTerrainContext(); + doCallRealMethod().when(other).isNaturalTerrainContext(); + ChunkContext context = new ChunkContext(0, 0, fixture.complex, false, ChunkContext.PrefillPlan.NONE, null); + assertFalse(fixture.complex.isNaturalTerrainContext()); + try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 5L, context)) { + assertTrue(fixture.complex.isNaturalTerrainContext()); + assertFalse(other.isNaturalTerrainContext()); + context.beginContent(); + assertFalse(fixture.complex.isNaturalTerrainContext()); + } + assertFalse(fixture.complex.isNaturalTerrainContext()); + } + + @Test + public void containmentReadsHistoricalFacesFromSavedColumnsWithoutGeneratingOldChunks() throws Exception { + Fixture fixture = fluidBoundary(); + TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class); + when(fixture.complex.getTransitionGenerationPlan().terrainSignatures()).thenReturn(snapshot); + SavedTerrainChunk saved = SavedTerrainChunk.capture(0, 0, 0, 16, "minecraft:noise", new SavedTerrainChunk.VoxelSource() { + @Override + public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) { + return new BoundaryColumnGeometry.Voxel("minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false); + } + + @Override + public String biome(int x, int y, int z) { + return "minecraft:plains"; + } + }); + when(snapshot.signatureAt(anyInt(), anyInt())).thenAnswer(invocation -> + Optional.of(saved.column(invocation.getArgument(0), invocation.getArgument(1)))); + + assertEquals(3, fixture.provider.height(16, 8, true)); + assertEquals(3, fixture.provider.height(16, 8, false)); + assertEquals(0, fixture.provider.height(17, 8, true)); + assertEquals(3, fixture.provider.height(17, 8, false)); + TerrainBoundarySignature bank = fixture.provider.column(16, 8); + + assertEquals("minecraft:stone", bank.geometry().voxelAt(2).stateKey()); + assertEquals("minecraft:water[level=0]", fixture.provider.column(17, 8).geometry().voxelAt(2).stateKey()); + assertFalse(fixture.computations.containsKey("0,0")); + verify(fixture.router, never()).recordNaturalTerrain(any()); + } + + @Test + public void containmentFailsIfAHistoricalFaceIsMissing() { + Fixture fixture = fluidBoundary(); + TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class); + when(fixture.complex.getTransitionGenerationPlan().terrainSignatures()).thenReturn(snapshot); + when(snapshot.signatureAt(anyInt(), anyInt())).thenReturn(Optional.empty()); + + IllegalStateException failure = assertThrows(IllegalStateException.class, () -> fixture.provider.column(16, 8)); + + assertTrue(failure.getCause() instanceof IOException); + assertFalse(fixture.computations.containsKey("0,0")); + verify(fixture.router, never()).recordNaturalTerrain(any()); + } + + private static Fixture fluidBoundary() { + Fixture fixture = new Fixture(); + TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class); + when(plan.hasTransitionAtChunk(anyInt(), anyInt())).thenAnswer(invocation -> (int) invocation.getArgument(0) == 1); + when(plan.isHistoricalBlock(anyInt(), anyInt())).thenAnswer(invocation -> (int) invocation.getArgument(0) < 16); + when(plan.newEpochWeightAt(anyInt(), anyInt())).thenReturn(0.5D); + when(fixture.complex.getTransitionGenerationPlan()).thenReturn(plan); + PlatformBlockState water = Fixture.block("minecraft:water[level=0]", false); + when(water.isFluid()).thenReturn(true); + fixture.terrainOverride = (x, z, blocks, biomes, multicore, context) -> { + for (int localX = 0; localX < 16; localX++) { + for (int localZ = 0; localZ < 16; localZ++) { + context.setTerrainHeight(localX, localZ, 0); + for (int y = 0; y < 16; y++) { + blocks.setRaw(localX, y, localZ, y == 0 ? fixture.stone : y < 4 ? water : fixture.air); + biomes.setRaw(localX, y, localZ, fixture.biome); + } + } + } + }; + return fixture; + } + + private static void assertColumnEquals(TerrainBoundarySignature expected, TerrainBoundarySignature actual) { + assertEquals(expected.column(), actual.column()); + assertEquals(expected.samples(), actual.samples()); + assertEquals(expected.geometry(), actual.geometry()); + } + + private static final class Fixture { + private final IrisEngine engine = mock(IrisEngine.class); + private final IrisComplex complex = mock(IrisComplex.class); + private final EngineMode mode = mock(EngineMode.class); + private final GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class); + private final PlatformBlockState air = block("minecraft:air", true); + private final PlatformBlockState stone = block("minecraft:stone", false); + private final PlatformBiome biome = mock(PlatformBiome.class); + private final Map computations = new HashMap<>(); + private final List events = new ArrayList<>(); + private final ResolvedTerrainProvider provider; + private long lastSessionId; + private EngineStage terrainOverride; + + @SuppressWarnings("unchecked") + private Fixture() { + when(engine.getComplex()).thenReturn(complex); + when(engine.getHeight()).thenReturn(16); + when(engine.getMinHeight()).thenReturn(0); + when(engine.getMode()).thenReturn(mode); + when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router)); + EnginePlatformHooks hooks = mock(EnginePlatformHooks.class); + when(engine.getPlatformHooks()).thenReturn(hooks); + when(mode.getEngine()).thenReturn(engine); + when(mode.getComplex()).thenReturn(complex); + when(biome.key()).thenReturn("minecraft:plains"); + ProceduralStream height = mock(ProceduralStream.class); + when(height.getDouble(anyDouble(), anyDouble())).thenReturn(3D); + ProceduralStream biomes = mock(ProceduralStream.class); + ProceduralStream regions = mock(ProceduralStream.class); + ProceduralStream materials = mock(ProceduralStream.class); + when(materials.get(anyDouble(), anyDouble())).thenReturn(stone); + when(complex.getRawHeightStream()).thenReturn(height); + when(complex.getTrueBiomeStream()).thenReturn(biomes); + when(complex.getCaveBiomeStream()).thenReturn(biomes); + when(complex.getRegionStream()).thenReturn(regions); + when(complex.getRockStream()).thenReturn(materials); + when(complex.getFluidStream()).thenReturn(materials); + provider = new ResolvedTerrainProvider(engine); + when(complex.getResolvedTerrain()).thenReturn(provider); + doAnswer(invocation -> { + int x = invocation.getArgument(0); + int z = invocation.getArgument(1); + Hunk blocks = invocation.getArgument(2); + Hunk physicalBiomes = invocation.getArgument(3); + ChunkContext context = invocation.getArgument(5); + fill(x, z, blocks, physicalBiomes, context); + return null; + }).when(mode).generateTerrain(anyInt(), anyInt(), any(), any(), anyBoolean(), any()); + } + + private void fill(int x, int z, Hunk blocks, + Hunk biomes, ChunkContext context) { + assertTrue(context.isNaturalTerrain()); + assertSame(context, IrisContext.require().getChunkContext()); + lastSessionId = IrisContext.require().getGenerationSessionId(); + computations.merge((x >> 4) + "," + (z >> 4), 1, Integer::sum); + events.add("terrain"); + if (terrainOverride != null) { + terrainOverride.generate(x, z, blocks, biomes, false, context); + return; + } + for (int localX = 0; localX < 16; localX++) { + for (int localZ = 0; localZ < 16; localZ++) { + int surface = 2 + Math.floorMod(x + localX + z + localZ, 5); + context.setTerrainHeight(localX, localZ, surface); + for (int y = 0; y < 16; y++) { + blocks.setRaw(localX, y, localZ, y <= surface ? stone : air); + biomes.setRaw(localX, y, localZ, biome); + } + } + } + } + + private static PlatformBlockState block(String key, boolean air) { + PlatformBlockState state = mock(PlatformBlockState.class); + when(state.key()).thenReturn(key); + when(state.isAir()).thenReturn(air); + return state; + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/StudioGenerationUpdateTest.java b/core/src/test/java/art/arcane/iris/engine/StudioGenerationUpdateTest.java new file mode 100644 index 000000000..efb36dbce --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/StudioGenerationUpdateTest.java @@ -0,0 +1,112 @@ +package art.arcane.iris.engine; + +import art.arcane.iris.engine.history.GenerationEpoch; +import art.arcane.iris.engine.history.GenerationHistory; +import art.arcane.iris.engine.history.GenerationHistoryPaths; +import art.arcane.iris.engine.history.GenerationPackFingerprint; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public final class StudioGenerationUpdateTest { + @Rule + public final TemporaryFolder temporary = new TemporaryFolder(); + + @Test + public void unchangedSourceNeedsNoSnapshotOrManifestUpdate() throws Exception { + Path source = temporary.newFolder("unchanged-pack").toPath(); + Files.createDirectories(source.resolve("dimensions")); + Files.writeString(source.resolve("dimensions/test.json"), "{}"); + String fingerprint = GenerationPackFingerprint.compute(source, GenerationPackFingerprint.CURRENT_VERSION); + IrisEngine engine = mock(IrisEngine.class); + GenerationHistory history = mock(GenerationHistory.class); + GenerationEpoch active = mock(GenerationEpoch.class); + when(engine.getStudioGenerationSource()).thenReturn(source); + when(history.activeEpoch()).thenReturn(active); + when(active.packFingerprintVersion()).thenReturn(GenerationPackFingerprint.CURRENT_VERSION); + when(active.packFingerprint()).thenReturn(fingerprint); + + assertNull(StudioGenerationUpdate.prepare(engine, history)); + + verify(history).activeEpoch(); + verifyNoMoreInteractions(history); + } + + @Test + public void invalidPackLeavesManifestUntouchedAndRemovesItsTemporarySnapshot() throws Exception { + Path source = temporary.newFolder("invalid-pack").toPath(); + Files.writeString(source.resolve("pack.json"), "{}"); + Path world = temporary.newFolder("invalid-world").toPath(); + GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(world); + Files.createDirectories(paths.generationRoot()); + Files.writeString(paths.manifest(), "unchanged manifest"); + IrisEngine engine = mock(IrisEngine.class); + GenerationHistory history = mock(GenerationHistory.class); + GenerationEpoch active = mock(GenerationEpoch.class); + when(engine.getStudioGenerationSource()).thenReturn(source); + when(history.activeEpoch()).thenReturn(active); + when(history.paths()).thenReturn(paths); + when(active.packFingerprintVersion()).thenReturn(GenerationPackFingerprint.CURRENT_VERSION); + when(active.packFingerprint()).thenReturn("0".repeat(64)); + + IOException failure = assertThrows(IOException.class, () -> StudioGenerationUpdate.prepare(engine, history)); + + assertTrue(failure.getMessage().contains("Missing dimensions/ folder")); + assertEquals("unchanged manifest", Files.readString(paths.manifest())); + try (Stream entries = Files.list(paths.generationRoot())) { + assertEquals(1L, entries.count()); + } + verify(history, times(2)).activeEpoch(); + verify(history).paths(); + verifyNoMoreInteractions(history); + } + + @Test + public void invalidNativeAnchorCannotStageOrPromoteStudioUpdate() throws Exception { + Path source = temporary.newFolder("invalid-anchor-pack").toPath(); + Files.createDirectories(source.resolve("dimensions")); + Files.writeString(source.resolve("dimensions/test.json"), """ + {"structures":[{"placementId":"village","anchor":"SURFACE", + "nativeStructures":[{"structure":"minecraft:village_plains"}]}]} + """); + GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension( + temporary.newFolder("invalid-anchor-world").toPath()); + Files.createDirectories(paths.generationRoot()); + Files.writeString(paths.manifest(), "unchanged manifest"); + IrisEngine engine = mock(IrisEngine.class); + GenerationHistory history = mock(GenerationHistory.class); + GenerationEpoch active = mock(GenerationEpoch.class); + when(engine.getStudioGenerationSource()).thenReturn(source); + when(history.activeEpoch()).thenReturn(active); + when(history.paths()).thenReturn(paths); + when(active.packFingerprintVersion()).thenReturn(GenerationPackFingerprint.CURRENT_VERSION); + when(active.packFingerprint()).thenReturn("0".repeat(64)); + + IOException failure = assertThrows(IOException.class, () -> StudioGenerationUpdate.prepare(engine, history)); + + assertTrue(failure.getMessage(), failure.getMessage().contains( + "Dimension 'test' structures[0].anchor must be omitted, null, or LEGACY for nativeStructures.")); + assertEquals("unchanged manifest", Files.readString(paths.manifest())); + try (Stream entries = Files.list(paths.generationRoot())) { + assertEquals(1L, entries.count()); + } + verify(history, times(2)).activeEpoch(); + verify(history).paths(); + verifyNoMoreInteractions(history); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/UpperDimensionTransitionTest.java b/core/src/test/java/art/arcane/iris/engine/UpperDimensionTransitionTest.java index 1def4757f..bc5b4ffee 100644 --- a/core/src/test/java/art/arcane/iris/engine/UpperDimensionTransitionTest.java +++ b/core/src/test/java/art/arcane/iris/engine/UpperDimensionTransitionTest.java @@ -1,17 +1,15 @@ package art.arcane.iris.engine; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.history.TerrainBoundarySignature; import art.arcane.iris.engine.history.TransitionGenerationPlan; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.util.project.stream.ProceduralStream; import org.junit.Test; -import java.util.List; -import java.util.OptionalInt; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -19,7 +17,7 @@ import static org.mockito.Mockito.when; public class UpperDimensionTransitionTest { @Test - public void gapLimitedTargetDoesNotCreateAnIntermediateCeilingDip() { + public void currentCeilingInputRespectsCurrentLowerTerrainGap() { IrisDimension dimension = mock(IrisDimension.class); when(dimension.getLoadKey()).thenReturn("main"); when(dimension.getUpperDimensionGap()).thenReturn(32); @@ -27,9 +25,6 @@ public class UpperDimensionTransitionTest { when(complex.getUnblendedNaturalHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 139D)); when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 75D + 4D * x)); TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class); - when(plan.terrainSampleAt(0, 8)).thenReturn(sample(75D, 0D)); - when(plan.terrainSampleAt(8, 8)).thenReturn(sample(75D, 0.5D)); - when(plan.terrainSampleAt(16, 8)).thenReturn(sample(75D, 1D)); when(complex.getTransitionGenerationPlan()).thenReturn(plan); Engine engine = mock(Engine.class); when(engine.getDimension()).thenReturn(dimension); @@ -38,13 +33,14 @@ public class UpperDimensionTransitionTest { UpperDimensionContext context = UpperDimensionContext.create(engine, dimension); - assertEquals(180, context.getEffectiveSurfaceY(0, 8)); - assertEquals(175, context.getEffectiveSurfaceY(8, 8)); + assertEquals(116, context.getEffectiveSurfaceY(0, 8)); + assertEquals(139, context.getEffectiveSurfaceY(8, 8)); assertEquals(171, context.getEffectiveSurfaceY(16, 8)); + verify(plan, never()).terrainSampleAt(anyInt(), anyInt()); } @Test - public void blendsFrozenEffectiveCeilingTowardTheUnblendedSelfReference() { + public void upperTerrainUsesUnblendedCurrentGeneratorBeforeGeometryReconciliation() { IrisDimension dimension = mock(IrisDimension.class); when(dimension.getLoadKey()).thenReturn("main"); when(dimension.getUpperDimensionGap()).thenReturn(32); @@ -52,7 +48,6 @@ public class UpperDimensionTransitionTest { when(complex.getUnblendedNaturalHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 80D)); when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 64D)); TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class); - when(plan.terrainSampleAt(-17, 8)).thenReturn(sample(40D, 0.5D)); when(complex.getTransitionGenerationPlan()).thenReturn(plan); Engine engine = mock(Engine.class); when(engine.getDimension()).thenReturn(dimension); @@ -61,7 +56,8 @@ public class UpperDimensionTransitionTest { UpperDimensionContext context = UpperDimensionContext.create(engine, dimension); - assertEquals(195, context.getEffectiveSurfaceY(-17, 8)); + assertEquals(175, context.getEffectiveSurfaceY(-17, 8)); + verify(plan, never()).terrainSampleAt(anyInt(), anyInt()); verify(complex, never()).getNaturalHeightStream(); verify(complex, never()).getNaturalTrueBiomeStream(); } @@ -69,10 +65,8 @@ public class UpperDimensionTransitionTest { @Test public void preservesBoundaryAndCurrentEndpointsAndFixedGap() { UpperDimensionContext.CeilingLayout layout = new UpperDimensionContext.CeilingLayout(384, 32); - double frozen = UpperDimensionContext.blendCeilingDepth(180D, sample(60D, 0D)); - double current = UpperDimensionContext.blendCeilingDepth(180D, sample(60D, 1D)); + double current = 180D; - assertEquals(323, UpperDimensionContext.effectiveSurfaceY(frozen, layout, 100)); assertEquals(203, UpperDimensionContext.effectiveSurfaceY(current, layout, 100)); assertEquals(252, UpperDimensionContext.effectiveSurfaceY(current, layout, 220)); assertEquals(188, -64 + UpperDimensionContext.effectiveSurfaceY(current, layout, 220)); @@ -80,16 +74,9 @@ public class UpperDimensionTransitionTest { } @Test - public void absentCeilingsRemainOutsideTheHunkAndAppearGradually() { + public void zeroDepthCeilingsRemainOutsideTheHunk() { UpperDimensionContext.CeilingLayout layout = new UpperDimensionContext.CeilingLayout(256, 32); - assertEquals(256, UpperDimensionContext.effectiveSurfaceY( - UpperDimensionContext.blendCeilingDepth(80D, sample(0D, 0D)), layout, 64)); - assertEquals(215, UpperDimensionContext.effectiveSurfaceY( - UpperDimensionContext.blendCeilingDepth(80D, sample(0D, 0.5D)), layout, 64)); - assertEquals(215, UpperDimensionContext.effectiveSurfaceY( - UpperDimensionContext.blendCeilingDepth(0D, sample(80D, 0.5D)), layout, 64)); - assertEquals(256, UpperDimensionContext.effectiveSurfaceY( - UpperDimensionContext.blendCeilingDepth(0D, sample(80D, 1D)), layout, 64)); + assertEquals(256, UpperDimensionContext.effectiveSurfaceY(0D, layout, 64)); assertEquals(256, UpperDimensionContext.effectiveSurfaceY(0.49D, layout, 64)); assertEquals(254, UpperDimensionContext.effectiveSurfaceY(0.51D, layout, 64)); } @@ -100,13 +87,4 @@ public class UpperDimensionTransitionTest { assertThrows(IllegalArgumentException.class, () -> new UpperDimensionContext.CeilingLayout(256, -1)); } - private static TransitionGenerationPlan.TerrainSample sample(double depth, double weight) { - TerrainBoundarySignature signature = new TerrainBoundarySignature( - new TerrainBoundarySignature.Column(15, 8, 64, 64, OptionalInt.empty(), - depth == 0D ? OptionalInt.empty() : OptionalInt.of((int) depth)), - new TerrainBoundarySignature.Samples( - new TerrainBoundarySignature.VerticalLayout(-64, 4, 1), - new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0}))); - return new TransitionGenerationPlan.TerrainSample(8D, weight, 0D, signature, 64D, 64D, depth); - } } diff --git a/core/src/test/java/art/arcane/iris/engine/actuator/IrisBiomeActuatorCoordinateTest.java b/core/src/test/java/art/arcane/iris/engine/actuator/IrisBiomeActuatorCoordinateTest.java index 5a016a09b..a4f2cd99a 100644 --- a/core/src/test/java/art/arcane/iris/engine/actuator/IrisBiomeActuatorCoordinateTest.java +++ b/core/src/test/java/art/arcane/iris/engine/actuator/IrisBiomeActuatorCoordinateTest.java @@ -37,6 +37,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; 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.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -47,6 +48,18 @@ public class IrisBiomeActuatorCoordinateTest { IrisPlatforms.unbind(); } + @Test + @SuppressWarnings("unchecked") + public void speculativeTerrainDoesNotPublishPersistentMetadata() { + Engine engine = mock(Engine.class); + ChunkContext context = mock(ChunkContext.class); + when(context.isSpeculativeTerrain()).thenReturn(true); + + IrisBiomeActuator.publishNaturalMetadata(engine, 0, 0, mock(Hunk.class), context); + + verifyNoMoreInteractions(engine); + } + @Test @SuppressWarnings("unchecked") public void derivativeScatterUsesEachWorldColumnCoordinate() { @@ -96,7 +109,7 @@ public class IrisBiomeActuatorCoordinateTest { @Test @SuppressWarnings("unchecked") - public void transitionColumnEmitsFrozenPhysicalKeyWithoutResolvingANewBiome() { + public void transitionColumnOverlaysFrozenPhysicalKeyAfterCurrentBiome() { bindPlatform(); IrisComplex complex = mock(IrisComplex.class); when(complex.allowsNewDiscreteContentAt(100, -200)).thenReturn(false); @@ -104,7 +117,7 @@ public class IrisBiomeActuatorCoordinateTest { TransitionGenerationPlan.TerrainSample terrainSample = mock(TransitionGenerationPlan.TerrainSample.class); when(complex.getTransitionGenerationPlan()).thenReturn(transitionPlan); when(transitionPlan.terrainSampleAt(100, -200)).thenReturn(terrainSample); - when(terrainSample.historicalPhysicalBiomeKeyAt(0)) + when(transitionPlan.historicalPhysicalBiomeKeyAt(100, 0, -200, terrainSample)) .thenReturn(Optional.of("iris:old-physical")); Engine engine = engine(complex); IrisBiomeActuator actuator = new IrisBiomeActuator(engine); @@ -112,16 +125,16 @@ public class IrisBiomeActuatorCoordinateTest { when(output.getWidth()).thenReturn(1); when(output.getDepth()).thenReturn(1); when(output.getHeight()).thenReturn(1); - ChunkedDataCache biomeCache = mock(ChunkedDataCache.class); + ChunkedDataCache biomeCache = currentBiomeCache(engine); ChunkContext context = mock(ChunkContext.class); when(context.getComplex()).thenReturn(complex); when(context.getBiome()).thenReturn(biomeCache); actuator.onActuate(100, -200, output, false, context); - verify(biomeCache, never()).get(anyInt(), anyInt()); + verify(biomeCache).get(0, 0); verify(IrisPlatforms.get().registries()).biome("iris:old-physical"); - verify(IrisPlatforms.get().biomeWriter(), never()).biomeIdFor(anyString()); + verify(IrisPlatforms.get().biomeWriter(), never()).biomeIdFor("iris:old-physical"); Mantle mantle = engine.getMantle().getMantle(); verify(mantle).set(eq(100), eq(0), eq(-200), argThat(value -> { MatterBiomeInject injection = (MatterBiomeInject) value; @@ -139,8 +152,8 @@ public class IrisBiomeActuatorCoordinateTest { TransitionGenerationPlan.TerrainSample terrainSample = mock(TransitionGenerationPlan.TerrainSample.class); when(complex.getTransitionGenerationPlan()).thenReturn(transitionPlan); when(transitionPlan.terrainSampleAt(100, -200)).thenReturn(terrainSample); - when(terrainSample.historicalPhysicalBiomeKeyAt(anyInt())).thenAnswer(invocation -> - Optional.of(invocation.getArgument(0) < -60 + when(transitionPlan.historicalPhysicalBiomeKeyAt(eq(100), anyInt(), eq(-200), same(terrainSample))).thenAnswer(invocation -> + Optional.of(invocation.getArgument(1) < -60 ? "iris:old-bottom" : "iris:old-top")); Engine engine = engine(complex); @@ -152,10 +165,14 @@ public class IrisBiomeActuatorCoordinateTest { when(output.getHeight()).thenReturn(8); ChunkContext context = mock(ChunkContext.class); when(context.getComplex()).thenReturn(complex); - when(context.getBiome()).thenReturn(mock(ChunkedDataCache.class)); + ChunkedDataCache biomeCache = currentBiomeCache(engine); + when(context.getBiome()).thenReturn(biomeCache); actuator.onActuate(100, -200, output, false, context); + verify(transitionPlan, times(1)).terrainSampleAt(100, -200); + verify(transitionPlan, never()).newEpochWeightAt(anyInt(), anyInt()); + verify(transitionPlan, never()).historicalPhysicalBiomeKeyAt(anyInt(), anyInt(), anyInt()); verify(output).set(eq(0), eq(0), eq(0), eq(0), eq(3), eq(0), any(PlatformBiome.class)); verify(output).set(eq(0), eq(4), eq(0), eq(0), eq(7), eq(0), any(PlatformBiome.class)); Mantle mantle = engine.getMantle().getMantle(); @@ -169,6 +186,16 @@ public class IrisBiomeActuatorCoordinateTest { })); } + @SuppressWarnings("unchecked") + private ChunkedDataCache currentBiomeCache(Engine engine) { + IrisBiome biome = mock(IrisBiome.class); + when(biome.getSkyBiomeKey(any(RNG.class), same(engine), anyDouble(), anyDouble(), anyDouble())) + .thenReturn("minecraft:plains"); + ChunkedDataCache cache = mock(ChunkedDataCache.class); + when(cache.get(anyInt(), anyInt())).thenReturn(biome); + return cache; + } + @SuppressWarnings("unchecked") private Engine engine(IrisComplex complex) { Mantle mantle = mock(Mantle.class); diff --git a/core/src/test/java/art/arcane/iris/engine/actuator/IrisDimensionStackActuatorMetadataTest.java b/core/src/test/java/art/arcane/iris/engine/actuator/IrisDimensionStackActuatorMetadataTest.java index 211bf3114..f8c089d7d 100644 --- a/core/src/test/java/art/arcane/iris/engine/actuator/IrisDimensionStackActuatorMetadataTest.java +++ b/core/src/test/java/art/arcane/iris/engine/actuator/IrisDimensionStackActuatorMetadataTest.java @@ -1,6 +1,17 @@ package art.arcane.iris.engine.actuator; import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell; +import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction; +import art.arcane.iris.engine.mantle.TerrainMatterView; +import art.arcane.iris.util.project.matter.IrisMatterSupport; +import art.arcane.iris.util.project.matter.slices.PreObjectMatterTest; +import art.arcane.volmlib.util.matter.IrisMatter; +import art.arcane.volmlib.util.matter.MatterCavern; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterSlice; @@ -12,6 +23,37 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class IrisDimensionStackActuatorMetadataTest { + @Test + @SuppressWarnings("unchecked") + public void stackCleanupPersistsNaturalCarveAndFluidFactsForLaterRerenders() throws IOException { + PreObjectMatterTest.setUpBukkit(); + IrisMatterSupport.ensureRegistered(); + Matter section = new IrisMatter(16, 16, 16); + MatterCavern cavern = new MatterCavern(true, "cave", (byte) 0); + HydrologyCaveCell hydrology = new HydrologyCaveCell(HydrologyCaveAction.WET_SOURCE, "river", "flooded"); + section.slice(MatterCavern.class).set(3, 4, 7, cavern); + section.slice(HydrologyCaveCell.class).set(3, 4, 7, hydrology); + MantleChunk chunk = mock(MantleChunk.class); + when(chunk.get(1)).thenReturn(section); + new IrisDimensionStackActuator.MetadataCleaner(chunk, 64).clear(3, 20, 7); + assertNull(section.getSlice(MatterCavern.class).get(3, 4, 7)); + assertNull(section.getSlice(HydrologyCaveCell.class).get(3, 4, 7)); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + section.write(bytes); + Matter restored = Matter.read(new ByteArrayInputStream(bytes.toByteArray())); + when(chunk.exists(1)).thenReturn(true); + when(chunk.get(1)).thenReturn(restored); + assertEquals(cavern, TerrainMatterView.get(chunk, 3, 20, 7, MatterCavern.class)); + assertEquals(hydrology, TerrainMatterView.get(chunk, 3, 20, 7, HydrologyCaveCell.class)); + } + + @Test + public void speculativeCleanupHasNoPersistentChunk() { + IrisDimensionStackActuator.MetadataCleaner cleaner = + new IrisDimensionStackActuator.MetadataCleaner(null, 64); + cleaner.clear(3, 20, 7); + } + @Test @SuppressWarnings("unchecked") public void overwrittenStackCellsClearHostHydrologyCaveOwnership() { diff --git a/core/src/test/java/art/arcane/iris/engine/actuator/IrisTransitionGeometryActuatorTest.java b/core/src/test/java/art/arcane/iris/engine/actuator/IrisTransitionGeometryActuatorTest.java new file mode 100644 index 000000000..feaabecb7 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/actuator/IrisTransitionGeometryActuatorTest.java @@ -0,0 +1,127 @@ +package art.arcane.iris.engine.actuator; + +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.history.BoundaryColumnGeometry; +import art.arcane.iris.engine.history.BoundaryGeometryInfluence; +import art.arcane.iris.engine.history.TransitionGenerationPlan; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.hunk.Hunk; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class IrisTransitionGeometryActuatorTest { + private IrisPlatform previous; + private PlatformRegistries registries; + private PlatformBlockState stone; + private PlatformBlockState air; + + @Before + public void bind() { + previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null; + IrisPlatforms.unbind(); + registries = mock(PlatformRegistries.class); + stone = state("minecraft:stone", false); + air = state("minecraft:air", true); + when(registries.air()).thenReturn(air); + when(registries.blockOrNull("minecraft:stone")).thenReturn(stone); + when(registries.blockOrNull("minecraft:air")).thenReturn(air); + IrisPlatform platform = mock(IrisPlatform.class); + when(platform.registries()).thenReturn(registries); + IrisPlatforms.bind(platform); + } + + @After + public void restore() { + IrisPlatforms.unbind(); + if (previous != null) { + IrisPlatforms.bind(previous); + } + } + + @Test + public void appliesCaveAndIslandGeometryThenUpdatesDecorationHeight() { + Hunk blocks = filled(); + ChunkContext context = context("minecraft:stone"); + Engine engine = mock(Engine.class); + + new IrisTransitionGeometryActuator(engine).generate(16, 0, blocks, + Hunk.newArrayHunk(16, 16, 16), false, context); + + assertSame(stone, blocks.getRaw(0, 0, 0)); + assertSame(air, blocks.getRaw(0, 1, 0)); + assertSame(air, blocks.getRaw(0, 2, 0)); + assertSame(stone, blocks.getRaw(0, 3, 0)); + assertSame(air, blocks.getRaw(0, 4, 0)); + verify(context).setTerrainHeight(0, 0, 3); + } + + @Test + public void refusesUnresolvableRetainedMaterialInsteadOfReplacingItWithAir() { + Engine engine = mock(Engine.class); + ChunkContext context = context("removed:stone"); + Hunk blocks = filled(); + Hunk biomes = Hunk.newArrayHunk(16, 16, 16); + + assertThrows(IllegalStateException.class, () -> new IrisTransitionGeometryActuator(engine) + .generate(16, 0, blocks, biomes, false, context)); + } + + private ChunkContext context(String material) { + ArrayList voxels = new ArrayList<>(16); + for (int y = 0; y < 16; y++) { + boolean solid = y == 0 || y == 3; + voxels.add(new BoundaryColumnGeometry.Voxel(solid ? material : "minecraft:air", + solid ? BoundaryColumnGeometry.Phase.SOLID : BoundaryColumnGeometry.Phase.AIR, "", false)); + } + BoundaryGeometryInfluence influence = new BoundaryGeometryInfluence(0D, 0D, + List.of(new BoundaryGeometryInfluence.Contribution(BoundaryColumnGeometry.fromVoxels(0, voxels), 1D))); + TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class); + when(plan.geometryAt(anyInt(), anyInt())).thenReturn(influence); + when(plan.hasTransitionAtChunk(anyInt(), anyInt())).thenReturn(true); + TransitionGenerationPlan.TerrainSample sample = mock(TransitionGenerationPlan.TerrainSample.class); + when(sample.historicalOceanFloorHeight()).thenReturn(3D); + when(plan.terrainSampleAt(anyInt(), anyInt())).thenReturn(sample); + IrisComplex complex = mock(IrisComplex.class); + when(complex.getTransitionGenerationPlan()).thenReturn(plan); + ChunkContext context = mock(ChunkContext.class); + when(context.getComplex()).thenReturn(complex); + return context; + } + + private Hunk filled() { + Hunk blocks = Hunk.newArrayHunk(16, 16, 16); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = 0; y < 16; y++) { + blocks.setRaw(x, y, z, stone); + } + } + } + return blocks; + } + + private static PlatformBlockState state(String key, boolean isAir) { + PlatformBlockState state = mock(PlatformBlockState.class); + when(state.key()).thenReturn(key); + when(state.isAir()).thenReturn(isAir); + when(state.isSolid()).thenReturn(!isAir); + return state; + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/decorator/IrisDecoratorCaveContextTest.java b/core/src/test/java/art/arcane/iris/engine/decorator/IrisDecoratorCaveContextTest.java index bb2d2d438..42b5bcd7b 100644 --- a/core/src/test/java/art/arcane/iris/engine/decorator/IrisDecoratorCaveContextTest.java +++ b/core/src/test/java/art/arcane/iris/engine/decorator/IrisDecoratorCaveContextTest.java @@ -2,6 +2,7 @@ package art.arcane.iris.engine.decorator; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.SeedManager; import art.arcane.iris.engine.object.InferredType; @@ -10,20 +11,45 @@ import art.arcane.iris.engine.object.IrisDecorationPart; import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformRegistries; import art.arcane.iris.util.project.hunk.Hunk; -import art.arcane.iris.util.project.stream.ProceduralStream; import org.junit.Test; +import org.junit.BeforeClass; +import org.junit.AfterClass; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; public class IrisDecoratorCaveContextTest { + @BeforeClass + public static void bindPlatform() { + IrisPlatforms.unbind(); + PlatformBlockState air = mock(PlatformBlockState.class); + doReturn(true).when(air).isAir(); + doReturn("minecraft:air").when(air).key(); + PlatformRegistries registries = mock(PlatformRegistries.class); + doReturn(air).when(registries).block(anyString()); + doReturn(air).when(registries).air(); + IrisPlatform platform = mock(IrisPlatform.class); + doReturn(registries).when(platform).registries(); + IrisPlatforms.bind(platform); + } + + @AfterClass + public static void unbindPlatform() { + IrisPlatforms.unbind(); + } + @Test @SuppressWarnings("unchecked") public void explicitCaveContextSkipsFluidWithoutMutatingBiome() { @@ -35,10 +61,10 @@ public class IrisDecoratorCaveContextTest { doReturn(63).when(dimension).getFluidHeight(); doReturn(dimension).when(engine).getDimension(); IrisComplex complex = mock(IrisComplex.class); - ProceduralStream fluidStream = mock(ProceduralStream.class); + EngineMantle mantle = mock(EngineMantle.class); doReturn(complex).when(engine).getComplex(); - doReturn(fluidStream).when(complex).getRiverWaterSurfaceStream(); - doReturn(63D).when(fluidStream).get(anyDouble(), anyDouble()); + doReturn(mantle).when(engine).getMantle(); + doReturn(63).when(mantle).getFluidHeight(anyInt(), anyInt()); IrisDecorator decorator = mock(IrisDecorator.class); doReturn(true).when(decorator).passesChanceGate(any(), anyDouble(), anyDouble(), any()); diff --git a/core/src/test/java/art/arcane/iris/engine/decorator/IrisShoreLineDecoratorTest.java b/core/src/test/java/art/arcane/iris/engine/decorator/IrisShoreLineDecoratorTest.java index 14ae48177..72b6a1049 100644 --- a/core/src/test/java/art/arcane/iris/engine/decorator/IrisShoreLineDecoratorTest.java +++ b/core/src/test/java/art/arcane/iris/engine/decorator/IrisShoreLineDecoratorTest.java @@ -2,6 +2,7 @@ package art.arcane.iris.engine.decorator; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.SeedManager; import art.arcane.iris.engine.object.IrisBiome; @@ -10,17 +11,25 @@ import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisSlopeClip; import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformRegistries; import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.project.stream.ProceduralStream; import org.bukkit.block.BlockSupport; import org.bukkit.block.data.BlockData; import org.junit.Test; +import org.junit.BeforeClass; +import org.junit.AfterClass; import static org.junit.Assert.assertSame; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -28,6 +37,25 @@ import static org.mockito.Mockito.when; public class IrisShoreLineDecoratorTest { private static final int FLUID_HEIGHT = 4; + @BeforeClass + public static void bindPlatform() { + IrisPlatforms.unbind(); + PlatformBlockState air = mock(PlatformBlockState.class); + doReturn(true).when(air).isAir(); + doReturn("minecraft:air").when(air).key(); + PlatformRegistries registries = mock(PlatformRegistries.class); + doReturn(air).when(registries).block(anyString()); + doReturn(air).when(registries).air(); + IrisPlatform platform = mock(IrisPlatform.class); + doReturn(registries).when(platform).registries(); + IrisPlatforms.bind(platform); + } + + @AfterClass + public static void unbindPlatform() { + IrisPlatforms.unbind(); + } + @Test public void carvedSurfaceRejectsShorelineDecoration() { Fixture fixture = createFixture(false); @@ -180,7 +208,7 @@ public class IrisShoreLineDecoratorTest { IrisSlopeClip slope = mock(IrisSlopeClip.class); PlatformBlockState decorant = mock(PlatformBlockState.class); ProceduralStream heightStream = mock(ProceduralStream.class); - ProceduralStream fluidStream = mock(ProceduralStream.class); + EngineMantle mantle = mock(EngineMantle.class); when(engine.getCacheID()).thenReturn(1); when(engine.getSeedManager()).thenReturn(seedManager); @@ -190,11 +218,11 @@ public class IrisShoreLineDecoratorTest { when(engine.getComplex()).thenReturn(complex); when(complex.getFluidHeight()).thenReturn((double) FLUID_HEIGHT); when(complex.getHeightStream()).thenReturn(heightStream); - when(complex.getRiverWaterSurfaceStream()).thenReturn(fluidStream); + when(engine.getMantle()).thenReturn(mantle); + when(mantle.getFluidHeight(anyInt(), anyInt())).thenReturn(FLUID_HEIGHT); when(heightStream.get(anyDouble(), anyDouble())).thenReturn( naturalShore ? (double) FLUID_HEIGHT - 1 : (double) FLUID_HEIGHT ); - when(fluidStream.get(anyDouble(), anyDouble())).thenReturn((double) FLUID_HEIGHT); when(engine.getData()).thenReturn(data); when(biome.getDecoratorBucket(IrisDecorationPart.SHORE_LINE)) .thenReturn(new IrisDecorator[]{decorator}); diff --git a/core/src/test/java/art/arcane/iris/engine/framework/EngineLifecycleTasksTest.java b/core/src/test/java/art/arcane/iris/engine/framework/EngineLifecycleTasksTest.java index 781cf0517..d63bdbb6d 100644 --- a/core/src/test/java/art/arcane/iris/engine/framework/EngineLifecycleTasksTest.java +++ b/core/src/test/java/art/arcane/iris/engine/framework/EngineLifecycleTasksTest.java @@ -4,14 +4,18 @@ import art.arcane.iris.util.project.context.IrisContext; import org.junit.Test; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.assertFalse; +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.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -20,8 +24,7 @@ public class EngineLifecycleTasksTest { public void activeLifecycleTaskHoldsGenerationDrainUntilCompletion() throws Exception { GenerationSessionManager manager = new GenerationSessionManager(); Engine engine = mock(Engine.class); - when(engine.acquireGenerationLease(anyString())) - .thenAnswer(invocation -> manager.acquire(invocation.getArgument(0))); + when(engine.getGenerationSessions()).thenReturn(manager); CountDownLatch entered = new CountDownLatch(1); CountDownLatch release = new CountDownLatch(1); AtomicReference taskFailure = new AtomicReference<>(); @@ -70,8 +73,7 @@ public class EngineLifecycleTasksTest { GenerationSessionManager manager = new GenerationSessionManager(); manager.sealAndAwait("hotload", 1_000L); Engine engine = mock(Engine.class); - when(engine.acquireGenerationLease(anyString())) - .thenAnswer(invocation -> manager.acquire(invocation.getArgument(0))); + when(engine.getGenerationSessions()).thenReturn(manager); when(engine.isClosing()).thenReturn(true); AtomicBoolean ran = new AtomicBoolean(); @@ -81,6 +83,58 @@ public class EngineLifecycleTasksTest { assertFalse(ran.get()); } + @Test + public void queuedLifecycleWorkCannotBlockTheOwnerThreadCheckpointDuringCutover() throws Exception { + GenerationSessionManager manager = new GenerationSessionManager(true); + Engine engine = mock(Engine.class); + when(engine.getGenerationSessions()).thenReturn(manager); + ExecutorService owner = Executors.newSingleThreadExecutor(); + ExecutorService reloader = Executors.newSingleThreadExecutor(); + AtomicBoolean lifecycleRan = new AtomicBoolean(); + AtomicBoolean checkpointRan = new AtomicBoolean(); + try { + CompletableFuture cutover = CompletableFuture.runAsync(() -> { + try (GenerationTransitionGate.Transition ignored = manager.transitionGate().beginTransition(2_000L)) { + owner.submit(() -> { + assertFalse(EngineLifecycleTasks.run(engine, "world_manager", () -> lifecycleRan.set(true))); + assertEquals("unavailable", EngineLifecycleTasks.call(engine, "manager_query", + () -> "unexpected", "unavailable")); + assertNull(IrisContext.get()); + }).get(1L, TimeUnit.SECONDS); + owner.submit(() -> checkpointRan.set(true)).get(1L, TimeUnit.SECONDS); + } catch (Exception failure) { + throw new IllegalStateException(failure); + } + }, reloader); + cutover.get(3L, TimeUnit.SECONDS); + assertFalse(lifecycleRan.get()); + assertTrue(checkpointRan.get()); + assertEquals(0, manager.activeLeases()); + assertTrue(owner.submit(() -> EngineLifecycleTasks.run(engine, "next_tick", + () -> lifecycleRan.set(true))).get(1L, TimeUnit.SECONDS)); + assertTrue(lifecycleRan.get()); + assertEquals(0, manager.activeLeases()); + } finally { + owner.shutdownNow(); + reloader.shutdownNow(); + assertTrue(owner.awaitTermination(2L, TimeUnit.SECONDS)); + assertTrue(reloader.awaitTermination(2L, TimeUnit.SECONDS)); + } + } + + @Test + public void nestedLifecycleWorkKeepsItsAlreadyAdmittedSession() throws Exception { + GenerationSessionManager manager = new GenerationSessionManager(true); + Engine engine = mock(Engine.class); + when(engine.getGenerationSessions()).thenReturn(manager); + try (GenerationSessionLease outer = manager.acquire("outer"); + IrisContext.Scope ignored = IrisContext.open(engine, outer.sessionId(), null)) { + assertTrue(EngineLifecycleTasks.run(engine, "nested", () -> assertEquals(2, manager.activeLeases()))); + assertEquals(1, manager.activeLeases()); + } + assertEquals(0, manager.activeLeases()); + } + private void waitForSeal(GenerationSessionManager manager) throws Exception { long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1L); while (System.nanoTime() < deadline) { diff --git a/core/src/test/java/art/arcane/iris/engine/framework/GenerationTransitionGateTest.java b/core/src/test/java/art/arcane/iris/engine/framework/GenerationTransitionGateTest.java new file mode 100644 index 000000000..4ef67dc18 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/framework/GenerationTransitionGateTest.java @@ -0,0 +1,255 @@ +package art.arcane.iris.engine.framework; + +import org.junit.Test; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +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.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public final class GenerationTransitionGateTest { + @Test + public void bothSessionAndRouteNestingOrdersDrainBeforeNewWorkEnters() throws Exception { + GenerationSessionManager sessions = new GenerationSessionManager(true); + GenerationTransitionGate gate = sessions.transitionGate(); + ExecutorService executor = Executors.newFixedThreadPool(2); + CountDownLatch requested = new CountDownLatch(1); + CountDownLatch acquired = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + GenerationSessionLease outer = sessions.acquire("manager"); + try { + CompletableFuture transition = CompletableFuture.runAsync(() -> { + requested.countDown(); + try (GenerationTransitionGate.Transition ignored = gate.beginTransition(5_000L)) { + acquired.countDown(); + await(release); + } + }, executor); + assertTrue(requested.await(5, TimeUnit.SECONDS)); + assertFalse(acquired.await(100, TimeUnit.MILLISECONDS)); + try (GenerationTransitionGate.Participation route = gate.enter(); + GenerationSessionLease nested = sessions.acquire("nested terrain")) { + assertTrue(nested.sessionId() == outer.sessionId()); + } + outer.close(); + assertTrue(acquired.await(5, TimeUnit.SECONDS)); + CompletableFuture fresh = CompletableFuture.supplyAsync(() -> { + try { + return sessions.acquire("fresh terrain"); + } catch (GenerationSessionException failure) { + throw new IllegalStateException(failure); + } + }, executor); + assertThrows(TimeoutException.class, () -> fresh.get(100, TimeUnit.MILLISECONDS)); + release.countDown(); + transition.get(5, TimeUnit.SECONDS); + fresh.get(5, TimeUnit.SECONDS).close(); + } finally { + outer.close(); + release.countDown(); + executor.shutdownNow(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } + + @Test + public void detachedWorkCanAttachOnAnotherThreadAndCloseAfterHandoff() throws Exception { + GenerationSessionManager sessions = new GenerationSessionManager(true); + GenerationTransitionGate gate = sessions.transitionGate(); + GenerationTransitionGate.Participation route = gate.enter(); + GenerationSessionLease lease = sessions.acquire("async pipeline"); + route.detachThread(); + lease.detachThread(); + IllegalStateException timedOut = assertThrows(IllegalStateException.class, () -> gate.beginTransition(20L)); + assertTrue(timedOut.getMessage().startsWith("Timed out")); + ExecutorService executor = Executors.newFixedThreadPool(2); + CountDownLatch requested = new CountDownLatch(1); + try { + CompletableFuture transition = CompletableFuture.runAsync(() -> { + requested.countDown(); + try (GenerationTransitionGate.Transition ignored = gate.beginTransition(5_000L)) { + } + }, executor); + assertTrue(requested.await(5, TimeUnit.SECONDS)); + CompletableFuture callback = CompletableFuture.runAsync(() -> { + try (GenerationTransitionGate.Participation attached = route.attach(); + GenerationSessionLease nested = sessions.acquire("async continuation")) { + assertTrue(nested.sessionId() == lease.sessionId()); + } catch (GenerationSessionException failure) { + throw new IllegalStateException(failure); + } finally { + lease.close(); + route.close(); + } + }, executor); + callback.get(5, TimeUnit.SECONDS); + transition.get(5, TimeUnit.SECONDS); + lease.detachThread(); + route.detachThread(); + try (GenerationTransitionGate.Transition ignored = gate.beginTransition(5_000L)) { + } + } finally { + lease.close(); + route.close(); + executor.shutdownNow(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } + + @Test + public void interruptedTransitionRestoresAdmission() throws Exception { + GenerationTransitionGate gate = new GenerationTransitionGate(true); + GenerationTransitionGate.Participation operation = gate.enter(); + CountDownLatch requested = new CountDownLatch(1); + AtomicReference failure = new AtomicReference<>(); + Thread worker = new Thread(() -> { + requested.countDown(); + try (GenerationTransitionGate.Transition ignored = gate.beginTransition(5_000L)) { + failure.set(new AssertionError("Transition entered while an operation was still active.")); + } catch (IllegalStateException expected) { + if (!Thread.currentThread().isInterrupted()) { + failure.set(new AssertionError("Interrupted status was lost.")); + } else { + failure.set(expected); + } + } + }); + try { + worker.start(); + assertTrue(requested.await(5, TimeUnit.SECONDS)); + worker.interrupt(); + worker.join(5_000L); + assertFalse(worker.isAlive()); + assertNotNull(failure.get()); + assertTrue(failure.get() instanceof IllegalStateException); + } finally { + operation.close(); + worker.interrupt(); + worker.join(5_000L); + } + try (GenerationTransitionGate.Transition ignored = gate.beginTransition(5_000L)) { + } + } + + @Test + public void productionGateReusesNoopParticipation() { + GenerationTransitionGate gate = new GenerationSessionManager().transitionGate(); + GenerationTransitionGate.Participation first = gate.enter(); + assertSame(first, gate.enter()); + assertSame(first, first.attach()); + first.detachThread(); + first.close(); + assertSame(first, gate.enter()); + assertThrows(IllegalStateException.class, () -> gate.beginTransition(100L)); + } + + @Test + public void interruptedManagerExitsWithoutRunningWhileTransitionRemainsExclusive() throws Exception { + GenerationSessionManager sessions = new GenerationSessionManager(true); + Engine engine = mock(Engine.class); + when(engine.isClosing()).thenReturn(true); + CountDownLatch requested = new CountDownLatch(1); + AtomicReference failure = new AtomicReference<>(); + AtomicBoolean taskRan = new AtomicBoolean(); + Thread manager = new Thread(() -> { + requested.countDown(); + try (GenerationSessionLease ignored = sessions.acquireForEngine(engine, "world manager loop")) { + taskRan.set(true); + } catch (GenerationSessionException expected) { + if (!expected.isExpectedTeardown() || !Thread.currentThread().isInterrupted()) { + failure.set(new AssertionError("Manager cancellation lost its teardown or interruption state.")); + } + } catch (Throwable unexpected) { + failure.set(unexpected); + } + }); + try (GenerationTransitionGate.Transition ignored = sessions.transitionGate().beginTransition(5_000L)) { + manager.start(); + assertTrue(requested.await(5, TimeUnit.SECONDS)); + manager.interrupt(); + manager.join(1_000L); + assertFalse("Manager must stop without waiting for the transition to finish", manager.isAlive()); + assertFalse(taskRan.get()); + assertNull(failure.get()); + assertEquals(0, sessions.activeLeases()); + } finally { + manager.interrupt(); + manager.join(5_000L); + } + try (GenerationSessionLease ignored = sessions.acquire("next runtime")) { + assertEquals(1, sessions.activeLeases()); + } + } + + @Test + public void nonblockingAdmissionRejectsPendingCutoverWithoutLeakingParticipation() throws Exception { + GenerationSessionManager sessions = new GenerationSessionManager(true); + Engine engine = mock(Engine.class); + GenerationSessionLease existing = sessions.acquire("existing"); + ExecutorService freshWorker = Executors.newSingleThreadExecutor(); + CountDownLatch transitioned = new CountDownLatch(1); + AtomicReference failure = new AtomicReference<>(); + Thread reloader = new Thread(() -> { + try (GenerationTransitionGate.Transition ignored = sessions.transitionGate().beginTransition(2_000L)) { + transitioned.countDown(); + } catch (Throwable error) { + failure.set(error); + } + }); + try { + reloader.start(); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1L); + while (reloader.getState() != Thread.State.TIMED_WAITING && System.nanoTime() < deadline) { + Thread.onSpinWait(); + } + assertEquals(Thread.State.TIMED_WAITING, reloader.getState()); + assertTrue(freshWorker.submit(() -> sessions.tryAcquireForEngine(engine, "new tick").isEmpty()) + .get(1L, TimeUnit.SECONDS)); + assertEquals(1, sessions.activeLeases()); + try (GenerationSessionLease nested = sessions.tryAcquireForEngine(engine, "existing nested").orElseThrow()) { + assertEquals(existing.sessionId(), nested.sessionId()); + } + existing.close(); + assertTrue(transitioned.await(1L, TimeUnit.SECONDS)); + reloader.join(1_000L); + assertNull(failure.get()); + assertEquals(0, sessions.activeLeases()); + Optional next = sessions.tryAcquireForEngine(engine, "next tick"); + assertTrue(next.isPresent()); + next.orElseThrow().close(); + assertEquals(0, sessions.activeLeases()); + } finally { + existing.close(); + reloader.interrupt(); + reloader.join(2_000L); + freshWorker.shutdownNow(); + assertTrue(freshWorker.awaitTermination(2L, TimeUnit.SECONDS)); + } + } + + private static void await(CountDownLatch latch) { + try { + if (!latch.await(5, TimeUnit.SECONDS)) { + throw new IllegalStateException("Timed out waiting for the test transition to finish."); + } + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(failure); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/history/ArchivedRegistryCompilerTest.java b/core/src/test/java/art/arcane/iris/engine/history/ArchivedRegistryCompilerTest.java new file mode 100644 index 000000000..fc15ed1dc --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/history/ArchivedRegistryCompilerTest.java @@ -0,0 +1,131 @@ +package art.arcane.iris.engine.history; + +import art.arcane.iris.core.IrisDatapackCompiler; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformGenerationRegistry; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.volmlib.util.collection.KList; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +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; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ArchivedRegistryCompilerTest { + @Rule + public final TemporaryFolder temporary = new TemporaryFolder(); + + @Test + public void rebuildsArchivedRegistryDefinitionsAndTagsWithoutTheOldPack() throws Exception { + IrisPlatform previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null; + IrisPlatforms.unbind(); + try { + Path firstPack = pack("old", "old_custom", "iris:old_tag"); + Path secondPack = pack("new", "new_custom", "iris:new_tag"); + Path world = temporary.newFolder("world").toPath(); + Path output = temporary.newFolder("output").toPath(); + Captured first = capture(firstPack); + Captured second = capture(secondPack); + assertFalse(first.registry().biomeTags().isEmpty()); + assertTrue(first.registry().biomeTags().values().stream().anyMatch(tags -> tags.contains("iris:old_tag"))); + GenerationHistory history = GenerationHistory.create(world, firstPack, first.fingerprint(), 77L, + first.dimension(), first.registry()); + String oldEpoch = history.activeEpoch().epochId(); + history.stageUpdate(secondPack, second.fingerprint(), second.dimension(), second.registry(), 256); + history.promotePending(List.of()); + new GenerationPackRepository(world).releaseArchivedPacks(history.manifest()); + assertFalse(Files.exists(history.paths().packRoot(oldEpoch))); + List packs = List.of(history.activePackRoot().toFile()); + + IrisDatapackCompiler.compile(packs, new KList<>(output.toFile()), List.of(), new DataFixerV1217(), false); + + for (String biome : first.registry().biomeTags().keySet()) { + int separator = biome.indexOf(':'); + Path biomeFile = output.resolve("data").resolve(biome.substring(0, separator)) + .resolve("worldgen/biome").resolve(biome.substring(separator + 1) + ".json"); + assertTrue(Files.isRegularFile(biomeFile)); + assertTrue(Files.readString(output.resolve("data/iris/tags/worldgen/biome/old_tag.json")).contains(biome)); + } + Map requirements = IrisDatapackCompiler.computeRegistryRequirements(packs, new DataFixerV1217()); + for (String biome : first.registry().biomeTags().keySet()) { + assertTrue(requirements.containsKey("worldgen/biome/" + biome)); + assertTrue(requirements.containsKey("worldgen/biome_tags/" + biome)); + } + assertEquals(history.manifest(), GenerationHistory.open(world).manifest()); + assertTrue(Files.isRegularFile(firstPack.resolve("biomes/test.json"))); + } finally { + if (previous != null) { + IrisPlatforms.bind(previous); + } + } + } + + private Captured capture(Path pack) throws Exception { + IrisData data = IrisData.openDatapackCompiler(pack.toFile()); + try { + IrisDimension dimension = data.getDimensionLoader().load("overworld"); + String fingerprint = GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION); + PlatformRegistries registries = mock(PlatformRegistries.class); + when(registries.blockTypeKeys()).thenReturn(List.of()); + when(registries.entityKeys()).thenReturn(List.of()); + GenerationRegistryContract contract = GenerationRegistryContractFactory.create(data, dimension, fingerprint, + new DataFixerV1217(), registries, new Registry()); + return new Captured(fingerprint, GenerationEpochContractFactory.create(dimension, "overworld", "iris:overworld"), contract); + } finally { + data.close(); + } + } + + private Path pack(String name, String custom, String tag) throws Exception { + Path pack = temporary.newFolder(name).toPath(); + Files.createDirectories(pack.resolve("dimensions")); + Files.createDirectories(pack.resolve("biomes")); + Files.writeString(pack.resolve("dimensions/overworld.json"), """ + {"name":"Test","environment":"NORMAL","logicalHeight":256,"dimensionHeight":{"min":-64,"max":320}} + """); + Files.writeString(pack.resolve("biomes/test.json"), """ + {"name":"Test","derivative":"minecraft:plains","vanillaDerivative":"minecraft:plains", + "customDerivitives":[{"id":"%s","tags":["%s"]}]} + """.formatted(custom, tag)); + return pack; + } + + private record Captured(String fingerprint, GenerationEpoch.DimensionContract dimension, GenerationRegistryContract registry) { + } + + private static final class Registry implements PlatformGenerationRegistry { + @Override + public String runtimeIdentity() { + return "bootstrap-test"; + } + + @Override + public String generatedDefinitionRendererIdentity() { + return "bootstrap-renderer-v1"; + } + + @Override + public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) { + return "iris:" + dimensionTypeKey; + } + + @Override + public Definition definition(String registryKey, String resourceKey) { + return Definition.resourceIdentity(registryKey, resourceKey); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/history/ArchivedSemanticQueryTest.java b/core/src/test/java/art/arcane/iris/engine/history/ArchivedSemanticQueryTest.java new file mode 100644 index 000000000..26dc62c80 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/history/ArchivedSemanticQueryTest.java @@ -0,0 +1,94 @@ +package art.arcane.iris.engine.history; + +import art.arcane.iris.core.nms.container.BlockPos; +import art.arcane.iris.core.nms.container.Pair; +import art.arcane.iris.engine.IrisEngine; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doCallRealMethod; +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 final class ArchivedSemanticQueryTest { + @Rule + public final TemporaryFolder temporary = new TemporaryFolder(); + + @Test + public void objectsAndPoiRemainQueryableAfterTheOldPackAndRuntimeAreGone() throws Exception { + Path world = temporary.newFolder("world").toPath(); + Path oldPack = pack("old"); + Path newPack = pack("new"); + GenerationEpoch.DimensionContract dimension = new GenerationEpoch.DimensionContract( + "overworld", "iris:overworld", "NORMAL", "OVERWORLD", 63, -64, 384, 384, 1D, + false, "none", 0, "0".repeat(64), + GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA, "c".repeat(64)); + GenerationHistory initial = GenerationHistory.create(world, oldPack, fingerprint(oldPack), 42L, + dimension, GenerationRegistryContract.empty()); + String oldEpoch = initial.activeEpoch().epochId(); + GenerationSemanticIndex index = GenerationSemanticIndex.loadRequired(world); + index.claimAndPersist(ChunkGenerationSemantics.builder(-2, 3, 1L) + .addObject("iris:removed_object") + .addPointOfInterest(new ChunkGenerationSemantics.PointOfInterest("buried_treasure", + new ChunkGenerationSemantics.BlockPosition(-30, 70, 51))) + .seal().build()); + Path regions = Files.createDirectories(world.resolve("region")); + SavedTerrainTestRegion.write(regions.resolve("r.-1.0.mca"), new int[][]{{-2, 3}}); + GenerationHistory updated = GenerationHistory.open(world); + updated.stageUpdate(newPack, fingerprint(newPack), dimension, GenerationRegistryContract.empty(), 32); + updated.prepareCurrentGenerator(32); + GenerationHistory archived = GenerationHistory.open(world); + assertFalse(Files.exists(archived.paths().packRoot(oldEpoch))); + IrisEngine engine = engine(archived); + + assertEquals(Set.of("iris:removed_object"), engine.getObjectsAt(-2, 3)); + assertEquals(Set.of(new Pair<>("buried_treasure", new BlockPos(-30, 70, 51))), engine.getPOIsAt(-2, 3)); + verify(engine, never()).getMantle(); + verify(engine, never()).openGenerationHistoryCoordinateScope(anyInt(), anyInt()); + } + + @Test + public void sealedEmptyRecordsDoNotFallBackToCurrentMantle() throws Exception { + GenerationHistory history = mock(GenerationHistory.class); + when(history.semantics(1, 2)).thenReturn(Optional.of(ChunkGenerationSemantics.builder(1, 2, 1L).seal().build())); + IrisEngine engine = engine(history); + + assertTrue(engine.getObjectsAt(1, 2).isEmpty()); + assertTrue(engine.getPOIsAt(1, 2).isEmpty()); + verify(engine, never()).getMantle(); + verify(engine, never()).openGenerationHistoryCoordinateScope(anyInt(), anyInt()); + } + + private static IrisEngine engine(GenerationHistory history) { + GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class); + when(router.history()).thenReturn(history); + IrisEngine engine = mock(IrisEngine.class); + when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router)); + doCallRealMethod().when(engine).getObjectsAt(anyInt(), anyInt()); + doCallRealMethod().when(engine).getPOIsAt(anyInt(), anyInt()); + return engine; + } + + private Path pack(String name) throws Exception { + Path pack = temporary.newFolder(name).toPath(); + Files.createDirectories(pack.resolve("dimensions")); + Files.writeString(pack.resolve("dimensions/overworld.json"), "{\"name\":\"" + name + "\"}"); + return pack; + } + + private static String fingerprint(Path pack) throws Exception { + return GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/history/BoundaryColumnGeometryTest.java b/core/src/test/java/art/arcane/iris/engine/history/BoundaryColumnGeometryTest.java new file mode 100644 index 000000000..11d925230 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/history/BoundaryColumnGeometryTest.java @@ -0,0 +1,168 @@ +package art.arcane.iris.engine.history; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.TreeSet; +import java.util.List; + +import static org.junit.Assert.assertArrayEquals; +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 BoundaryColumnGeometryTest { + private static final BoundaryColumnGeometry.Voxel STONE = new BoundaryColumnGeometry.Voxel( + "minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false); + private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel( + "minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false); + + @Test + public void preservesEveryVerticalRunIncludingCavesAndIslands() { + BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-4, + List.of(STONE, STONE, AIR, AIR, STONE, AIR)); + + assertArrayEquals(new int[]{2, 4, 5, 6}, geometry.runEnds()); + assertEquals(STONE, geometry.voxelAt(-4)); + assertEquals(AIR, geometry.voxelAt(-2)); + assertEquals(STONE, geometry.voxelAt(0)); + assertEquals(AIR, geometry.voxelAt(1)); + assertEquals(AIR, geometry.voxelAt(Integer.MAX_VALUE)); + assertArrayEquals(new double[]{1.5D, 0.5D, -0.5D, -0.5D, 0.5D, -0.5D}, + geometry.solidDistances(), 0D); + } + + @Test + public void groundReferenceSelectsSurfaceBelowUpperRoofAndAboveCaves() { + ArrayList voxels = new ArrayList<>(384); + for (int offset = 0; offset < 384; offset++) { + boolean solid = offset <= 50 && (offset < 10 || offset > 20) || offset >= 300; + voxels.add(solid ? STONE : AIR); + } + BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-64, voxels); + + assertEquals(50, geometry.surfaceOffsetNear(50D)); + assertEquals(50, geometry.surfaceOffsetNear(70D)); + } + + @Test + public void enclosedOpenQueriesIncludeUpperCavesAndExcludeOpenSky() { + BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-4, + List.of(STONE, AIR, STONE, AIR, STONE, AIR)); + + assertTrue(geometry.isEnclosedOpenAt(-3)); + assertTrue(geometry.isEnclosedOpenAt(-1)); + assertFalse(geometry.isEnclosedOpenAt(1)); + assertFalse(geometry.isEnclosedOpenAt(-4)); + assertFalse(geometry.isEnclosedOpenAt(-5)); + } + + @Test + public void preservesFluidAndProtectionWithoutRegistryObjects() { + BoundaryColumnGeometry.Voxel waterlogged = new BoundaryColumnGeometry.Voxel( + "minecraft:oak_slab[type=bottom,waterlogged=true]", BoundaryColumnGeometry.Phase.SOLID, + "minecraft:water[level=0]", true); + BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(3, List.of(waterlogged)); + + assertEquals(waterlogged, geometry.voxelAt(3)); + assertTrue(geometry.solidDistances()[0] < 0D); + assertThrows(IllegalArgumentException.class, () -> new BoundaryColumnGeometry.Voxel( + "minecraft:water", BoundaryColumnGeometry.Phase.FLUID, "", false)); + } + + @Test + public void repeatedVoxelOptimizationPreservesCanonicalPaletteAndRuns() { + List states = List.of(STONE, AIR, + new BoundaryColumnGeometry.Voxel("minecraft:cave_air", BoundaryColumnGeometry.Phase.AIR, "", false), + new BoundaryColumnGeometry.Voxel("minecraft:water[level=0]", BoundaryColumnGeometry.Phase.FLUID, + "minecraft:water[level=0]", false), + new BoundaryColumnGeometry.Voxel("minecraft:water[level=3]", BoundaryColumnGeometry.Phase.FLUID, + "minecraft:water[level=3]", false), + new BoundaryColumnGeometry.Voxel("minecraft:lava[level=0]", BoundaryColumnGeometry.Phase.FLUID, + "minecraft:lava[level=0]", false), + new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", true), + new BoundaryColumnGeometry.Voxel("minecraft:oak_slab[type=bottom,waterlogged=true]", + BoundaryColumnGeometry.Phase.SOLID, "minecraft:water[level=0]", true)); + assertCanonicalEncoding(List.of()); + for (BoundaryColumnGeometry.Voxel state : states) { + assertCanonicalEncoding(Collections.nCopies(768, state)); + } + Random random = new Random(42069); + for (int sample = 0; sample < 64; sample++) { + ArrayList voxels = new ArrayList<>(768); + while (voxels.size() < 768) { + BoundaryColumnGeometry.Voxel state = states.get(random.nextInt(states.size())); + int end = Math.min(768, voxels.size() + (sample % 2 == 0 ? 1 : 1 + random.nextInt(96))); + while (voxels.size() < end) { + voxels.add(new BoundaryColumnGeometry.Voxel(state.stateKey(), state.phase(), + state.fluidStateKey(), state.protectedContent())); + } + } + assertCanonicalEncoding(voxels); + } + } + + @Test + public void voxelFactoryKeepsNullHeightAndCoordinateValidation() { + assertThrows(NullPointerException.class, () -> BoundaryColumnGeometry.fromVoxels(0, null)); + assertThrows(NullPointerException.class, () -> BoundaryColumnGeometry.fromVoxels(0, + Arrays.asList(STONE, STONE, null))); + assertThrows(IllegalArgumentException.class, () -> BoundaryColumnGeometry.fromVoxels(0, + Collections.nCopies(BoundaryColumnGeometry.MAXIMUM_HEIGHT + 1, STONE))); + assertThrows(ArithmeticException.class, () -> BoundaryColumnGeometry.fromVoxels(Integer.MAX_VALUE, + List.of(STONE, STONE))); + } + + private static void assertCanonicalEncoding(List voxels) { + TreeSet used = new TreeSet<>(Comparator + .comparing(BoundaryColumnGeometry.Voxel::stateKey) + .thenComparing(BoundaryColumnGeometry.Voxel::phase) + .thenComparing(BoundaryColumnGeometry.Voxel::fluidStateKey) + .thenComparing(BoundaryColumnGeometry.Voxel::protectedContent)); + used.addAll(voxels); + List palette = List.copyOf(used); + Map indices = new HashMap<>(); + for (int index = 0; index < palette.size(); index++) { + indices.put(palette.get(index), (short) index); + } + int[] ends = new int[voxels.size()]; + short[] values = new short[voxels.size()]; + int runs = 0; + for (int offset = 0; offset < voxels.size(); offset++) { + short value = indices.get(voxels.get(offset)); + if (runs == 0 || values[runs - 1] != value) { + values[runs++] = value; + } + ends[runs - 1] = offset + 1; + } + BoundaryColumnGeometry actual = BoundaryColumnGeometry.fromVoxels(-256, voxels); + assertEquals(palette, actual.palette()); + assertArrayEquals(Arrays.copyOf(ends, runs), actual.runEnds()); + assertArrayEquals(Arrays.copyOf(values, runs), actual.paletteIndices()); + assertEquals(voxels, actual.voxels()); + } + + @Test + public void validatesAndOwnsCompactArrays() { + int[] ends = new int[]{2}; + short[] indices = new short[]{0}; + BoundaryColumnGeometry geometry = new BoundaryColumnGeometry(0, List.of(STONE), ends, indices); + ends[0] = 1; + indices[0] = 42; + geometry.runEnds()[0] = 19; + + assertEquals(2, geometry.height()); + assertEquals(STONE, geometry.voxelAt(1)); + assertThrows(IllegalArgumentException.class, () -> new BoundaryColumnGeometry(0, + List.of(STONE), new int[]{2, 1}, new short[]{0, 0})); + assertThrows(ArithmeticException.class, () -> new BoundaryColumnGeometry(Integer.MAX_VALUE, + List.of(STONE), new int[]{2}, new short[]{0})); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/history/ChunkGenerationSemanticsTest.java b/core/src/test/java/art/arcane/iris/engine/history/ChunkGenerationSemanticsTest.java index 86a4c1dfe..0f2692f82 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/ChunkGenerationSemanticsTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/ChunkGenerationSemanticsTest.java @@ -53,7 +53,7 @@ public final class ChunkGenerationSemanticsTest { ); assertThrows( IllegalArgumentException.class, - () -> ChunkGenerationSemantics.builder(0, 0, 1L).addSurfaceBiome("iris:Forest") + () -> ChunkGenerationSemantics.builder(0, 0, 1L).addSurfaceBiome("iris:For\nest") ); assertThrows( IllegalArgumentException.class, @@ -61,6 +61,26 @@ public final class ChunkGenerationSemanticsTest { ); } + @Test + public void packIdentifiersPreserveCaseAndFilenameCharacters() { + ChunkGenerationSemantics semantics = ChunkGenerationSemantics.builder(0, 0, 1L) + .addObject("trees/mixed/AmySmol10") + .addObject("trees/mixed/amysmol10") + .addSurfaceBiome("mountain/Cute_Cliffs+") + .addCaveBiome("Caves/Crystal Gallery") + .addRegion("Région/Highlands") + .addRiverProfile("Rivers/Cold+") + .addStructure("Structures/Tower+", 1, 64, 1) + .build(); + + assertEquals(Set.of("trees/mixed/AmySmol10", "trees/mixed/amysmol10"), semantics.objectKeys()); + assertEquals(Set.of("mountain/Cute_Cliffs+"), semantics.surfaceBiomeKeys()); + assertEquals(Set.of("Caves/Crystal Gallery"), semantics.caveBiomeKeys()); + assertEquals(Set.of("Région/Highlands"), semantics.regionKeys()); + assertEquals(Set.of("Rivers/Cold+"), semantics.riverProfileKeys()); + assertEquals("Structures/Tower+", semantics.structures().iterator().next().key()); + } + @Test public void equivalentBuilderOrderProducesEqualRecords() { ChunkGenerationSemantics first = ChunkGenerationSemantics.builder(2, 3, 4L) diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationBoundaryStoreTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationBoundaryStoreTest.java index b2b24c783..3e782eca2 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/GenerationBoundaryStoreTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationBoundaryStoreTest.java @@ -13,6 +13,8 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -22,6 +24,7 @@ import java.util.zip.CRC32; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -226,6 +229,73 @@ public class GenerationBoundaryStoreTest { assertTrue(boundary.cachedRegionCount() <= 64); } + @Test + public void absentRegionsDoNotWaitForTheMaskCacheMonitor() throws Exception { + GenerationBoundaryStore store = new GenerationBoundaryStore(temporaryFolder.newFolder("boundary-missing-region").toPath()); + store.publishPacked(1L, new long[]{GenerationBoundary.packChunk(0, 0)}); + GenerationBoundary boundary = store.load(1L); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + Future iteration = executor.submit(() -> { + boundary.forEachHistoricalChunk((x, z) -> { + entered.countDown(); + try { + release.await(); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted boundary iteration", failure); + } + }); + return null; + }); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + Future missing = executor.submit(() -> boundary.isHistoricalChunk(-33, 64)); + assertFalse(missing.get(5, TimeUnit.SECONDS)); + release.countDown(); + iteration.get(5, TimeUnit.SECONDS); + assertEquals(1, boundary.cachedRegionCount()); + } finally { + release.countDown(); + executor.shutdownNow(); + } + } + + @Test + public void knownRegionHitsKeepAccessOrderAndEvictedMasksAreRevalidated() throws Exception { + GenerationBoundaryStore store = new GenerationBoundaryStore(temporaryFolder.newFolder("boundary-cache-revalidation").toPath()); + long[] chunks = new long[65]; + for (int region = 0; region < chunks.length; region++) { + chunks[region] = GenerationBoundary.packChunk((region << 5) + (region & 31), region >>> 5); + } + store.publishPacked(1L, chunks); + GenerationBoundary boundary = store.load(1L); + for (int region = 0; region < 64; region++) { + assertTrue(boundary.isHistoricalChunk((region << 5) + (region & 31), region >>> 5)); + } + assertTrue(boundary.isHistoricalChunk(0, 0)); + assertTrue(boundary.isHistoricalChunk(64 << 5, 2)); + assertEquals(64, boundary.cachedRegionCount()); + int corrupted = 0; + try (Stream files = Files.list(store.directory())) { + for (Path file : files.filter(path -> path.getFileName().toString().endsWith(".irbm")).toList()) { + byte[] bytes = Files.readAllBytes(file); + if (bytes[8] == 1 || bytes[8] == 2) { + bytes[8] ^= 16; + Files.write(file, bytes); + corrupted++; + } + } + } + assertEquals(2, corrupted); + assertTrue(boundary.isHistoricalChunk(0, 0)); + IllegalStateException failure = assertThrows(IllegalStateException.class, + () -> boundary.isHistoricalChunk(33, 0)); + assertTrue(failure.getCause() instanceof IOException); + assertEquals(64, boundary.cachedRegionCount()); + } + private void assertStorageLinkRejected(String component) throws Exception { Path container = temporaryFolder.newFolder("boundary-linked-" + component).toPath(); Path dimensionRoot = container.resolve("dimension"); diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationBuildRevisionTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationBuildRevisionTest.java new file mode 100644 index 000000000..6703106c0 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationBuildRevisionTest.java @@ -0,0 +1,56 @@ +package art.arcane.iris.engine.history; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; + +public class GenerationBuildRevisionTest { + @Test + public void fingerprintUsesTheCompleteManifest() throws Exception { + byte[] initial = revision("a".repeat(64)); + assertEquals(GenerationBuildRevision.fingerprint(1, "example.KernelV1", initial), + GenerationBuildRevision.fingerprint(1, "example.KernelV1", initial)); + assertNotEquals(GenerationBuildRevision.fingerprint(1, "example.KernelV1", initial), + GenerationBuildRevision.fingerprint(1, "example.KernelV1", revision("b".repeat(64)))); + } + + @Test + public void rejectsMismatchedAbiFactoryAndMalformedHashes() { + assertThrows(IOException.class, + () -> GenerationBuildRevision.fingerprint(2, "example.KernelV1", revision("a".repeat(64)))); + assertThrows(IOException.class, + () -> GenerationBuildRevision.fingerprint(1, "example.OtherKernel", revision("a".repeat(64)))); + assertThrows(IOException.class, + () -> GenerationBuildRevision.fingerprint(1, "example.KernelV1", revision("not-a-hash"))); + assertThrows(IOException.class, + () -> GenerationBuildRevision.fingerprint(1, "example.KernelV1", new byte[0])); + } + + @Test + public void rejectsDuplicateAndUnsortedEntries() { + String source = new String(revision("a".repeat(64)), StandardCharsets.UTF_8); + String duplicate = source + "dependency\tmath\t" + "a".repeat(64) + "\n"; + assertThrows(IOException.class, () -> GenerationBuildRevision.fingerprint(1, + "example.KernelV1", duplicate.getBytes(StandardCharsets.UTF_8))); + String unordered = source + "scope\tother\n"; + assertThrows(IOException.class, () -> GenerationBuildRevision.fingerprint(1, + "example.KernelV1", unordered.getBytes(StandardCharsets.UTF_8))); + } + + @Test + public void standardKernelUsesThePackagedBuildRevision() { + assertEquals(GenerationBuildRevision.requireFingerprint(1, GenerationKernelV1.class), + GenerationKernelV1.IMPLEMENTATION_FINGERPRINT); + } + + private static byte[] revision(String fingerprint) { + return ("iris-generation-build-revision-v1\nabi\t1\nfactory\texample.KernelV1\n" + + "algorithm\t1\t1\nscope\tgeneration\nsource\tgeneration/Noise.java\t" + fingerprint + + "\ndependency\tmath\t" + "c".repeat(64) + "\n").getBytes(StandardCharsets.UTF_8); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationEpochContractFactoryTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationEpochContractFactoryTest.java index e5c7a91ad..ecc86f347 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/GenerationEpochContractFactoryTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationEpochContractFactoryTest.java @@ -117,7 +117,7 @@ public class GenerationEpochContractFactoryTest { assertNotEquals(first, changedPack); assertEquals(true, first.hasSameLayout(changedPack)); dimension.setUpperDimensionGap(33); - assertEquals(false, first.hasSameLayout(GenerationEpochContractFactory.create( + assertEquals(true, first.hasSameLayout(GenerationEpochContractFactory.create( dimension, "overworld", "iris:overworld"))); when(loader.load("ceiling", false)).thenReturn(null); diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationFindCatalogTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationFindCatalogTest.java index 515b553d2..4e4de0a90 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/GenerationFindCatalogTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationFindCatalogTest.java @@ -3,34 +3,28 @@ package art.arcane.iris.engine.history; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.ResourceLoader; import art.arcane.iris.engine.IrisEngine; -import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisObject; -import art.arcane.iris.engine.object.IrisObjectPlacement; import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.engine.object.IrisStructurePlacement; -import art.arcane.iris.util.common.director.specialhandlers.LocatableObjectHandler; -import art.arcane.iris.util.common.director.specialhandlers.ReachableBiomeHandler; -import art.arcane.iris.util.common.director.specialhandlers.ReachableRegionHandler; import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; import org.junit.Test; -import org.mockito.MockedStatic; import java.io.IOException; import java.io.UncheckedIOException; -import java.nio.file.Path; import java.util.Optional; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertSame; 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.anyLong; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; 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; @@ -38,246 +32,111 @@ import static org.mockito.Mockito.when; public class GenerationFindCatalogTest { @Test - public void parsesRemovedNamesFromRetainedPackWithoutBuildingRuntime() throws Exception { + public void findsRecordedNamesWithoutLoadingArchivedPacksOrRuntimes() throws Exception { Fixture fixture = new Fixture(); - try (MockedStatic data = mockStatic(IrisData.class)) { - data.when(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile())).thenReturn(fixture.oldData); - - assertSame(fixture.meadow, fixture.biomeHandler().parse("meadow", false)); - assertSame(fixture.oldRegion, fixture.regionHandler().parse("old-region", false)); - assertEquals("old-tree", fixture.objectHandler().parse("old-tree", false)); - assertTrue(GenerationFindCatalog.hasObjectPlacement(fixture.engine, "/OLD-TREE.iob")); - assertFalse(GenerationFindCatalog.hasObjectPlacement(fixture.engine, "unplaced")); - assertEquals("old meadow", GenerationFindCatalog.biome(fixture.engine, "meadow").getName()); - - data.verify(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile()), times(1)); - data.verifyNoMoreInteractions(); - verify(fixture.oldData).close(); - verify(fixture.history, never()).packRoot(2L); - verify(fixture.engine, never()).getActiveGenerationRuntimeBinding(); - } + assertEquals("meadow", GenerationFindCatalog.biome(fixture.engine, "MEADOW").getLoadKey()); + assertEquals("old-region", GenerationFindCatalog.region(fixture.engine, "old-region").getLoadKey()); + assertTrue(GenerationFindCatalog.hasObjectPlacement(fixture.engine, "/OLD-TREE.iob")); + assertTrue(GenerationFindCatalog.hasRetainedStructurePlacement(fixture.engine, "old-house")); + assertTrue(GenerationFindCatalog.objectKeys(fixture.engine).contains("new-tree")); + assertFalse(GenerationFindCatalog.hasObjectPlacement(fixture.engine, "unplaced")); + assertNull(GenerationFindCatalog.biome(fixture.engine, "unrecorded")); + verify(fixture.history, times(1)).forEachRecordedSemantic(any()); + verify(fixture.history, never()).packRoot(anyLong()); + verify(fixture.engine, never()).getActiveGenerationRuntimeBinding(); } @Test - public void activeDefinitionsWinHistoricalKeyCollisions() throws Exception { + public void retainedPackKeysKeepCaseAndDistinctCaseVariants() throws Exception { Fixture fixture = new Fixture(); - IrisBiome activeMeadow = biome("meadow", "new meadow"); - IrisRegion activeRegion = region("old-region"); - when(fixture.engine.getAllBiomes()).thenReturn(new KList<>(activeMeadow)); - when(fixture.activeDimension.getAllRegions(fixture.engine)).thenReturn(new KList<>(activeRegion)); - try (MockedStatic data = mockStatic(IrisData.class)) { - data.when(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile())).thenReturn(fixture.oldData); + doAnswer(invocation -> { + GenerationSemanticIndex.RecordConsumer consumer = invocation.getArgument(0); + consumer.accept(ChunkGenerationSemantics.builder(0, 0, 1L) + .addSurfaceBiome("mountain/Cute_Cliffs+") + .addSurfaceBiome("mountain/cute_cliffs+") + .addRegion("Regions/Highlands") + .addObject("trees/mixed/AmySmol10") + .addObject("trees/mixed/amysmol10") + .addStructure("Structures/Tower+", 8, 70, 8) + .seal().build()); + return null; + }).when(fixture.history).forEachRecordedSemantic(any()); - assertSame(activeMeadow, fixture.biomeHandler().parse("MEADOW", false)); - assertSame(activeRegion, fixture.regionHandler().parse("OLD-REGION", false)); - assertEquals(1, GenerationFindCatalog.biomes(fixture.engine).size()); - assertEquals(1, GenerationFindCatalog.regions(fixture.engine).size()); - } + assertEquals("mountain/Cute_Cliffs+", + GenerationFindCatalog.biome(fixture.engine, "mountain/Cute_Cliffs+").getLoadKey()); + assertEquals("mountain/cute_cliffs+", + GenerationFindCatalog.biome(fixture.engine, "mountain/cute_cliffs+").getLoadKey()); + assertEquals(2, GenerationFindCatalog.biomes(fixture.engine).size()); + assertEquals("Regions/Highlands", + GenerationFindCatalog.region(fixture.engine, "Regions/Highlands").getLoadKey()); + assertTrue(GenerationFindCatalog.objectKeys(fixture.engine).contains("trees/mixed/AmySmol10")); + assertTrue(GenerationFindCatalog.objectKeys(fixture.engine).contains("trees/mixed/amysmol10")); + assertTrue(GenerationFindCatalog.hasObjectPlacement(fixture.engine, "trees/mixed/AmySmol10")); + assertTrue(GenerationFindCatalog.retainedStructureKeys(fixture.engine).contains("Structures/Tower+")); } @Test - public void unrelatedAuthoringAndUnreachableFilesAreNotFallbackCandidates() throws Exception { + public void activeDefinitionsWinRecordedKeyCollisions() throws Exception { Fixture fixture = new Fixture(); - try (MockedStatic data = mockStatic(IrisData.class)) { - data.when(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile())).thenReturn(fixture.oldData); - - assertThrows(DirectorParsingException.class, () -> fixture.biomeHandler().parse("unused", false)); - assertThrows(DirectorParsingException.class, () -> fixture.regionHandler().parse("unused", false)); - assertThrows(DirectorParsingException.class, () -> fixture.objectHandler().parse("unplaced", false)); - verify(fixture.biomeLoader, never()).load("unused"); - data.verify(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile())); - data.verifyNoMoreInteractions(); - } + IrisBiome active = new IrisBiome().setName("New Meadow"); + active.setLoadKey("meadow"); + when(fixture.engine.getAllBiomes()).thenReturn(new KList<>(active)); + assertSame(active, GenerationFindCatalog.biome(fixture.engine, "meadow")); + assertEquals(1, GenerationFindCatalog.biomes(fixture.engine).size()); } @Test - public void missingRetainedPackFailsInsteadOfSearchingAuthoringPacks() throws Exception { + public void doesNotTreatUnsealedOrCurrentClaimsAsHistoricalDefinitions() throws Exception { Fixture fixture = new Fixture(); - IOException missingPack = new IOException("missing immutable pack"); - when(fixture.history.packRoot(1L)).thenThrow(missingPack); - - UncheckedIOException failure = assertThrows(UncheckedIOException.class, - () -> GenerationFindCatalog.biomes(fixture.engine)); - - assertSame(missingPack, failure.getCause()); + doAnswer(invocation -> { + GenerationSemanticIndex.RecordConsumer consumer = invocation.getArgument(0); + consumer.accept(ChunkGenerationSemantics.builder(0, 0, 1L).addSurfaceBiome("unsealed").build()); + consumer.accept(ChunkGenerationSemantics.builder(1, 0, 2L).addSurfaceBiome("current").seal().build()); + return null; + }).when(fixture.history).forEachRecordedSemantic(any()); + assertNull(GenerationFindCatalog.biome(fixture.engine, "unsealed")); + assertNull(GenerationFindCatalog.biome(fixture.engine, "current")); } @Test - public void retainsEditableStructureKeysFromEveryOwningPlacementScope() throws Exception { + public void reportsUnreadableRecordedSemantics() throws Exception { Fixture fixture = new Fixture(); - fixture.oldDimension.setStructures(new KList<>(structure("old-house"))); - fixture.oldRegion.setStructures(new KList<>(structure("old-ruin"))); - fixture.meadow.setStructures(new KList<>(structure("old-tower"))); - try (MockedStatic data = mockStatic(IrisData.class)) { - data.when(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile())).thenReturn(fixture.oldData); - - assertTrue(GenerationFindCatalog.hasRetainedStructurePlacement(fixture.engine, "OLD-HOUSE")); - assertTrue(GenerationFindCatalog.hasRetainedStructurePlacement(fixture.engine, "old-ruin")); - assertTrue(GenerationFindCatalog.hasRetainedStructurePlacement(fixture.engine, "old-tower")); - assertFalse(GenerationFindCatalog.hasRetainedStructurePlacement(fixture.engine, "unused")); - assertEquals(3, GenerationFindCatalog.retainedStructureKeys(fixture.engine).size()); - } - } - - @Test - public void parsesRemovedObjectsFromTheSeparateRetainedUpperDimension() throws Exception { - Fixture fixture = new Fixture(); - fixture.oldDimension.setUpperDimension("ceiling"); - IrisDimension upper = new IrisDimension().setRegions(new KList<>("sky-region")); - IrisRegion skyRegion = region("sky-region").setLandBiomes(new KList<>("sky-biome")); - IrisBiome skyBiome = biome("sky-biome", "ceiling biome"); - skyRegion.setObjects(new KList<>(object("upper-region-object"))); - skyBiome.setObjects(new KList<>(object("upper-biome-object"))); - when(fixture.dimensionLoader.load("ceiling", false)).thenReturn(upper); - when(fixture.regionLoader.load("sky-region")).thenReturn(skyRegion); - when(fixture.biomeLoader.load("sky-biome")).thenReturn(skyBiome); - try (MockedStatic data = mockStatic(IrisData.class)) { - data.when(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile())).thenReturn(fixture.oldData); - - assertEquals("upper-biome-object", fixture.objectHandler().parse("upper-biome-object", false)); - assertEquals("upper-region-object", fixture.objectHandler().parse("upper-region-object", false)); - assertTrue(GenerationFindCatalog.hasObjectPlacement(fixture.engine, "upper-biome-object")); - assertTrue(GenerationFindCatalog.hasObjectPlacement(fixture.engine, "upper-region-object")); - assertNull(GenerationFindCatalog.biome(fixture.engine, "sky-biome")); - assertNull(GenerationFindCatalog.region(fixture.engine, "sky-region")); - data.verify(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile())); - data.verifyNoMoreInteractions(); - verify(fixture.oldData).close(); - } - } - - @Test - public void missingRetainedUpperDimensionDoesNotFallBackToAnotherPack() throws Exception { - Fixture fixture = new Fixture(); - fixture.oldDimension.setUpperDimension("missing-upper"); - try (MockedStatic data = mockStatic(IrisData.class)) { - data.when(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile())).thenReturn(fixture.oldData); - - assertThrows(IllegalStateException.class, () -> GenerationFindCatalog.objectKeys(fixture.engine)); - data.verify(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile())); - data.verifyNoMoreInteractions(); - verify(fixture.oldData).close(); - } - } - - private static IrisStructurePlacement structure(String key) { - return new IrisStructurePlacement().setStructures(new KList<>(key)); - } - - private static IrisObjectPlacement object(String key) { - return new IrisObjectPlacement().setPlace(new KList<>(key)); - } - - private static IrisBiome biome(String key, String name) { - IrisBiome biome = new IrisBiome().setName(name); - biome.setLoadKey(key); - return biome; - } - - private static IrisRegion region(String key) { - IrisRegion region = new IrisRegion(); - region.setLoadKey(key); - return region; + IOException failure = new IOException("unreadable semantic index"); + doThrow(failure).when(fixture.history).forEachRecordedSemantic(any()); + assertSame(failure, assertThrows(UncheckedIOException.class, + () -> GenerationFindCatalog.biomes(fixture.engine)).getCause()); } private static final class Fixture { private final IrisEngine engine = mock(IrisEngine.class); private final GenerationHistory history = mock(GenerationHistory.class); - private final IrisDimension activeDimension = mock(IrisDimension.class); - private final IrisData oldData = mock(IrisData.class); - private final ResourceLoader dimensionLoader; - private final ResourceLoader regionLoader; - private final ResourceLoader biomeLoader; - private final IrisDimension oldDimension = new IrisDimension().setRegions(new KList<>("old-region")); - private final Path oldPack = Path.of("retained-test-pack").toAbsolutePath(); - private final IrisBiome meadow = biome("meadow", "old meadow"); - private final IrisRegion oldRegion = region("old-region"); @SuppressWarnings("unchecked") private Fixture() throws IOException { GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class); GenerationManifest manifest = mock(GenerationManifest.class); GenerationActivation active = mock(GenerationActivation.class); - GenerationActivation old = mock(GenerationActivation.class); - GenerationEpoch epoch = mock(GenerationEpoch.class); - GenerationEpoch.DimensionContract contract = mock(GenerationEpoch.DimensionContract.class); when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router)); when(router.history()).thenReturn(history); when(history.manifest()).thenReturn(manifest); when(manifest.activeActivation()).thenReturn(active); when(active.activationId()).thenReturn(2L); - when(active.parentActivationId()).thenReturn(1L); - when(manifest.activation(1L)).thenReturn(Optional.of(old)); - when(old.epochId()).thenReturn("old"); - when(old.parentActivationId()).thenReturn(null); - when(manifest.epoch("old")).thenReturn(Optional.of(epoch)); - when(epoch.dimensionContract()).thenReturn(contract); - when(epoch.packFingerprint()).thenReturn("old-pack"); - when(contract.dimensionKey()).thenReturn("world"); - when(history.packRoot(1L)).thenReturn(oldPack); - - IrisData activeData = mock(IrisData.class); - ResourceLoader objectLoader = mock(ResourceLoader.class); - when(engine.getData()).thenReturn(activeData); - when(activeData.getObjectLoader()).thenReturn(objectLoader); - when(objectLoader.getPossibleKeys()).thenReturn(new String[]{"new-tree"}); - when(engine.getDimension()).thenReturn(activeDimension); - when(engine.getAllBiomes()).thenReturn(new KList<>(biome("plateau", "new plateau"))); - when(activeDimension.getAllRegions(engine)).thenReturn(new KList<>(region("new-region"))); - - dimensionLoader = mock(ResourceLoader.class); - regionLoader = mock(ResourceLoader.class); - biomeLoader = mock(ResourceLoader.class); - oldRegion.setLandBiomes(new KList<>("meadow")); - meadow.setObjects(new KList<>(new IrisObjectPlacement().setPlace(new KList<>("old-tree")))); - when(oldData.getDimensionLoader()).thenReturn(dimensionLoader); - when(oldData.getRegionLoader()).thenReturn(regionLoader); - when(oldData.getBiomeLoader()).thenReturn(biomeLoader); - when(dimensionLoader.load("world", false)).thenReturn(oldDimension); - when(regionLoader.load("old-region")).thenReturn(oldRegion); - when(biomeLoader.load("meadow")).thenReturn(meadow); - } - - private ReachableBiomeHandler biomeHandler() { - return new ReachableBiomeHandler() { - @Override - public Engine engine() { - return engine; - } - - @Override - public IrisData data() { - throw new AssertionError("Find must not use the authoring context"); - } - }; - } - - private ReachableRegionHandler regionHandler() { - return new ReachableRegionHandler() { - @Override - public Engine engine() { - return engine; - } - - @Override - public IrisData data() { - throw new AssertionError("Find must not use the authoring context"); - } - }; - } - - private LocatableObjectHandler objectHandler() { - return new LocatableObjectHandler() { - @Override - public Engine engine() { - return engine; - } - - @Override - public IrisData data() { - throw new AssertionError("Find must not use the authoring context"); - } - }; + IrisData data = mock(IrisData.class); + ResourceLoader objects = mock(ResourceLoader.class); + when(engine.getData()).thenReturn(data); + when(data.getObjectLoader()).thenReturn(objects); + when(objects.getPossibleKeys()).thenReturn(new String[]{"new-tree"}); + IrisDimension dimension = mock(IrisDimension.class); + when(engine.getDimension()).thenReturn(dimension); + when(engine.getAllBiomes()).thenReturn(new KList<>()); + when(dimension.getAllRegions(engine)).thenReturn(new KList()); + doAnswer(invocation -> { + GenerationSemanticIndex.RecordConsumer consumer = invocation.getArgument(0); + consumer.accept(ChunkGenerationSemantics.builder(0, 0, 1L).addSurfaceBiome("meadow") + .addRegion("old-region").addObject("old-tree").addStructure("old-house", 8, 70, 8) + .seal().build()); + return null; + }).when(history).forEachRecordedSemantic(any()); } } } diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouterTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouterTest.java index da273328f..b1d6556bc 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouterTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouterTest.java @@ -2,10 +2,10 @@ package art.arcane.iris.engine.history; import art.arcane.iris.engine.IrisEngine; import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.framework.GenerationSessionManager; import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.util.common.data.B; import art.arcane.iris.spi.PlatformBlockState; -import art.arcane.iris.util.nbt.common.mca.MCAFile; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.matter.Matter; import org.junit.Rule; @@ -15,10 +15,8 @@ import org.mockito.MockedStatic; import org.junit.rules.TemporaryFolder; import java.io.IOException; -import java.io.RandomAccessFile; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -34,9 +32,10 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -50,8 +49,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public final class GenerationHistoryRuntimeRouterTest { - private static final int SECTOR_BYTES = 4_096; - @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -65,7 +62,7 @@ public final class GenerationHistoryRuntimeRouterTest { } @Test - public void promotionSamplesEveryBoundaryColumnThroughItsOwningRuntime() throws Exception { + public void promotionSamplesSavedBoundaryWithoutOpeningHistoricalRuntimeScopes() throws Exception { Path world = temporaryFolder.newFolder("router-promotion-world").toPath(); Path packA = createPack("router-promotion-a", "alpha"); Path packB = createPack("router-promotion-b", "beta"); @@ -84,13 +81,12 @@ public final class GenerationHistoryRuntimeRouterTest { history, (routedEngine, blockX, blockZ) -> { assertSame(engine, routedEngine); - assertSame(first, scoped.get()); + assertNull(scoped.get()); samples[0]++; return signature(blockX, blockZ); }, 256, - runtimes, - GenerationHistoryRuntimeRouter.DEFAULT_RUNTIME_CACHE_CAPACITY + runtimes ); GenerationActivation activated = history.activeActivation(); @@ -100,7 +96,7 @@ public final class GenerationHistoryRuntimeRouterTest { assertEquals(history.boundary(2L).exposedBlockColumns().size(), samples[0]); verify(engine).setDefaultGenerationRuntime(second); try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(0, 0)) { - assertSame(first, scoped.get()); + assertSame(second, scoped.get()); } try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(1, 0)) { assertSame(second, scoped.get()); @@ -109,7 +105,7 @@ public final class GenerationHistoryRuntimeRouterTest { } @Test - public void repeatedEpochActivationKeepsADistinctMantleAndBinding() throws Exception { + public void repeatedPackActivationUsesOnlyItsCurrentMantleAndBinding() throws Exception { Path world = temporaryFolder.newFolder("router-repeat-world").toPath(); Path packA = createPack("router-repeat-a", "alpha"); Path packB = createPack("router-repeat-b", "beta"); @@ -123,35 +119,27 @@ public final class GenerationHistoryRuntimeRouterTest { GenerationActivation third = promoteWithSignatures(history); IrisEngine engine = mock(IrisEngine.class); FakeRuntimeFactory runtimes = new FakeRuntimeFactory(); - IrisEngine.GenerationRuntimeBinding repeated = runtimes.binding(history, third); - when(engine.getActiveGenerationRuntimeBinding()).thenReturn(repeated); + IrisEngine.GenerationRuntimeBinding current = runtimes.binding(history, third); + when(engine.getActiveGenerationRuntimeBinding()).thenReturn(current); AtomicReference scoped = installScopeTracking(engine); - GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach( - engine, - history, - (ignored, blockX, blockZ) -> signature(blockX, blockZ), - runtimes - ); - IrisEngine.GenerationRuntimeBinding first; - try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(0, 0)) { - first = scoped.get(); + try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach( + engine, history, (ignored, x, z) -> signature(x, z), runtimes)) { + for (int chunkX = 0; chunkX < 3; chunkX++) { + try (GenerationHistoryRuntimeRouter.RuntimeStage stage = router.openStage(chunkX, 0)) { + assertSame(current, scoped.get()); + assertEquals(3L, stage.activation().activationId()); + } + } + assertEquals(history.manifest().activation(1L).orElseThrow().epochId(), third.epochId()); + assertEquals(Set.of(3L), runtimes.bindings.keySet()); + assertEquals(history.paths().activationMantleRoot(3L), current.mantleStorageDirectory()); + assertEquals(1L, history.resolveActivation(0, 0).activationId()); + assertEquals(2L, history.resolveActivation(1, 0).activationId()); } - try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(2, 0)) { - assertSame(repeated, scoped.get()); - } - - assertEquals(3L, third.activationId()); - assertEquals(history.manifest().activation(1L).orElseThrow().epochId(), third.epochId()); - assertNotSame(first, repeated); - assertNotSame(runtimes.bindings.get(1L), runtimes.bindings.get(3L)); - assertEquals(history.paths().activationMantleRoot(1L), runtimes.mantles.get(1L)); - assertEquals(history.paths().activationMantleRoot(3L), runtimes.mantles.get(3L)); - assertNotEquals(runtimes.mantles.get(1L), runtimes.mantles.get(3L)); - router.close(); } @Test - public void preloadKeepsHistoricalActivationBindingsLazy() throws Exception { + public void preloadAndRoutesDoNotConstructHistoricalBindings() throws Exception { Path world = temporaryFolder.newFolder("router-lazy-world").toPath(); Path packA = createPack("router-lazy-a", "alpha"); Path packB = createPack("router-lazy-b", "beta"); @@ -180,110 +168,92 @@ public final class GenerationHistoryRuntimeRouterTest { assertEquals(Set.of(3L), runtimes.bindings.keySet()); try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(0, 0)) { - assertEquals(Set.of(1L, 3L), runtimes.bindings.keySet()); + assertEquals(Set.of(3L), runtimes.bindings.keySet()); } try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(1, 0)) { - assertEquals(Set.of(1L, 2L, 3L), runtimes.bindings.keySet()); + assertEquals(Set.of(3L), runtimes.bindings.keySet()); } router.close(); } @Test - public void leastRecentlyUsedIdleRuntimeIsEvictedAndReloaded() throws Exception { - Path world = temporaryFolder.newFolder("router-eviction-world").toPath(); - GenerationHistory history = createThreeActivationHistory(world, "router-eviction"); + public void historicalRoutesWorkAfterArchivedPacksAreReleased() throws Exception { + Path world = temporaryFolder.newFolder("router-archived-world").toPath(); + GenerationHistory history = createThreeActivationHistory(world, "router-archived"); + new GenerationPackRepository(world).releaseArchivedPacks(history.manifest()); + assertFalse(Files.exists(history.paths().packRoot(history.manifest().activation(1L).orElseThrow().epochId()))); IrisEngine engine = mock(IrisEngine.class); FakeRuntimeFactory runtimes = new FakeRuntimeFactory(); - IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation()); - when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active); - installScopeTracking(engine); - GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach( - engine, - history, - (ignored, blockX, blockZ) -> signature(blockX, blockZ), - runtimes, - 2 - ); - - try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(0, 0)) { + IrisEngine.GenerationRuntimeBinding current = runtimes.binding(history, history.activeActivation()); + when(engine.getActiveGenerationRuntimeBinding()).thenReturn(current); + try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach( + engine, history, (ignored, x, z) -> signature(x, z), runtimes)) { + for (int chunkX = 0; chunkX < 3; chunkX++) { + try (GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(chunkX, 0)) { + assertEquals(3L, route.activation().activationId()); + } + } + assertEquals(Set.of(3L), runtimes.bindings.keySet()); + assertEquals(0, runtimes.loadCount(1L)); + assertEquals(0, runtimes.loadCount(2L)); } - IrisEngine.GenerationRuntimeBinding firstLoad = runtimes.bindings.get(1L); - try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(1, 0)) { - } - - verify(engine).closeDetachedGenerationRuntime(firstLoad); - assertEquals(1, runtimes.loadCount(1L)); - try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(0, 0)) { - } - assertEquals(2, runtimes.loadCount(1L)); - router.close(); + assertEquals(history.manifest(), GenerationHistory.open(world).manifest()); } @Test - public void reacquisitionWaitsUntilPreviousActivationMantleRetires() throws Exception { - Path world = temporaryFolder.newFolder("router-retirement-reload-world").toPath(); - GenerationHistory history = createThreeActivationHistory(world, "router-retirement-reload"); + public void closeWaitsForAnInFlightPromotionLoad() throws Exception { + Path world = temporaryFolder.newFolder("router-promotion-close-world").toPath(); + GenerationHistory history = createHistory(world, createPack("router-promotion-close-a", "alpha")); IrisEngine engine = mock(IrisEngine.class); + when(engine.isStudio()).thenReturn(true); FakeRuntimeFactory runtimes = new FakeRuntimeFactory(); - IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation()); - when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active); + IrisEngine.GenerationRuntimeBinding initial = runtimes.binding(history, history.activeActivation()); + when(engine.getActiveGenerationRuntimeBinding()).thenReturn(initial); + stage(history, createPack("router-promotion-close-b", "beta")); + runtimes.blockLoad(2L); GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach( - engine, history, (ignored, x, z) -> signature(x, z), runtimes, 1); - GenerationHistoryRuntimeRouter.RuntimeRoute original = router.openRoute(0, 0); - IrisEngine.GenerationRuntimeBinding first = runtimes.bindings.get(1L); - CountDownLatch retirementStarted = new CountDownLatch(1); - CountDownLatch releaseRetirement = new CountDownLatch(1); - CountDownLatch reloadStarted = new CountDownLatch(1); - doAnswer(invocation -> { - retirementStarted.countDown(); - assertTrue(releaseRetirement.await(5L, TimeUnit.SECONDS)); - return null; - }).when(engine).closeDetachedGenerationRuntime(first); + engine, history, (ignored, x, z) -> signature(x, z), runtimes); ExecutorService executor = Executors.newFixedThreadPool(2); try { - CompletableFuture retiring = CompletableFuture.runAsync(original::close, executor); - assertTrue(retirementStarted.await(5L, TimeUnit.SECONDS)); - CompletableFuture reloading = - CompletableFuture.supplyAsync(() -> { - reloadStarted.countDown(); - return openRouteUnchecked(router, 0, 0); - }, executor); - assertTrue(reloadStarted.await(5L, TimeUnit.SECONDS)); - assertThrows(TimeoutException.class, () -> reloading.get(100L, TimeUnit.MILLISECONDS)); - assertEquals(1, runtimes.loadCount(1L)); - - releaseRetirement.countDown(); - retiring.get(5L, TimeUnit.SECONDS); - try (GenerationHistoryRuntimeRouter.RuntimeRoute reloaded = reloading.get(5L, TimeUnit.SECONDS)) { - assertEquals(1L, reloaded.activation().activationId()); - assertEquals(2, runtimes.loadCount(1L)); - } + CompletableFuture promoting = CompletableFuture.runAsync(() -> { + try (GenerationHistoryRuntimeRouter.StudioCutover cutover = router.beginStudioCutover(5_000L)) { + cutover.promotePending(); + } catch (IOException failure) { + throw new IllegalStateException(failure); + } + }, executor); + assertTrue(runtimes.loadStarted.await(5L, TimeUnit.SECONDS)); + CompletableFuture closing = CompletableFuture.runAsync(router::close, executor); + awaitClosed(router); + assertThrows(TimeoutException.class, () -> closing.get(100L, TimeUnit.MILLISECONDS)); + runtimes.releaseLoad.countDown(); + promoting.get(5L, TimeUnit.SECONDS); + closing.get(5L, TimeUnit.SECONDS); + assertEquals(1, runtimes.loadCount(2L)); + verify(engine).detachGenerationHistoryRuntimeRouter(router); } finally { - releaseRetirement.countDown(); + runtimes.releaseLoad.countDown(); executor.shutdownNow(); - router.close(); } } @Test - public void failedRetirementRejectsReopeningTheSameActivationMantle() throws Exception { + public void failedOutgoingRuntimeRetirementReportsTheCommittedCutoverFailure() throws Exception { Path world = temporaryFolder.newFolder("router-retirement-failure-world").toPath(); - GenerationHistory history = createThreeActivationHistory(world, "router-retirement-failure"); + GenerationHistory history = createHistory(world, createPack("router-retirement-failure-a", "alpha")); IrisEngine engine = mock(IrisEngine.class); FakeRuntimeFactory runtimes = new FakeRuntimeFactory(); - IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation()); - when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active); - GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach( - engine, history, (ignored, x, z) -> signature(x, z), runtimes, 1); - GenerationHistoryRuntimeRouter.RuntimeRoute original = router.openRoute(0, 0); + IrisEngine.GenerationRuntimeBinding outgoing = runtimes.binding(history, history.activeActivation()); + when(engine.getActiveGenerationRuntimeBinding()).thenReturn(outgoing); + stage(history, createPack("router-retirement-failure-b", "beta")); doAnswer(invocation -> { throw new IllegalStateException("Mantle did not close"); - }).when(engine).closeDetachedGenerationRuntime(runtimes.bindings.get(1L)); - - assertThrows(IllegalStateException.class, original::close); - assertThrows(IOException.class, () -> router.openRoute(0, 0)); - assertEquals(1, runtimes.loadCount(1L)); - router.close(); + }).when(engine).closeDetachedGenerationRuntime(outgoing); + assertThrows(IllegalStateException.class, () -> GenerationHistoryRuntimeRouter.attachAndPromotePending( + engine, history, (ignored, x, z) -> signature(x, z), 256, runtimes)); + assertEquals(2L, history.activeActivation().activationId()); + verify(engine).setDefaultGenerationRuntime(runtimes.bindings.get(2L)); + assertEquals(0, runtimes.loadCount(1L)); } @Test @@ -299,22 +269,20 @@ public final class GenerationHistoryRuntimeRouterTest { engine, history, (ignored, blockX, blockZ) -> signature(blockX, blockZ), - runtimes, - 1 + runtimes ); GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(0, 0); - IrisEngine.GenerationRuntimeBinding historical = runtimes.bindings.get(1L); try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope outer = route.openRuntimeScope(); GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope inner = route.openRuntimeScope()) { assertThrows(IllegalStateException.class, route::close); - verify(engine, never()).closeDetachedGenerationRuntime(historical); + verify(engine, never()).closeDetachedGenerationRuntime(active); } - verify(engine, never()).closeDetachedGenerationRuntime(historical); + verify(engine, never()).closeDetachedGenerationRuntime(active); route.close(); - verify(engine).closeDetachedGenerationRuntime(historical); + verify(engine, never()).closeDetachedGenerationRuntime(active); router.close(); } @@ -348,7 +316,7 @@ public final class GenerationHistoryRuntimeRouterTest { } @Test - public void concurrentRoutesDeduplicateHistoricalRuntimeLoad() throws Exception { + public void concurrentHistoricalRoutesShareTheCurrentRuntimeWithoutLoadingOldFactories() throws Exception { Path world = temporaryFolder.newFolder("router-deduplicated-load-world").toPath(); GenerationHistory history = createThreeActivationHistory(world, "router-deduplicated-load"); IrisEngine engine = mock(IrisEngine.class); @@ -356,13 +324,11 @@ public final class GenerationHistoryRuntimeRouterTest { IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation()); when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active); installScopeTracking(engine); - runtimes.blockLoad(1L); GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach( engine, history, (ignored, blockX, blockZ) -> signature(blockX, blockZ), - runtimes, - 2 + runtimes ); ExecutorService executor = Executors.newFixedThreadPool(2); CountDownLatch ready = new CountDownLatch(2); @@ -374,12 +340,12 @@ public final class GenerationHistoryRuntimeRouterTest { router, executor, ready, start, 0, 0); assertTrue(ready.await(5L, TimeUnit.SECONDS)); start.countDown(); - assertTrue(runtimes.loadStarted.await(5L, TimeUnit.SECONDS)); - runtimes.releaseLoad.countDown(); GenerationHistoryRuntimeRouter.RuntimeRoute firstRoute = first.get(5L, TimeUnit.SECONDS); GenerationHistoryRuntimeRouter.RuntimeRoute secondRoute = second.get(5L, TimeUnit.SECONDS); - assertEquals(1, runtimes.loadCount(1L)); + assertEquals(0, runtimes.loadCount(1L)); + assertEquals(3L, firstRoute.activation().activationId()); + assertEquals(3L, secondRoute.activation().activationId()); firstRoute.close(); secondRoute.close(); } finally { @@ -389,80 +355,50 @@ public final class GenerationHistoryRuntimeRouterTest { } @Test - public void closeRejectsNewRoutesAndAwaitsLoadAndLease() throws Exception { + public void closeRejectsNewRoutesAndAwaitsExistingLeases() throws Exception { Path world = temporaryFolder.newFolder("router-close-await-world").toPath(); GenerationHistory history = createThreeActivationHistory(world, "router-close-await"); IrisEngine engine = mock(IrisEngine.class); FakeRuntimeFactory runtimes = new FakeRuntimeFactory(); IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation()); when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active); - installScopeTracking(engine); - runtimes.blockLoad(1L); GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach( - engine, - history, - (ignored, blockX, blockZ) -> signature(blockX, blockZ), - runtimes, - 4 - ); - ExecutorService executor = Executors.newFixedThreadPool(2); + engine, history, (ignored, x, z) -> signature(x, z), runtimes); + GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(0, 0); + ExecutorService executor = Executors.newSingleThreadExecutor(); try { - CompletableFuture routeFuture = CompletableFuture.supplyAsync( - () -> openRouteUnchecked(router, 0, 0), - executor - ); - assertTrue(runtimes.loadStarted.await(5L, TimeUnit.SECONDS)); - CountDownLatch closeInvoked = new CountDownLatch(1); - CompletableFuture closeFuture = CompletableFuture.runAsync(() -> { - closeInvoked.countDown(); - router.close(); - }, executor); - assertTrue(closeInvoked.await(5L, TimeUnit.SECONDS)); + CompletableFuture closing = CompletableFuture.runAsync(router::close, executor); awaitClosed(router); - assertThrows(IllegalStateException.class, () -> router.openRoute(2, 0)); - runtimes.releaseLoad.countDown(); - GenerationHistoryRuntimeRouter.RuntimeRoute route = routeFuture.get(5L, TimeUnit.SECONDS); - assertThrows(TimeoutException.class, () -> closeFuture.get(100L, TimeUnit.MILLISECONDS)); + assertThrows(TimeoutException.class, () -> closing.get(100L, TimeUnit.MILLISECONDS)); route.close(); - closeFuture.get(5L, TimeUnit.SECONDS); + closing.get(5L, TimeUnit.SECONDS); } finally { + route.close(); executor.shutdownNow(); } - verify(engine).detachGenerationHistoryRuntimeRouter(router); - verify(engine).closeDetachedGenerationRuntime(runtimes.bindings.get(1L)); verify(engine, never()).closeDetachedGenerationRuntime(active); } @Test - public void closeRetiresEveryCachedHistoricalRuntimeButKeepsDefault() throws Exception { - Path world = temporaryFolder.newFolder("router-close-cached-world").toPath(); - GenerationHistory history = createThreeActivationHistory(world, "router-close-cached"); + public void closeLeavesTheSingleDefaultRuntimeOwnedByTheEngine() throws Exception { + Path world = temporaryFolder.newFolder("router-close-current-world").toPath(); + GenerationHistory history = createThreeActivationHistory(world, "router-close-current"); IrisEngine engine = mock(IrisEngine.class); FakeRuntimeFactory runtimes = new FakeRuntimeFactory(); - IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation()); - when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active); + IrisEngine.GenerationRuntimeBinding initial = runtimes.binding(history, history.activeActivation()); + when(engine.getActiveGenerationRuntimeBinding()).thenReturn(initial); installScopeTracking(engine); - GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach( - engine, - history, - (ignored, blockX, blockZ) -> signature(blockX, blockZ), - runtimes, - 4 - ); - try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(0, 0)) { + try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach( + engine, history, (ignored, x, z) -> signature(x, z), runtimes)) { + try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(0, 0)) { + } + try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(1, 0)) { + } } - try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(1, 0)) { - } - IrisEngine.GenerationRuntimeBinding first = runtimes.bindings.get(1L); - IrisEngine.GenerationRuntimeBinding second = runtimes.bindings.get(2L); - - router.close(); - - verify(engine).closeDetachedGenerationRuntime(first); - verify(engine).closeDetachedGenerationRuntime(second); - verify(engine, never()).closeDetachedGenerationRuntime(active); + assertEquals(Set.of(3L), runtimes.bindings.keySet()); + verify(engine, never()).closeDetachedGenerationRuntime(any()); } @Test @@ -484,8 +420,7 @@ public final class GenerationHistoryRuntimeRouterTest { history, (ignored, blockX, blockZ) -> signature(blockX, blockZ), 256, - runtimes, - 1 + runtimes ); assertEquals(2L, history.activeActivation().activationId()); @@ -716,6 +651,8 @@ public final class GenerationHistoryRuntimeRouterTest { GenerationKernelRegistry.Version versionTwo = new GenerationKernelRegistry.Version(2, 1, 1); GenerationKernelRegistry kernels = kernels(versionTwo); GenerationHistory history = createHistory(world, pack, versionOne, kernels); + history = GenerationHistory.open(world, new GenerationKernelRegistry(versionTwo, + List.of(kernels.requireSupported(versionTwo)))); IrisEngine engine = mock(IrisEngine.class); FakeRuntimeFactory runtimes = new FakeRuntimeFactory(); IrisEngine.GenerationRuntimeBinding first = runtimes.binding(history, history.activeActivation()); @@ -741,7 +678,163 @@ public final class GenerationHistoryRuntimeRouterTest { } @Test - public void attachPromotesPendingPackThenRestagesItForTheCurrentKernel() throws Exception { + public void changedBuildRevisionWithTheSameAbiAutomaticallyCreatesANewActivation() throws Exception { + Path world = temporaryFolder.newFolder("router-build-revision-world").toPath(); + Path pack = createPack("router-build-revision-pack", "alpha"); + GenerationKernelRegistry.Version version = new GenerationKernelRegistry.Version(1, 1, 1); + GenerationHistory original = createHistory(world, pack, version, kernels(version)); + GenerationKernelRegistry upgraded = new GenerationKernelRegistry(version, List.of( + new GenerationKernelRegistry.Kernel(1, "9".repeat(64), Map.of( + new GenerationKernelRegistry.AlgorithmVersion(1, 1), + (engine, plan) -> { throw new AssertionError("Mock runtime factory owns this test."); })))); + GenerationHistory history = GenerationHistory.open(world, upgraded); + IrisEngine engine = mock(IrisEngine.class); + FakeRuntimeFactory runtimes = new FakeRuntimeFactory(); + IrisEngine.GenerationRuntimeBinding initial = runtimes.binding(history, history.activeActivation()); + when(engine.getActiveGenerationRuntimeBinding()).thenReturn(initial); + try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attachAndPromotePending( + engine, history, (ignored, x, z) -> signature(x, z), 256, runtimes)) { + assertEquals(2L, history.activeActivation().activationId()); + assertEquals(version, history.activeEpoch().kernelVersion()); + assertEquals("9".repeat(64), history.activeEpoch().kernelImplementationFingerprint()); + assertEquals(original.activeEpoch().packFingerprint(), history.activeEpoch().packFingerprint()); + assertEquals(0, runtimes.loadCount(1L)); + try (GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(0, 0)) { + assertEquals(2L, route.activation().activationId()); + } + } + } + + @Test + public void liveStudioPromotionPreservesChunksAndRoutesExpansionAcrossRepeatedUpdates() throws Exception { + Path world = temporaryFolder.newFolder("live-studio-world").toPath(); + Path packA = createPack("live-studio-a", "flat-lowland"); + Path packB = createPack("live-studio-b", "massive-mountains"); + Path packC = createPack("live-studio-c", "deep-ocean"); + GenerationHistory history = createHistory(world, packA); + Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca"); + writeRegion(region, new int[][]{{0, 0}}); + byte[] original = Files.readAllBytes(region); + IrisEngine engine = mock(IrisEngine.class); + when(engine.isStudio()).thenReturn(true); + FakeRuntimeFactory runtimes = new FakeRuntimeFactory(); + IrisEngine.GenerationRuntimeBinding first = runtimes.binding(history, history.activeActivation()); + when(engine.getActiveGenerationRuntimeBinding()).thenReturn(first); + AtomicReference scoped = installScopeTracking(engine); + try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach( + engine, history, (ignored, x, z) -> signature(x, z), runtimes)) { + try (GenerationHistoryRuntimeRouter.RuntimeStage stage = router.openStage(0, 0)) { + assertEquals(1L, stage.activation().activationId()); + } + stage(history, packB); + try (GenerationHistoryRuntimeRouter.StudioCutover cutover = router.beginStudioCutover(5_000L)) { + assertEquals(2L, cutover.promotePending().activationId()); + } + assertArrayEquals(original, Files.readAllBytes(region)); + try (GenerationHistoryRuntimeRouter.RuntimeStage stage = router.openStage(0, 0)) { + assertSame(runtimes.bindings.get(2L), scoped.get()); + assertEquals(2L, stage.activation().activationId()); + } + try (GenerationHistoryRuntimeRouter.RuntimeStage stage = router.openStage(1, 0)) { + assertSame(runtimes.bindings.get(2L), scoped.get()); + assertEquals(2L, stage.activation().activationId()); + } + writeRegion(region, new int[][]{{0, 0}, {1, 0}}); + byte[] expanded = Files.readAllBytes(region); + stage(history, packC); + try (GenerationHistoryRuntimeRouter.StudioCutover cutover = router.beginStudioCutover(5_000L)) { + assertEquals(3L, cutover.promotePending().activationId()); + } + assertArrayEquals(expanded, Files.readAllBytes(region)); + assertEquals(1L, history.resolveActivation(0, 0).activationId()); + assertEquals(2L, history.resolveActivation(1, 0).activationId()); + assertEquals(3L, history.resolveActivation(2, 0).activationId()); + } + GenerationHistory reopened = GenerationHistory.open(world); + assertEquals(1L, reopened.resolveActivation(0, 0).activationId()); + assertEquals(2L, reopened.resolveActivation(1, 0).activationId()); + assertEquals(3L, reopened.resolveActivation(2, 0).activationId()); + } + + @Test + public void liveStudioCutoverDrainsExistingRoutesAndBlocksNewRoutesUntilPublicationCompletes() throws Exception { + Path world = temporaryFolder.newFolder("studio-drain-world").toPath(); + Path pack = createPack("studio-drain-pack", "alpha"); + GenerationHistory history = createHistory(world, pack); + IrisEngine engine = mock(IrisEngine.class); + when(engine.isStudio()).thenReturn(true); + FakeRuntimeFactory runtimes = new FakeRuntimeFactory(); + IrisEngine.GenerationRuntimeBinding initialBinding = runtimes.binding(history, history.activeActivation()); + when(engine.getActiveGenerationRuntimeBinding()).thenReturn(initialBinding); + ExecutorService executor = Executors.newFixedThreadPool(2); + CountDownLatch cutoverRequested = new CountDownLatch(1); + CountDownLatch cutoverAcquired = new CountDownLatch(1); + CountDownLatch releaseCutover = new CountDownLatch(1); + try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach( + engine, history, (ignored, x, z) -> signature(x, z), runtimes)) { + GenerationHistoryRuntimeRouter.RuntimeRoute initial = router.openRoute(0, 0); + try { + CompletableFuture cutover = CompletableFuture.runAsync(() -> { + cutoverRequested.countDown(); + try (GenerationHistoryRuntimeRouter.StudioCutover ignored = router.beginStudioCutover(5_000L)) { + cutoverAcquired.countDown(); + if (!releaseCutover.await(5, TimeUnit.SECONDS)) { + throw new IllegalStateException("Timed out waiting to release the Studio cutover."); + } + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(interrupted); + } + }, executor); + assertTrue(cutoverRequested.await(5, TimeUnit.SECONDS)); + assertFalse(cutoverAcquired.await(100, TimeUnit.MILLISECONDS)); + initial.close(); + assertTrue(cutoverAcquired.await(5, TimeUnit.SECONDS)); + CompletableFuture waiting = + CompletableFuture.supplyAsync(() -> openRouteUnchecked(router, 1, 0), executor); + assertThrows(TimeoutException.class, () -> waiting.get(100, TimeUnit.MILLISECONDS)); + releaseCutover.countDown(); + cutover.get(5, TimeUnit.SECONDS); + try (GenerationHistoryRuntimeRouter.RuntimeRoute route = waiting.get(5, TimeUnit.SECONDS)) { + assertEquals(1L, route.activation().activationId()); + } + } finally { + initial.close(); + releaseCutover.countDown(); + } + } finally { + releaseCutover.countDown(); + executor.shutdownNow(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } + + @Test + public void liveCutoverRejectsProductionWorldsAndNestedGenerationScopes() throws Exception { + Path world = temporaryFolder.newFolder("studio-guard-world").toPath(); + Path pack = createPack("studio-guard-pack", "alpha"); + GenerationHistory history = createHistory(world, pack); + IrisEngine engine = mock(IrisEngine.class); + FakeRuntimeFactory runtimes = new FakeRuntimeFactory(); + IrisEngine.GenerationRuntimeBinding initialBinding = runtimes.binding(history, history.activeActivation()); + when(engine.getActiveGenerationRuntimeBinding()).thenReturn(initialBinding); + installScopeTracking(engine); + try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach( + engine, history, (ignored, x, z) -> signature(x, z), runtimes)) { + assertThrows(IllegalStateException.class, () -> router.beginStudioCutover(5_000L)); + when(engine.isStudio()).thenReturn(true); + try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(0, 0)) { + assertThrows(IllegalStateException.class, () -> router.beginStudioCutover(5_000L)); + } + try (GenerationHistoryRuntimeRouter.StudioCutover cutover = router.beginStudioCutover(5_000L)) { + assertThrows(IllegalStateException.class, () -> router.beginStudioCutover(5_000L)); + assertEquals(1L, cutover.promotePending().activationId()); + } + } + } + + @Test + public void pendingOldRevisionPromotesWithoutLoadingItsArchivedImplementation() throws Exception { Path world = temporaryFolder.newFolder("router-pending-kernel-world").toPath(); Path packA = createPack("router-pending-kernel-a", "alpha"); Path packB = createPack("router-pending-kernel-b", "beta"); @@ -775,7 +868,7 @@ public final class GenerationHistoryRuntimeRouterTest { assertEquals(versionTwo, history.activeEpoch().kernelVersion()); assertEquals(GenerationPackFingerprint.compute(packB, GenerationPackFingerprint.CURRENT_VERSION), history.activeEpoch().packFingerprint()); - verify(engine).setDefaultGenerationRuntime(runtimes.bindings.get(2L)); + assertEquals(0, runtimes.loadCount(2L)); verify(engine).setDefaultGenerationRuntime(runtimes.bindings.get(3L)); router.close(); } @@ -957,18 +1050,11 @@ public final class GenerationHistoryRuntimeRouterTest { new TerrainBoundarySignature.VerticalLayout(-64, 64, 2), new TerrainBoundarySignature.BiomeEncoding(List.of("iris:test"), new short[]{0, 0}) ) - ); + , BoundaryColumnGeometry.empty()); } private static void writeRegion(Path file, int[][] chunks) throws IOException { - try (RandomAccessFile output = new RandomAccessFile(file.toFile(), "rw")) { - output.setLength((long) (2 + chunks.length) * SECTOR_BYTES); - for (int index = 0; index < chunks.length; index++) { - int chunkIndex = MCAFile.getChunkIndex(chunks[index][0], chunks[index][1]); - output.seek((long) chunkIndex * Integer.BYTES); - output.writeInt((2 + index) << Byte.SIZE | 1); - } - } + SavedTerrainTestRegion.write(file, chunks); } private static final class FakeRuntimeFactory @@ -989,6 +1075,9 @@ public final class GenerationHistoryRuntimeRouterTest { GenerationEpoch epoch, IrisEngine.GenerationRuntimeBinding binding ) { + if (engine.getGenerationSessions() == null) { + when(engine.getGenerationSessions()).thenReturn(new GenerationSessionManager(true)); + } } @Override @@ -998,6 +1087,10 @@ public final class GenerationHistoryRuntimeRouterTest { GenerationActivation activation, GenerationEpoch epoch ) throws IOException { + if (!epoch.kernelVersion().equals(history.currentKernelVersion()) + || !history.usesCurrentGenerator()) { + throw new AssertionError("Archived generator implementations must never load."); + } synchronized (loadCounts) { loadCounts.computeIfAbsent(activation.activationId(), ignored -> new AtomicInteger()).incrementAndGet(); } diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryStoreTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryStoreTest.java index 4ccb2d06b..98f0286d3 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryStoreTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryStoreTest.java @@ -90,8 +90,8 @@ public class GenerationHistoryStoreTest { store.activatePending(pending.activationId()); try (Stream entries = Files.list(directory)) { - List names = entries.map(path -> path.getFileName().toString()).toList(); - assertEquals(List.of(GenerationHistoryStore.MANIFEST_FILE_NAME), names); + List names = entries.map(path -> path.getFileName().toString()).sorted().toList(); + assertEquals(List.of("epochs", GenerationHistoryStore.MANIFEST_FILE_NAME), names); } GenerationHistoryStore reopened = GenerationHistoryStore.open(directory, CLOCK); assertEquals(store.manifest(), reopened.manifest()); @@ -139,12 +139,10 @@ public class GenerationHistoryStoreTest { public void failsClosedOnForgedEpochIdentityAndUnknownFields() throws Exception { Path forgedDirectory = temporaryFolder.getRoot().toPath().resolve("forged"); GenerationHistoryStore.initialize(forgedDirectory, epoch(PACK_A), CLOCK); - JsonObject forged = readJson(forgedDirectory); - forged.getAsJsonArray("epochs") - .get(0) - .getAsJsonObject() - .addProperty("epochId", PACK_B); - writeJson(forgedDirectory, forged); + Path metadata = forgedDirectory.resolve("epochs").resolve(epoch(PACK_A).epochId()).resolve("epoch.json"); + JsonObject forged = JsonParser.parseString(Files.readString(metadata)).getAsJsonObject(); + forged.addProperty("epochId", PACK_B); + Files.writeString(metadata, forged.toString()); assertThrows(IOException.class, () -> GenerationHistoryStore.open(forgedDirectory, CLOCK)); @@ -274,6 +272,42 @@ public class GenerationHistoryStoreTest { assertThrows(IllegalStateException.class, store::activeEpoch); } + @Test + public void manifestKeepsOnlyEpochReferencesAndMetadataSurvivesReopen() throws Exception { + Path directory = temporaryFolder.getRoot().toPath().resolve("epoch-index"); + GenerationEpoch first = epoch(PACK_A); + GenerationHistoryStore store = GenerationHistoryStore.initialize(directory, first, CLOCK); + store.preparePendingActivation(epoch(PACK_B), 256); + JsonObject index = readJson(directory); + assertTrue(index.getAsJsonArray("epochs").get(0).isJsonPrimitive()); + assertFalse(Files.readString(directory.resolve("manifest.json")).contains("registryContract")); + assertTrue(Files.isRegularFile(directory.resolve("epochs").resolve(first.epochId()).resolve("epoch.json"))); + assertEquals(store.manifest(), GenerationHistoryStore.open(directory, CLOCK).manifest()); + } + + @Test + public void failedEpochPublicationLeavesTheCommittedManifestUsable() throws Exception { + Path directory = temporaryFolder.getRoot().toPath().resolve("metadata-failure"); + GenerationHistoryStore store = GenerationHistoryStore.initialize(directory, epoch(PACK_A), CLOCK); + byte[] original = Files.readAllBytes(directory.resolve("manifest.json")); + GenerationEpoch replacement = epoch(PACK_B); + Files.writeString(directory.resolve("epochs").resolve(replacement.epochId()), "blocked"); + assertThrows(IOException.class, () -> store.preparePendingActivation(replacement, 256)); + assertArrayEquals(original, Files.readAllBytes(directory.resolve("manifest.json"))); + assertEquals(epoch(PACK_A), store.activeEpoch()); + assertFalse(store.pendingEpoch().isPresent()); + assertEquals(store.manifest(), GenerationHistoryStore.open(directory, CLOCK).manifest()); + } + + @Test + public void rejectsMissingReferencedEpochMetadata() throws Exception { + Path directory = temporaryFolder.getRoot().toPath().resolve("missing-metadata"); + GenerationEpoch first = epoch(PACK_A); + GenerationHistoryStore.initialize(directory, first, CLOCK); + Files.delete(directory.resolve("epochs").resolve(first.epochId()).resolve("epoch.json")); + assertThrows(IOException.class, () -> GenerationHistoryStore.open(directory, CLOCK)); + } + private static boolean initializeAfterBarrier( Path directory, GenerationEpoch epoch, diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryTest.java index 32976a033..ab68f8828 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryTest.java @@ -1,12 +1,10 @@ package art.arcane.iris.engine.history; -import art.arcane.iris.util.nbt.common.mca.MCAFile; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import java.io.IOException; -import java.io.RandomAccessFile; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; @@ -19,13 +17,12 @@ import java.util.Set; import java.util.stream.Stream; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public final class GenerationHistoryTest { - private static final int SECTOR_BYTES = 4_096; - @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -106,18 +103,16 @@ public final class GenerationHistoryTest { } @Test - public void openRejectsUnsupportedGeneratorAndRngVersions() throws Exception { + public void historicalExecutionVersionsDoNotPreventOpeningSavedTerrain() throws Exception { Path abiWorld = temporaryFolder.newFolder("abi-world").toPath(); Path abiPack = createPack("abi-pack", "alpha"); initializeRawHistory(abiWorld, abiPack, 2, GenerationKernelRegistry.standard().current().rngVersion()); - IOException abiFailure = assertThrows(IOException.class, () -> GenerationHistory.open(abiWorld)); - assertTrue(abiFailure.getMessage().contains("generation ABI")); + assertEquals(2, GenerationHistory.open(abiWorld).activeEpoch().generatorAbi()); Path rngWorld = temporaryFolder.newFolder("rng-world").toPath(); Path rngPack = createPack("rng-pack", "alpha"); initializeRawHistory(rngWorld, rngPack, GenerationKernelRegistry.standard().current().generatorAbi(), 2); - IOException rngFailure = assertThrows(IOException.class, () -> GenerationHistory.open(rngWorld)); - assertTrue(rngFailure.getMessage().contains("RNG/seed derivation version")); + assertEquals(2, GenerationHistory.open(rngWorld).activeEpoch().rngVersion()); } @Test @@ -426,7 +421,7 @@ public final class GenerationHistoryTest { } @Test - public void retainedAbiRegistryReopensEveryHistoricalEpoch() throws Exception { + public void savedHistoryOpensWithoutRetainingItsExecutableKernel() throws Exception { Path world = temporaryFolder.newFolder("retained-abi-world").toPath(); Path packA = createPack("retained-abi-pack-a", "alpha"); Path packB = createPack("retained-abi-pack-b", "beta"); @@ -478,12 +473,51 @@ public final class GenerationHistoryTest { ); history.promotePending(List.of()); - assertThrows(IOException.class, () -> GenerationHistory.open(world)); + assertEquals(2, GenerationHistory.open(world).activeEpoch().generatorAbi()); GenerationHistory reopened = GenerationHistory.open(world, kernels); assertEquals(2, reopened.activeEpoch().generatorAbi()); assertEquals(1, reopened.manifest().activation(1L).orElseThrow().activationId()); } + @Test + public void changedBuildExpandsFromSavedTerrainWithoutTheOldFactory() throws Exception { + Path world = temporaryFolder.newFolder("build-upgrade-world").toPath(); + Path pack = createPack("build-upgrade-pack", "alpha"); + GenerationKernelRegistry.Version oldVersion = new GenerationKernelRegistry.Version(101, 1, 1); + GenerationKernelRegistry.Version newVersion = new GenerationKernelRegistry.Version(102, 1, 1); + GenerationKernelRegistry firstBuild = singleKernel(oldVersion, "a"); + GenerationKernelRegistry secondBuild = singleKernel(newVersion, "b"); + GenerationHistory.create(world, pack, fingerprint(pack), 42L, + contract(), GenerationRegistryContract.empty(), oldVersion, firstBuild); + Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca"); + SavedTerrainTestRegion.write(region, new int[][]{{0, 0}}); + byte[] savedBlocks = Files.readAllBytes(region); + + GenerationHistory upgraded = GenerationHistory.open(world, secondBuild); + assertFalse(upgraded.usesCurrentGenerator()); + upgraded.prepareCurrentGenerator(32); + assertTrue(upgraded.usesCurrentGenerator()); + assertEquals(2L, upgraded.activeActivation().activationId()); + assertEquals(oldVersion, upgraded.resolveEpoch(0, 0).kernelVersion()); + try (GenerationHistory.GenerationStage generation = upgraded.openStage(1, 0)) { + assertEquals(newVersion, generation.epoch().kernelVersion()); + } + assertTrue(upgraded.terrainSignatures(2L).size() > 0); + assertArrayEquals(savedBlocks, Files.readAllBytes(region)); + GenerationHistory reopened = GenerationHistory.open(world, secondBuild); + assertTrue(reopened.usesCurrentGenerator()); + assertEquals(2L, reopened.activeActivation().activationId()); + } + + private static GenerationKernelRegistry singleKernel(GenerationKernelRegistry.Version version, String fingerprint) { + return new GenerationKernelRegistry(version, List.of(new GenerationKernelRegistry.Kernel( + version.generatorAbi(), fingerprint.repeat(64), + Map.of(new GenerationKernelRegistry.AlgorithmVersion(version.rngVersion(), version.seedDerivationVersion()), + (engine, transition) -> { + throw new AssertionError("Saved terrain capture invoked a generator factory"); + })))); + } + @Test public void openRejectsWorldSeedMismatch() throws Exception { Path world = temporaryFolder.newFolder("seed-world").toPath(); @@ -548,7 +582,7 @@ public final class GenerationHistoryTest { } @Test - public void pendingCutoverRejectsOwnershipForUngeneratedChunks() throws Exception { + public void pendingCutoverDiscardsOutgoingOwnershipForUnstoredChunks() throws Exception { Path world = temporaryFolder.newFolder("pending-forged-world").toPath(); Path packA = createPack("pending-forged-pack-a", "alpha"); Path packB = createPack("pending-forged-pack-b", "beta"); @@ -558,11 +592,16 @@ public final class GenerationHistoryTest { assertTrue(forged.assign(9, 9, 1L)); forged.persist(); - assertThrows(IOException.class, () -> GenerationHistory.open(world)); + GenerationHistory recovered = GenerationHistory.open(world); + assertEquals(1, recovered.explicitChunkCount()); + recovered.promotePending(List.of()); + assertEquals(0, recovered.explicitChunkCount()); + assertEquals(2L, recovered.resolveActivation(9, 9).activationId()); + assertEquals(0, GenerationHistory.open(world).explicitChunkCount()); } @Test - public void sealedGenerationClaimSurvivesCrashBeforeChunkAllocationAndJoinsCutoverOwnership() throws Exception { + public void sealedGenerationClaimWithoutStoredTerrainIsDiscardedAfterCrash() throws Exception { Path world = temporaryFolder.newFolder("semantic-claim-cutover-world").toPath(); Path packA = createPack("semantic-claim-cutover-pack-a", "alpha"); Path packB = createPack("semantic-claim-cutover-pack-b", "beta"); @@ -592,14 +631,76 @@ public final class GenerationHistoryTest { } } GenerationHistory recovered = GenerationHistory.open(recoveredWorld); - recovered.promotePending(signaturesForChunks(new int[][]{{9, 9}})); + recovered.promotePending(List.of()); - assertEquals(1L, recovered.resolveActivation(9, 9).activationId()); + assertEquals(2L, recovered.resolveActivation(9, 9).activationId()); assertEquals(2L, recovered.resolveActivation(10, 9).activationId()); - assertEquals(1, recovered.explicitChunkCount()); + assertEquals(0, recovered.explicitChunkCount()); GenerationHistory reopened = GenerationHistory.open(recoveredWorld); - assertEquals(Optional.of(claim), reopened.semantics(9, 9)); - assertEquals(1L, reopened.resolveActivation(9, 9).activationId()); + assertTrue(reopened.semantics(9, 9).isEmpty()); + assertEquals(2L, reopened.resolveActivation(9, 9).activationId()); + try (GenerationHistory.GenerationStage replacement = reopened.openStage(9, 9)) { + assertTrue(reopened.claimGeneratedSemantics(replacement, + ChunkGenerationSemantics.builder(9, 9, 2L).addObject("replacement").seal().build())); + } + } + + @Test + public void coldRecoveryUsesNativeNoiseStatusAsTheMinimumStoredTerrain() throws Exception { + Path world = temporaryFolder.newFolder("native-status-recovery-world").toPath(); + createHistory(world, createPack("native-status-recovery-pack", "alpha")); + Path regions = Files.createDirectories(world.resolve("region")); + SavedTerrainTestRegion.write(regions.resolve("r.0.0.mca"), new int[][]{{0, 0}}, "minecraft:noise"); + SavedTerrainTestRegion.write(regions.resolve("r.1.0.mca"), new int[][]{{32, 0}}, "minecraft:biomes"); + GenerationSemanticIndex index = GenerationSemanticIndex.loadRequired(world); + index.claimAndPersist(ChunkGenerationSemantics.builder(0, 0, 1L).addObject("saved-noise").seal().build()); + index.claimAndPersist(ChunkGenerationSemantics.builder(32, 0, 1L).addObject("unsaved-terrain").seal().build()); + + GenerationHistory recovered = GenerationHistory.open(world); + recovered.prepareCurrentGenerator(64); + + assertTrue(recovered.semantics(0, 0).isPresent()); + assertTrue(recovered.semantics(32, 0).isEmpty()); + assertEquals("minecraft:biomes", SavedTerrainChunk.readStatus(world, 32, 0)); + assertEquals(0, recovered.explicitChunkCount()); + GenerationHistory reopened = GenerationHistory.open(world); + assertTrue(reopened.semantics(0, 0).isPresent()); + assertTrue(reopened.semantics(32, 0).isEmpty()); + } + + @Test + public void coldRecoveryKeepsStoredClaimsAndHistoricalMetadataButReleasesOldPacks() throws Exception { + Path world = temporaryFolder.newFolder("cold-recovery-world").toPath(); + Path packA = createPack("cold-recovery-a", "alpha"); + Path packB = createPack("cold-recovery-b", "beta"); + GenerationHistory initial = createHistory(world, packA); + String oldEpoch = initial.activeEpoch().epochId(); + Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca"); + writeRegion(region, new int[][]{{0, 0}}); + GenerationSemanticIndex initialIndex = GenerationSemanticIndex.loadRequired(world); + initialIndex.claimAndPersist(ChunkGenerationSemantics.builder(0, 0, 1L).addObject("historical").seal().build()); + GenerationHistory first = GenerationHistory.open(world); + stage(first, packB); + first.promotePending(signaturesForChunks(new int[][]{{0, 0}})); + GenerationSemanticIndex index = GenerationSemanticIndex.loadRequired(world); + index.claimAndPersist(ChunkGenerationSemantics.builder(1, 0, 2L).addObject("saved").seal().build()); + index.claimAndPersist(ChunkGenerationSemantics.builder(2, 0, 2L).addObject("orphan").seal().build()); + writeRegion(region, new int[][]{{1, 0}}); + + GenerationHistory recovered = GenerationHistory.open(world); + recovered.prepareCurrentGenerator(64); + + assertEquals(2L, recovered.activeActivation().activationId()); + assertEquals(1, recovered.explicitChunkCount()); + assertTrue(recovered.semantics(0, 0).isPresent()); + assertTrue(recovered.semantics(1, 0).isPresent()); + assertTrue(recovered.semantics(2, 0).isEmpty()); + assertFalse(Files.exists(recovered.paths().packRoot(oldEpoch))); + assertTrue(Files.isDirectory(recovered.activePackRoot())); + assertTrue(Files.isDirectory(packA)); + GenerationHistory reopened = GenerationHistory.open(world); + assertEquals(1L, reopened.resolveActivation(0, 0).activationId()); + assertTrue(reopened.semantics(2, 0).isEmpty()); } @Test @@ -773,17 +874,10 @@ public final class GenerationHistoryTest { new short[]{0, 0} ) ) - ); + , BoundaryColumnGeometry.empty()); } private static void writeRegion(Path file, int[][] chunks) throws IOException { - try (RandomAccessFile output = new RandomAccessFile(file.toFile(), "rw")) { - output.setLength((long) (2 + chunks.length) * SECTOR_BYTES); - for (int index = 0; index < chunks.length; index++) { - int chunkIndex = MCAFile.getChunkIndex(chunks[index][0], chunks[index][1]); - output.seek((long) chunkIndex * Integer.BYTES); - output.writeInt((2 + index) << Byte.SIZE | 1); - } - } + SavedTerrainTestRegion.write(file, chunks); } } diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationKernelCatalogTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationKernelCatalogTest.java index 31981f692..dd4a4698c 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/GenerationKernelCatalogTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationKernelCatalogTest.java @@ -11,7 +11,7 @@ import static org.junit.Assert.assertThrows; public class GenerationKernelCatalogTest { @Test - public void appendsNewRegistrationsWithoutChangingAnExistingSeal() throws Exception { + public void parsesMultipleKernelRegistrations() throws Exception { GenerationKernelCatalog.Catalog catalog = parse("current\t2\t3\t4\nkernel\t1\nkernel\t2\n"); assertEquals(new GenerationKernelRegistry.Version(2, 3, 4), catalog.current()); assertEquals(List.of(1, 2), catalog.abis()); @@ -26,11 +26,11 @@ public class GenerationKernelCatalogTest { } @Test - public void installedCatalogUsesOnlyItsSealedFactoryAndAlgorithms() throws Exception { + public void installedCatalogUsesItsBuildFactoryAndAlgorithms() throws Exception { GenerationKernelRegistry registry = GenerationKernelCatalog.load(); - GenerationKernelSourceSeal.Descriptor seal = GenerationKernelSourceSeal.load(1); - assertEquals("art.arcane.iris.engine.history.GenerationKernelV1", seal.factoryClass()); - assertEquals(seal.fingerprint(), registry.select(new GenerationKernelRegistry.Version(1, 1, 1)) + GenerationBuildRevision.Descriptor revision = GenerationBuildRevision.load(1); + assertEquals("art.arcane.iris.engine.history.GenerationKernelV1", revision.factoryClass()); + assertEquals(revision.fingerprint(), registry.select(new GenerationKernelRegistry.Version(1, 1, 1)) .implementationFingerprint()); assertThrows(IOException.class, () -> registry.select(new GenerationKernelRegistry.Version(1, 2, 1))); } diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationKernelSourceSealTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationKernelSourceSealTest.java deleted file mode 100644 index 07fbbb256..000000000 --- a/core/src/test/java/art/arcane/iris/engine/history/GenerationKernelSourceSealTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package art.arcane.iris.engine.history; - -import org.junit.Test; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertThrows; - -public class GenerationKernelSourceSealTest { - @Test - public void fingerprintUsesTheCompleteManifest() throws Exception { - byte[] initial = seal("a".repeat(64)); - assertEquals(GenerationKernelSourceSeal.fingerprint(1, "example.KernelV1", initial), - GenerationKernelSourceSeal.fingerprint(1, "example.KernelV1", initial)); - assertNotEquals(GenerationKernelSourceSeal.fingerprint(1, "example.KernelV1", initial), - GenerationKernelSourceSeal.fingerprint(1, "example.KernelV1", seal("b".repeat(64)))); - } - - @Test - public void rejectsMismatchedAbiFactoryAndMalformedHashes() { - assertThrows(IOException.class, - () -> GenerationKernelSourceSeal.fingerprint(2, "example.KernelV1", seal("a".repeat(64)))); - assertThrows(IOException.class, - () -> GenerationKernelSourceSeal.fingerprint(1, "example.OtherKernel", seal("a".repeat(64)))); - assertThrows(IOException.class, - () -> GenerationKernelSourceSeal.fingerprint(1, "example.KernelV1", seal("not-a-hash"))); - assertThrows(IOException.class, - () -> GenerationKernelSourceSeal.fingerprint(1, "example.KernelV1", new byte[0])); - } - - @Test - public void rejectsDuplicateAndUnsortedEntries() { - String source = new String(seal("a".repeat(64)), StandardCharsets.UTF_8); - String duplicate = source + "dependency\tmath\t" + "a".repeat(64) + "\n"; - assertThrows(IOException.class, () -> GenerationKernelSourceSeal.fingerprint(1, - "example.KernelV1", duplicate.getBytes(StandardCharsets.UTF_8))); - String unordered = source + "scope\tother\n"; - assertThrows(IOException.class, () -> GenerationKernelSourceSeal.fingerprint(1, - "example.KernelV1", unordered.getBytes(StandardCharsets.UTF_8))); - } - - @Test - public void standardKernelUsesThePackagedSourceSeal() { - assertEquals(GenerationKernelSourceSeal.requireFingerprint(1, GenerationKernelV1.class), - GenerationKernelV1.IMPLEMENTATION_FINGERPRINT); - } - - private static byte[] seal(String fingerprint) { - return ("iris-generation-kernel-seal-v1\nabi\t1\nfactory\texample.KernelV1\n" - + "algorithm\t1\t1\nscope\tgeneration\nsource\tgeneration/Noise.java\t" + fingerprint - + "\ndependency\tmath\t" + "c".repeat(64) + "\n").getBytes(StandardCharsets.UTF_8); - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationOrphanRecoveryTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationOrphanRecoveryTest.java new file mode 100644 index 000000000..11599ad41 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationOrphanRecoveryTest.java @@ -0,0 +1,61 @@ +package art.arcane.iris.engine.history; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.nio.file.Path; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public final class GenerationOrphanRecoveryTest { + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void removesOnlySelectedOwnershipWithoutStoredNativeTerrain() throws Exception { + Path root = temporaryFolder.newFolder("ownership").toPath(); + ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(root); + ownership.assign(-33, -1, 2L); + ownership.assign(-32, -1, 2L); + ownership.assign(0, 0, 1L); + ownership.persist(); + WorldChunkInventory stored = WorldChunkInventory.ofPackedChunks(ChunkGenerationOwnership.packChunk(-32, -1)); + + assertEquals(1, ownership.discardUnstoredClaims(stored, Set.of(2L))); + ChunkGenerationOwnership reopened = ChunkGenerationOwnership.load(root); + assertFalse(reopened.isExplicitlyAssigned(-33, -1)); + assertTrue(reopened.isExplicitlyAssigned(-32, -1)); + assertEquals(1L, reopened.explicitActivation(0, 0)); + assertEquals(2, reopened.explicitChunkCount()); + assertEquals(0, reopened.discardUnstoredClaims(stored, Set.of(2L))); + reopened.assign(-33, -1, 3L); + reopened.persist(); + assertEquals(3L, ChunkGenerationOwnership.load(root).explicitActivation(-33, -1)); + } + + @Test + public void journalClaimsCannotResurrectAfterRecoveryAndNewGeneration() throws Exception { + Path root = temporaryFolder.newFolder("semantics").toPath(); + GenerationSemanticIndex index = GenerationSemanticIndex.load(root); + index.claimAndPersist(ChunkGenerationSemantics.builder(-33, -1, 2L).addObject("orphan").seal().build()); + index.claimAndPersist(ChunkGenerationSemantics.builder(-32, -1, 2L).addObject("saved").seal().build()); + index.claimAndPersist(ChunkGenerationSemantics.builder(0, 0, 1L).addObject("older").seal().build()); + WorldChunkInventory stored = WorldChunkInventory.ofPackedChunks(ChunkGenerationOwnership.packChunk(-32, -1)); + + assertEquals(1, index.discardUnstoredClaims(stored, Set.of(2L))); + GenerationSemanticIndex reopened = GenerationSemanticIndex.load(root); + assertTrue(reopened.get(-33, -1).isEmpty()); + assertTrue(reopened.hasSealedClaim(-32, -1, 2L)); + assertTrue(reopened.hasSealedClaim(0, 0, 1L)); + assertEquals(2, reopened.recordCount()); + assertEquals(0, reopened.discardUnstoredClaims(stored, Set.of(2L))); + reopened.claimAndPersist(ChunkGenerationSemantics.builder(-33, -1, 3L).addObject("replacement").seal().build()); + GenerationSemanticIndex replaced = GenerationSemanticIndex.load(root); + assertTrue(replaced.hasSealedClaim(-33, -1, 3L)); + assertEquals(Set.of("replacement"), replaced.get(-33, -1).orElseThrow().objectKeys()); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationPackRepositoryTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationPackRepositoryTest.java index 21415ca0e..53ec9164e 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/GenerationPackRepositoryTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationPackRepositoryTest.java @@ -148,6 +148,48 @@ public class GenerationPackRepositoryTest { } } + @Test + public void releasesOnlyArchivedPacksAndCanRepublishTheirSnapshot() throws Exception { + Path dimensionRoot = temporaryFolder.newFolder("retention-world").toPath(); + Path firstSource = createPack("retention-first", "{\"version\":1}"); + Path secondSource = createPack("retention-second", "{\"version\":2}"); + Path pendingSource = createPack("retention-pending", "{\"version\":3}"); + GenerationPackRepository repository = new GenerationPackRepository(dimensionRoot); + GenerationEpoch first = epoch(fingerprint(firstSource)); + GenerationEpoch second = epoch(fingerprint(secondSource)); + GenerationEpoch pending = epoch(fingerprint(pendingSource)); + repository.publish(first.epochId(), first.packFingerprint(), first.packFingerprintVersion(), firstSource); + GenerationHistoryStore store = GenerationHistoryStore.initialize(repository.generationRoot(), first); + repository.publish(second.epochId(), second.packFingerprint(), second.packFingerprintVersion(), secondSource); + GenerationActivation activation = store.preparePendingActivation(second, 256); + store.completePendingTransition(activation.activationId(), digest('d'), digest('e')); + store.activatePending(activation.activationId()); + repository.publish(pending.epochId(), pending.packFingerprint(), pending.packFingerprintVersion(), pendingSource); + store.preparePendingActivation(pending, 256); + + repository.releaseArchivedPacks(store.manifest()); + repository.releaseArchivedPacks(store.manifest()); + + assertFalse(Files.exists(repository.packRoot(first.epochId()))); + assertTrue(Files.isRegularFile(repository.epochRoot(first.epochId()).resolve("epoch.json"))); + assertTrue(Files.isDirectory(repository.packRoot(second.epochId()))); + assertTrue(Files.isDirectory(repository.packRoot(pending.epochId()))); + assertTrue(Files.isRegularFile(firstSource.resolve("dimensions/main.json"))); + assertEquals(store.manifest(), GenerationHistoryStore.open(repository.generationRoot()).manifest()); + Path republished = repository.publish(first.epochId(), first.packFingerprint(), first.packFingerprintVersion(), firstSource); + assertEquals(Files.readString(firstSource.resolve("dimensions/main.json")), + Files.readString(republished.resolve("dimensions/main.json"))); + } + + private static GenerationEpoch epoch(String fingerprint) { + GenerationEpoch.DimensionContract contract = new GenerationEpoch.DimensionContract( + "overworld", "iris:overworld_type", "NORMAL", "OVERWORLD", 127, + -64, 384, 384, 1D, false, "none", 0, "0".repeat(64), + GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA, digest('c')); + return GenerationEpoch.create(new GenerationEpoch.Spec(fingerprint, GenerationPackFingerprint.CURRENT_VERSION, + 42L, 1, 1, 1, GenerationKernelV1.IMPLEMENTATION_FINGERPRINT, contract, GenerationRegistryContract.empty())); + } + private void assertStorageLinkRejected(String component) throws Exception { Path container = temporaryFolder.newFolder("linked-storage-" + component).toPath(); Path dimensionRoot = container.resolve("dimension"); diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationRegistryContractTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationRegistryContractTest.java index 502ce1645..3debbd347 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/GenerationRegistryContractTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationRegistryContractTest.java @@ -7,6 +7,7 @@ import org.junit.Test; import java.util.LinkedHashMap; import java.util.Map; +import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; @@ -38,7 +39,7 @@ public class GenerationRegistryContractTest { ); assertEquals( - "b9165819aa1cdf4e7024a85ca1b63801c3cfaaefb1898de7fd83b0adf366b16e", + "bf3d52711738c90740c8ac35d7462041625b42705cc0ce7dd82dbd59e0a8244a", contract.fingerprint() ); assertEquals(desert, contract.definitions().firstKey()); @@ -49,6 +50,21 @@ public class GenerationRegistryContractTest { ); } + @Test + public void biomeTagsAreCanonicalPersistedAndFingerprintProtected() { + GenerationRegistryContract base = GenerationRegistryContract.fromDefinitions( + Map.of(key("minecraft:worldgen/biome", "iris:old"), DEFINITION_A)); + GenerationRegistryContract tagged = base.withBiomeTags(Map.of("iris:old", + List.of("minecraft:is_overworld", "iris:humid", "iris:humid"))); + assertEquals(List.of("iris:humid", "minecraft:is_overworld"), tagged.biomeTags().get("iris:old")); + assertNotEquals(base.fingerprint(), tagged.fingerprint()); + assertEquals(tagged, GenerationRegistryContract.fromJson(tagged.toJson())); + JsonObject tampered = tagged.toJson(); + tampered.getAsJsonObject("biomeTags").getAsJsonArray("iris:old").add("iris:new"); + assertThrows(IllegalArgumentException.class, () -> GenerationRegistryContract.fromJson(tampered)); + assertThrows(IllegalArgumentException.class, () -> base.withBiomeTags(Map.of("iris:missing", List.of("iris:humid")))); + } + @Test public void derivesStableFingerprintIndependentOfMapIterationOrder() { GenerationRegistryContract.PhysicalResourceKey plains = key( diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationSemanticCaptureTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationSemanticCaptureTest.java new file mode 100644 index 000000000..a0dd420eb --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationSemanticCaptureTest.java @@ -0,0 +1,147 @@ +package art.arcane.iris.engine.history; + +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.IrisEngine; +import art.arcane.iris.core.nms.container.BlockPos; +import art.arcane.iris.core.nms.container.Pair; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.StructurePlacementMarker; +import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.volmlib.util.function.Consumer4; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterStructurePOI; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.MockedStatic; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +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.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public final class GenerationSemanticCaptureTest { + @BeforeClass + public static void initializeMantleBlockState() throws Exception { + PlatformBlockState air = mock(PlatformBlockState.class); + try (MockedStatic blocks = mockStatic(B.class)) { + blocks.when(() -> B.getState("AIR")).thenReturn(air); + Class.forName(EngineMantle.class.getName()); + } + } + + @Test + public void captureKeepsWorldPoiCoordinatesAndInternalHeight() { + Fixture fixture = new Fixture(); + doAnswer(invocation -> { + Consumer4 consumer = invocation.getArgument(3); + consumer.accept(2, 70, 3, MatterStructurePOI.BURIED_TREASURE); + return null; + }).when(fixture.mantle).iterateChunk(eq(-2), eq(3), eq(MatterStructurePOI.class), any()); + doAnswer(invocation -> { + Consumer4 consumer = invocation.getArgument(3); + consumer.accept(2, 70, 3, StructurePlacementMarker.encodeStructure("iris:old_chest", 3, "iris:ruin")); + return null; + }).when(fixture.mantle).iterateChunk(eq(-2), eq(3), eq(String.class), any()); + + ChunkGenerationSemantics captured = GenerationSemanticCapture.capture(fixture.engine, -2, 3, 5L); + + assertEquals(Set.of(new ChunkGenerationSemantics.PointOfInterest("buried_treasure", + new ChunkGenerationSemantics.BlockPosition(-30, 70, 51))), captured.pointsOfInterest()); + assertEquals(Set.of("iris:old_chest"), captured.objectKeys()); + assertTrue(captured.sealed()); + } + + @Test + public void caveSpaceRejectsCavesAndHydrologyFactsWhereReconciledTerrainIsSolid() { + Fixture fixture = new Fixture(); + doAnswer(invocation -> { + Consumer4 consumer = invocation.getArgument(3); + consumer.accept(1, 10, 2, new MatterCavern(true, "iris:open", (byte) 0)); + consumer.accept(3, 11, 4, new MatterCavern(true, "iris:filled", (byte) 0)); + return null; + }).when(fixture.mantle).iterateChunk(eq(-2), eq(3), eq(MatterCavern.class), any()); + HydrologyCaveCell filled = mock(HydrologyCaveCell.class); + doAnswer(invocation -> { + Consumer4 consumer = invocation.getArgument(3); + consumer.accept(5, 12, 6, filled); + return null; + }).when(fixture.mantle).iterateChunk(eq(-2), eq(3), eq(HydrologyCaveCell.class), any()); + GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class); + GenerationActivation activation = mock(GenerationActivation.class); + when(activation.activationId()).thenReturn(5L); + when(stage.activation()).thenReturn(activation); + when(stage.chunkX()).thenReturn(-2); + when(stage.chunkZ()).thenReturn(3); + List checked = new ArrayList<>(); + + ChunkGenerationSemantics captured = GenerationSemanticCapture.capture(fixture.engine, stage, (x, y, z) -> { + checked.add(new ChunkGenerationSemantics.BlockPosition(x, y, z)); + return x == 1 && y == 10 && z == 2; + }); + + assertEquals(Set.of("iris:open"), captured.caveBiomeKeys()); + assertTrue(captured.riverProfileKeys().isEmpty()); + assertEquals(List.of(new ChunkGenerationSemantics.BlockPosition(1, 10, 2), + new ChunkGenerationSemantics.BlockPosition(3, 11, 4), + new ChunkGenerationSemantics.BlockPosition(5, 12, 6)), checked); + verify(filled, never()).fluidProfileKey(); + verify(fixture.engine, never()).getCaveBiome(anyInt(), anyInt(), anyInt()); + } + + @Test + @SuppressWarnings("unchecked") + public void unsealedPoiQueryUsesMantleWithWorldHorizontalCoordinates() { + IrisEngine engine = mock(IrisEngine.class); + GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class); + GenerationHistory history = mock(GenerationHistory.class); + when(router.history()).thenReturn(history); + when(history.semantics(-2, 3)).thenReturn(Optional.of(ChunkGenerationSemantics.builder(-2, 3, 5L) + .addPointOfInterest(new ChunkGenerationSemantics.PointOfInterest("incomplete", + new ChunkGenerationSemantics.BlockPosition(-31, 0, 49))).build())); + when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router)); + EngineMantle engineMantle = mock(EngineMantle.class); + Mantle mantle = mock(Mantle.class); + when(engine.getMantle()).thenReturn(engineMantle); + when(engineMantle.getMantle()).thenReturn(mantle); + doAnswer(invocation -> { + Consumer4 consumer = invocation.getArgument(3); + consumer.accept(2, 70, 3, MatterStructurePOI.BURIED_TREASURE); + return null; + }).when(mantle).iterateChunk(eq(-2), eq(3), eq(MatterStructurePOI.class), any()); + doCallRealMethod().when(engine).getPOIsAt(anyInt(), anyInt()); + + assertEquals(Set.of(new Pair<>("buried_treasure", new BlockPos(-30, 70, 51))), engine.getPOIsAt(-2, 3)); + } + + private static final class Fixture { + private final Engine engine = mock(Engine.class); + @SuppressWarnings("unchecked") + private final Mantle mantle = mock(Mantle.class); + + private Fixture() { + IrisComplex complex = mock(IrisComplex.class); + EngineMantle engineMantle = mock(EngineMantle.class); + when(engine.getComplex()).thenReturn(complex); + when(engine.getMantle()).thenReturn(engineMantle); + when(engineMantle.getMantle()).thenReturn(mantle); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationSemanticIndexTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationSemanticIndexTest.java index 61f4fecb0..d7a8bea96 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/GenerationSemanticIndexTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationSemanticIndexTest.java @@ -60,6 +60,83 @@ public final class GenerationSemanticIndexTest { assertEquals(new ChunkGenerationSemantics.BlockPosition(-520, 72, -7), structure.exactPosition().orElseThrow()); } + @Test + public void packFilenameKeysSurviveJournalCompactionAndExactQueries() throws Exception { + Path root = temporaryFolder.newFolder("pack-filename-keys").toPath(); + GenerationSemanticIndex index = GenerationSemanticIndex.initialize(root); + ChunkGenerationSemantics original = ChunkGenerationSemantics.builder(-33, -1, 5L) + .addObject("trees/mixed/AmySmol10") + .addObject("trees/mixed/amysmol10") + .addSurfaceBiome("mountain/Cute_Cliffs+") + .addRegion("Regions/Highlands") + .addStructure("Structures/Tower+", -520, 72, -7) + .seal().build(); + index.claimAndPersist(original); + GenerationSemanticIndex replayed = GenerationSemanticIndex.load(root); + assertEquals(original, replayed.get(-33, -1).orElseThrow()); + replayed.compactJournals(); + GenerationSemanticIndex compacted = GenerationSemanticIndex.load(root); + assertEquals(original, compacted.get(-33, -1).orElseThrow()); + assertEquals("trees/mixed/AmySmol10", + requiredMatch(compacted, GenerationSemanticIndex.SemanticKind.OBJECT, + "trees/mixed/AmySmol10").key()); + assertEquals("trees/mixed/amysmol10", + requiredMatch(compacted, GenerationSemanticIndex.SemanticKind.OBJECT, + "trees/mixed/amysmol10").key()); + assertEquals("mountain/Cute_Cliffs+", + requiredMatch(compacted, GenerationSemanticIndex.SemanticKind.SURFACE_BIOME, + "mountain/Cute_Cliffs+").key()); + assertEquals("Structures/Tower+", + requiredMatch(compacted, GenerationSemanticIndex.SemanticKind.STRUCTURE, + "Structures/Tower+").key()); + assertTrue(compacted.findNearest(GenerationSemanticIndex.Query.acrossActivations( + GenerationSemanticIndex.SemanticKind.SURFACE_BIOME, "mountain/cute_cliffs+", + new ChunkGenerationSemantics.BlockPosition(-520, 72, -7), 2)).isEmpty()); + } + + @Test + public void pointsOfInterestSurviveJournalReplayAndCompaction() throws Exception { + Path root = temporaryFolder.newFolder("poi-journal").toPath(); + GenerationSemanticIndex index = GenerationSemanticIndex.initialize(root); + ChunkGenerationSemantics original = ChunkGenerationSemantics.builder(-2, 3, 5L) + .addPointOfInterest(new ChunkGenerationSemantics.PointOfInterest("buried_treasure", + new ChunkGenerationSemantics.BlockPosition(-31, 102, 55))) + .addPointOfInterest(new ChunkGenerationSemantics.PointOfInterest("custom:altar", + new ChunkGenerationSemantics.BlockPosition(-22, 78, 61))) + .seal().build(); + index.claimAndPersist(original); + GenerationSemanticIndex replayed = GenerationSemanticIndex.load(root); + assertEquals(original, replayed.get(-2, 3).orElseThrow()); + replayed.compactJournals(); + assertEquals(original, GenerationSemanticIndex.load(root).get(-2, 3).orElseThrow()); + } + + @Test + public void pointsOfInterestRejectNoncanonicalOrderAndExcessiveCounts() throws Exception { + for (int corruption = 0; corruption < 2; corruption++) { + Path root = temporaryFolder.newFolder("poi-invalid-" + corruption).toPath(); + GenerationSemanticIndex index = GenerationSemanticIndex.initialize(root); + index.recordAndPersist(ChunkGenerationSemantics.builder(0, 0, 1L) + .addPointOfInterest(new ChunkGenerationSemantics.PointOfInterest("buried_treasure", + new ChunkGenerationSemantics.BlockPosition(1, 12, 2))) + .addPointOfInterest(new ChunkGenerationSemantics.PointOfInterest("buried_treasure", + new ChunkGenerationSemantics.BlockPosition(2, 12, 2))) + .seal().build()); + Path shard = onlyShard(index.storageDirectory()); + byte[] encoded = Files.readAllBytes(shard); + int countOffset = encoded.length - Integer.BYTES - Short.BYTES - 2 * (Short.BYTES + 3 * Integer.BYTES); + if (corruption == 0) { + System.arraycopy(encoded, countOffset + Short.BYTES, encoded, + countOffset + Short.BYTES + 14, 14); + } else { + ByteBuffer.wrap(encoded).putShort(countOffset, (short) (ChunkGenerationSemantics.MAX_STRUCTURES + 1)); + } + rewriteChecksum(encoded); + replaceReferencedShard(root, shard, encoded); + assertThrows(IOException.class, () -> GenerationSemanticIndex.load(root).forEachRecord(record -> { })); + } + } + @Test public void stagedFactsMergeMonotonicallyAndSealDurably() throws Exception { Path dimensionRoot = temporaryFolder.newFolder("staged").toPath(); @@ -370,13 +447,13 @@ public final class GenerationSemanticIndexTest { Path shard = onlyShard(dimensionRoot.resolve("iris/generation/semantics")); byte[] encoded = Files.readAllBytes(shard); encoded[4] = 0; - encoded[5] = 6; + encoded[5] = 7; rewriteChecksum(encoded); replaceReferencedShard(dimensionRoot, shard, encoded); IOException error = assertThrows(IOException.class, () -> GenerationSemanticIndex.load(dimensionRoot)); - assertTrue(error.getMessage().contains("unsupported format version 6")); + assertTrue(error.getMessage().contains("unsupported format version 7")); } @Test @@ -639,6 +716,8 @@ public final class GenerationSemanticIndexTest { 72, (chunkZ << 4) + 9 ) + .addPointOfInterest(new ChunkGenerationSemantics.PointOfInterest("buried_treasure", + new ChunkGenerationSemantics.BlockPosition((chunkX << 4) + 2, 91, (chunkZ << 4) + 5))) .seal() .build(); } diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationSemanticQueriesTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationSemanticQueriesTest.java index 22be83934..a38b6c5a5 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/GenerationSemanticQueriesTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationSemanticQueriesTest.java @@ -63,7 +63,7 @@ public final class GenerationSemanticQueriesTest { assertTrue(filter.accept(3, 0)); return nearer; }); - assertSame(nearer, GenerationSemanticQueries.nearestStructure(engine, "OLD-TOWER", 0, 0, 20)); + assertSame(nearer, GenerationSemanticQueries.nearestStructure(engine, "old-tower", 0, 0, 20)); structures.when(() -> IrisStructureLocator.locate( eq(engine), eq("old-tower"), eq(0), eq(0), eq(20), any())) @@ -85,6 +85,32 @@ public final class GenerationSemanticQueriesTest { } } + @Test + public void structureQueryPreservesTheRecordedPackIdentifier() { + IrisEngine engine = mock(IrisEngine.class); + GenerationHistory history = mock(GenerationHistory.class); + GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class); + when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router)); + when(router.history()).thenReturn(history); + GenerationSemanticIndex.Match recorded = new GenerationSemanticIndex.Match( + GenerationSemanticIndex.SemanticKind.STRUCTURE, "Structures/Tower+", + new GenerationSemanticIndex.ChunkReference(1, 0, 1L), + Optional.of(new ChunkGenerationSemantics.BlockPosition(20, 73, 5))); + when(history.findRecorded(any())).thenAnswer(invocation -> { + GenerationSemanticIndex.Query query = invocation.getArgument(0); + assertEquals("Structures/Tower+", query.key()); + return Optional.of(recorded); + }); + try (MockedStatic structures = mockStatic(IrisStructureLocator.class)) { + structures.when(() -> IrisStructureLocator.locate( + eq(engine), eq("Structures/Tower+"), eq(0), eq(0), eq(20), any())) + .thenReturn(new IrisStructureLocator.LocateResult( + IrisStructureLocator.LocateStatus.NOT_FOUND, 0, 0, 0)); + assertEquals(foundStructure(20, 73, 5), + GenerationSemanticQueries.nearestStructure(engine, "Structures/Tower+", 0, 0, 20)); + } + } + @Test public void exhaustedStructureSearchDoesNotClaimARecordedResultIsNearest() { IrisEngine engine = mock(IrisEngine.class); diff --git a/core/src/test/java/art/arcane/iris/engine/history/IrisBoundarySignatureSamplerTest.java b/core/src/test/java/art/arcane/iris/engine/history/IrisBoundarySignatureSamplerTest.java index 278c12f6f..7143212c1 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/IrisBoundarySignatureSamplerTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/IrisBoundarySignatureSamplerTest.java @@ -1,95 +1,76 @@ package art.arcane.iris.engine.history; -import art.arcane.iris.engine.DimensionStackContext; -import art.arcane.iris.engine.DimensionStackLayout; -import art.arcane.iris.engine.DimensionTerrainContext; -import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.IrisEngine; -import art.arcane.iris.engine.UpperDimensionContext; -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisWorld; -import art.arcane.iris.util.project.stream.ProceduralStream; -import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.engine.framework.EnginePlatformHooks; import org.junit.Test; -import java.util.Optional; -import java.util.OptionalInt; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyDouble; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.same; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; 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 IrisBoundarySignatureSamplerTest { @Test - public void recordsTheEffectiveCeilingInInternalCoordinatesAndBiomesInWorldCoordinates() throws Exception { - IrisEngine engine = engine(); - UpperDimensionContext upper = mock(UpperDimensionContext.class); - when(engine.getUpperContext()).thenReturn(upper); - when(upper.getEffectiveSurfaceY(-17, 8)).thenReturn(200); - - TerrainBoundarySignature signature = IrisBoundarySignatureSampler.INSTANCE.sample(engine, -17, 8); - - assertEquals(OptionalInt.of(55), signature.upperCeilingDepth()); - assertEquals(64, signature.oceanFloorHeight()); - assertEquals(-64, signature.sampleY(0)); - assertEquals(188, signature.sampleY(63)); - verify(upper).getEffectiveSurfaceY(-17, 8); - } - - @Test - public void recordsAbsentUpperMassWithAndWithoutAnUpperContext() throws Exception { - IrisEngine engine = engine(); - assertTrue(IrisBoundarySignatureSampler.INSTANCE.sample(engine, -17, 8).upperCeilingDepth().isEmpty()); - UpperDimensionContext upper = mock(UpperDimensionContext.class); - when(engine.getUpperContext()).thenReturn(upper); - when(upper.getEffectiveSurfaceY(-17, 8)).thenReturn(256); - assertTrue(IrisBoundarySignatureSampler.INSTANCE.sample(engine, -17, 8).upperCeilingDepth().isEmpty()); - } - - @Test - public void recordsTheOwningStackLayerBiomeAtEachVerticalSample() throws Exception { - IrisEngine engine = engine(); - IrisComplex complex = engine.getComplex(); - when(complex.historicalPhysicalBiomeKeyAt(anyInt(), anyInt(), anyInt())) - .thenReturn(Optional.empty()); - DimensionStackContext stackContext = mock(DimensionStackContext.class); - DimensionStackLayout layout = mock(DimensionStackLayout.class); - DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class); - DimensionTerrainContext terrainContext = mock(DimensionTerrainContext.class); - IrisBiome biome = mock(IrisBiome.class); - when(engine.getDimensionStackContext()).thenReturn(stackContext); - when(stackContext.getLayout(-17, 8)).thenReturn(layout); - when(layout.layerAt(anyInt())).thenReturn(layer); - when(layer.terrainContext()).thenReturn(terrainContext); - when(layer.biome()).thenReturn(biome); - when(biome.getSkyBiomeKey(any(RNG.class), same(engine), anyDouble(), anyDouble(), anyDouble())) - .thenReturn("minecraft:forest"); - - TerrainBoundarySignature signature = IrisBoundarySignatureSampler.INSTANCE.sample(engine, -17, 8); - - assertEquals("minecraft:forest", signature.biomeAtSample(0)); - assertEquals("minecraft:forest", signature.biomeAtSample(63)); - } - - private static IrisEngine engine() { + public void capturesActualPlatformChunkOnceAndCheckpointsAfterSampling() throws Exception { IrisEngine engine = mock(IrisEngine.class); - IrisComplex complex = mock(IrisComplex.class); - when(engine.getComplex()).thenReturn(complex); - IrisWorld world = mock(IrisWorld.class); - when(world.getRawWorldSeed()).thenReturn(1337L); - when(engine.getWorld()).thenReturn(world); - when(engine.getHeight()).thenReturn(256); - when(engine.getMinHeight()).thenReturn(-64); - when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 64D)); - when(complex.getRiverWaterSurfaceStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 64D)); - when(complex.historicalPhysicalBiomeKeyAt(anyInt(), anyInt(), anyInt())) - .thenReturn(Optional.of("minecraft:plains")); - return engine; + EnginePlatformHooks hooks = mock(EnginePlatformHooks.class); + when(engine.getPlatformHooks()).thenReturn(hooks); + SavedTerrainChunk terrain = terrain(); + when(hooks.captureSavedTerrainChunk(engine, -2, 0)).thenReturn(CompletableFuture.completedFuture(terrain)); + when(hooks.flushSavedTerrainCapture(engine)).thenReturn(CompletableFuture.completedFuture(null)); + try (TerrainBoundarySignatureStore.SignatureSampler capture = IrisBoundarySignatureSampler.INSTANCE.open(engine)) { + assertSame(terrain.column(-17, 8), capture.sample(-17, 8)); + assertEquals("example:physical", capture.sample(-32, 8).biomeAtSample(0)); + verify(hooks, never()).flushSavedTerrainCapture(engine); + } + verify(hooks).captureSavedTerrainChunk(engine, -2, 0); + verify(hooks).flushSavedTerrainCapture(engine); + verify(engine, never()).getComplex(); + } + + @Test + public void platformCaptureAndSaveFailuresReachTheCaller() throws Exception { + IrisEngine engine = mock(IrisEngine.class); + EnginePlatformHooks hooks = mock(EnginePlatformHooks.class); + when(engine.getPlatformHooks()).thenReturn(hooks); + when(hooks.captureSavedTerrainChunk(engine, -2, 0)) + .thenReturn(CompletableFuture.failedFuture(new IOException("capture failure"))); + when(hooks.flushSavedTerrainCapture(engine)) + .thenReturn(CompletableFuture.failedFuture(new IOException("save failure"))); + TerrainBoundarySignatureStore.SignatureSampler capture = IrisBoundarySignatureSampler.INSTANCE.open(engine); + assertThrows(IOException.class, () -> capture.sample(-17, 8)); + assertThrows(IOException.class, capture::close); + verify(hooks).flushSavedTerrainCapture(engine); + } + + @Test + public void neverWaitsOnThePlatformTickThread() throws Exception { + IrisEngine engine = mock(IrisEngine.class); + EnginePlatformHooks hooks = mock(EnginePlatformHooks.class); + when(engine.getPlatformHooks()).thenReturn(hooks); + when(hooks.isMainThread()).thenReturn(true); + assertThrows(IOException.class, () -> IrisBoundarySignatureSampler.INSTANCE.open(engine)); + assertThrows(IOException.class, () -> IrisBoundarySignatureSampler.checkpoint(engine)); + verify(hooks, never()).flushSavedTerrainCapture(engine); + } + + private static SavedTerrainChunk terrain() throws IOException { + return SavedTerrainChunk.captureBoundary(-2, 0, -16, 16, "minecraft:noise", new SavedTerrainChunk.VoxelSource() { + @Override + public BoundaryColumnGeometry.Voxel voxel(int localX, int y, int localZ) { + return new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false); + } + + @Override + public String biome(int localX, int y, int localZ) { + return "example:physical"; + } + }); } } diff --git a/core/src/test/java/art/arcane/iris/engine/history/SavedTerrainChunkTest.java b/core/src/test/java/art/arcane/iris/engine/history/SavedTerrainChunkTest.java new file mode 100644 index 000000000..4210546ae --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/history/SavedTerrainChunkTest.java @@ -0,0 +1,180 @@ +package art.arcane.iris.engine.history; + +import art.arcane.volmlib.util.nbt.io.NBTUtil; +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 org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.GZIPOutputStream; + +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 final class SavedTerrainChunkTest { + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void readsModernPaletteAtNegativeCoordinatesWithEveryCompression() throws Exception { + for (int compression = 1; compression <= 4; compression++) { + Path world = temporaryFolder.newFolder("compression-" + compression).toPath(); + writeChunk(world, root("minecraft:full"), compression, false); + SavedTerrainChunk chunk = SavedTerrainChunk.read(world, -1, -2, -16, 16); + assertEquals("minecraft:full", chunk.nativeStatus()); + TerrainBoundarySignature column = chunk.column(-16, -32); + assertEquals("minecraft:stone", column.geometry().voxelAt(-16).stateKey()); + assertEquals("minecraft:water[level=0]", column.geometry().voxelAt(-15).stateKey()); + assertEquals(BoundaryColumnGeometry.Phase.FLUID, column.geometry().voxelAt(-15).phase()); + assertEquals("example:saved_biome", column.biomeAtSample(0)); + assertFalse(chunk.hasColumn(1, 1)); + } + } + + @Test + public void externalChunkAndPartialNativeStagePreserveAvailableTerrain() throws Exception { + Path world = temporaryFolder.newFolder("external").toPath(); + writeChunk(world, root("minecraft:noise"), 2, true); + assertEquals("minecraft:noise", SavedTerrainChunk.readStatus(world, -1, -2)); + assertTrue(SavedTerrainChunk.hasTerrain("minecraft:noise")); + assertFalse(SavedTerrainChunk.isComplete("minecraft:noise")); + assertFalse(SavedTerrainChunk.hasTerrain("minecraft:biomes")); + assertEquals("minecraft:noise", SavedTerrainChunk.read(world, -1, -2, -16, 16).nativeStatus()); + } + + @Test + public void verifiesPreTerrainStructureStampInNativePayload() throws Exception { + for (boolean bukkit : new boolean[]{false, true}) { + Path world = temporaryFolder.newFolder("structures-" + bukkit).toPath(); + CompoundTag saved = root("minecraft:structure_starts"); + CompoundTag metadata = saved; + if (bukkit) { + metadata = new CompoundTag(); + saved.put("ChunkBukkitValues", metadata); + } + metadata.putLong(NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY, 7); + writeChunk(world, saved, 2, false); + SavedTerrainChunk.verifyCheckpoint(world, -1, -2, "minecraft:structure_starts", null, 7); + assertThrows(IOException.class, () -> SavedTerrainChunk.verifyCheckpoint( + world, -1, -2, "minecraft:structure_starts", null, 8)); + assertFalse(SavedTerrainChunk.hasTerrain(SavedTerrainChunk.readStatus(world, -1, -2))); + } + } + + @Test + public void receiptsPreserveNaturalBoundaryAfterSavedBlocksChange() throws Exception { + Path world = temporaryFolder.newFolder("receipt").toPath(); + CompoundTag initial = root("minecraft:full"); + writeChunk(world, initial, 2, false); + SavedTerrainChunk natural = SavedTerrainChunk.read(world, -1, -2, -16, 16); + byte[] receipt = NativeTerrainReceipt.encode(natural, 3, "test-epoch"); + NativeTerrainReceipt.Decoded decoded = NativeTerrainReceipt.decode(receipt, "minecraft:full"); + assertEquals(3, decoded.activationId()); + assertEquals("test-epoch", decoded.epochId()); + assertFalse(decoded.terrain().hasColumn(3, 3)); + CompoundTag bukkitValues = new CompoundTag(); + bukkitValues.putByteArray(NativeTerrainReceipt.NBT_KEY, receipt); + initial.put("ChunkBukkitValues", bukkitValues); + initial.remove("sections"); + writeChunk(world, initial, 2, false); + SavedTerrainChunk restored = SavedTerrainChunk.read(world, -1, -2, -16, 16); + assertEquals(natural.column(-16, -32).geometry(), restored.column(-16, -32).geometry()); + assertThrows(IOException.class, () -> NativeTerrainReceipt.restore(receipt, "minecraft:full", 1, 2, -16, 16)); + assertThrows(IOException.class, () -> NativeTerrainReceipt.restore(receipt, "minecraft:full", -1, -2, 0, 16)); + receipt[receipt.length - 5] ^= 1; + assertThrows(IOException.class, () -> NativeTerrainReceipt.decode(receipt, "minecraft:full")); + } + + @Test + public void rejectsMissingBiomesAndInvalidPaletteIndices() throws Exception { + Path world = temporaryFolder.newFolder("invalid").toPath(); + CompoundTag root = root("minecraft:full"); + CompoundTag section = (CompoundTag) root.getListTag("sections").get(0); + section.remove("biomes"); + writeChunk(world, root, 2, false); + assertThrows(IOException.class, () -> SavedTerrainChunk.read(world, -1, -2, -16, 16)); + root = root("minecraft:full"); + section = (CompoundTag) root.getListTag("sections").get(0); + long[] data = section.getCompoundTag("block_states").getLongArray("data"); + data[0] = 15; + writeChunk(world, root, 2, false); + assertThrows(IOException.class, () -> SavedTerrainChunk.read(world, -1, -2, -16, 16)); + } + + private static CompoundTag root(String status) { + CompoundTag root = new CompoundTag(); + root.putInt("xPos", -1); + root.putInt("zPos", -2); + root.putString("Status", status); + CompoundTag section = new CompoundTag(); + section.putByte("Y", (byte) -1); + CompoundTag blocks = new CompoundTag(); + ListTag palette = new ListTag<>(CompoundTag.class); + CompoundTag stone = new CompoundTag(); + stone.putString("Name", "minecraft:stone"); + palette.add(stone); + CompoundTag water = new CompoundTag(); + water.putString("Name", "minecraft:water"); + CompoundTag properties = new CompoundTag(); + properties.putString("level", "0"); + water.put("Properties", properties); + palette.add(water); + blocks.put("palette", palette); + long[] data = new long[256]; + for (int index = 16; index < 32; index++) { + data[index] = 0x1111111111111111L; + } + blocks.putLongArray("data", data); + section.put("block_states", blocks); + CompoundTag biomes = new CompoundTag(); + ListTag biomePalette = new ListTag<>(StringTag.class); + biomePalette.add(new StringTag("example:saved_biome")); + biomes.put("palette", biomePalette); + section.put("biomes", biomes); + ListTag sections = new ListTag<>(CompoundTag.class); + sections.add(section); + root.put("sections", sections); + return root; + } + + private static void writeChunk(Path world, CompoundTag root, int compression, boolean external) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (OutputStream output = switch (compression) { + case 1 -> new GZIPOutputStream(bytes); + case 2 -> new DeflaterOutputStream(bytes); + case 4 -> new LZ4BlockOutputStream(bytes); + default -> bytes; + }) { + NBTUtil.write(root, output, false); + } + byte[] payload = bytes.toByteArray(); + Path region = Files.createDirectories(world.resolve("region")); + try (RandomAccessFile file = new RandomAccessFile(region.resolve("r.-1.-1.mca").toFile(), "rw")) { + int sectors = external ? 1 : Math.ceilDiv(payload.length + 5, 4096); + file.setLength((2L + sectors) * 4096); + file.seek((30L * 32 + 31) * 4); + file.writeInt((2 << 8) | sectors); + file.seek(8192); + file.writeInt(external ? 1 : payload.length + 1); + file.writeByte(compression | (external ? 128 : 0)); + if (!external) { + file.write(payload); + } + } + if (external) { + Files.write(region.resolve("c.-1.-2.mcc"), payload); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/history/SavedTerrainTestRegion.java b/core/src/test/java/art/arcane/iris/engine/history/SavedTerrainTestRegion.java new file mode 100644 index 000000000..05f44bb09 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/history/SavedTerrainTestRegion.java @@ -0,0 +1,88 @@ +package art.arcane.iris.engine.history; + +import art.arcane.iris.util.nbt.common.mca.MCAFile; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.file.Path; + +final class SavedTerrainTestRegion { + private SavedTerrainTestRegion() { + } + + static void write(Path file, int[][] chunks) throws IOException { + write(file, chunks, "minecraft:full"); + } + + static void write(Path file, int[][] chunks, String status) throws IOException { + String[] location = file.getFileName().toString().split("\\."); + int regionX = Integer.parseInt(location[1]); + int regionZ = Integer.parseInt(location[2]); + try (RandomAccessFile output = new RandomAccessFile(file.toFile(), "rw")) { + output.setLength(0L); + output.setLength((long) (2 + chunks.length) * 4096); + for (int index = 0; index < chunks.length; index++) { + int localX = chunks[index][0] & 31; + int localZ = chunks[index][1] & 31; + byte[] chunk = chunk(regionX * 32 + localX, regionZ * 32 + localZ, status); + int chunkIndex = MCAFile.getChunkIndex(localX, localZ); + output.seek((long) chunkIndex * Integer.BYTES); + output.writeInt((2 + index) << Byte.SIZE | 1); + output.seek((long) (2 + index) * 4096); + output.writeInt(chunk.length + 1); + output.writeByte(3); + output.write(chunk); + } + } + } + + private static byte[] chunk(int x, int z, String status) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (DataOutputStream output = new DataOutputStream(bytes)) { + output.writeByte(10); + output.writeUTF(""); + output.writeByte(3); + output.writeUTF("xPos"); + output.writeInt(x); + output.writeByte(3); + output.writeUTF("zPos"); + output.writeInt(z); + output.writeByte(8); + output.writeUTF("Status"); + output.writeUTF(status); + output.writeByte(9); + output.writeUTF("sections"); + output.writeByte(10); + output.writeInt(24); + for (int section = -4; section < 20; section++) { + output.writeByte(1); + output.writeUTF("Y"); + output.writeByte(section); + output.writeByte(10); + output.writeUTF("block_states"); + output.writeByte(9); + output.writeUTF("palette"); + output.writeByte(10); + output.writeInt(1); + output.writeByte(8); + output.writeUTF("Name"); + output.writeUTF("minecraft:air"); + output.writeByte(0); + output.writeByte(0); + output.writeByte(10); + output.writeUTF("biomes"); + output.writeByte(9); + output.writeUTF("palette"); + output.writeByte(8); + output.writeInt(1); + output.writeUTF("minecraft:plains"); + output.writeByte(0); + output.writeByte(0); + } + output.writeByte(0); + } + return bytes.toByteArray(); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/history/TerrainBoundarySignatureStoreTest.java b/core/src/test/java/art/arcane/iris/engine/history/TerrainBoundarySignatureStoreTest.java index 554f517b5..4cd09bac1 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/TerrainBoundarySignatureStoreTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/TerrainBoundarySignatureStoreTest.java @@ -38,9 +38,9 @@ public class TerrainBoundarySignatureStoreTest { new TerrainBoundarySignature.VerticalLayout(-64, 4, 0), new TerrainBoundarySignature.BiomeEncoding(List.of(), new short[0])); TerrainBoundarySignature present = new TerrainBoundarySignature( - new TerrainBoundarySignature.Column(-17, 8, 64, 64, OptionalInt.empty(), OptionalInt.of(80)), samples); + new TerrainBoundarySignature.Column(-17, 8, 64, 64, OptionalInt.empty(), OptionalInt.of(80)), samples, BoundaryColumnGeometry.empty()); TerrainBoundarySignature absent = new TerrainBoundarySignature( - new TerrainBoundarySignature.Column(257, 8, 64, 64, OptionalInt.empty(), OptionalInt.empty()), samples); + new TerrainBoundarySignature.Column(257, 8, 64, 64, OptionalInt.empty(), OptionalInt.empty()), samples, BoundaryColumnGeometry.empty()); store.publish(2L, List.of(present, absent)); TerrainBoundarySignatureStore.Snapshot reopened = store.load(2L); @@ -422,7 +422,7 @@ public class TerrainBoundarySignatureStoreTest { OptionalInt.empty() ), new TerrainBoundarySignature.Samples(layout, biomeEncoding) - ); + , BoundaryColumnGeometry.empty()); } private static void assertSignatureEquals( diff --git a/core/src/test/java/art/arcane/iris/engine/history/TerrainBoundarySignatureTest.java b/core/src/test/java/art/arcane/iris/engine/history/TerrainBoundarySignatureTest.java index 7547bf600..2d090e3b4 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/TerrainBoundarySignatureTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/TerrainBoundarySignatureTest.java @@ -24,7 +24,7 @@ public class TerrainBoundarySignatureTest { new TerrainBoundarySignature.VerticalLayout(-64, 32, 3), biomeEncoding ) - ); + , BoundaryColumnGeometry.empty()); palette.set(0, "changed"); biomeIndices[0] = 1; diff --git a/core/src/test/java/art/arcane/iris/engine/history/TransitionFluidContainmentTest.java b/core/src/test/java/art/arcane/iris/engine/history/TransitionFluidContainmentTest.java new file mode 100644 index 000000000..a74152d9b --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/history/TransitionFluidContainmentTest.java @@ -0,0 +1,166 @@ +package art.arcane.iris.engine.history; + +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public final class TransitionFluidContainmentTest { + private static final BoundaryColumnGeometry.Voxel AIR = voxel("minecraft:air", BoundaryColumnGeometry.Phase.AIR); + private static final BoundaryColumnGeometry.Voxel BANK = voxel("minecraft:deepslate", BoundaryColumnGeometry.Phase.SOLID); + private static final BoundaryColumnGeometry.Voxel WATER = voxel("minecraft:water[level=0]", BoundaryColumnGeometry.Phase.FLUID); + + @Test + public void fluidBanksUseTheSameRawHaloAcrossChunkEdgesAndRequestOrder() throws Exception { + TransitionGenerationPlan plan = band(); + SavedTerrainChunk left = raw(0, 0); + SavedTerrainChunk right = raw(1, 0); + TransitionFluidContainment.ColumnSource source = (x, z) -> { + List voxels = new ArrayList<>(16); + for (int y = 0; y < 16; y++) { + voxels.add(y == 0 ? BANK : x < 16 && y <= 4 ? WATER : AIR); + } + return BoundaryColumnGeometry.fromVoxels(0, voxels); + }; + + SavedTerrainChunk leftFirst = TransitionFluidContainment.contain(left, plan, source); + SavedTerrainChunk rightSecond = TransitionFluidContainment.contain(right, plan, source); + SavedTerrainChunk rightFirst = TransitionFluidContainment.contain(right, plan, source); + SavedTerrainChunk leftSecond = TransitionFluidContainment.contain(left, plan, source); + + assertEquals(BANK, leftFirst.column(15, 8).geometry().voxelAt(2)); + assertEquals(WATER, leftFirst.column(14, 8).geometry().voxelAt(2)); + assertEquals(AIR, rightSecond.column(16, 8).geometry().voxelAt(2)); + assertEquals(leftFirst.column(15, 8).geometry(), leftSecond.column(15, 8).geometry()); + assertEquals(rightSecond.column(16, 8).geometry(), rightFirst.column(16, 8).geometry()); + assertEquals(WATER, left.column(15, 8).geometry().voxelAt(2)); + } + + @Test + public void unsupportedFluidGetsABottomWhileOpenFluidSurfacesStayOpen() throws Exception { + SavedTerrainChunk floating = SavedTerrainChunk.capture(0, 0, 0, 16, "minecraft:noise", new SavedTerrainChunk.VoxelSource() { + @Override + public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) { + return y >= 3 && y <= 5 ? WATER : AIR; + } + + @Override + public String biome(int x, int y, int z) { + return "minecraft:plains"; + } + }); + SavedTerrainChunk result = TransitionFluidContainment.contain(floating, band(), + (x, z) -> floating.column(Math.floorMod(x, 16), Math.floorMod(z, 16)).geometry()); + + assertEquals("minecraft:stone", result.column(8, 8).geometry().voxelAt(3).stateKey()); + assertEquals(WATER, result.column(8, 8).geometry().voxelAt(4)); + assertEquals(WATER, result.column(8, 8).geometry().voxelAt(5)); + assertEquals(AIR, result.column(8, 8).geometry().voxelAt(6)); + } + + @Test + public void historicalAndOutsideBandColumnsStayUnchanged() throws Exception { + SavedTerrainChunk raw = raw(0, 0); + TransitionGenerationPlan historical = band(); + when(historical.isHistoricalBlock(anyInt(), anyInt())).thenReturn(true); + TransitionGenerationPlan outside = band(); + when(outside.newEpochWeightAt(anyInt(), anyInt())).thenReturn(1D); + TransitionFluidContainment.ColumnSource forbidden = (x, z) -> { + throw new AssertionError("Immutable columns must not need a halo"); + }; + + assertSame(raw, TransitionFluidContainment.contain(raw, historical, forbidden)); + assertSame(raw, TransitionFluidContainment.contain(raw, outside, forbidden)); + } + + @Test + public void missingHaloFailsInsteadOfTreatingUnknownTerrainAsAir() throws Exception { + assertThrows(IOException.class, () -> TransitionFluidContainment.contain(raw(0, 0), band(), + (x, z) -> { throw new IOException("Missing saved boundary column"); })); + } + + @Test + public void protectedFluidAndWaterloggedStatesArePreserved() throws Exception { + BoundaryColumnGeometry.Voxel protectedWater = new BoundaryColumnGeometry.Voxel( + WATER.stateKey(), WATER.phase(), WATER.fluidStateKey(), true); + BoundaryColumnGeometry.Voxel waterlogged = new BoundaryColumnGeometry.Voxel( + "minecraft:oak_slab[type=bottom,waterlogged=true]", BoundaryColumnGeometry.Phase.SOLID, + "minecraft:water[level=0]", false); + TransitionFluidContainment.ColumnSource forbidden = (x, z) -> { + throw new AssertionError("Protected or waterlogged blocks must not be replaced"); + }; + for (BoundaryColumnGeometry.Voxel voxel : new BoundaryColumnGeometry.Voxel[]{protectedWater, waterlogged}) { + SavedTerrainChunk raw = uniform(voxel); + assertSame(raw, TransitionFluidContainment.contain(raw, band(), forbidden)); + } + } + + @Test + public void unsupportedFluidCannotUseFallingSandAsItsBank() throws Exception { + BoundaryColumnGeometry.Voxel sand = voxel("minecraft:sand", BoundaryColumnGeometry.Phase.SOLID); + SavedTerrainChunk raw = SavedTerrainChunk.capture(0, 0, 0, 16, "minecraft:noise", new SavedTerrainChunk.VoxelSource() { + @Override + public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) { + return y == 0 ? sand : y == 2 ? WATER : AIR; + } + + @Override + public String biome(int x, int y, int z) { + return "minecraft:plains"; + } + }); + SavedTerrainChunk corrected = TransitionFluidContainment.contain(raw, band(), + (x, z) -> raw.column(Math.floorMod(x, 16), Math.floorMod(z, 16)).geometry()); + assertEquals("minecraft:stone", corrected.column(8, 8).geometry().voxelAt(2).stateKey()); + assertEquals(sand, corrected.column(8, 8).geometry().voxelAt(0)); + } + + private static SavedTerrainChunk uniform(BoundaryColumnGeometry.Voxel voxel) throws IOException { + return SavedTerrainChunk.capture(0, 0, 0, 16, "minecraft:noise", new SavedTerrainChunk.VoxelSource() { + @Override + public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) { + return voxel; + } + + @Override + public String biome(int x, int y, int z) { + return "minecraft:plains"; + } + }); + } + + private static TransitionGenerationPlan band() { + TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class); + when(plan.newEpochWeightAt(anyInt(), anyInt())).thenReturn(0.5D); + return plan; + } + + private static SavedTerrainChunk raw(int chunkX, int chunkZ) throws IOException { + return SavedTerrainChunk.capture(chunkX, chunkZ, 0, 16, "minecraft:noise", new SavedTerrainChunk.VoxelSource() { + @Override + public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) { + if (y == 0) { + return BANK; + } + return chunkX * 16 + x < 16 && y <= 4 ? WATER : AIR; + } + + @Override + public String biome(int x, int y, int z) { + return "minecraft:plains"; + } + }); + } + + private static BoundaryColumnGeometry.Voxel voxel(String key, BoundaryColumnGeometry.Phase phase) { + return new BoundaryColumnGeometry.Voxel(key, phase, phase == BoundaryColumnGeometry.Phase.FLUID ? key : "", false); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/history/TransitionGenerationPlanTest.java b/core/src/test/java/art/arcane/iris/engine/history/TransitionGenerationPlanTest.java index 9b48eea1f..17c98daaa 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/TransitionGenerationPlanTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/TransitionGenerationPlanTest.java @@ -10,6 +10,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Optional; import java.util.OptionalInt; import static org.junit.Assert.assertEquals; @@ -30,9 +31,9 @@ public class TransitionGenerationPlanTest { new TerrainBoundarySignature.VerticalLayout(-64, 4, 0), new TerrainBoundarySignature.BiomeEncoding(List.of(), new short[0])); TerrainBoundarySignature present = new TerrainBoundarySignature( - new TerrainBoundarySignature.Column(15, 0, 64, 64, OptionalInt.empty(), OptionalInt.of(100)), samples); + new TerrainBoundarySignature.Column(15, 0, 64, 64, OptionalInt.empty(), OptionalInt.of(100)), samples, BoundaryColumnGeometry.empty()); TerrainBoundarySignature absent = new TerrainBoundarySignature( - new TerrainBoundarySignature.Column(15, 2, 64, 64, OptionalInt.empty(), OptionalInt.empty()), samples); + new TerrainBoundarySignature.Column(15, 2, 64, 64, OptionalInt.empty(), OptionalInt.empty()), samples, BoundaryColumnGeometry.empty()); TransitionBoundarySampler sampler = new TransitionBoundarySampler(32, store.publish(2L, List.of(present, absent))); assertEquals(100D, sampler.sample(16, 0).historicalUpperCeilingDepth(), 0D); @@ -91,7 +92,7 @@ public class TransitionGenerationPlanTest { } @Test - public void samplesFrozenTerrainAndSeparatelyTapersHydrology() throws Exception { + public void samplesTerrainAndHydrologyWithinTheSameFiniteWidth() throws Exception { GenerationBoundary boundary = GenerationBoundary.freeze("terrain", List.of( new GenerationBoundary.ChunkCoordinate(0, 0) )); @@ -106,15 +107,15 @@ public class TransitionGenerationPlanTest { assertNotNull(edge.nearestSignature()); assertEquals(1D, edge.distanceToHistoricalTerrain(), 0D); assertEquals(64D, edge.historicalSurfaceHeight(), 0D); - assertEquals(0D, edge.hydrologyWeight(), 0D); + assertEquals(edge.newEpochWeight(), edge.hydrologyWeight(), 0D); assertTrue(edge.newEpochWeight() > 0D); assertTrue(edge.newEpochWeight() < 0.001D); assertEquals(1D, plan.newEpochWeightAt(47, 8), 0D); - assertEquals(0D, plan.hydrologyWeightAt(47, 8), 0D); - assertEquals(0.5D, plan.hydrologyWeightAt(63, 8), 0D); + assertEquals(1D, plan.hydrologyWeightAt(47, 8), 0D); + assertEquals(1D, plan.hydrologyWeightAt(63, 8), 0D); assertEquals(1D, plan.hydrologyWeightAt(79, 8), 0D); - assertFalse(plan.allowsNewFootprint(31, 8, 48, 8)); + assertTrue(plan.allowsNewFootprint(31, 8, 48, 8)); assertTrue(plan.allowsNewFootprint(47, 8, 63, 8)); } @@ -152,7 +153,7 @@ public class TransitionGenerationPlanTest { } @Test - public void rejectsFootprintsWhoseInteriorCrossesHistoryOrTheTerrainBand() throws Exception { + public void rejectsFootprintsCrossingHistoryAndAllowsTheTerrainBand() throws Exception { GenerationBoundary boundary = GenerationBoundary.freeze("footprints", List.of( new GenerationBoundary.ChunkCoordinate(0, 0) )); @@ -164,7 +165,7 @@ public class TransitionGenerationPlanTest { ); assertFalse(plan.allowsNewFootprint(-20, -20, 40, 40)); - assertFalse(plan.allowsNewFootprint(16, -20, 16, 35)); + assertTrue(plan.allowsNewFootprint(16, -20, 16, 35)); assertTrue(plan.allowsNewFootprint(31, 8, 31, 8)); } @@ -188,6 +189,20 @@ public class TransitionGenerationPlanTest { assertEquals("iris:middle", plan.historicalPhysicalBiomeKeyAt(16, 14, 8).orElseThrow()); assertEquals("iris:high", plan.historicalPhysicalBiomeKeyAt(16, 18, 8).orElseThrow()); assertTrue(plan.historicalPhysicalBiomeKeyAt(31, 14, 8).isEmpty()); + for (int blockX = -2; blockX <= 34; blockX++) { + for (int blockZ : new int[]{-2, 8, 17}) { + TransitionGenerationPlan.TerrainSample sample = plan.terrainSampleAt(blockX, blockZ); + for (int blockY = -16; blockY <= 48; blockY++) { + double weight = GenerationBlend.newEpochWeight( + Math.max(0D, sample.distanceToHistoricalTerrain() - 1D), Math.max(1, plan.widthBlocks() - 1)); + Optional expected = sample.newEpochWeight() != 1D + && GenerationBlend.usesHistoricalMaterial(blockX, blockY, blockZ, weight) + ? sample.historicalPhysicalBiomeKeyAt(blockY) : Optional.empty(); + assertEquals(expected, plan.historicalPhysicalBiomeKeyAt(blockX, blockY, blockZ, sample)); + assertEquals(expected, plan.historicalPhysicalBiomeKeyAt(blockX, blockY, blockZ)); + } + } + } } @Test @@ -230,7 +245,7 @@ public class TransitionGenerationPlanTest { for (int localX = 0; localX < 16; localX++) { for (int localZ = 0; localZ < 16; localZ++) { assertMatchesExhaustive(signatures, sampler, chunk.chunkX() * 16 + localX, - chunk.chunkZ() * 16 + localZ, width * 2); + chunk.chunkZ() * 16 + localZ, width); } } } @@ -244,7 +259,7 @@ public class TransitionGenerationPlanTest { heightSignature(0, -64, 80), heightSignature(0, 64, 100), heightSignature(65, 0, 500) ); - TransitionBoundarySampler sampler = new TransitionBoundarySampler(32, snapshot("radius-ties", signatures)); + TransitionBoundarySampler sampler = new TransitionBoundarySampler(64, snapshot("radius-ties", signatures)); assertMatchesExhaustive(signatures, sampler, 0, 0, 64); assertEquals(70D, sampler.sample(0, 0).historicalSurfaceHeight(), 0D); @@ -267,7 +282,7 @@ public class TransitionGenerationPlanTest { TransitionBoundarySampler sampler = new TransitionBoundarySampler(16_384, snapshot); for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { - assertMatchesExhaustive(signatures, sampler, x, z, 32_768); + assertMatchesExhaustive(signatures, sampler, x, z, 16_384); } } assertEquals(1L, sampler.candidateBuildCount()); @@ -459,7 +474,7 @@ public class TransitionGenerationPlanTest { new TerrainBoundarySignature.VerticalLayout(0, 1, 1), new TerrainBoundarySignature.BiomeEncoding(List.of("iris:test"), new short[]{0}) ) - ); + , BoundaryColumnGeometry.empty()); } private static TerrainBoundarySignature biomeSignature(int blockX, int blockZ) { @@ -471,7 +486,7 @@ public class TransitionGenerationPlanTest { List.of("iris:low", "iris:middle", "iris:high"), new short[]{0, 1, 2}) ) - ); + , BoundaryColumnGeometry.empty()); } private static TerrainBoundarySignature heightSignature(int blockX, int blockZ, int height) { @@ -481,6 +496,6 @@ public class TransitionGenerationPlanTest { new TerrainBoundarySignature.VerticalLayout(0, 1, 1), new TerrainBoundarySignature.BiomeEncoding(List.of("iris:test"), new short[]{0}) ) - ); + , BoundaryColumnGeometry.empty()); } } diff --git a/core/src/test/java/art/arcane/iris/engine/history/TransitionGeometryBlenderTest.java b/core/src/test/java/art/arcane/iris/engine/history/TransitionGeometryBlenderTest.java new file mode 100644 index 000000000..5d384c94f --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/history/TransitionGeometryBlenderTest.java @@ -0,0 +1,201 @@ +package art.arcane.iris.engine.history; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class TransitionGeometryBlenderTest { + private static final BoundaryColumnGeometry.Voxel STONE = new BoundaryColumnGeometry.Voxel( + "minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false); + private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel( + "minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false); + private static final BoundaryColumnGeometry.Voxel WATER = new BoundaryColumnGeometry.Voxel( + "minecraft:water[level=0]", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water[level=0]", false); + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void preservesCaveIslandAndWaterCrossSectionsAtFirstNewFace() throws Exception { + BoundaryColumnGeometry old = BoundaryColumnGeometry.fromVoxels(-4, + List.of(STONE, AIR, WATER, AIR, STONE, STONE, AIR, AIR)); + BoundaryColumnGeometry current = filled(STONE, 8); + TransitionGenerationPlan plan = plan(List.of(signature(15, 0, old))); + + BoundaryColumnGeometry blended = TransitionGeometryBlender.blendColumn(plan, 16, 0, current); + + assertEquals(old, blended); + assertSame(current, TransitionGeometryBlender.blendColumn(plan, 47, 0, current)); + assertSame(current, TransitionGeometryBlender.blendColumn(plan, 15, 0, current)); + } + + @Test + public void narrowHistoricalCaveContinuesBeforeTaperingIntoTallSolidTerrain() throws Exception { + ArrayList oldVoxels = new ArrayList<>(768); + for (int offset = 0; offset < 768; offset++) { + oldVoxels.add(offset == 384 ? AIR : STONE); + } + BoundaryColumnGeometry old = BoundaryColumnGeometry.fromVoxels(-4, oldVoxels); + BoundaryColumnGeometry current = filled(STONE, 768); + TransitionGenerationPlan plan = plan(List.of(signature(15, 0, old))); + + for (int x = 16; x <= 23; x++) { + assertEquals(AIR, TransitionGeometryBlender.blendColumn(plan, x, 0, current).voxelAt(380)); + } + assertSame(current, TransitionGeometryBlender.blendColumn(plan, 47, 0, current)); + } + + @Test + public void giantGroundChangesKeepGroundReferenceSeparateFromUpperRoof() { + ArrayList oldVoxels = new ArrayList<>(512); + ArrayList newVoxels = new ArrayList<>(512); + for (int offset = 0; offset < 512; offset++) { + oldVoxels.add(offset <= 20 || offset >= 480 ? STONE : AIR); + newVoxels.add(offset <= 380 || offset >= 480 ? STONE : AIR); + } + BoundaryColumnGeometry old = BoundaryColumnGeometry.fromVoxels(-64, oldVoxels); + BoundaryColumnGeometry current = BoundaryColumnGeometry.fromVoxels(-64, newVoxels); + BoundaryGeometryInfluence influence = new BoundaryGeometryInfluence(0.5D, 0.5D, + List.of(new BoundaryGeometryInfluence.Contribution(old, 1D))); + + BoundaryColumnGeometry blended = TransitionGeometryBlender.blendColumn(influence, 31, 0, current); + + assertEquals(200, blended.surfaceOffsetNear(200D)); + assertEquals(STONE, blended.voxelAt(416)); + assertEquals(STONE, blended.voxelAt(447)); + } + + @Test + public void conflictingFluidFamiliesProduceANewSideSolidPlug() throws Exception { + BoundaryColumnGeometry.Voxel lava = new BoundaryColumnGeometry.Voxel("minecraft:lava[level=0]", + BoundaryColumnGeometry.Phase.FLUID, "minecraft:lava[level=0]", false); + BoundaryColumnGeometry current = filled(lava, 8); + TransitionGenerationPlan plan = plan(List.of(signature(15, 0, filled(WATER, 8)))); + + BoundaryColumnGeometry blended = TransitionGeometryBlender.blendColumn(plan, 16, 0, current); + + for (BoundaryColumnGeometry.Voxel voxel : blended.voxels()) { + assertEquals(BoundaryColumnGeometry.Phase.SOLID, voxel.phase()); + assertEquals("minecraft:obsidian", voxel.stateKey()); + } + assertSame(current, TransitionGeometryBlender.blendColumn(plan, 47, 0, current)); + } + + @Test + public void differentLevelsOfTheSameFluidDoNotCreateSolidPlugs() throws Exception { + BoundaryColumnGeometry.Voxel flowing = new BoundaryColumnGeometry.Voxel("minecraft:water[level=3]", + BoundaryColumnGeometry.Phase.FLUID, "minecraft:water[level=3]", false); + BoundaryColumnGeometry current = filled(flowing, 8); + TransitionGenerationPlan plan = plan(List.of(signature(15, 0, filled(WATER, 8)))); + + assertEquals(WATER, TransitionGeometryBlender.blendColumn(plan, 16, 0, current).voxelAt(0)); + } + + @Test + public void repeatedOrReorderedChunkRequestsDoNotExtendInfluence() throws Exception { + BoundaryColumnGeometry old = BoundaryColumnGeometry.fromVoxels(-4, + List.of(STONE, STONE, AIR, AIR, STONE, AIR, AIR, AIR)); + BoundaryColumnGeometry current = filled(STONE, 8); + TransitionGenerationPlan plan = plan(List.of(signature(15, 0, old), signature(0, 15, old))); + BoundaryColumnGeometry before = TransitionGeometryBlender.blendColumn(plan, 25, 6, current); + + for (int x = 46; x >= 16; x--) { + TransitionGeometryBlender.blendColumn(plan, x, 0, current); + } + + assertEquals(before, TransitionGeometryBlender.blendColumn(plan, 25, 6, current)); + assertSame(current, TransitionGeometryBlender.blendColumn(plan, 80, 0, current)); + } + + @Test + public void cornerResultsDoNotDependOnSnapshotInsertionOrder() throws Exception { + BoundaryColumnGeometry lower = BoundaryColumnGeometry.fromVoxels(-4, + List.of(STONE, STONE, AIR, AIR, AIR, AIR, AIR, AIR)); + BoundaryColumnGeometry upper = BoundaryColumnGeometry.fromVoxels(-4, + List.of(AIR, AIR, AIR, AIR, STONE, STONE, AIR, AIR)); + TerrainBoundarySignature west = signature(15, 0, lower); + TerrainBoundarySignature south = signature(0, 15, upper); + TransitionGenerationPlan first = plan(List.of(west, south)); + TransitionGenerationPlan second = plan(List.of(south, west)); + BoundaryColumnGeometry current = filled(STONE, 8); + + assertEquals(TransitionGeometryBlender.blendColumn(first, 16, 16, current), + TransitionGeometryBlender.blendColumn(second, 16, 16, current)); + } + + @Test + public void doesNotExtrudeProtectedObjectsOrOverwriteCurrentProtection() { + BoundaryColumnGeometry.Voxel protectedBlock = new BoundaryColumnGeometry.Voxel( + "minecraft:gold_block", BoundaryColumnGeometry.Phase.SOLID, "", true); + BoundaryColumnGeometry old = BoundaryColumnGeometry.fromVoxels(-4, List.of(protectedBlock)); + BoundaryColumnGeometry current = BoundaryColumnGeometry.fromVoxels(-4, List.of(STONE)); + BoundaryGeometryInfluence influence = new BoundaryGeometryInfluence(0D, 0D, + List.of(new BoundaryGeometryInfluence.Contribution(old, 1D))); + + assertSame(current, TransitionGeometryBlender.blendColumn(influence, 16, 0, current)); + BoundaryColumnGeometry protectedCurrent = BoundaryColumnGeometry.fromVoxels(-4, List.of(protectedBlock)); + BoundaryGeometryInfluence naturalInfluence = new BoundaryGeometryInfluence(0D, 0D, + List.of(new BoundaryGeometryInfluence.Contribution(current, 1D))); + assertSame(protectedCurrent, TransitionGeometryBlender.blendColumn(naturalInfluence, 16, 0, protectedCurrent)); + } + + @Test + public void rejectsIncompatibleVerticalLayoutAndMissingGeometry() { + BoundaryColumnGeometry old = filled(STONE, 8); + BoundaryGeometryInfluence influence = new BoundaryGeometryInfluence(0.5D, 0.5D, + List.of(new BoundaryGeometryInfluence.Contribution(old, 1D))); + + assertThrows(IllegalArgumentException.class, () -> TransitionGeometryBlender.blendColumn( + influence, 16, 0, filled(AIR, 4))); + assertThrows(IllegalArgumentException.class, () -> new BoundaryGeometryInfluence.Contribution( + BoundaryColumnGeometry.empty(), 1D)); + } + + @Test + public void roundTripsGeometryAndIncludesItInSnapshotIdentity() throws Exception { + TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore( + temporaryFolder.newFolder().toPath()); + BoundaryColumnGeometry old = BoundaryColumnGeometry.fromVoxels(-4, + List.of(STONE, AIR, WATER, AIR, STONE, STONE, AIR, AIR)); + TerrainBoundarySignatureStore.Snapshot snapshot = store.publish(2L, List.of(signature(15, 0, old))); + + assertEquals(old, store.load(2L).signatureAt(15, 0).orElseThrow().geometry()); + TerrainBoundarySignatureStore other = new TerrainBoundarySignatureStore(temporaryFolder.newFolder().toPath()); + TerrainBoundarySignatureStore.Snapshot changed = other.publish(2L, + List.of(signature(15, 0, filled(STONE, 8)))); + assertTrue(!snapshot.identity().equals(changed.identity())); + } + + private TransitionGenerationPlan plan(List signatures) throws Exception { + GenerationBoundary boundary = GenerationBoundary.freeze("frontier", + List.of(new GenerationBoundary.ChunkCoordinate(0, 0))); + TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(temporaryFolder.newFolder().toPath()); + TerrainBoundarySignatureStore.Snapshot snapshot = store.publish(2L, signatures); + return new TransitionGenerationPlan(new TransitionGenerationPlan.Specification(2L, "old", "new", + GenerationTransition.CURRENT_ALGORITHM_VERSION, 32, boundary.identity(), snapshot.identity()), + boundary, snapshot); + } + + private static BoundaryColumnGeometry filled(BoundaryColumnGeometry.Voxel voxel, int height) { + ArrayList voxels = new ArrayList<>(height); + for (int index = 0; index < height; index++) { + voxels.add(voxel); + } + return BoundaryColumnGeometry.fromVoxels(-4, voxels); + } + + private static TerrainBoundarySignature signature(int x, int z, BoundaryColumnGeometry geometry) { + return new TerrainBoundarySignature(new TerrainBoundarySignature.Column(x, z, 2, 2, + OptionalInt.empty(), OptionalInt.empty()), new TerrainBoundarySignature.Samples( + new TerrainBoundarySignature.VerticalLayout(-4, 4, 0), + new TerrainBoundarySignature.BiomeEncoding(List.of(), new short[0])), geometry); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/history/WorldChunkInventoryTest.java b/core/src/test/java/art/arcane/iris/engine/history/WorldChunkInventoryTest.java index ef9a25bea..7c396a761 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/WorldChunkInventoryTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/WorldChunkInventoryTest.java @@ -7,10 +7,14 @@ import org.junit.rules.TemporaryFolder; import java.io.IOException; import java.io.RandomAccessFile; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Random; +import java.util.zip.DeflaterOutputStream; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -59,6 +63,86 @@ public final class WorldChunkInventoryTest { assertTrue(inventory.contains(7, 11)); } + @Test + public void liveRegionMayEndImmediatelyAfterACompressedChunk() throws Exception { + Path world = temporaryFolder.newFolder("world-unpadded").toPath(); + Path regionDirectory = Files.createDirectory(world.resolve("region")); + Path region = regionDirectory.resolve("r.0.0.mca"); + byte[] payload = compressedChunk(10_000); + writeUnpaddedRegion(region, payload, 2); + + assertTrue(Files.size(region) % SECTOR_BYTES != 0L); + WorldChunkInventory inventory = WorldChunkInventory.scan(world); + assertEquals(1, inventory.size()); + assertTrue(inventory.contains(7, 11)); + assertTrue(WorldChunkInventory.isDurablyAllocated(world, 7, 11)); + assertFalse(WorldChunkInventory.isDurablyAllocated(world, 8, 11)); + } + + @Test + public void unpaddedExternalChunkStubStillOwnsItsChunk() throws Exception { + Path regionDirectory = temporaryFolder.newFolder("region-external").toPath(); + Path region = regionDirectory.resolve("r.0.0.mca"); + writeUnpaddedRegion(region, new byte[0], 0x82); + + WorldChunkInventory inventory = WorldChunkInventory.scanRegionDirectory(regionDirectory); + + assertEquals(1, inventory.size()); + assertTrue(inventory.contains(7, 11)); + } + + @Test + public void missingPayloadBytesInTheFinalSectorFailClosed() throws Exception { + Path regionDirectory = temporaryFolder.newFolder("region-partial-payload").toPath(); + Path region = regionDirectory.resolve("r.0.0.mca"); + writeUnpaddedRegion(region, compressedChunk(32), 2); + try (RandomAccessFile output = new RandomAccessFile(region.toFile(), "rw")) { + output.setLength(output.length() - 1L); + } + + IOException error = assertThrows(IOException.class, + () -> WorldChunkInventory.scanRegionDirectory(regionDirectory)); + assertTrue(error.getMessage().contains("Truncated chunk payload")); + } + + @Test + public void invalidFinalSectorPayloadLengthsFailClosed() throws Exception { + Path regionDirectory = temporaryFolder.newFolder("region-invalid-payload").toPath(); + Path region = regionDirectory.resolve("r.0.0.mca"); + writeUnpaddedRegion(region, compressedChunk(32), 2); + for (int invalidLength : new int[]{-1, 0, Integer.MAX_VALUE, SECTOR_BYTES}) { + try (RandomAccessFile output = new RandomAccessFile(region.toFile(), "rw")) { + output.seek(2L * SECTOR_BYTES); + output.writeInt(invalidLength); + } + assertThrows(IOException.class, + () -> WorldChunkInventory.scanRegionDirectory(regionDirectory)); + } + } + + @Test + public void incompleteFinalSectorLengthPrefixFailsClosed() throws Exception { + Path regionDirectory = temporaryFolder.newFolder("region-incomplete-length").toPath(); + Path region = regionDirectory.resolve("r.0.0.mca"); + try (RandomAccessFile output = new RandomAccessFile(region.toFile(), "rw")) { + output.setLength(SECTOR_BYTES * 2L + Integer.BYTES); + writeLocation(output, 7, 11, 2, 1); + } + + assertThrows(IOException.class, + () -> WorldChunkInventory.scanRegionDirectory(regionDirectory)); + } + + @Test + public void newlyOpenedEmptyRegionHasNoDurableAllocations() throws Exception { + Path world = temporaryFolder.newFolder("world-empty-region").toPath(); + Path regionDirectory = Files.createDirectory(world.resolve("region")); + Files.createFile(regionDirectory.resolve("r.0.0.mca")); + + assertTrue(WorldChunkInventory.scan(world).isEmpty()); + assertFalse(WorldChunkInventory.isDurablyAllocated(world, 0, 0)); + } + @Test public void inventoryRemainsUsableAfterTheRegionFilesDisappear() throws Exception { Path regionDirectory = temporaryFolder.newFolder("region-memory").toPath(); @@ -198,6 +282,34 @@ public final class WorldChunkInventoryTest { } } + private static void writeUnpaddedRegion(Path path, byte[] payload, int compression) throws IOException { + try (RandomAccessFile output = new RandomAccessFile(path.toFile(), "rw")) { + output.setLength(2L * SECTOR_BYTES); + int sectors = (payload.length + Integer.BYTES + 1 + SECTOR_BYTES - 1) / SECTOR_BYTES; + output.seek(2L * SECTOR_BYTES); + output.writeInt(payload.length + 1); + output.writeByte(compression); + output.write(payload); + writeLocation(output, 7, 11, 2, sectors); + } + } + + private static byte[] compressedChunk(int dataLength) throws IOException { + byte[] values = new byte[dataLength]; + new Random(19L).nextBytes(values); + ByteArrayOutputStream compressed = new ByteArrayOutputStream(); + try (DataOutputStream nbt = new DataOutputStream(new DeflaterOutputStream(compressed))) { + nbt.writeByte(10); + nbt.writeUTF(""); + nbt.writeByte(7); + nbt.writeUTF("data"); + nbt.writeInt(values.length); + nbt.write(values); + nbt.writeByte(0); + } + return compressed.toByteArray(); + } + private static void writeLocation( RandomAccessFile file, int localChunkX, diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/MatterGeneratorConcurrencyTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/MatterGeneratorConcurrencyTest.java index 00d58f0fe..6d887a849 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/MatterGeneratorConcurrencyTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/MatterGeneratorConcurrencyTest.java @@ -34,6 +34,8 @@ import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; @@ -148,6 +150,71 @@ public class MatterGeneratorConcurrencyTest { assertNull(IrisContext.get()); } + @Test + public void terrainPhasePreparesContentReachWithoutPublishingPlanned() { + GeneratorFixture fixture = new GeneratorFixture(true); + AtomicInteger terrainRuns = new AtomicInteger(); + AtomicInteger contentRuns = new AtomicInteger(); + RecordingComponent terrain = new RecordingComponent(ReservedFlag.CARVED, 0, 0) { + @Override + public MatterGenerationPhase getGenerationPhase() { + return MatterGenerationPhase.TERRAIN; + } + + @Override + public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { + terrainRuns.incrementAndGet(); + } + }; + RecordingComponent content = new RecordingComponent(ReservedFlag.OBJECT, 1, 16) { + @Override + public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { + assertEquals(9, terrainRuns.get()); + contentRuns.incrementAndGet(); + } + }; + TestMatterGenerator generator = fixture.generator(List.of( + new MantlePass(List.of(terrain), 1, 0), + new MantlePass(List.of(content), 1, 0))); + + generator.generateTerrainMatter(0, 0, true, fixture.context); + assertEquals(9, terrainRuns.get()); + assertEquals(0, contentRuns.get()); + assertFalse(fixture.mantle.getChunk(0, 0).isFlagged(MantleFlag.PLANNED)); + assertFalse(fixture.mantle.getChunk(0, 0).isFlagged(ReservedFlag.OBJECT)); + + generator.generateContentMatter(0, 0, true, fixture.context); + generator.generateTerrainMatter(0, 0, true, fixture.context); + generator.generateContentMatter(0, 0, true, fixture.context); + assertEquals(9, terrainRuns.get()); + assertEquals(9, contentRuns.get()); + assertTrue(fixture.mantle.getChunk(0, 0).isFlagged(MantleFlag.PLANNED)); + } + + @Test + public void contentPhaseRequiresTerrainEvenWhenItsExplicitPrerequisiteIsMissing() { + GeneratorFixture fixture = new GeneratorFixture(true); + RecordingComponent terrain = new RecordingComponent(ReservedFlag.CARVED, 0, 0) { + @Override + public MatterGenerationPhase getGenerationPhase() { + return MatterGenerationPhase.TERRAIN; + } + }; + RecordingComponent content = new RecordingComponent(ReservedFlag.OBJECT, 1, 0) { + @Override + public MantleFlag[] getPrerequisiteFlags() { + return new MantleFlag[]{ReservedFlag.CARVED}; + } + }; + TestMatterGenerator generator = fixture.generator(List.of( + new MantlePass(List.of(terrain), 0, 0), + new MantlePass(List.of(content), 0, 0))); + + assertThrows(IllegalStateException.class, + () -> generator.generateContentMatter(0, 0, false, fixture.context)); + assertFalse(fixture.mantle.getChunk(0, 0).isFlagged(MantleFlag.PLANNED)); + } + private static void await(CountDownLatch latch) { try { assertTrue(latch.await(5, TimeUnit.SECONDS)); @@ -251,6 +318,11 @@ public class MatterGeneratorConcurrencyTest { Set flagged = ConcurrentHashMap.newKeySet(); when(chunk.use()).thenReturn(chunk); when(chunk.isFlagged(any())).thenAnswer(invocation -> flagged.contains(invocation.getArgument(0))); + doAnswer(invocation -> { + Object flag = invocation.getArgument(0); + boolean enabled = invocation.getArgument(1); + return enabled ? flagged.add(flag) : flagged.remove(flag); + }).when(chunk).flag(any(), anyBoolean()); doAnswer(invocation -> { Object flag = invocation.getArgument(0); Runnable task = invocation.getArgument(1); diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/ResolvedTerrainPlacementTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/ResolvedTerrainPlacementTest.java new file mode 100644 index 000000000..b9b3f47fd --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/mantle/ResolvedTerrainPlacementTest.java @@ -0,0 +1,139 @@ +package art.arcane.iris.engine.mantle; + +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.history.BoundaryColumnGeometry; +import art.arcane.iris.engine.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.util.project.matter.IrisMatterSupport; +import art.arcane.iris.util.project.matter.PreObjectMatterCell; +import art.arcane.iris.util.project.matter.slices.PreObjectMatterTest; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.IrisMatter; +import art.arcane.volmlib.util.matter.Matter; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.doReturn; +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 ResolvedTerrainPlacementTest { + private EngineMantle engineMantle; + private Mantle mantle; + private PlatformBlockState stone; + private PlatformBlockState air; + private Matter matter; + + @Before + @SuppressWarnings("unchecked") + public void setUp() { + PreObjectMatterTest.setUpBukkit(); + IrisPlatforms.unbind(); + IrisPlatform platform = mock(IrisPlatform.class); + PlatformRegistries registries = mock(PlatformRegistries.class); + stone = mock(PlatformBlockState.class); + air = mock(PlatformBlockState.class); + when(air.isAir()).thenReturn(true); + when(registries.block(anyString())).thenReturn(air); + when(registries.blockOrNull("minecraft:stone")).thenReturn(stone); + when(registries.blockOrNull("minecraft:air")).thenReturn(air); + when(platform.registries()).thenReturn(registries); + IrisPlatforms.bind(platform); + IrisMatterSupport.ensureRegistered(); + + BoundaryColumnGeometry.Voxel solid = new BoundaryColumnGeometry.Voxel( + "minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false); + BoundaryColumnGeometry.Voxel open = new BoundaryColumnGeometry.Voxel( + "minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false); + TerrainBoundarySignature column = mock(TerrainBoundarySignature.class); + when(column.geometry()).thenReturn(BoundaryColumnGeometry.fromVoxels(-64, + List.of(solid, open, solid, open))); + when(column.oceanFloorHeight()).thenReturn(2); + when(column.surfaceHeight()).thenReturn(5); + when(column.fluidHeight()).thenReturn(OptionalInt.of(5)); + IrisComplex complex = mock(IrisComplex.class); + when(complex.resolvedTerrainColumn(anyInt(), anyInt())).thenReturn(Optional.of(column)); + when(complex.resolvedTerrainHeight(anyInt(), anyInt(), eq(true))) + .thenReturn(OptionalInt.of(2)); + when(complex.resolvedTerrainHeight(anyInt(), anyInt(), eq(false))) + .thenReturn(OptionalInt.of(5)); + Engine engine = mock(Engine.class); + when(engine.getMinHeight()).thenReturn(-64); + when(engine.getComplex()).thenReturn(complex); + mantle = mock(Mantle.class); + when(mantle.getWorldHeight()).thenReturn(4); + MantleChunk chunk = mock(MantleChunk.class); + when(mantle.getChunk(0, 0)).thenReturn(chunk); + when(chunk.use()).thenReturn(chunk); + when(chunk.exists(0)).thenReturn(true); + matter = new IrisMatter(16, 16, 16); + when(chunk.get(0)).thenReturn(matter); + engineMantle = mock(EngineMantle.class, CALLS_REAL_METHODS); + doReturn(engine).when(engineMantle).getEngine(); + doReturn(mantle).when(engineMantle).getMantle(); + } + + @After + public void tearDown() { + IrisPlatforms.unbind(); + } + + @Test + public void placementUsesResolvedSurfaceFluidAndEnclosedOccupancy() { + assertEquals(2, engineMantle.getHighest(0, 0, null, true)); + assertEquals(5, engineMantle.getHighest(0, 0, null, false)); + assertEquals(5, engineMantle.getFluidHeight(0, 0)); + assertSame(stone, engineMantle.get(0, 0, 0)); + assertSame(air, engineMantle.get(0, 1, 0)); + assertTrue(engineMantle.isCarved(0, 1, 0)); + assertFalse(engineMantle.isCarved(0, 3, 0)); + } + + @Test + public void scalarPlacementQueriesDoNotReloadColumnGeometry() { + Engine engine = mock(Engine.class, CALLS_REAL_METHODS); + IrisComplex complex = engineMantle.getComplex(); + doReturn(complex).when(engine).getComplex(); + + assertEquals(2, engine.getHeight(0, 0, true)); + assertEquals(5, engine.getHeight(0, 0, false)); + assertEquals(2, engineMantle.getHighest(0, 0, null, true)); + assertEquals(5, engineMantle.getHighest(0, 0, null, false)); + verify(complex, never()).resolvedTerrainColumn(anyInt(), anyInt()); + } + + @Test + public void writerPrerequisitesIgnoreContentWhileLiveOccupancyIncludesBlockOnlyFills() { + matter.slice(PlatformBlockState.class).set(0, 1, 0, stone); + matter.slice(PreObjectMatterCell.class).set(0, 1, 0, PreObjectMatterCell.block(null)); + try (MantleWriter writer = new MantleWriter(engineMantle, mantle, 0, 0, 0, false)) { + assertSame(air, writer.getPrerequisiteBlock(0, 1, 0)); + assertSame(stone, writer.get(0, 1, 0)); + assertTrue(writer.isPrerequisiteCarved(0, 1, 0)); + assertFalse(writer.isCarved(0, 1, 0)); + assertArrayEquals(new byte[]{0, 1, 0, 0}, writer.getPrerequisiteCarvedColumn(0, 0, 4)); + assertArrayEquals(new byte[]{0, 0, 0, 0}, writer.getCarvedColumn(0, 0, 4)); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/TerrainMatterViewTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/TerrainMatterViewTest.java new file mode 100644 index 000000000..c7741337c --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/mantle/TerrainMatterViewTest.java @@ -0,0 +1,87 @@ +package art.arcane.iris.engine.mantle; + +import art.arcane.iris.util.project.matter.IrisMatterSupport; +import art.arcane.iris.util.project.matter.PreObjectMatterCell; +import art.arcane.iris.util.project.matter.slices.PreObjectMatterTest; +import art.arcane.volmlib.util.function.Consumer4; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.IrisMatter; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TerrainMatterViewTest { + @BeforeClass + public static void registerMatter() { + PreObjectMatterTest.setUpBukkit(); + IrisMatterSupport.ensureRegistered(); + } + + @Test + public void persistedJournalRestoresDeletedAndReplacedTerrainAndExcludesNewContent() throws IOException { + Matter matter = new IrisMatter(16, 16, 16); + MatterCavern natural = new MatterCavern(true, "iris:natural", (byte) 0); + MatterCavern content = new MatterCavern(true, "iris:object", (byte) 0); + matter.slice(MatterCavern.class).set(1, 3, 2, content); + matter.slice(MatterCavern.class).set(2, 3, 2, content); + matter.slice(MatterCavern.class).set(4, 3, 2, natural); + matter.slice(PreObjectMatterCell.class).set(1, 3, 2, PreObjectMatterCell.cavern(null)); + matter.slice(PreObjectMatterCell.class).set(2, 3, 2, PreObjectMatterCell.cavern(natural)); + matter.slice(PreObjectMatterCell.class).set(3, 3, 2, PreObjectMatterCell.cavern(natural)); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + matter.write(bytes); + Matter restored = Matter.read(new ByteArrayInputStream(bytes.toByteArray())); + MantleChunk chunk = chunk(restored); + Map values = new HashMap<>(); + TerrainMatterView.iterate(chunk, MatterCavern.class, (x, y, z, cavern) -> { + assertEquals(3, y.intValue()); + assertEquals(2, z.intValue()); + assertNull(values.put(x, cavern)); + }); + + assertEquals(Map.of(2, natural, 3, natural, 4, natural), values); + assertNull(TerrainMatterView.get(chunk, 1, 3, 2, MatterCavern.class)); + assertEquals(natural, TerrainMatterView.get(chunk, 3, 3, 2, MatterCavern.class)); + } + + @SuppressWarnings("unchecked") + private static MantleChunk chunk(Matter matter) { + MantleChunk chunk = mock(MantleChunk.class); + when(chunk.exists(0)).thenReturn(true); + when(chunk.get(0)).thenReturn(matter); + doAnswer(invocation -> { + Class type = invocation.getArgument(0); + Consumer4 consumer = invocation.getArgument(1); + if (!matter.hasSlice(type)) { + return null; + } + for (int x = 0; x < 16; x++) { + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + Object value = matter.getSlice(type).get(x, y, z); + if (value != null) { + consumer.accept(x, y, z, value); + } + } + } + } + return null; + }).when(chunk).iterate(any(), any()); + return chunk; + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleHydrologyCaveVoxelViewTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleHydrologyCaveVoxelViewTest.java index e56e0f26c..1c35de24f 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleHydrologyCaveVoxelViewTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleHydrologyCaveVoxelViewTest.java @@ -5,6 +5,7 @@ import art.arcane.iris.engine.hydrology.cave.CaveVoxel; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.volmlib.util.mantle.flag.ReservedFlag; import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterCavern; import org.junit.Test; @@ -28,6 +29,7 @@ public class MantleHydrologyCaveVoxelViewTest { @SuppressWarnings("unchecked") public void authoritativeChunkFlagControlsWhetherCarvingInputIsRequired() { Mantle mantle = mock(Mantle.class); + doReturn(mock(MantleChunk.class)).when(mantle).getChunk(anyInt(), anyInt()); when(mantle.hasFlag(2, -3, ReservedFlag.CARVED)).thenReturn(true); assertFalse(MantleHydrologyCaveVoxelView.requiresCarvingInput(mantle, 2, -3)); @@ -41,6 +43,7 @@ public class MantleHydrologyCaveVoxelViewTest { @SuppressWarnings("unchecked") public void mapsCarvedAndStoredMatterAndLoadsEachChunkOnce() { Mantle mantle = mock(Mantle.class); + doReturn(mock(MantleChunk.class)).when(mantle).getChunk(anyInt(), anyInt()); MatterCavern air = new MatterCavern(true, "", (byte) 0); MatterCavern water = new MatterCavern(true, "", (byte) 1); MatterCavern lava = new MatterCavern(true, "", (byte) 2); @@ -85,6 +88,7 @@ public class MantleHydrologyCaveVoxelViewTest { @SuppressWarnings("unchecked") public void carvedVerticalShaftIsOpenToSurface() { Mantle mantle = mock(Mantle.class); + doReturn(mock(MantleChunk.class)).when(mantle).getChunk(anyInt(), anyInt()); MatterCavern air = new MatterCavern(true, "", (byte) 0); doAnswer(invocation -> { int x = invocation.getArgument(0); diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponentTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponentTest.java index e73e22ffc..cd9202f68 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponentTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponentTest.java @@ -692,6 +692,81 @@ public class MantleHydrologyComponentTest { .get(new CavePosition(9, 22, 8)).action()); } + @Test + public void omittedHistoricalHaloVolumeDoesNotPublishItsGuardInNewChunk() { + HydrologyColumnLayer pool = cavePool(); + HydrologyColumnSample historical = sample(-1, 8, false, 80, List.of(pool)); + HydrologyColumnSample current = sample(8, 8, false, 80, List.of()); + + MantleHydrologyComponent.Publication publication = compile( + samples(current), + List.of(acceptedPlan(historical, pool)), + new TestCaveVoxelView() + ); + + assertEmpty(publication); + } + + @Test + public void includedHaloVolumePublishesOnlyItsGuardInsideTargetChunk() { + HydrologyColumnLayer pool = cavePool(); + HydrologyColumnSample neighbor = sample(-1, 8, false, 80, List.of(pool)); + + MantleHydrologyComponent.Publication publication = compile( + samples(neighbor), + List.of(acceptedPlan(neighbor, pool)), + new TestCaveVoxelView() + ); + + assertEquals(1, publication.caveCells().size()); + assertEquals(HydrologyCaveAction.SEAL_GUARD, + publication.caveCells().get(new CavePosition(0, 22, 8)).action()); + assertTrue(publication.surfaceWrites().isEmpty()); + assertTrue(publication.fluidUpdates().isEmpty()); + } + + @Test + public void includedCaveColumnStillRejectsGuardWithoutPublishedOwner() { + HydrologyColumnLayer pool = cavePool(); + HydrologyColumnSample sample = sample(8, 8, false, 80, List.of(pool)); + HydrologyCavePlan plan = acceptedPlan(sample, pool); + CavePosition extraVolume = new CavePosition(8, 35, 8); + CavePosition extraGuard = new CavePosition(9, 35, 8); + LinkedHashMap actions = new LinkedHashMap<>(plan.actions()); + actions.put(extraVolume, HydrologyCaveAction.WET_SOURCE); + actions.put(extraGuard, HydrologyCaveAction.SEAL_GUARD); + LinkedHashMap preconditions = + new LinkedHashMap<>(plan.baselinePreconditions()); + preconditions.put(extraVolume, new CaveVoxelPrecondition(CaveVoxel.SOLID, false)); + preconditions.put(extraGuard, new CaveVoxelPrecondition(CaveVoxel.SOLID, false)); + HydrologyCavePlan malformed = new HydrologyCavePlan(plan.source(), plan.rejection(), + actions, preconditions, plan.arbitrationWinnerSourceId()); + + IllegalStateException failure = assertThrows(IllegalStateException.class, + () -> compile(samples(sample), List.of(malformed), new TestCaveVoxelView())); + + assertTrue(failure.getMessage().contains("guard has no adjacent cave volume")); + assertTrue(failure.getMessage().contains(extraGuard.toString())); + } + + @Test + public void includedCaveLayerStillRejectsMissingAcceptedAction() { + HydrologyColumnLayer pool = cavePool(); + HydrologyColumnSample sample = sample(8, 8, false, 80, List.of(pool)); + HydrologyCavePlan plan = acceptedPlan(sample, pool); + CavePosition missing = new CavePosition(8, 25, 8); + LinkedHashMap actions = new LinkedHashMap<>(plan.actions()); + actions.remove(missing); + HydrologyCavePlan malformed = new HydrologyCavePlan(plan.source(), plan.rejection(), + actions, plan.baselinePreconditions(), plan.arbitrationWinnerSourceId()); + + IllegalStateException failure = assertThrows(IllegalStateException.class, + () -> compile(samples(sample), List.of(malformed), new TestCaveVoxelView())); + + assertTrue(failure.getMessage().contains("layer is absent from its containment plan")); + assertTrue(failure.getMessage().contains(missing.toString())); + } + @Test public void lavaPreconditionMismatchPublishesNothing() { HydrologyColumnLayer pool = cavePool(); diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentCaveAnchorTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentCaveAnchorTest.java index f9c3251f7..349d5756f 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentCaveAnchorTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentCaveAnchorTest.java @@ -1,5 +1,7 @@ package art.arcane.iris.engine.mantle.components; +import art.arcane.iris.engine.history.BoundaryColumnGeometry; + import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction; import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell; @@ -10,6 +12,20 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class MantleObjectComponentCaveAnchorTest { + @Test + public void retainedCaveAnchorsUseResolvedFluidAndOccupancyWithoutRawCarveFacts() { + BoundaryColumnGeometry.Voxel air = new BoundaryColumnGeometry.Voxel( + "minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false); + BoundaryColumnGeometry.Voxel water = new BoundaryColumnGeometry.Voxel( + "minecraft:water[level=0]", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water[level=0]", false); + BoundaryColumnGeometry.Voxel stone = new BoundaryColumnGeometry.Voxel( + "minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false); + assertTrue(MantleObjectComponent.acceptsResolvedCaveAnchor(false, air)); + assertFalse(MantleObjectComponent.acceptsResolvedCaveAnchor(false, water)); + assertTrue(MantleObjectComponent.acceptsResolvedCaveAnchor(true, water)); + assertFalse(MantleObjectComponent.acceptsResolvedCaveAnchor(true, stone)); + } + @Test public void biomeOwnedPlacementsRejectForeignCaveBands() { IrisBiome frozen = biome("carving/ice"); diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierContextTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierContextTest.java new file mode 100644 index 000000000..97a057836 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierContextTest.java @@ -0,0 +1,180 @@ +package art.arcane.iris.engine.modifier; + +import art.arcane.iris.engine.IrisEngine; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisDepositGenerator; +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.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.Matter; +import org.junit.Test; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +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.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class IrisDepositModifierContextTest { + @BeforeClass + public static void bindPlatform() { + IrisPlatforms.unbind(); + PlatformBlockState block = mock(PlatformBlockState.class); + PlatformRegistries registries = mock(PlatformRegistries.class); + when(registries.block(anyString())).thenReturn(block); + IrisPlatform platform = mock(IrisPlatform.class); + when(platform.registries()).thenReturn(registries); + IrisPlatforms.bind(platform); + } + + @AfterClass + public static void unbindPlatform() { + IrisPlatforms.unbind(); + } + + @Test + public void workerDepositsRetainRuntimeAndChunkContextUntilCompletion() throws Exception { + IrisEngine engine = mock(IrisEngine.class, RETURNS_DEEP_STUBS); + try (Fixture fixture = new Fixture(engine, true)) { + fixture.generateAndCheckCleanup(); + verify(engine, times(3)).captureGenerationRuntimeBinding(); + verify(engine, times(3)).openGenerationRuntimeScope(fixture.binding); + assertEquals(3, fixture.closedScopes.get()); + } + } + + @Test + public void unscopedDepositCallsDoNotInventARuntimeBinding() throws Exception { + IrisEngine engine = mock(IrisEngine.class, RETURNS_DEEP_STUBS); + try (Fixture fixture = new Fixture(engine, false)) { + fixture.generateAndCheckCleanup(); + verify(engine, never()).captureGenerationRuntimeBinding(); + verify(engine, never()).openGenerationRuntimeScope(any()); + } + } + + @Test + public void nonIrisEngineWorkersStillReceiveTheirChunkContext() throws Exception { + try (Fixture fixture = new Fixture(mock(Engine.class, RETURNS_DEEP_STUBS), false)) { + fixture.generateAndCheckCleanup(); + } + } + + private static final class Fixture implements AutoCloseable { + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private final Engine engine; + private final ChunkContext context = mock(ChunkContext.class, RETURNS_DEEP_STUBS); + private final IrisEngine.GenerationRuntimeBinding binding = mock(IrisEngine.GenerationRuntimeBinding.class); + private final ThreadLocal workerRuntime = ThreadLocal.withInitial(() -> false); + private final AtomicInteger completed = new AtomicInteger(); + private final AtomicInteger closedScopes = new AtomicInteger(); + private final IrisDepositModifier modifier; + private final MantleChunk chunk; + + @SuppressWarnings("unchecked") + private Fixture(Engine engine, boolean scoped) { + this.engine = engine; + Thread caller = Thread.currentThread(); + when(context.getGenerationSessionId()).thenReturn(71L); + MultiBurst pool = mock(MultiBurst.class); + BurstExecutor burst = new BurstExecutor(executor, 3); + burst.setMulticore(true); + when(engine.burst()).thenReturn(pool); + when(pool.burst(true)).thenReturn(burst); + IrisDepositGenerator dimensionDeposit = mock(IrisDepositGenerator.class); + IrisDepositGenerator regionDeposit = mock(IrisDepositGenerator.class); + IrisDepositGenerator biomeDeposit = mock(IrisDepositGenerator.class); + when(engine.getDimension().getDeposits()).thenReturn(new KList<>(dimensionDeposit)); + when(context.getRegion().get(7, 7).getDeposits()) + .thenReturn(new KList<>(regionDeposit)); + when(context.getBiome().get(7, 7).getDeposits()) + .thenReturn(new KList<>(biomeDeposit)); + chunk = mock(MantleChunk.class); + Mantle mantle = engine.getMantle().getMantle(); + doReturn(chunk).when(mantle).getChunk(2, -3); + when(chunk.use()).thenReturn(chunk); + if (engine instanceof IrisEngine irisEngine) { + when(irisEngine.hasGenerationRuntimeScope()).thenReturn(scoped); + when(irisEngine.captureGenerationRuntimeBinding()).thenReturn(binding); + IrisEngine.GenerationRuntimeScope scope = mock(IrisEngine.GenerationRuntimeScope.class); + when(irisEngine.openGenerationRuntimeScope(binding)).thenAnswer(invocation -> { + assertNotSame(caller, Thread.currentThread()); + workerRuntime.set(true); + return scope; + }); + doAnswer(invocation -> { + workerRuntime.remove(); + closedScopes.incrementAndGet(); + return null; + }).when(scope).close(); + } + modifier = spy(new IrisDepositModifier(engine)); + doAnswer(invocation -> { + assertNotSame(caller, Thread.currentThread()); + assertEquals(scoped, workerRuntime.get()); + assertSame(engine, IrisContext.require().getEngine()); + assertSame(context, IrisContext.require().getChunkContext()); + assertEquals(71L, IrisContext.require().getGenerationSessionId()); + completed.incrementAndGet(); + return null; + }).when(modifier).generate(any(), same(chunk), any(), any(), eq(2), eq(-3), eq(false), same(context)); + doAnswer(invocation -> { + assertEquals(3, completed.get()); + assertEquals(scoped ? 3 : 0, closedScopes.get()); + return null; + }).when(chunk).release(); + } + + private void generateAndCheckCleanup() throws Exception { + ChunkContext callerContext = mock(ChunkContext.class); + try (IrisContext.Scope ignored = IrisContext.open(engine, 19L, callerContext)) { + modifier.generateDeposits(Hunk.newArrayHunk(16, 16, 16), + 2, -3, true, context); + assertSame(callerContext, IrisContext.require().getChunkContext()); + assertEquals(19L, IrisContext.require().getGenerationSessionId()); + } + assertEquals(3, completed.get()); + verify(chunk).release(); + executor.submit(() -> { + assertNull(IrisContext.get()); + assertFalse(workerRuntime.get()); + }).get(10L, TimeUnit.SECONDS); + } + + @Override + public void close() throws Exception { + executor.shutdownNow(); + assertTrue(executor.awaitTermination(10L, TimeUnit.SECONDS)); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/platform/BukkitChunkGeneratorGenerationHistoryContractTest.java b/core/src/test/java/art/arcane/iris/engine/platform/BukkitChunkGeneratorGenerationHistoryContractTest.java index 596f549d0..3bf633b24 100644 --- a/core/src/test/java/art/arcane/iris/engine/platform/BukkitChunkGeneratorGenerationHistoryContractTest.java +++ b/core/src/test/java/art/arcane/iris/engine/platform/BukkitChunkGeneratorGenerationHistoryContractTest.java @@ -35,6 +35,29 @@ public final class BukkitChunkGeneratorGenerationHistoryContractTest { assertTrue(createEngine.contains("if (generationHistory == null)")); } + @Test + public void studioLoadsItsSnapshotBeforeAnyMutablePackFallback() throws IOException { + String target = method(source(), "public EngineTarget getTarget()"); + String historyTarget = method(source(), "private EngineTarget loadActiveGenerationHistoryTarget()"); + + assertBefore(target, "if (generationHistory != null)", "IrisData.openRuntime(dataLocation)"); + assertBefore(target, "return loadActiveGenerationHistoryTarget();", "IrisData.openRuntime(dataLocation)"); + assertBefore(historyTarget, "generationHistory.activePackRoot()", "IrisData.openRuntime(packRoot.toFile())"); + } + + @Test + public void studioWatchAndHotloadReadAuthoringSourceWithoutChangingTheRuntimeSnapshot() throws IOException { + String source = source(); + String constructor = method(source, "public BukkitChunkGenerator("); + String createEngine = method(source, "private IrisEngine createEngine(EngineTarget engineTarget)"); + + assertTrue(constructor.contains("new ReactiveFolder(\n dataLocation,")); + assertTrue(constructor.contains("new KList<>(\".iob\", \".json\", \".png\")")); + assertTrue(createEngine.contains("if (studio)")); + assertBefore(createEngine, "new IrisEngine(", "configureStudioGenerationSource(dataLocation.toPath())"); + assertBefore(createEngine, "configureStudioGenerationSource(dataLocation.toPath())", "return createdEngine;"); + } + @Test public void publicBukkitTerrainClaimsSemanticsOnlyAfterSuccessfulApplication() throws IOException { String generateNoise = method( diff --git a/core/src/test/java/art/arcane/iris/util/common/director/DirectorAuthoringDataTest.java b/core/src/test/java/art/arcane/iris/util/common/director/DirectorAuthoringDataTest.java new file mode 100644 index 000000000..bf00429cd --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/common/director/DirectorAuthoringDataTest.java @@ -0,0 +1,94 @@ +package art.arcane.iris.util.common.director; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.pack.PackDirectoryResolver; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.MockedStatic; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class DirectorAuthoringDataTest { + @Rule + public final TemporaryFolder temporary = new TemporaryFolder(); + + @Test + public void studioCommandsWriteTheEditablePackWithoutTouchingTheEpoch() throws Exception { + Path source = temporary.newFolder("source").toPath(); + Path epoch = temporary.newFolder("epoch").toPath(); + Files.writeString(source.resolve("object.iob"), "editable"); + Files.writeString(epoch.resolve("object.iob"), "retained"); + Engine engine = mock(Engine.class); + IrisData editable = mock(IrisData.class); + IrisData retained = mock(IrisData.class); + when(engine.isStudio()).thenReturn(true); + when(engine.getPackSource()).thenReturn(source); + when(engine.getData()).thenReturn(retained); + when(editable.getDataFolder()).thenReturn(source.toFile()); + when(retained.getDataFolder()).thenReturn(epoch.toFile()); + DirectorExecutor command = mock(DirectorExecutor.class, CALLS_REAL_METHODS); + when(command.engine()).thenReturn(engine); + try (MockedStatic data = mockStatic(IrisData.class)) { + data.when(() -> IrisData.get(source.toFile())).thenReturn(editable); + IrisData selected = command.data(); + assertSame(editable, selected); + Files.writeString(selected.getDataFolder().toPath().resolve("object.iob"), "edited"); + assertEquals("edited", Files.readString(source.resolve("object.iob"))); + assertEquals("retained", Files.readString(epoch.resolve("object.iob"))); + data.verify(() -> IrisData.get(epoch.toFile()), never()); + } + verify(engine, never()).getData(); + } + + @Test + public void productionCommandsResolveOnlyAnUnambiguousInstalledSource() throws Exception { + File packs = temporary.newFolder("packs"); + File first = new File(packs, "first"); + File second = new File(packs, "second"); + Engine engine = mock(Engine.class); + IrisDimension dimension = mock(IrisDimension.class); + IrisPlatform platform = mock(IrisPlatform.class); + IrisData firstData = mock(IrisData.class, RETURNS_DEEP_STUBS); + IrisData secondData = mock(IrisData.class, RETURNS_DEEP_STUBS); + when(engine.getDimension()).thenReturn(dimension); + when(dimension.getLoadKey()).thenReturn("overworld"); + when(platform.packsFolder()).thenReturn(packs); + when(firstData.getDimensionLoader().getPossibleKeys()).thenReturn(new String[]{"overworld"}); + when(secondData.getDimensionLoader().getPossibleKeys()).thenReturn(new String[]{"underworld"}); + try (MockedStatic data = mockStatic(IrisData.class); + MockedStatic platforms = mockStatic(IrisPlatforms.class); + MockedStatic directories = mockStatic(PackDirectoryResolver.class)) { + platforms.when(IrisPlatforms::get).thenReturn(platform); + directories.when(() -> PackDirectoryResolver.listVisiblePackDirectories(packs)).thenReturn(List.of(first, second)); + data.when(() -> IrisData.get(first)).thenReturn(firstData); + data.when(() -> IrisData.get(second)).thenReturn(secondData); + assertSame(firstData, DirectorExecutor.authoringData(engine)); + when(secondData.getDimensionLoader().getPossibleKeys()).thenReturn(new String[]{"overworld"}); + assertNull(DirectorExecutor.authoringData(engine)); + when(firstData.getDimensionLoader().getPossibleKeys()).thenReturn(new String[]{"other"}); + when(secondData.getDimensionLoader().getPossibleKeys()).thenReturn(new String[]{"underworld"}); + assertNull(DirectorExecutor.authoringData(engine)); + } + verify(engine, never()).getData(); + verify(engine, never()).getPackSource(); + } +} diff --git a/core/src/test/java/art/arcane/iris/util/project/context/ChunkContextPrefillPlanTest.java b/core/src/test/java/art/arcane/iris/util/project/context/ChunkContextPrefillPlanTest.java index 41717d103..a2eb3aea0 100644 --- a/core/src/test/java/art/arcane/iris/util/project/context/ChunkContextPrefillPlanTest.java +++ b/core/src/test/java/art/arcane/iris/util/project/context/ChunkContextPrefillPlanTest.java @@ -4,9 +4,11 @@ import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.util.project.stream.ProceduralStream; import org.bukkit.block.data.BlockData; import org.junit.Test; +import org.mockito.MockedStatic; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -22,6 +24,7 @@ import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -88,8 +91,39 @@ public class ChunkContextPrefillPlanTest { @Test public void singleOrNoFillTaskPrefillsInline() { - assertFalse(ChunkContext.shouldPrefillAsync(0)); - assertFalse(ChunkContext.shouldPrefillAsync(1)); + assertFalse(ChunkContext.shouldPrefillAsync(ChunkContext.PrefillPlan.NO_CAVE, 0)); + assertFalse(ChunkContext.shouldPrefillAsync(ChunkContext.PrefillPlan.NO_CAVE, 1)); + } + + @Test + public void naturalTerrainPrefillsAllRequiredCachesWithoutConsultingSharedExecutor() { + AtomicInteger caveCalls = new AtomicInteger(); + AtomicInteger heightCalls = new AtomicInteger(); + AtomicInteger biomeCalls = new AtomicInteger(); + AtomicInteger rockCalls = new AtomicInteger(); + AtomicInteger fluidCalls = new AtomicInteger(); + AtomicInteger regionCalls = new AtomicInteger(); + try (MockedStatic platforms = mockStatic(IrisPlatforms.class)) { + platforms.when(IrisPlatforms::isBound).thenReturn(true); + assertFalse(ChunkContext.shouldPrefillAsync(ChunkContext.PrefillPlan.NATURAL_TERRAIN, 5)); + ChunkContext context = createContext(ChunkContext.PrefillPlan.NATURAL_TERRAIN, + caveCalls, heightCalls, biomeCalls, rockCalls, fluidCalls, regionCalls); + + assertEquals(256, heightCalls.get()); + assertEquals(256, biomeCalls.get()); + assertEquals(256, rockCalls.get()); + assertEquals(256, fluidCalls.get()); + assertEquals(256, regionCalls.get()); + assertEquals(0, caveCalls.get()); + assertEquals(34051D, context.getHeight().getDouble(2, 3), 0D); + assertEquals(34051, context.getRoundedHeight(2, 3)); + context.getBiome().get(2, 3); + context.getRegion().get(2, 3); + assertEquals(256, heightCalls.get()); + assertEquals(256, biomeCalls.get()); + assertEquals(256, regionCalls.get()); + platforms.verifyNoInteractions(); + } } @Test @@ -243,7 +277,7 @@ public class ChunkContextPrefillPlanTest { return region; }).when(regionStream).get(anyDouble(), anyDouble()); - doReturn(heightStream).when(complex).getHeightStream(); + doReturn(heightStream).when(complex).getRawHeightStream(); doReturn(biomeStream).when(complex).getTrueBiomeStream(); doReturn(caveStream).when(complex).getCaveBiomeStream(); doReturn(rockStream).when(complex).getRockStream(); diff --git a/core/src/test/java/art/arcane/iris/util/project/context/ChunkedDoubleDataCacheOverrideTest.java b/core/src/test/java/art/arcane/iris/util/project/context/ChunkedDoubleDataCacheOverrideTest.java new file mode 100644 index 000000000..ec3c3da4c --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/context/ChunkedDoubleDataCacheOverrideTest.java @@ -0,0 +1,24 @@ +package art.arcane.iris.util.project.context; + +import art.arcane.iris.util.project.stream.ProceduralStream; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ChunkedDoubleDataCacheOverrideTest { + @Test + public void reconciledHeightOverridesCachedAndUncachedStreams() { + ProceduralStream stream = mock(ProceduralStream.class); + when(stream.getDouble(17D, 34D)).thenReturn(400D); + for (boolean cached : new boolean[]{true, false}) { + ChunkedDoubleDataCache cache = new ChunkedDoubleDataCache(stream, 16, 32, cached); + assertEquals(400D, cache.getDouble(1, 2), 0D); + cache.setDouble(1, 2, 23D); + assertEquals(23D, cache.getDouble(1, 2), 0D); + assertThrows(IllegalArgumentException.class, () -> cache.setDouble(-1, 2, 10D)); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/util/project/matter/slices/PreObjectMatterTest.java b/core/src/test/java/art/arcane/iris/util/project/matter/slices/PreObjectMatterTest.java index 6544d51e9..754590d14 100644 --- a/core/src/test/java/art/arcane/iris/util/project/matter/slices/PreObjectMatterTest.java +++ b/core/src/test/java/art/arcane/iris/util/project/matter/slices/PreObjectMatterTest.java @@ -1,6 +1,8 @@ package art.arcane.iris.util.project.matter.slices; import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell; +import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction; import art.arcane.iris.util.project.matter.IrisMatterSupport; import art.arcane.iris.util.project.matter.PreObjectMatterCell; import art.arcane.volmlib.util.matter.IrisMatter; @@ -58,7 +60,8 @@ public class PreObjectMatterTest { MatterCavern cavern = new MatterCavern(true, "iris:flooded", (byte) 1); PreObjectMatterCell expected = PreObjectMatterCell.block(null) .captureString("object-owner") - .captureCavern(cavern); + .captureCavern(cavern) + .captureHydrology(new HydrologyCaveCell(HydrologyCaveAction.WET_SOURCE, "river", "flooded")); Matter matter = new IrisMatter(16, 16, 16); matter.slice(PreObjectMatterCell.class).set(2, 3, 4, expected); @@ -120,7 +123,7 @@ public class PreObjectMatterTest { assertThrows(IOException.class, () -> matter.readNode(input(0))); assertThrows(IOException.class, () -> matter.readNode(input(1 << 1))); - assertThrows(IOException.class, () -> matter.readNode(input(1 << 6))); + assertThrows(IOException.class, () -> matter.readNode(input(1 << 7))); } @Test diff --git a/gradle/generation-build-revision.gradle b/gradle/generation-build-revision.gradle new file mode 100644 index 000000000..9db8aec3e --- /dev/null +++ b/gradle/generation-build-revision.gradle @@ -0,0 +1,91 @@ +import art.arcane.iris.buildtools.GenerationBuildRevision +import art.arcane.iris.buildtools.GenerationRevisionScope +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.ResolvedArtifact +import org.gradle.api.tasks.TaskProvider +import org.gradle.jvm.tasks.Jar + +import java.nio.file.Path + +Configuration kernelDependencies = configurations.create('generationKernelDependencies') { + canBeConsumed = false + canBeResolved = true + extendsFrom(configurations.implementation, configurations.slim, configurations.annotationProcessor) +} +dependencies.add(kernelDependencies.name, libs.guava) + +GenerationBuildRevision.CaptureOptions revisionScope = GenerationRevisionScope.current(rootProject.projectDir.toPath()) +File catalogFile = file('generation-kernels/catalog.tsv') +TaskProvider kernelAgentJar = project(':core:agent').tasks.named('jar', Jar) +File generatedResources = layout.buildDirectory.dir('generated/generation-revision').get().asFile +File generatedCatalog = new File(generatedResources, 'META-INF/iris/generation-kernels/catalog.tsv') +File generatedRevision = new File(generatedResources, 'META-INF/iris/generation-kernels/abi-1.revision') + +Closure> resolvedKernelDependencies = { + Map artifacts = new TreeMap<>() + for (ResolvedArtifact artifact : kernelDependencies.resolvedConfiguration.resolvedArtifacts) { + String group = artifact.moduleVersion.id.group + String name = artifact.moduleVersion.id.name + if (group == 'art.arcane' && name == 'spi') { + continue + } + boolean volmLib = (group == 'com.github.VolmitSoftware' && name == 'VolmLib') + || group == 'com.github.VolmitSoftware.VolmLib' + || (group == 'art.arcane' && name == 'shared') + String key = volmLib ? 'volmlib' : "${group}:${name}".toString() + if (artifact.classifier != null) { + key += ':' + artifact.classifier + } + if (artifacts.putIfAbsent(key, artifact.file.toPath()) != null) { + throw new GradleException("Duplicate generation dependency identity: ${key}") + } + } + if (!artifacts.containsKey('volmlib')) { + throw new GradleException('The generation revision must include the resolved VolmLib artifact.') + } + artifacts.put('iris-agent', kernelAgentJar.get().archiveFile.get().asFile.toPath()) + return artifacts +} + +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") + 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 }) + outputs.files(generatedRevision, generatedCatalog) + doLast { + GenerationBuildRevision.SourceManifest captured = GenerationBuildRevision.capture(revisionScope, resolvedKernelDependencies()) + GenerationBuildRevision.verifyCatalog(catalogFile.toPath(), [(revisionScope.abi()): captured]) + GenerationBuildRevision.write(generatedRevision.toPath(), captured) + generatedCatalog.text = catalogFile.getText('UTF-8') + } +} + +TaskProvider verifyRevision = tasks.register('verifyGenerationBuildRevision') { + group = 'verification' + description = 'Verifies that the packaged revision describes the inputs of this build.' + dependsOn(revisionResources) + doLast { + GenerationBuildRevision.verifySnapshot(rootProject.projectDir.toPath(), generatedRevision.toPath(), resolvedKernelDependencies()) + GenerationBuildRevision.verifyCatalog(generatedCatalog.toPath(), [(revisionScope.abi()): GenerationBuildRevision.read(generatedRevision.toPath())]) + } +} + +tasks.named('processResources').configure { + dependsOn(revisionResources) + from(generatedResources) +} +tasks.named('test', Test).configure { + dependsOn(revisionResources) + classpath = files(generatedResources) + classpath +} +tasks.named('check').configure { dependsOn(verifyRevision) } +tasks.withType(Jar).configureEach { dependsOn(verifyRevision) } +rootProject.tasks.named('check').configure { dependsOn(verifyRevision) } +rootProject.tasks.withType(Jar).configureEach { dependsOn(verifyRevision) } +rootProject.tasks.matching { Task task -> task.name in ['fabricJar', 'forgeJar', 'neoforgeJar'] } + .configureEach { dependsOn(verifyRevision) } +rootProject.subprojects.findAll { Project candidate -> candidate.path.startsWith(':adapters:bukkit:') } + .each { Project adapter -> adapter.tasks.withType(Jar).configureEach { dependsOn(verifyRevision) } } diff --git a/gradle/generation-kernel-seals.gradle b/gradle/generation-kernel-seals.gradle deleted file mode 100644 index 3ee1589ee..000000000 --- a/gradle/generation-kernel-seals.gradle +++ /dev/null @@ -1,133 +0,0 @@ -import art.arcane.iris.buildtools.GenerationKernelSeal -import org.gradle.api.artifacts.Configuration -import org.gradle.api.artifacts.ResolvedArtifact -import org.gradle.api.tasks.TaskProvider -import org.gradle.jvm.tasks.Jar - -import java.nio.file.Path - -Configuration kernelDependencies = configurations.create('generationKernelDependencies') { - canBeConsumed = false - canBeResolved = true - extendsFrom(configurations.implementation, configurations.slim, configurations.annotationProcessor) -} -dependencies.add(kernelDependencies.name, libs.guava) - -List generationSources = [ - 'lombok.config', - 'core/agent/build.gradle', - 'core/agent/src/main/java', - 'core/src/main/java', - 'spi/src/main/java', - 'adapters/bukkit/plugin/src/main/java', - 'adapters/bukkit/nms/v26_2_R1/src/main/java', - 'adapters/minecraft-common/src/main/java', - 'adapters/modded-common/src/main/java', - 'adapters/client-common/src/main/java', - 'adapters/fabric/src/main/java', - 'adapters/forge/src/main/java', - 'adapters/neoforge/src/main/java' -] -Map kernelScopes = [ - (1): new GenerationKernelSeal.CaptureOptions(rootProject.projectDir.toPath(), 1, - 'art.arcane.iris.engine.history.GenerationKernelV1', [new GenerationKernelSeal.AlgorithmVersion(1, 1)], generationSources, - ['core/src/main/java/art/arcane/iris/engine/kernels']) -] -File sealDirectory = file('generation-kernels') -FileTree sealFiles = fileTree(sealDirectory) { include('abi-*.seal') } -TaskProvider kernelAgentJar = project(':core:agent').tasks.named('jar', Jar) - -Closure> resolvedKernelDependencies = { - Map artifacts = new TreeMap<>() - for (ResolvedArtifact artifact : kernelDependencies.resolvedConfiguration.resolvedArtifacts) { - String group = artifact.moduleVersion.id.group - String name = artifact.moduleVersion.id.name - if (group == 'art.arcane' && name == 'spi') { - continue - } - boolean volmLib = (group == 'com.github.VolmitSoftware' && name == 'VolmLib') - || group == 'com.github.VolmitSoftware.VolmLib' - || (group == 'art.arcane' && name == 'shared') - String key = volmLib ? 'volmlib' : "${group}:${name}".toString() - if (artifact.classifier != null) { - key += ':' + artifact.classifier - } - if (artifacts.putIfAbsent(key, artifact.file.toPath()) != null) { - throw new GradleException("Duplicate generation dependency identity: ${key}") - } - } - if (!artifacts.containsKey('volmlib')) { - throw new GradleException('The generation seal must include the resolved VolmLib artifact.') - } - artifacts.put('iris-agent', kernelAgentJar.get().archiveFile.get().asFile.toPath()) - return artifacts -} - -TaskProvider verifySeals = tasks.register('verifyGenerationKernelSeals') { - group = 'verification' - description = 'Rejects generation source or dependency changes under a retained generation ABI.' - dependsOn(kernelDependencies, kernelAgentJar) - inputs.files(sealFiles, kernelDependencies, kernelAgentJar.flatMap { Jar jar -> jar.archiveFile }) - doLast { - Map dependencies = resolvedKernelDependencies() - Map manifests = new TreeMap<>() - for (Map.Entry entry : kernelScopes.entrySet()) { - File seal = new File(sealDirectory, "abi-${entry.key}.seal") - GenerationKernelSeal.SourceManifest recorded = GenerationKernelSeal.read(seal.toPath()) - GenerationKernelSeal.CaptureOptions scope = entry.value - if (recorded.abi() != scope.abi() || recorded.factoryClass() != scope.factoryClass() || recorded.algorithms() != scope.algorithms() - || recorded.roots() != scope.roots() || recorded.exclusions() != scope.exclusions()) { - throw new GradleException("Generation ABI ${entry.key} source scope no longer matches its seal.") - } - GenerationKernelSeal.verify(rootProject.projectDir.toPath(), seal.toPath(), dependencies) - manifests.put(entry.key, recorded) - } - GenerationKernelSeal.verifyCatalog(new File(sealDirectory, 'catalog.tsv').toPath(), manifests) - Set expectedNames = kernelScopes.keySet().collect { Integer abi -> "abi-${abi}.seal".toString() }.toSet() - if (sealFiles.files.collect { File seal -> seal.name }.toSet() != expectedNames) { - throw new GradleException('Every retained generation seal must have an explicit build scope.') - } - } -} - -tasks.register('captureGenerationKernelSeal') { - group = 'build setup' - description = 'Creates new compatibility metadata for an explicitly selected, previously unsealed generation ABI.' - dependsOn(kernelDependencies, kernelAgentJar) - doLast { - String requested = providers.gradleProperty('generationKernelAbi').orNull - if (requested == null || !(requested ==~ '[1-9][0-9]*')) { - throw new GradleException('Select a new generation ABI with -PgenerationKernelAbi=.') - } - int abi = Integer.parseInt(requested) - GenerationKernelSeal.CaptureOptions scope = kernelScopes.get(abi) - if (scope == null) { - throw new GradleException("Generation ABI ${abi} has no explicit source scope.") - } - GenerationKernelSeal.SourceManifest captured = GenerationKernelSeal.capture(scope, resolvedKernelDependencies()) - GenerationKernelSeal.writeNew(new File(sealDirectory, "abi-${abi}.seal").toPath(), captured) - logger.lifecycle("Sealed generation ABI ${abi}: ${GenerationKernelSeal.fingerprint(captured)}") - } -} - -TaskProvider sealResources = tasks.register('generateGenerationKernelSealResources', Sync) { - from(sealFiles) - from(new File(sealDirectory, 'catalog.tsv')) - into(layout.buildDirectory.dir('generated/kernel-seals/META-INF/iris/generation-kernels')) -} -tasks.named('processResources').configure { - dependsOn(sealResources) - from(layout.buildDirectory.dir('generated/kernel-seals')) -} -tasks.named('test', Test).configure { - dependsOn(sealResources) - classpath = files(layout.buildDirectory.dir('generated/kernel-seals')) + classpath -} -tasks.named('check').configure { dependsOn(verifySeals) } -tasks.withType(Jar).configureEach { dependsOn(verifySeals) } -rootProject.tasks.named('check').configure { dependsOn(verifySeals) } -rootProject.tasks.withType(Jar).configureEach { dependsOn(verifySeals) } -rootProject.tasks.matching { Task task -> task.name in ['fabricJar', 'forgeJar', 'neoforgeJar'] } - .configureEach { dependsOn(verifySeals) } -rootProject.subprojects.findAll { Project candidate -> candidate.path.startsWith(':adapters:bukkit:') } - .each { Project adapter -> adapter.tasks.withType(Jar).configureEach { dependsOn(verifySeals) } } diff --git a/gradle/modded-generation-kernel-seals.gradle b/gradle/modded-generation-build-revision.gradle similarity index 70% rename from gradle/modded-generation-kernel-seals.gradle rename to gradle/modded-generation-build-revision.gradle index f23256210..15b14bc51 100644 --- a/gradle/modded-generation-kernel-seals.gradle +++ b/gradle/modded-generation-build-revision.gradle @@ -8,17 +8,17 @@ IncludedBuild coreBuild = gradle.includedBuilds.find { IncludedBuild candidate - candidate.projectDir.canonicalFile == file('../..').canonicalFile } if (coreBuild == null) { - throw new GradleException('The modded artifact requires the included Iris core build and its generation seal.') + throw new GradleException('The modded artifact requires the included Iris core build and its generation build revision.') } -TaskReference kernelSealVerification = coreBuild.task(':core:verifyGenerationKernelSeals') -TaskProvider resolvedSealVerification = tasks.register('verifyResolvedGenerationKernelDependencies', JavaExec) { +TaskReference buildRevisionVerification = coreBuild.task(':core:verifyGenerationBuildRevision') +TaskProvider resolvedRevisionVerification = tasks.register('verifyResolvedGenerationKernelDependencies', JavaExec) { group = 'verification' - description = 'Checks the actual bundled generation library contents against the retained kernel seal.' - dependsOn(kernelSealVerification) + description = 'Checks the actual bundled generation library contents against the current build revision.' + dependsOn(buildRevisionVerification) classpath = files('../../buildSrc/build/libs/buildSrc.jar') - mainClass.set('art.arcane.iris.buildtools.GenerationKernelSeal') + mainClass.set('art.arcane.iris.buildtools.GenerationBuildRevision') doFirst { - List arguments = ['verify-dependencies', file('../../core/generation-kernels/abi-1.seal').absolutePath] + List arguments = ['verify-dependencies', file('../../core/build/generated/generation-revision/META-INF/iris/generation-kernels/abi-1.revision').absolutePath] for (ResolvedArtifact artifact : configurations.bundle.resolvedConfiguration.resolvedArtifacts) { String group = artifact.moduleVersion.id.group String name = artifact.moduleVersion.id.name @@ -45,7 +45,7 @@ TaskProvider resolvedSealVerification = tasks.register('verifyResolved } } gradle.projectsEvaluated { - resolvedSealVerification.configure { dependsOn(configurations.bundle) } + resolvedRevisionVerification.configure { dependsOn(configurations.bundle) } } -tasks.withType(Jar).configureEach { dependsOn(resolvedSealVerification) } -tasks.named('check').configure { dependsOn(resolvedSealVerification) } +tasks.withType(Jar).configureEach { dependsOn(resolvedRevisionVerification) } +tasks.named('check').configure { dependsOn(resolvedRevisionVerification) }